L14 Abrechnungsübersicht: Datenmodell, Migration, Rechte und Modul

Neue Tenant-Tabellen work_order_milestones und billing_records (RLS, TENANT_MODELS, PII-Felder), Partial-Unique-Indizes je Quelle, billing_record_id an time_entries/material_usages. Rechte billing:read/billing:write (Backoffice, Mandantenadmin), Modul billing, Events milestone.*.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 10:35:20 +02:00
co-authored by Claude Opus 5
parent 9b087168d1
commit 6358d5762c
11 changed files with 220 additions and 3 deletions
@@ -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';