Merge lane/abrechnung in feature/craftvia-mvp

Konflikte gelöst (additiv): Benachrichtigungstexte (Planung + Meilensteine), Event-Typen,
Navigation, Job-Queues/Prozessoren (geocode-site, planning-watch, billing-pdf), OpenAPI-Tags,
Smoke-Prüfungen (zweiter Mandant: Planung + Abrechnung).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 10:37:52 +02:00
co-authored by Claude Opus 5
79 changed files with 4481 additions and 40 deletions
+92
View File
@@ -750,6 +750,8 @@ model WorkOrder {
voiceNotes VoiceNote[]
reports Report[]
documents Document[]
milestones WorkOrderMilestone[] // L14
billingRecords BillingRecord[] // L14
@@unique([tenantId, number])
@@index([tenantId, status])
@@ -872,6 +874,8 @@ model MaterialUsage {
photoId String? @map("photo_id")
recordedById String? @map("recorded_by_id")
clientId String? @map("client_id") // offline local id
/// L14: billed billing record containing this position (set on „abgerechnet", cleared on void)
billingRecordId String? @map("billing_record_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -879,6 +883,7 @@ model MaterialUsage {
materialPlan MaterialPlan? @relation(fields: [materialPlanId], references: [id], onDelete: SetNull)
workSession WorkSession? @relation(fields: [workSessionId], references: [id], onDelete: SetNull)
@@index([tenantId, billingRecordId])
@@index([tenantId, workOrderId])
@@unique([tenantId, clientId])
@@map("material_usages")
@@ -966,11 +971,14 @@ model TimeEntry {
/// proposed correction of this entry {startedAt, endedAt, type, reason, requestedAt}; old values stay valid until approval
pendingChange Json? @map("pending_change")
note String?
/// L14: billed billing record containing this entry (set on „abgerechnet", cleared on void)
billingRecordId String? @map("billing_record_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
workSession WorkSession @relation(fields: [workSessionId], references: [id], onDelete: Cascade)
@@index([tenantId, billingRecordId])
@@index([tenantId, workSessionId])
@@index([tenantId, userId, startedAt])
@@index([tenantId, approvalStatus])
@@ -1341,3 +1349,87 @@ model AiGeneration {
@@index([tenantId, entityType, entityId])
@@map("ai_generations")
}
// ---------- L14 Abrechnungsübersicht (keine Buchhaltung: keine Preise/Beträge/Steuern) ----------
enum MilestoneStatus {
open
reached // reported by technician / team lead
confirmed // confirmed by back office → billing record
billed
}
enum BillingRecordKind {
order_completion
milestone
daily_report
}
enum BillingRecordStatus {
open
billed
voided
}
/// Optional billing section of a work order (defined by back office, reported by the field, confirmed by back office).
model WorkOrderMilestone {
id String @id @default(cuid())
tenantId String @map("tenant_id")
workOrderId String @map("work_order_id")
title String
description String?
sortOrder Int @default(0) @map("sort_order")
status MilestoneStatus @default(open)
reachedById String? @map("reached_by_id")
reachedAt DateTime? @map("reached_at")
reachedNote String? @map("reached_note")
confirmedById String? @map("confirmed_by_id")
confirmedAt DateTime? @map("confirmed_at")
rejectedById String? @map("rejected_by_id")
rejectedAt DateTime? @map("rejected_at")
rejectionReason String? @map("rejection_reason")
/// billed record this milestone belongs to (plain id, no FK)
billingRecordId String? @map("billing_record_id")
createdById String? @map("created_by_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
@@index([tenantId, workOrderId])
@@index([tenantId, status])
@@map("work_order_milestones")
}
/// Billing overview entry: an order completion, a confirmed milestone or an approved daily report that the
/// accounting (outside Craftvia) can invoice. `snapshot` freezes the statement when marked as billed.
/// Partial unique indexes (SQL, migration `*_abrechnung`): one non-voided record per source.
model BillingRecord {
id String @id @default(cuid())
tenantId String @map("tenant_id")
workOrderId String @map("work_order_id")
kind BillingRecordKind
milestoneId String? @map("milestone_id")
reportId String? @map("report_id")
periodFrom DateTime @map("period_from")
periodTo DateTime @map("period_to")
status BillingRecordStatus @default(open)
snapshot Json?
pdfDocumentId String? @map("pdf_document_id")
pdfChecksum String? @map("pdf_checksum")
invoiceNumber String? @map("invoice_number")
billedById String? @map("billed_by_id")
billedAt DateTime? @map("billed_at")
voidedById String? @map("voided_by_id")
voidedAt DateTime? @map("voided_at")
voidReason String? @map("void_reason")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
@@index([tenantId, status, createdAt])
@@index([tenantId, workOrderId])
@@map("billing_records")
}