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") }