Auto-create a contract when a room-assigned invitation is accepted

Accepting an invitation with a roomId previously only set User.roomId
— no Contract row was created. Since every tenant-facing feature
(Miet-Ampel, Vertragsdokumente, Kündigungsfristen, ...) is driven by
Contract, not User.roomId, the tenant would silently show up nowhere
despite "having" a room. Now the accept transaction also creates an
active Contract (rent/deposit defaulted from the room's baseRent/
utilityPauschal, same defaults already used by the "Wieder aufnehmen"
flow) and marks the room OCCUPIED, unless the room already has an
active contract (guards against a stale/duplicate invite race).
This commit is contained in:
Giuseppe Lombardo 2026-08-13 12:02:03 +00:00
parent 48fb4118fd
commit 72008ac59b

View File

@ -220,6 +220,34 @@ invitationsRouter.post('/invitations/:token/accept', async (req: Request, res: R
where: { id: invitation.id },
data: { status: 'ACCEPTED', acceptedAt: new Date() },
});
// War der Einladung ein Zimmer zugeordnet, entsteht daraus jetzt auch ein
// Mietvertrag — sonst taucht der Mieter zwar mit roomId beim User auf,
// aber nirgendwo in Miet-Ampel/Vertragsdokumenten/Kündigungsfristen, da
// die überall an Contract hängen, nicht an User.roomId. Warmmiete/Kaution
// werden aus den bereits am Zimmer hinterlegten Werten übernommen (wie
// beim „Wieder aufnehmen“-Formular) — der Vermieter kann sie danach über
// den Vertragsdokumente-Bereich jederzeit anpassen.
if (invitation.roomId && invitation.role === UserRole.TENANT) {
const room = await tx.room.findUnique({ where: { id: invitation.roomId } });
const activeContract = room
? await tx.contract.findFirst({ where: { roomId: room.id, isActive: true } })
: null;
if (room && !activeContract) {
await tx.contract.create({
data: {
userId: created.id,
roomId: room.id,
startDate: new Date(),
totalWarmRent: Number(room.baseRent) + Number(room.utilityPauschal),
depositAmount: Number(room.baseRent) * 3,
isActive: true,
},
});
await tx.room.update({ where: { id: room.id }, data: { status: 'OCCUPIED' } });
}
}
return created;
});