From ba0429c26bd65bdd7284bf614637c78a5beb84e8 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Wed, 12 Aug 2026 16:42:35 +0000 Subject: [PATCH] add prisma/schema.prisma --- prisma/schema.prisma | 471 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 prisma/schema.prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..5759b32 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,471 @@ +// ============================================================================ +// WG- & Vermieter-Management-App — Prisma Schema (PostgreSQL) +// Objekt: 170 qm 4er-WG, Nackenheim (Mainz), Einzelzimmervermietung +// ============================================================================ + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ---------------------------------------------------------------------------- +// ENUMS +// ---------------------------------------------------------------------------- + +enum UserRole { + LANDLORD + TENANT + CRAFTSMAN + ADMIN // Property-Manager / Vertretung des Vermieters +} + +enum RoomStatus { + OCCUPIED + VACANT + MAINTENANCE +} + +enum RentAdjustmentType { + NONE + INDEX_MIETE // Indexmietklausel + STAFFEL_MIETE // Staffelmietklausel +} + +enum PaymentStatus { + PENDING + PAID + PARTIAL + OVERDUE + MATCH_UNCERTAIN // Betrag/Referenz weicht ab -> manuelle Prüfung nötig +} + +enum TicketCategory { + SANITAER + ELEKTRIK + MOEBEL + HEIZUNG + SCHIMMEL + SCHLUESSEL_SCHLOSS + SONSTIGES +} + +enum TicketPriority { + LOW + MEDIUM + HIGH + EMERGENCY +} + +enum TicketStatus { + OPEN + IN_PROGRESS + RESOLVED + CLOSED +} + +enum CleaningArea { + BATHROOM_1 + BATHROOM_2 + KITCHEN + HALLWAY_LAUNDRY +} + +enum CleaningStatus { + PENDING + COMPLETED + VERIFIED + MISSED +} + +enum ItemCondition { + NEW + GOOD + WEAR_AND_TEAR + DAMAGED +} + +enum ExpenseCategory { + HAUSHALT // Putzmittel, Klopapier etc. + LEBENSMITTEL_GEMEINSAM + REPARATUR_VORAUSLAGE + SONSTIGES +} + +enum SettlementStatus { + OPEN + SETTLED +} + +enum HandoverType { + MOVE_IN + MOVE_OUT +} + +enum GuestCodeStatus { + ACTIVE + EXPIRED + REVOKED +} + +// ---------------------------------------------------------------------------- +// USERS +// ---------------------------------------------------------------------------- + +model User { + id String @id @default(uuid()) + email String @unique + passwordHash String @map("password_hash") + fullName String @map("full_name") + phoneNumber String? @map("phone_number") + role UserRole + avatarUrl String? @map("avatar_url") + pushToken String? @map("push_token") // OneSignal Player ID + + // Ein Mieter ist aktuell genau einem Zimmer zugeordnet (Bequemlichkeits-FK, + // die verbindliche Quelle bleibt Contract). + roomId String? @map("room_id") + room Room? @relation("RoomOccupant", fields: [roomId], references: [id]) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relations + contracts Contract[] + createdTickets Ticket[] @relation("TicketCreator") + assignedTickets Ticket[] @relation("TicketAssignee") + cleaningTasks CleaningTask[] + expensesPaid Expense[] + expenseShares ExpenseShare[] + handoverProtocols HandoverProtocol[] + guestCodesIssued GuestCode[] + notifications Notification[] + + @@map("users") +} + +// ---------------------------------------------------------------------------- +// ROOMS +// ---------------------------------------------------------------------------- + +model Room { + id String @id @default(uuid()) + roomNumber String @map("room_number") // "Zimmer 1" .. "Zimmer 4" + sizeSqm Decimal @map("size_sqm") @db.Decimal(5, 2) // 20.0 / 16.0 + baseRent Decimal @map("base_rent") @db.Decimal(8, 2) // Nettokaltmiete + utilityPauschal Decimal @map("utility_pauschal") @db.Decimal(8, 2) + status RoomStatus @default(VACANT) + floorPlanUrl String? @map("floor_plan_url") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + occupants User[] @relation("RoomOccupant") + contracts Contract[] + tickets Ticket[] + inventoryItems Inventory[] + meterReadings MeterReading[] + + @@map("rooms") +} + +// ---------------------------------------------------------------------------- +// CONTRACTS +// ---------------------------------------------------------------------------- + +model Contract { + id String @id @default(uuid()) + userId String @map("user_id") + user User @relation(fields: [userId], references: [id]) + roomId String @map("room_id") + room Room @relation(fields: [roomId], references: [id]) + + startDate DateTime @map("start_date") + endDate DateTime? @map("end_date") // NULL = unbefristet + + totalWarmRent Decimal @map("total_warm_rent") @db.Decimal(8, 2) + depositAmount Decimal @map("deposit_amount") @db.Decimal(8, 2) + paymentDueDay Int @default(3) @map("payment_due_day") // Fälligkeit: 3. Werktag + + // Kündigungsfristen & Anpassungsklauseln + noticePeriodMonths Int @default(3) @map("notice_period_months") + rentAdjustmentType RentAdjustmentType @default(NONE) @map("rent_adjustment_type") + rentAdjustmentClause String? @map("rent_adjustment_clause") @db.Text // Freitext der Klausel + nextAdjustmentDate DateTime? @map("next_adjustment_date") + + isActive Boolean @default(true) @map("is_active") + + contractPdfUrl String? @map("contract_pdf_url") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + payments Payment[] + handoverProtocols HandoverProtocol[] + + @@index([roomId, isActive]) + @@map("contracts") +} + +// ---------------------------------------------------------------------------- +// PAYMENTS (Banking API Abgleich) +// ---------------------------------------------------------------------------- + +model Payment { + id String @id @default(uuid()) + contractId String @map("contract_id") + contract Contract @relation(fields: [contractId], references: [id]) + + amount Decimal @db.Decimal(8, 2) + dueDate DateTime @map("due_date") + paidAt DateTime? @map("paid_at") + + status PaymentStatus @default(PENDING) + + // Banking-API (FinAPI / Nordigen / GoCardless) + bankTransactionId String? @unique @map("bank_transaction_id") + bankReferenceText String? @map("bank_reference_text") + matchedAutomatically Boolean @default(false) @map("matched_automatically") + + dunningSentAt DateTime? @map("dunning_sent_at") // Zeitpunkt Mahn-Notification + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([contractId, dueDate]) + @@index([status]) + @@map("payments") +} + +// ---------------------------------------------------------------------------- +// TICKETS (Schadensmelder) +// ---------------------------------------------------------------------------- + +model Ticket { + id String @id @default(uuid()) + creatorId String @map("creator_id") + creator User @relation("TicketCreator", fields: [creatorId], references: [id]) + assigneeId String? @map("assignee_id") // Handwerker + assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id]) + roomId String? @map("room_id") // NULL = Gemeinschaftsbereich + room Room? @relation(fields: [roomId], references: [id]) + + title String + description String @db.Text + category TicketCategory + priority TicketPriority @default(MEDIUM) + status TicketStatus @default(OPEN) + + imageUrls String[] @map("image_urls") // Foto-Beweise + pdfExportUrl String? @map("pdf_export_url") // 1-Klick-PDF für Handwerker + + resolvedAt DateTime? @map("resolved_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([status, priority]) + @@map("tickets") +} + +// ---------------------------------------------------------------------------- +// CLEANING TASKS (Putzplan) +// ---------------------------------------------------------------------------- + +model CleaningTask { + id String @id @default(uuid()) + assignedUserId String @map("assigned_user_id") + assignedUser User @relation(fields: [assignedUserId], references: [id]) + + area CleaningArea + weekOf DateTime @map("week_of") // Montag der jeweiligen Rotationswoche + + dueDate DateTime @map("due_date") + completedAt DateTime? @map("completed_at") + + proofImageUrl String? @map("proof_image_url") + status CleaningStatus @default(PENDING) + verifiedBy String? @map("verified_by") // optionale Mitbewohner-Bestätigung + + reminderSentAt DateTime? @map("reminder_sent_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([assignedUserId, weekOf]) + @@index([status, dueDate]) + @@map("cleaning_tasks") +} + +// ---------------------------------------------------------------------------- +// INVENTORY (Möbel & Geräte) +// ---------------------------------------------------------------------------- + +model Inventory { + id String @id @default(uuid()) + roomId String? @map("room_id") // NULL = Gemeinschaftsfläche + room Room? @relation(fields: [roomId], references: [id]) + + itemName String @map("item_name") + purchaseDate DateTime? @map("purchase_date") + purchasePrice Decimal? @map("purchase_price") @db.Decimal(8, 2) + condition ItemCondition @default(GOOD) + manualPdfUrl String? @map("manual_pdf_url") // Anleitung (z.B. Entkalkung) + photoUrl String? @map("photo_url") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + handoverItems HandoverItem[] + + @@map("inventory") +} + +// ---------------------------------------------------------------------------- +// EXPENSES (WG-Kasse) +// ---------------------------------------------------------------------------- + +model Expense { + id String @id @default(uuid()) + payerId String @map("payer_id") + payer User @relation(fields: [payerId], references: [id]) + + amount Decimal @db.Decimal(8, 2) + description String + category ExpenseCategory @default(HAUSHALT) + receiptUrl String? @map("receipt_url") + + createdAt DateTime @default(now()) @map("created_at") + + shares ExpenseShare[] + + @@map("expenses") +} + +// Automatischer Split (Standard: durch 4), pro Mieter ein Anteil +model ExpenseShare { + id String @id @default(uuid()) + expenseId String @map("expense_id") + expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade) + userId String @map("user_id") + user User @relation(fields: [userId], references: [id]) + + shareAmount Decimal @map("share_amount") @db.Decimal(8, 2) + settlementStatus SettlementStatus @default(OPEN) @map("settlement_status") + settledAt DateTime? @map("settled_at") + + @@unique([expenseId, userId]) + @@map("expense_shares") +} + +// ---------------------------------------------------------------------------- +// HANDOVER PROTOCOLS (Übergabeprotokoll bei Ein-/Auszug) +// ---------------------------------------------------------------------------- + +model HandoverProtocol { + id String @id @default(uuid()) + contractId String @map("contract_id") + contract Contract @relation(fields: [contractId], references: [id]) + userId String @map("user_id") + user User @relation(fields: [userId], references: [id]) + + type HandoverType + protocolDate DateTime @map("protocol_date") + signatureUrl String? @map("signature_url") // digitale Unterschrift (Bild/SVG) + pdfUrl String? @map("pdf_url") + + createdAt DateTime @default(now()) @map("created_at") + + items HandoverItem[] + meterReadings MeterReading[] + + @@map("handover_protocols") +} + +model HandoverItem { + id String @id @default(uuid()) + protocolId String @map("protocol_id") + protocol HandoverProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade) + inventoryId String? @map("inventory_id") + inventory Inventory? @relation(fields: [inventoryId], references: [id]) + + condition ItemCondition + note String? + photoUrl String? @map("photo_url") + + @@map("handover_items") +} + +model MeterReading { + id String @id @default(uuid()) + protocolId String? @map("protocol_id") + protocol HandoverProtocol? @relation(fields: [protocolId], references: [id]) + roomId String? @map("room_id") + room Room? @relation(fields: [roomId], references: [id]) + + meterType String @map("meter_type") // STROM, WASSER, GAS + value Decimal @db.Decimal(10, 2) + readAt DateTime @map("read_at") + photoUrl String? @map("photo_url") + + @@map("meter_readings") +} + +// ---------------------------------------------------------------------------- +// SMART LOCK / GUEST CODES +// ---------------------------------------------------------------------------- + +model GuestCode { + id String @id @default(uuid()) + issuedById String @map("issued_by_id") + issuedBy User @relation(fields: [issuedById], references: [id]) + + code String @unique + lockDeviceId String @map("lock_device_id") // Nuki/Tuya Device-ID + validFrom DateTime @map("valid_from") + validUntil DateTime @map("valid_until") + status GuestCodeStatus @default(ACTIVE) + + createdAt DateTime @default(now()) @map("created_at") + + @@map("guest_codes") +} + +// ---------------------------------------------------------------------------- +// UNMATCHED TRANSACTIONS (Banking-Abgleich ohne eindeutigen Treffer) +// ---------------------------------------------------------------------------- + +model UnmatchedTransactionLog { + id String @id @default(uuid()) + bankTransactionId String @unique @map("bank_transaction_id") + amount Decimal @db.Decimal(8, 2) + remittanceInfo String @map("remittance_info") @db.Text + reason String @db.Text + resolvedPaymentId String? @map("resolved_payment_id") + createdAt DateTime @default(now()) @map("created_at") + + @@map("unmatched_transaction_logs") +} + +// ---------------------------------------------------------------------------- +// NOTIFICATIONS (Push-Log) +// ---------------------------------------------------------------------------- + +model Notification { + id String @id @default(uuid()) + userId String @map("user_id") + user User @relation(fields: [userId], references: [id]) + + type String // z.B. "RENT_OVERDUE", "CLEANING_REMINDER", "TRASH_TOMORROW" + title String + body String + payload Json? + sentAt DateTime @default(now()) @map("sent_at") + readAt DateTime? @map("read_at") + + @@index([userId, sentAt]) + @@map("notifications") +}