From 5f08df324f382d6e44dc8691bcf2db9c34a22c86 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 14 Sep 2026 18:19:19 +0200 Subject: [PATCH] =?UTF-8?q?L10b=20Betrieb=20&=20Aufr=C3=A4umen:=20Migratio?= =?UTF-8?q?nen=20clientId=20je=20Mandant=20und=20KI-Token-Kontingent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 20260915090000_betrieb_client_id_per_tenant (Aufräumpunkt f, L8 offener Punkt 6): globale Unique-Indizes auf client_id (material_usages, work_sessions, time_entries, activity_notes, photos, voice_notes, reports, signatures) → @@unique([tenantId, clientId]). Offline-IDs sind nur je Mandant eindeutig; ein Replay derselben ID in einem anderen Mandanten scheiterte bisher mit internem Fehler. Keine neuen Tabellen. - 20260915091000_betrieb_ai_token_limit (Aufräumpunkt k): tenant_settings.ai_monthly_token_limit (NULL = Plattform-Vorgabe AI_MONTHLY_TOKEN_LIMIT, 0 = unbegrenzt). Keine neue Tabelle. Co-Authored-By: Claude Opus 5 --- .../migration.sql | 53 +++++++++++++++++++ .../migration.sql | 7 +++ prisma/schema.prisma | 26 ++++++--- 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20260915090000_betrieb_client_id_per_tenant/migration.sql create mode 100644 prisma/migrations/20260915091000_betrieb_ai_token_limit/migration.sql diff --git a/prisma/migrations/20260915090000_betrieb_client_id_per_tenant/migration.sql b/prisma/migrations/20260915090000_betrieb_client_id_per_tenant/migration.sql new file mode 100644 index 0000000..7247323 --- /dev/null +++ b/prisma/migrations/20260915090000_betrieb_client_id_per_tenant/migration.sql @@ -0,0 +1,53 @@ +-- L10b Betrieb & Aufräumen (Aufräumpunkt f, L8 offener Punkt 6): +-- Offline client ids are generated per device and only need to be unique within a tenant. +-- A global unique index let a replay with the same client id in another tenant fail with an +-- internal error (and leaked the existence of the id across tenants). Tables already carry +-- tenant RLS; no new tables. + +-- DropIndex +DROP INDEX "activity_notes_client_id_key"; + +-- DropIndex +DROP INDEX "material_usages_client_id_key"; + +-- DropIndex +DROP INDEX "photos_client_id_key"; + +-- DropIndex +DROP INDEX "reports_client_id_key"; + +-- DropIndex +DROP INDEX "signatures_client_id_key"; + +-- DropIndex +DROP INDEX "time_entries_client_id_key"; + +-- DropIndex +DROP INDEX "voice_notes_client_id_key"; + +-- DropIndex +DROP INDEX "work_sessions_client_id_key"; + +-- CreateIndex +CREATE UNIQUE INDEX "activity_notes_tenant_id_client_id_key" ON "activity_notes"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "material_usages_tenant_id_client_id_key" ON "material_usages"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "photos_tenant_id_client_id_key" ON "photos"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "reports_tenant_id_client_id_key" ON "reports"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "signatures_tenant_id_client_id_key" ON "signatures"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "time_entries_tenant_id_client_id_key" ON "time_entries"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "voice_notes_tenant_id_client_id_key" ON "voice_notes"("tenant_id", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "work_sessions_tenant_id_client_id_key" ON "work_sessions"("tenant_id", "client_id"); diff --git a/prisma/migrations/20260915091000_betrieb_ai_token_limit/migration.sql b/prisma/migrations/20260915091000_betrieb_ai_token_limit/migration.sql new file mode 100644 index 0000000..552b81c --- /dev/null +++ b/prisma/migrations/20260915091000_betrieb_ai_token_limit/migration.sql @@ -0,0 +1,7 @@ +-- L10b Betrieb & Aufräumen (Aufräumpunkt k, Spec §31 Kostenlimit): +-- Optional monthly AI token budget (input + output tokens of AiGeneration) per tenant. +-- NULL = platform default from env AI_MONTHLY_TOKEN_LIMIT, 0 = unlimited. +-- tenant_settings is already tenant-bound (RLS, TENANT_MODELS) — no new table. + +-- AlterTable +ALTER TABLE "tenant_settings" ADD COLUMN "ai_monthly_token_limit" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a89235a..bc29a6c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -77,6 +77,8 @@ model TenantSettings { billingRecipients String[] @default([]) @map("billing_recipients") // Lotse (L9): "sie" | "du"; null = neutral without pronouns (Brandbook §9.2) lotseAddressForm String? @map("lotse_address_form") + // L10b (Spec §31): monthly AI token budget (input + output) per tenant; null = env AI_MONTHLY_TOKEN_LIMIT, 0 = unlimited + aiMonthlyTokenLimit Int? @map("ai_monthly_token_limit") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -856,7 +858,7 @@ model MaterialUsage { notes String? photoId String? @map("photo_id") recordedById String? @map("recorded_by_id") - clientId String? @unique @map("client_id") // offline local id + clientId String? @map("client_id") // offline local id createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -865,6 +867,7 @@ model MaterialUsage { workSession WorkSession? @relation(fields: [workSessionId], references: [id], onDelete: SetNull) @@index([tenantId, workOrderId]) + @@unique([tenantId, clientId]) @@map("material_usages") } @@ -890,7 +893,7 @@ model WorkSession { startLng Float? @map("start_lng") startedOffline Boolean @default(false) @map("started_offline") deviceInfo String? @map("device_info") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -901,6 +904,7 @@ model WorkSession { @@index([tenantId, workOrderId]) @@index([tenantId, userId, status]) + @@unique([tenantId, clientId]) @@map("work_sessions") } @@ -925,7 +929,7 @@ model TimeEntry { corrected Boolean @default(false) correctionReason String? @map("correction_reason") correctedById String? @map("corrected_by_id") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -933,6 +937,7 @@ model TimeEntry { @@index([tenantId, workSessionId]) @@index([tenantId, userId, startedAt]) + @@unique([tenantId, clientId]) @@map("time_entries") } @@ -956,7 +961,7 @@ model ActivityNote { kind ActivityNoteKind @default(general) text String voiceNoteId String? @unique @map("voice_note_id") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") @@ -965,6 +970,7 @@ model ActivityNote { voiceNote VoiceNote? @relation(fields: [voiceNoteId], references: [id], onDelete: SetNull) @@index([tenantId, workOrderId]) + @@unique([tenantId, clientId]) @@map("activity_notes") } @@ -1060,7 +1066,7 @@ model Photo { longitude Float? takenById String? @map("taken_by_id") includeInReport Boolean @default(true) @map("include_in_report") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade) @@ -1068,6 +1074,7 @@ model Photo { photoRequirement PhotoRequirement? @relation(fields: [photoRequirementId], references: [id], onDelete: SetNull) @@index([tenantId, workOrderId]) + @@unique([tenantId, clientId]) @@map("photos") } @@ -1090,7 +1097,7 @@ model VoiceNote { transcriptionModel String? @map("transcription_model") recordedById String? @map("recorded_by_id") recordedAt DateTime @map("recorded_at") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -1098,6 +1105,7 @@ model VoiceNote { activityNote ActivityNote? @@index([tenantId, workOrderId]) + @@unique([tenantId, clientId]) @@map("voice_notes") } @@ -1140,7 +1148,7 @@ model Report { approvedById String? @map("approved_by_id") approvedAt DateTime? @map("approved_at") rejectionReason String? @map("rejection_reason") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -1150,6 +1158,7 @@ model Report { @@unique([lineageId, version]) @@index([tenantId, workOrderId]) @@index([tenantId, status]) + @@unique([tenantId, clientId]) @@map("reports") } @@ -1173,12 +1182,13 @@ model Signature { reason String? // required for absent/refused/later signedAt DateTime @map("signed_at") capturedById String? @map("captured_by_id") - clientId String? @unique @map("client_id") + clientId String? @map("client_id") createdAt DateTime @default(now()) @map("created_at") report Report @relation(fields: [reportId], references: [id], onDelete: Cascade) @@index([tenantId]) + @@unique([tenantId, clientId]) @@map("signatures") }