L10b Betrieb & Aufräumen: Migrationen clientId je Mandant und KI-Token-Kontingent

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:19:19 +02:00
co-authored by Claude Opus 5
parent 49f05c8db3
commit 5f08df324f
3 changed files with 78 additions and 8 deletions
@@ -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");
@@ -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;
+18 -8
View File
@@ -77,6 +77,8 @@ model TenantSettings {
billingRecipients String[] @default([]) @map("billing_recipients") billingRecipients String[] @default([]) @map("billing_recipients")
// Lotse (L9): "sie" | "du"; null = neutral without pronouns (Brandbook §9.2) // Lotse (L9): "sie" | "du"; null = neutral without pronouns (Brandbook §9.2)
lotseAddressForm String? @map("lotse_address_form") 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") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -856,7 +858,7 @@ model MaterialUsage {
notes String? notes String?
photoId String? @map("photo_id") photoId String? @map("photo_id")
recordedById String? @map("recorded_by_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") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -865,6 +867,7 @@ model MaterialUsage {
workSession WorkSession? @relation(fields: [workSessionId], references: [id], onDelete: SetNull) workSession WorkSession? @relation(fields: [workSessionId], references: [id], onDelete: SetNull)
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@unique([tenantId, clientId])
@@map("material_usages") @@map("material_usages")
} }
@@ -890,7 +893,7 @@ model WorkSession {
startLng Float? @map("start_lng") startLng Float? @map("start_lng")
startedOffline Boolean @default(false) @map("started_offline") startedOffline Boolean @default(false) @map("started_offline")
deviceInfo String? @map("device_info") deviceInfo String? @map("device_info")
clientId String? @unique @map("client_id") clientId String? @map("client_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -901,6 +904,7 @@ model WorkSession {
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@index([tenantId, userId, status]) @@index([tenantId, userId, status])
@@unique([tenantId, clientId])
@@map("work_sessions") @@map("work_sessions")
} }
@@ -925,7 +929,7 @@ model TimeEntry {
corrected Boolean @default(false) corrected Boolean @default(false)
correctionReason String? @map("correction_reason") correctionReason String? @map("correction_reason")
correctedById String? @map("corrected_by_id") correctedById String? @map("corrected_by_id")
clientId String? @unique @map("client_id") clientId String? @map("client_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -933,6 +937,7 @@ model TimeEntry {
@@index([tenantId, workSessionId]) @@index([tenantId, workSessionId])
@@index([tenantId, userId, startedAt]) @@index([tenantId, userId, startedAt])
@@unique([tenantId, clientId])
@@map("time_entries") @@map("time_entries")
} }
@@ -956,7 +961,7 @@ model ActivityNote {
kind ActivityNoteKind @default(general) kind ActivityNoteKind @default(general)
text String text String
voiceNoteId String? @unique @map("voice_note_id") 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") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
@@ -965,6 +970,7 @@ model ActivityNote {
voiceNote VoiceNote? @relation(fields: [voiceNoteId], references: [id], onDelete: SetNull) voiceNote VoiceNote? @relation(fields: [voiceNoteId], references: [id], onDelete: SetNull)
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@unique([tenantId, clientId])
@@map("activity_notes") @@map("activity_notes")
} }
@@ -1060,7 +1066,7 @@ model Photo {
longitude Float? longitude Float?
takenById String? @map("taken_by_id") takenById String? @map("taken_by_id")
includeInReport Boolean @default(true) @map("include_in_report") 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") createdAt DateTime @default(now()) @map("created_at")
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade) workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
@@ -1068,6 +1074,7 @@ model Photo {
photoRequirement PhotoRequirement? @relation(fields: [photoRequirementId], references: [id], onDelete: SetNull) photoRequirement PhotoRequirement? @relation(fields: [photoRequirementId], references: [id], onDelete: SetNull)
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@unique([tenantId, clientId])
@@map("photos") @@map("photos")
} }
@@ -1090,7 +1097,7 @@ model VoiceNote {
transcriptionModel String? @map("transcription_model") transcriptionModel String? @map("transcription_model")
recordedById String? @map("recorded_by_id") recordedById String? @map("recorded_by_id")
recordedAt DateTime @map("recorded_at") recordedAt DateTime @map("recorded_at")
clientId String? @unique @map("client_id") clientId String? @map("client_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -1098,6 +1105,7 @@ model VoiceNote {
activityNote ActivityNote? activityNote ActivityNote?
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@unique([tenantId, clientId])
@@map("voice_notes") @@map("voice_notes")
} }
@@ -1140,7 +1148,7 @@ model Report {
approvedById String? @map("approved_by_id") approvedById String? @map("approved_by_id")
approvedAt DateTime? @map("approved_at") approvedAt DateTime? @map("approved_at")
rejectionReason String? @map("rejection_reason") rejectionReason String? @map("rejection_reason")
clientId String? @unique @map("client_id") clientId String? @map("client_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -1150,6 +1158,7 @@ model Report {
@@unique([lineageId, version]) @@unique([lineageId, version])
@@index([tenantId, workOrderId]) @@index([tenantId, workOrderId])
@@index([tenantId, status]) @@index([tenantId, status])
@@unique([tenantId, clientId])
@@map("reports") @@map("reports")
} }
@@ -1173,12 +1182,13 @@ model Signature {
reason String? // required for absent/refused/later reason String? // required for absent/refused/later
signedAt DateTime @map("signed_at") signedAt DateTime @map("signed_at")
capturedById String? @map("captured_by_id") capturedById String? @map("captured_by_id")
clientId String? @unique @map("client_id") clientId String? @map("client_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
report Report @relation(fields: [reportId], references: [id], onDelete: Cascade) report Report @relation(fields: [reportId], references: [id], onDelete: Cascade)
@@index([tenantId]) @@index([tenantId])
@@unique([tenantId, clientId])
@@map("signatures") @@map("signatures")
} }