-- CreateEnum CREATE TYPE "SubscriptionCollectionMethod" AS ENUM ('STRIPE', 'BANK_TRANSFER', 'CHECK'); -- CreateEnum CREATE TYPE "BillingPaymentChannel" AS ENUM ('ONLINE', 'OFFLINE'); -- CreateEnum CREATE TYPE "ManualPaymentMethod" AS ENUM ('BANK_TRANSFER', 'CHECK'); -- CreateEnum CREATE TYPE "ManualPaymentSubmissionStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'REJECTED'); -- CreateEnum CREATE TYPE "ManualPaymentDocumentKind" AS ENUM ('BANK_TRANSFER_RECEIPT', 'CHECK_COPY', 'OTHER_SUPPORTING_EVIDENCE'); -- CreateEnum CREATE TYPE "ManualPaymentDocumentScanStatus" AS ENUM ('UPLOADED', 'SCANNING', 'CLEAN', 'QUARANTINED', 'SCAN_FAILED'); -- CreateEnum CREATE TYPE "CollectionsCaseStatus" AS ENUM ('SCHEDULED', 'PRE_DUE', 'GRACE_PERIOD', 'RESOLVED', 'SUSPENDED'); -- CreateEnum CREATE TYPE "CollectionsCallTaskType" AS ENUM ('PRE_EXPIRY_48H_CALL', 'PAYMENT_PROMISE_FOLLOW_UP'); -- CreateEnum CREATE TYPE "CollectionsCallTaskStatus" AS ENUM ('OPEN', 'COMPLETED', 'CANCELLED'); -- CreateEnum CREATE TYPE "CollectionsCallOutcome" AS ENUM ('CONTACTED', 'NO_ANSWER', 'PAYMENT_PROMISED', 'ISSUE_ESCALATED'); -- CreateEnum CREATE TYPE "CollectionsOverrideType" AS ENUM ('PAYMENT_DISPUTE', 'MANUAL_EXTENSION'); -- CreateEnum CREATE TYPE "CollectionsOverrideStatus" AS ENUM ('ACTIVE', 'REVOKED', 'EXPIRED'); -- AlterEnum -- This migration adds more than one value to an enum. -- With PostgreSQL versions 11 and earlier, this is not possible -- in a single migration. This can be worked around by creating -- multiple migrations, each migration adding only one value to -- the enum. ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_PAYMENT_DUE_14D'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_PAYMENT_DUE_7D'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_PAYMENT_DUE_48H'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_PAYMENT_DUE_24H'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_GRACE_DAILY'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_GRACE_FINAL'; ALTER TYPE "NotificationType" ADD VALUE 'COLLECTIONS_CALL_REQUIRED'; ALTER TYPE "NotificationType" ADD VALUE 'MANUAL_PAYMENT_EVIDENCE_SUBMITTED'; ALTER TYPE "NotificationType" ADD VALUE 'SUBSCRIPTION_PAYMENT_CONFIRMED'; ALTER TYPE "NotificationType" ADD VALUE 'MANUAL_PAYMENT_EVIDENCE_REJECTED'; ALTER TYPE "NotificationType" ADD VALUE 'COLLECTIONS_OVERRIDE_CHANGED'; -- AlterEnum -- This migration adds more than one value to an enum. -- With PostgreSQL versions 11 and earlier, this is not possible -- in a single migration. This can be worked around by creating -- multiple migrations, each migration adding only one value to -- the enum. ALTER TYPE "NotificationRecipientType" ADD VALUE 'BILLING_CONTACT'; ALTER TYPE "NotificationRecipientType" ADD VALUE 'ADMIN'; -- AlterTable ALTER TABLE "billing_accounts" ADD COLUMN "collectionsOwnerAdminId" TEXT, ADD COLUMN "defaultCommunicationLocale" TEXT NOT NULL DEFAULT 'en', ADD COLUMN "enabledCommunicationLocales" TEXT[] DEFAULT ARRAY['en']::TEXT[], ADD COLUMN "reminderLocalTime" TEXT NOT NULL DEFAULT '09:00', ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'Africa/Casablanca'; -- AlterTable ALTER TABLE "billing_invoices" ADD COLUMN "checkoutIdempotencyKey" TEXT, ADD COLUMN "collectionMethod" "SubscriptionCollectionMethod" NOT NULL DEFAULT 'STRIPE', ADD COLUMN "renewalKey" TEXT, ADD COLUMN "requestedBillingPeriod" "BillingPeriod", ADD COLUMN "requestedPlan" "Plan"; -- AlterTable ALTER TABLE "billing_payment_attempts" ADD COLUMN "channel" "BillingPaymentChannel" NOT NULL DEFAULT 'ONLINE', ADD COLUMN "confirmedAt" TIMESTAMP(3), ADD COLUMN "confirmedByAdminId" TEXT, ADD COLUMN "externalReference" TEXT, ADD COLUMN "idempotencyKey" TEXT, ADD COLUMN "manualMethod" "ManualPaymentMethod", ADD COLUMN "normalizedExternalReference" TEXT, ADD COLUMN "note" TEXT, ADD COLUMN "receivedAt" TIMESTAMP(3); -- AlterTable ALTER TABLE "notification_recipients" ADD COLUMN "adminUserId" TEXT, ADD COLUMN "billingContactId" TEXT; -- AlterTable ALTER TABLE "admin_users" ADD COLUMN "preferredLocale" TEXT NOT NULL DEFAULT 'en'; -- CreateTable CREATE TABLE "billing_contacts" ( "id" TEXT NOT NULL, "billingAccountId" TEXT NOT NULL, "companyId" TEXT NOT NULL, "employeeId" TEXT, "email" TEXT NOT NULL, "locale" TEXT, "isPrimary" BOOLEAN NOT NULL DEFAULT false, "receivePaymentNotices" BOOLEAN NOT NULL DEFAULT true, "isActive" BOOLEAN NOT NULL DEFAULT true, "verifiedAt" TIMESTAMP(3), "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "billing_contacts_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "manual_payment_submissions" ( "id" TEXT NOT NULL, "invoiceId" TEXT NOT NULL, "billingAccountId" TEXT NOT NULL, "companyId" TEXT NOT NULL, "method" "ManualPaymentMethod" NOT NULL, "submittedReference" TEXT NOT NULL, "normalizedSubmittedReference" TEXT NOT NULL, "status" "ManualPaymentSubmissionStatus" NOT NULL DEFAULT 'DRAFT', "submittedByEmployeeId" TEXT NOT NULL, "submittedAt" TIMESTAMP(3), "reviewedByAdminId" TEXT, "reviewedAt" TIMESTAMP(3), "rejectionReason" TEXT, "idempotencyKey" TEXT NOT NULL, "paymentAttemptId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "manual_payment_submissions_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "manual_payment_documents" ( "id" TEXT NOT NULL, "submissionId" TEXT NOT NULL, "invoiceId" TEXT NOT NULL, "companyId" TEXT NOT NULL, "kind" "ManualPaymentDocumentKind" NOT NULL, "storageKey" TEXT NOT NULL, "originalFilename" TEXT NOT NULL, "detectedMimeType" TEXT NOT NULL, "detectedExtension" TEXT NOT NULL, "byteSize" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "scanStatus" "ManualPaymentDocumentScanStatus" NOT NULL DEFAULT 'UPLOADED', "scannerResultCode" TEXT, "uploadedByEmployeeId" TEXT NOT NULL, "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "deletedAt" TIMESTAMP(3), CONSTRAINT "manual_payment_documents_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "collections_cases" ( "id" TEXT NOT NULL, "invoiceId" TEXT NOT NULL, "subscriptionId" TEXT NOT NULL, "billingAccountId" TEXT NOT NULL, "companyId" TEXT NOT NULL, "status" "CollectionsCaseStatus" NOT NULL DEFAULT 'SCHEDULED', "originalExpirationAt" TIMESTAMP(3) NOT NULL, "reminder14At" TIMESTAMP(3) NOT NULL, "reminder7At" TIMESTAMP(3) NOT NULL, "reminder48At" TIMESTAMP(3) NOT NULL, "reminder24At" TIMESTAMP(3) NOT NULL, "graceStartedAt" TIMESTAMP(3), "finalSuspensionAt" TIMESTAMP(3) NOT NULL, "resolvedAt" TIMESTAMP(3), "suspendedAt" TIMESTAMP(3), "nextActionAt" TIMESTAMP(3), "collectionsOwnerAdminId" TEXT, "resolutionPaymentAttemptId" TEXT, "processingLeaseUntil" TIMESTAMP(3), "processingBy" TEXT, "version" INTEGER NOT NULL DEFAULT 0, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "collections_cases_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "collections_call_tasks" ( "id" TEXT NOT NULL, "collectionsCaseId" TEXT NOT NULL, "taskType" "CollectionsCallTaskType" NOT NULL, "assignedAdminId" TEXT NOT NULL, "billingContactId" TEXT, "dueAt" TIMESTAMP(3) NOT NULL, "status" "CollectionsCallTaskStatus" NOT NULL DEFAULT 'OPEN', "outcome" "CollectionsCallOutcome", "note" TEXT, "promisedPaymentAt" TIMESTAMP(3), "nextFollowUpAt" TIMESTAMP(3), "completedByAdminId" TEXT, "completedAt" TIMESTAMP(3), "cancellationReason" TEXT, "companyDefaultLocale" TEXT NOT NULL, "contactLocale" TEXT NOT NULL, "customerScript" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "collections_call_tasks_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "collections_overrides" ( "id" TEXT NOT NULL, "collectionsCaseId" TEXT NOT NULL, "type" "CollectionsOverrideType" NOT NULL, "status" "CollectionsOverrideStatus" NOT NULL DEFAULT 'ACTIVE', "reason" TEXT NOT NULL, "expiresAt" TIMESTAMP(3) NOT NULL, "revisedSuspensionAt" TIMESTAMP(3), "pauseSuspension" BOOLEAN NOT NULL DEFAULT true, "pauseNotifications" BOOLEAN NOT NULL DEFAULT false, "createdByAdminId" TEXT NOT NULL, "revokedByAdminId" TEXT, "revokedAt" TIMESTAMP(3), "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "collections_overrides_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "collections_events" ( "id" TEXT NOT NULL, "collectionsCaseId" TEXT NOT NULL, "companyId" TEXT NOT NULL, "eventType" TEXT NOT NULL, "idempotencyKey" TEXT NOT NULL, "scheduledFor" TIMESTAMP(3), "occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "actorType" TEXT NOT NULL DEFAULT 'system', "actorId" TEXT, "payload" JSONB NOT NULL DEFAULT '{}', CONSTRAINT "collections_events_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE INDEX "billing_contacts_companyId_isActive_idx" ON "billing_contacts"("companyId", "isActive"); -- CreateIndex CREATE INDEX "billing_contacts_employeeId_idx" ON "billing_contacts"("employeeId"); -- CreateIndex CREATE UNIQUE INDEX "billing_contacts_billingAccountId_email_key" ON "billing_contacts"("billingAccountId", "email"); -- CreateIndex CREATE UNIQUE INDEX "manual_payment_submissions_paymentAttemptId_key" ON "manual_payment_submissions"("paymentAttemptId"); -- CreateIndex CREATE INDEX "manual_payment_submissions_invoiceId_status_idx" ON "manual_payment_submissions"("invoiceId", "status"); -- CreateIndex CREATE INDEX "manual_payment_submissions_status_submittedAt_idx" ON "manual_payment_submissions"("status", "submittedAt"); -- CreateIndex CREATE UNIQUE INDEX "manual_payment_submissions_billingAccountId_idempotencyKey_key" ON "manual_payment_submissions"("billingAccountId", "idempotencyKey"); -- CreateIndex CREATE UNIQUE INDEX "manual_payment_documents_storageKey_key" ON "manual_payment_documents"("storageKey"); -- CreateIndex CREATE INDEX "manual_payment_documents_invoiceId_idx" ON "manual_payment_documents"("invoiceId"); -- CreateIndex CREATE INDEX "manual_payment_documents_companyId_idx" ON "manual_payment_documents"("companyId"); -- CreateIndex CREATE INDEX "manual_payment_documents_scanStatus_uploadedAt_idx" ON "manual_payment_documents"("scanStatus", "uploadedAt"); -- CreateIndex CREATE UNIQUE INDEX "manual_payment_documents_submissionId_sha256_key" ON "manual_payment_documents"("submissionId", "sha256"); -- CreateIndex CREATE UNIQUE INDEX "collections_cases_invoiceId_key" ON "collections_cases"("invoiceId"); -- CreateIndex CREATE UNIQUE INDEX "collections_cases_resolutionPaymentAttemptId_key" ON "collections_cases"("resolutionPaymentAttemptId"); -- CreateIndex CREATE INDEX "collections_cases_status_nextActionAt_idx" ON "collections_cases"("status", "nextActionAt"); -- CreateIndex CREATE UNIQUE INDEX "collections_cases_subscriptionId_originalExpirationAt_key" ON "collections_cases"("subscriptionId", "originalExpirationAt"); -- CreateIndex CREATE INDEX "collections_call_tasks_assignedAdminId_status_dueAt_idx" ON "collections_call_tasks"("assignedAdminId", "status", "dueAt"); -- CreateIndex CREATE UNIQUE INDEX "collections_call_tasks_collectionsCaseId_taskType_key" ON "collections_call_tasks"("collectionsCaseId", "taskType"); -- CreateIndex CREATE INDEX "collections_overrides_collectionsCaseId_status_expiresAt_idx" ON "collections_overrides"("collectionsCaseId", "status", "expiresAt"); -- CreateIndex CREATE INDEX "collections_events_companyId_occurredAt_idx" ON "collections_events"("companyId", "occurredAt"); -- CreateIndex CREATE UNIQUE INDEX "collections_events_collectionsCaseId_idempotencyKey_key" ON "collections_events"("collectionsCaseId", "idempotencyKey"); -- CreateIndex CREATE UNIQUE INDEX "billing_invoices_renewalKey_key" ON "billing_invoices"("renewalKey"); -- CreateIndex CREATE INDEX "billing_invoices_collectionMethod_status_dueAt_idx" ON "billing_invoices"("collectionMethod", "status", "dueAt"); -- CreateIndex CREATE UNIQUE INDEX "billing_invoices_billingAccountId_checkoutIdempotencyKey_key" ON "billing_invoices"("billingAccountId", "checkoutIdempotencyKey"); -- CreateIndex CREATE UNIQUE INDEX "billing_payment_attempts_billingAccountId_idempotencyKey_key" ON "billing_payment_attempts"("billingAccountId", "idempotencyKey"); -- CreateIndex CREATE UNIQUE INDEX "billing_payment_attempts_billingAccountId_manualMethod_norm_key" ON "billing_payment_attempts"("billingAccountId", "manualMethod", "normalizedExternalReference"); -- CreateIndex CREATE INDEX "notification_recipients_billingContactId_readAt_createdAt_idx" ON "notification_recipients"("billingContactId", "readAt", "createdAt"); -- CreateIndex CREATE INDEX "notification_recipients_adminUserId_readAt_createdAt_idx" ON "notification_recipients"("adminUserId", "readAt", "createdAt"); -- AddForeignKey ALTER TABLE "billing_accounts" ADD CONSTRAINT "billing_accounts_collectionsOwnerAdminId_fkey" FOREIGN KEY ("collectionsOwnerAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "billing_contacts" ADD CONSTRAINT "billing_contacts_billingAccountId_fkey" FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "billing_contacts" ADD CONSTRAINT "billing_contacts_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "billing_contacts" ADD CONSTRAINT "billing_contacts_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "billing_payment_attempts" ADD CONSTRAINT "billing_payment_attempts_confirmedByAdminId_fkey" FOREIGN KEY ("confirmedByAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_billingAccountId_fkey" FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_submittedByEmployeeId_fkey" FOREIGN KEY ("submittedByEmployeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_reviewedByAdminId_fkey" FOREIGN KEY ("reviewedByAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_paymentAttemptId_fkey" FOREIGN KEY ("paymentAttemptId") REFERENCES "billing_payment_attempts"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_documents" ADD CONSTRAINT "manual_payment_documents_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "manual_payment_submissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_documents" ADD CONSTRAINT "manual_payment_documents_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "manual_payment_documents" ADD CONSTRAINT "manual_payment_documents_uploadedByEmployeeId_fkey" FOREIGN KEY ("uploadedByEmployeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_billingAccountId_fkey" FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_collectionsOwnerAdminId_fkey" FOREIGN KEY ("collectionsOwnerAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_cases" ADD CONSTRAINT "collections_cases_resolutionPaymentAttemptId_fkey" FOREIGN KEY ("resolutionPaymentAttemptId") REFERENCES "billing_payment_attempts"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_call_tasks" ADD CONSTRAINT "collections_call_tasks_collectionsCaseId_fkey" FOREIGN KEY ("collectionsCaseId") REFERENCES "collections_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_call_tasks" ADD CONSTRAINT "collections_call_tasks_assignedAdminId_fkey" FOREIGN KEY ("assignedAdminId") REFERENCES "admin_users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_call_tasks" ADD CONSTRAINT "collections_call_tasks_billingContactId_fkey" FOREIGN KEY ("billingContactId") REFERENCES "billing_contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_call_tasks" ADD CONSTRAINT "collections_call_tasks_completedByAdminId_fkey" FOREIGN KEY ("completedByAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_overrides" ADD CONSTRAINT "collections_overrides_collectionsCaseId_fkey" FOREIGN KEY ("collectionsCaseId") REFERENCES "collections_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_overrides" ADD CONSTRAINT "collections_overrides_createdByAdminId_fkey" FOREIGN KEY ("createdByAdminId") REFERENCES "admin_users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_overrides" ADD CONSTRAINT "collections_overrides_revokedByAdminId_fkey" FOREIGN KEY ("revokedByAdminId") REFERENCES "admin_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_events" ADD CONSTRAINT "collections_events_collectionsCaseId_fkey" FOREIGN KEY ("collectionsCaseId") REFERENCES "collections_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "collections_events" ADD CONSTRAINT "collections_events_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "notification_recipients" ADD CONSTRAINT "notification_recipients_billingContactId_fkey" FOREIGN KEY ("billingContactId") REFERENCES "billing_contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "notification_recipients" ADD CONSTRAINT "notification_recipients_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "admin_users"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- Backfill communication policy from existing supported brand language and -- create an explicit primary owner contact. Accounts remain visibly flagged -- for owner review because the legacy schema had no verified IANA timezone. UPDATE "billing_accounts" AS account SET "defaultCommunicationLocale" = CASE WHEN brand."defaultLocale" IN ('ar', 'en', 'fr') THEN brand."defaultLocale" ELSE 'en' END, "enabledCommunicationLocales" = ARRAY[CASE WHEN brand."defaultLocale" IN ('ar', 'en', 'fr') THEN brand."defaultLocale" ELSE 'en' END]::TEXT[], "metadata" = COALESCE(account."metadata", '{}'::jsonb) || '{"communicationSettingsRequireOwnerReview":true}'::jsonb FROM "brand_settings" AS brand WHERE brand."companyId" = account."companyId"; INSERT INTO "billing_contacts" ( "id", "billingAccountId", "companyId", "employeeId", "email", "locale", "isPrimary", "receivePaymentNotices", "isActive", "verifiedAt", "createdAt", "updatedAt" ) SELECT 'bc_' || md5(random()::text || clock_timestamp()::text || account."id"), account."id", account."companyId", owner."id", owner."email", CASE WHEN owner."preferredLanguage" IN ('ar', 'en', 'fr') THEN owner."preferredLanguage" ELSE account."defaultCommunicationLocale" END, true, true, true, COALESCE(owner."emailVerified", CURRENT_TIMESTAMP), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM "billing_accounts" AS account JOIN LATERAL ( SELECT employee.* FROM "employees" AS employee WHERE employee."companyId" = account."companyId" AND employee."role" = 'OWNER' AND employee."isActive" = true ORDER BY employee."createdAt" ASC LIMIT 1 ) AS owner ON true WHERE NOT EXISTS ( SELECT 1 FROM "billing_contacts" AS contact WHERE contact."billingAccountId" = account."id" ); UPDATE "billing_accounts" AS account SET "collectionsOwnerAdminId" = admin."id" FROM LATERAL ( SELECT "id" FROM "admin_users" WHERE "isActive" = true AND "role" IN ('FINANCE', 'ADMIN', 'SUPER_ADMIN') ORDER BY "createdAt" ASC LIMIT 1 ) AS admin WHERE account."collectionsOwnerAdminId" IS NULL; -- Database invariants that Prisma cannot express directly. ALTER TABLE "billing_accounts" ALTER COLUMN "enabledCommunicationLocales" SET NOT NULL, ALTER COLUMN "defaultCommunicationLocale" SET NOT NULL, ALTER COLUMN "timezone" SET NOT NULL, ALTER COLUMN "reminderLocalTime" SET NOT NULL; ALTER TABLE "billing_accounts" ADD CONSTRAINT "billing_accounts_supported_locales_check" CHECK ( cardinality("enabledCommunicationLocales") BETWEEN 1 AND 3 AND "enabledCommunicationLocales" <@ ARRAY['ar', 'en', 'fr']::TEXT[] AND "defaultCommunicationLocale" = ANY("enabledCommunicationLocales") ), ADD CONSTRAINT "billing_accounts_reminder_time_check" CHECK ("reminderLocalTime" ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'); ALTER TABLE "billing_contacts" ADD CONSTRAINT "billing_contacts_supported_locale_check" CHECK ("locale" IS NULL OR "locale" IN ('ar', 'en', 'fr')); ALTER TABLE "admin_users" ADD CONSTRAINT "admin_users_supported_locale_check" CHECK ("preferredLocale" IN ('ar', 'en', 'fr')); ALTER TABLE "billing_payment_attempts" ADD CONSTRAINT "billing_payment_attempts_offline_confirmation_check" CHECK ( "channel" <> 'OFFLINE' OR "status" <> 'SUCCEEDED' OR ( "manualMethod" IS NOT NULL AND length(trim(COALESCE("externalReference", ''))) >= 3 AND length(trim(COALESCE("normalizedExternalReference", ''))) >= 3 AND "receivedAt" IS NOT NULL AND "confirmedAt" IS NOT NULL AND "confirmedByAdminId" IS NOT NULL AND "idempotencyKey" IS NOT NULL ) ); ALTER TABLE "manual_payment_documents" ADD CONSTRAINT "manual_payment_documents_size_check" CHECK ("byteSize" > 0 AND "byteSize" <= 10485760), ADD CONSTRAINT "manual_payment_documents_mime_check" CHECK ("detectedMimeType" IN ('application/pdf', 'image/jpeg', 'image/png')); ALTER TABLE "manual_payment_submissions" ADD CONSTRAINT "manual_payment_submissions_reference_check" CHECK (length(trim("submittedReference")) >= 3 AND length(trim("normalizedSubmittedReference")) >= 3); ALTER TABLE "collections_overrides" ADD CONSTRAINT "collections_overrides_finite_window_check" CHECK ("expiresAt" > "createdAt"); ALTER TABLE "notification_recipients" ADD CONSTRAINT "notification_recipients_exact_actor_check" CHECK (num_nonnulls("employeeId", "renterId", "billingContactId", "adminUserId") = 1);