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:
@@ -0,0 +1,100 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MilestoneStatus" AS ENUM ('open', 'reached', 'confirmed', 'billed');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BillingRecordKind" AS ENUM ('order_completion', 'milestone', 'daily_report');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BillingRecordStatus" AS ENUM ('open', 'billed', 'voided');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "material_usages" ADD COLUMN "billing_record_id" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "time_entries" ADD COLUMN "billing_record_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "work_order_milestones" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenant_id" TEXT NOT NULL,
|
||||
"work_order_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"status" "MilestoneStatus" NOT NULL DEFAULT 'open',
|
||||
"reached_by_id" TEXT,
|
||||
"reached_at" TIMESTAMP(3),
|
||||
"reached_note" TEXT,
|
||||
"confirmed_by_id" TEXT,
|
||||
"confirmed_at" TIMESTAMP(3),
|
||||
"rejected_by_id" TEXT,
|
||||
"rejected_at" TIMESTAMP(3),
|
||||
"rejection_reason" TEXT,
|
||||
"billing_record_id" TEXT,
|
||||
"created_by_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "work_order_milestones_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "billing_records" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenant_id" TEXT NOT NULL,
|
||||
"work_order_id" TEXT NOT NULL,
|
||||
"kind" "BillingRecordKind" NOT NULL,
|
||||
"milestone_id" TEXT,
|
||||
"report_id" TEXT,
|
||||
"period_from" TIMESTAMP(3) NOT NULL,
|
||||
"period_to" TIMESTAMP(3) NOT NULL,
|
||||
"status" "BillingRecordStatus" NOT NULL DEFAULT 'open',
|
||||
"snapshot" JSONB,
|
||||
"pdf_document_id" TEXT,
|
||||
"pdf_checksum" TEXT,
|
||||
"invoice_number" TEXT,
|
||||
"billed_by_id" TEXT,
|
||||
"billed_at" TIMESTAMP(3),
|
||||
"voided_by_id" TEXT,
|
||||
"voided_at" TIMESTAMP(3),
|
||||
"void_reason" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "billing_records_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "work_order_milestones_tenant_id_work_order_id_idx" ON "work_order_milestones"("tenant_id", "work_order_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "work_order_milestones_tenant_id_status_idx" ON "work_order_milestones"("tenant_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "billing_records_tenant_id_status_created_at_idx" ON "billing_records"("tenant_id", "status", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "billing_records_tenant_id_work_order_id_idx" ON "billing_records"("tenant_id", "work_order_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "material_usages_tenant_id_billing_record_id_idx" ON "material_usages"("tenant_id", "billing_record_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "time_entries_tenant_id_billing_record_id_idx" ON "time_entries"("tenant_id", "billing_record_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "work_order_milestones" ADD CONSTRAINT "work_order_milestones_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "billing_records" ADD CONSTRAINT "billing_records_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
-- L14 Abrechnungsübersicht: Mandantentrennung (RLS) für die neuen Tabellen
|
||||
SELECT enable_tenant_rls('work_order_milestones');
|
||||
SELECT enable_tenant_rls('billing_records');
|
||||
|
||||
-- Höchstens ein nicht stornierter Abrechnungseintrag je Quelle (Stornierung legt einen neuen offenen Eintrag an)
|
||||
CREATE UNIQUE INDEX "billing_records_order_completion_active_key" ON "billing_records"("tenant_id", "work_order_id") WHERE "kind" = 'order_completion' AND "status" <> 'voided';
|
||||
CREATE UNIQUE INDEX "billing_records_milestone_active_key" ON "billing_records"("tenant_id", "milestone_id") WHERE "kind" = 'milestone' AND "status" <> 'voided';
|
||||
CREATE UNIQUE INDEX "billing_records_daily_report_active_key" ON "billing_records"("tenant_id", "report_id") WHERE "kind" = 'daily_report' AND "status" <> 'voided';
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user