Adds trashCalendarSyncJob.ts, which pulls Restmüll/Biomüll/Gelbe Tonne/ Papiertonne dates for Nackenheim (CityId 35, Verbandsgemeinde Bodenheim) from the public, keyless API behind lk.kaw-mainz-bingen.de's official waste calendar, and upserts them into trash_schedule. Adds a unique (type, date) constraint to prevent duplicate entries on reruns. Run via `npm run trash-sync:run`, same standalone-script pattern as the other reminder jobs.
715 lines
23 KiB
Plaintext
715 lines
23 KiB
Plaintext
// ============================================================================
|
|
// WG- & Vermieter-Management-App — Prisma Schema (PostgreSQL)
|
|
// Objekt: 170 qm 4er-WG, Nackenheim (Mainz), Einzelzimmervermietung
|
|
// ============================================================================
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
// Explizit gesetzt, da schema.prisma im Repo-Root liegt, aber node_modules
|
|
// nur in backend/ existiert: ohne dieses `output` sucht Prisma das
|
|
// "Projekt-Root" ausgehend vom Schema-Pfad nach oben, findet dort kein
|
|
// package.json und weicht auf einen unzuverlässigen Auto-Install-Fallback
|
|
// aus (schreibt dann z. B. nach /root/node_modules statt backend/).
|
|
output = "../backend/node_modules/.prisma/client"
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
enum InvitationStatus {
|
|
PENDING
|
|
ACCEPTED
|
|
EXPIRED
|
|
REVOKED
|
|
}
|
|
|
|
enum StorageLocation {
|
|
FRIDGE
|
|
FREEZER
|
|
KITCHEN_CABINET_1
|
|
KITCHEN_CABINET_2
|
|
KITCHEN_CABINET_3
|
|
PANTRY
|
|
}
|
|
|
|
enum TrashType {
|
|
RESTMUELL
|
|
BIOMUELL
|
|
GELBER_SACK
|
|
PAPIER
|
|
GLAS
|
|
}
|
|
|
|
enum DocumentCategory {
|
|
HAUSORDNUNG
|
|
WLAN
|
|
VERTRAG
|
|
TUTORIAL
|
|
SONSTIGES
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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[]
|
|
cleaningAbsences CleaningAbsence[]
|
|
expensesPaid Expense[]
|
|
expenseShares ExpenseShare[]
|
|
handoverProtocols HandoverProtocol[]
|
|
guestCodesIssued GuestCode[]
|
|
notifications Notification[]
|
|
invitationsSent Invitation[] @relation("InvitationsSent")
|
|
storageSlots StorageSlot[]
|
|
documentsUploaded Document[]
|
|
utilityStatementsCreated UtilityStatement[]
|
|
utilityStatementShares UtilityStatementShare[]
|
|
ratingsGiven TenantRating[] @relation("RatingsGiven")
|
|
ratingsReceived TenantRating[] @relation("RatingsReceived")
|
|
|
|
@@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[]
|
|
invitations Invitation[]
|
|
utilityStatementShares UtilityStatementShare[]
|
|
|
|
@@map("rooms")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// INVITATIONS (Vermieter lädt Mieter per E-Mail ein)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
model Invitation {
|
|
id String @id @default(uuid())
|
|
email String
|
|
token String @unique
|
|
role UserRole @default(TENANT)
|
|
roomId String? @map("room_id")
|
|
room Room? @relation(fields: [roomId], references: [id])
|
|
invitedById String @map("invited_by_id")
|
|
invitedBy User @relation("InvitationsSent", fields: [invitedById], references: [id])
|
|
status InvitationStatus @default(PENDING)
|
|
expiresAt DateTime @map("expires_at")
|
|
acceptedAt DateTime? @map("accepted_at")
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([email])
|
|
@@index([status])
|
|
@@map("invitations")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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 (interner User-Account, optional)
|
|
assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id])
|
|
craftsmanId String? @map("craftsman_id") // Handwerker-Kontakt (externer Betrieb ohne Login)
|
|
craftsman Craftsman? @relation(fields: [craftsmanId], 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")
|
|
}
|
|
|
|
// Abwesenheiten im Putzplan-Kalender (Urlaub, "kann nicht putzen" o. Ä.).
|
|
// Für alle Mieter sichtbar (gemeinschaftliche Organisationsinfo), aber nur
|
|
// vom jeweiligen Mieter selbst oder Vermieter/Admin anlegbar/löschbar.
|
|
model CleaningAbsence {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
startDate DateTime @map("start_date")
|
|
endDate DateTime @map("end_date")
|
|
note String?
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([userId, startDate])
|
|
@@map("cleaning_absences")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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)
|
|
photoUrls String[] @default([]) @map("photo_urls") // mehrere Fotos möglich
|
|
|
|
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")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// KÜCHEN-/SCHRANKPLANER & MÜLL-KALENDER
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Ein einzelnes Fach im Kühlschrank/Gefrierfach/Schrank, das sich ein Mieter
|
|
// reservieren ("beanspruchen") kann. NULL userId = frei/gemeinschaftlich.
|
|
model StorageSlot {
|
|
id String @id @default(uuid())
|
|
location StorageLocation
|
|
label String // z. B. "Fach 1 oben"
|
|
userId String? @map("user_id")
|
|
user User? @relation(fields: [userId], references: [id])
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("storage_slots")
|
|
}
|
|
|
|
// Müll-Kalender: Abholtermine je Mülltyp, für den Vorabend-Push.
|
|
model TrashSchedule {
|
|
id String @id @default(uuid())
|
|
type TrashType
|
|
date DateTime
|
|
reminderSentAt DateTime? @map("reminder_sent_at")
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@unique([type, date])
|
|
@@index([date])
|
|
@@map("trash_schedule")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// DOKUMENTEN-SAFE & TUTORIALS
|
|
// ----------------------------------------------------------------------------
|
|
|
|
model Document {
|
|
id String @id @default(uuid())
|
|
title String
|
|
category DocumentCategory @default(SONSTIGES)
|
|
url String // Link zur Datei/zum Video (PDF, Bild, YouTube o. Ä.)
|
|
description String?
|
|
uploadedById String @map("uploaded_by_id")
|
|
uploadedBy User @relation(fields: [uploadedById], references: [id])
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("documents")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// NEBENKOSTENABRECHNUNG (Utility-Statements)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Eine Nebenkostenabrechnung für einen Zeitraum (z. B. ein Jahr). Der
|
|
// Vermieter trägt den Gesamtbetrag ein; die App verteilt ihn automatisch
|
|
// proportional zur Zimmergröße (sizeSqm) auf alle aktuell aktiven Mieter —
|
|
// dieselbe faire Grundlage wie bei der Nettokaltmiete.
|
|
model UtilityStatement {
|
|
id String @id @default(uuid())
|
|
periodStart DateTime @map("period_start")
|
|
periodEnd DateTime @map("period_end")
|
|
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
|
description String?
|
|
createdById String @map("created_by_id")
|
|
createdBy User @relation(fields: [createdById], references: [id])
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
shares UtilityStatementShare[]
|
|
|
|
@@map("utility_statements")
|
|
}
|
|
|
|
// Anteil eines einzelnen Mieters an einer Nebenkostenabrechnung. Für den
|
|
// betroffenen Mieter selbst sichtbar, für alle anderen Mieter NICHT (private
|
|
// Abrechnungsdaten) — nur der Vermieter/Admin sieht alle Anteile.
|
|
model UtilityStatementShare {
|
|
id String @id @default(uuid())
|
|
statementId String @map("statement_id")
|
|
statement UtilityStatement @relation(fields: [statementId], references: [id])
|
|
userId String @map("user_id")
|
|
user User @relation(fields: [userId], references: [id])
|
|
roomId String? @map("room_id")
|
|
room Room? @relation(fields: [roomId], references: [id])
|
|
|
|
shareAmount Decimal @map("share_amount") @db.Decimal(8, 2)
|
|
settlementStatus SettlementStatus @default(OPEN) @map("settlement_status")
|
|
settledAt DateTime? @map("settled_at")
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([userId])
|
|
@@index([statementId])
|
|
@@map("utility_statement_shares")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// HANDWERKER-KONTAKTE (Anbindung für Schadensmeldungen)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
enum CraftsmanTrade {
|
|
SANITAER
|
|
ELEKTRIK
|
|
HEIZUNG
|
|
SCHREINEREI
|
|
MALER
|
|
SCHLUESSELDIENST
|
|
SONSTIGES
|
|
}
|
|
|
|
// Kontaktkarte eines Handwerksbetriebs (kein eigener Login nötig). Kann
|
|
// Tickets zugewiesen werden, damit direkt klar ist, wer sich kümmert und wie
|
|
// er erreichbar ist.
|
|
model Craftsman {
|
|
id String @id @default(uuid())
|
|
name String
|
|
trade CraftsmanTrade @default(SONSTIGES)
|
|
phone String?
|
|
email String?
|
|
notes String?
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tickets Ticket[]
|
|
|
|
@@map("craftsmen")
|
|
}
|
|
|
|
// Peer-Bewertungssystem für (Zwischen-)Mieter: jeder Nutzer kann jeden Mieter
|
|
// bewerten (z. B. Sauberkeit, Zuverlässigkeit, WG-Verträglichkeit). Bewusst
|
|
// freistehend und nicht an einen Contract gekoppelt. Einsehbar nur für
|
|
// Vermieter/Admin (siehe ratings.ts) — Mitbewohner sehen keine Bewertungen,
|
|
// um WG-interne Konflikte durch offene Kritik zu vermeiden.
|
|
model TenantRating {
|
|
id String @id @default(uuid())
|
|
raterId String @map("rater_id")
|
|
rater User @relation("RatingsGiven", fields: [raterId], references: [id])
|
|
ratedUserId String @map("rated_user_id")
|
|
ratedUser User @relation("RatingsReceived", fields: [ratedUserId], references: [id])
|
|
score Int // 1 (sehr schlecht) bis 5 (sehr gut)
|
|
comment String? @db.Text
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([ratedUserId])
|
|
@@map("tenant_ratings")
|
|
}
|