fix billing and 2fa admin
Build & Push / Pipeline Tests (push) Failing after 1m58s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 58s
Test / API Unit Tests (push) Successful in 1m9s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 45s
Test / API Integration Tests (push) Failing after 1m9s

This commit is contained in:
root
2026-08-10 22:35:55 -04:00
parent 10ca76fc1e
commit 5f06256271
73 changed files with 8803 additions and 570 deletions
@@ -0,0 +1,538 @@
-- 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);
@@ -0,0 +1,12 @@
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_enum e
JOIN pg_type t ON t.oid = e.enumtypid
WHERE t.typname = 'NotificationType'
AND e.enumlabel = 'MANUAL_PAYMENT_EVIDENCE_SUBMITTED'
) THEN
ALTER TYPE "NotificationType" ADD VALUE 'MANUAL_PAYMENT_EVIDENCE_SUBMITTED';
END IF;
END $$;
@@ -0,0 +1,27 @@
-- Keep only the newest active manual payment submission per invoice before
-- installing the invariant. Historical approved/rejected submissions remain.
WITH ranked_active_submissions AS (
SELECT
"id",
ROW_NUMBER() OVER (
PARTITION BY "invoiceId"
ORDER BY "createdAt" DESC, "id" DESC
) AS row_number
FROM "manual_payment_submissions"
WHERE "status" IN ('DRAFT', 'SUBMITTED', 'UNDER_REVIEW')
)
UPDATE "manual_payment_submissions" AS submission
SET
"status" = 'REJECTED',
"rejectionReason" = COALESCE(
submission."rejectionReason",
'Superseded by a newer active manual payment submission.'
),
"reviewedAt" = COALESCE(submission."reviewedAt", CURRENT_TIMESTAMP)
FROM ranked_active_submissions AS ranked
WHERE submission."id" = ranked."id"
AND ranked.row_number > 1;
CREATE UNIQUE INDEX "manual_payment_submissions_one_active_per_invoice"
ON "manual_payment_submissions" ("invoiceId")
WHERE "status" IN ('DRAFT', 'SUBMITTED', 'UNDER_REVIEW');
@@ -0,0 +1,39 @@
-- Keep a single primary billing account per company. Prefer the account with
-- the highest open balance, then the most invoices, then the newest account.
WITH account_scores AS (
SELECT
account."id",
account."companyId",
COALESCE(SUM(
CASE
WHEN invoice."status" IN ('OPEN', 'PAYMENT_PENDING', 'PAST_DUE', 'PARTIALLY_PAID')
THEN invoice."amountDue"
ELSE 0
END
), 0) AS open_balance,
COUNT(invoice."id") AS invoice_count,
account."createdAt"
FROM "billing_accounts" AS account
LEFT JOIN "billing_invoices" AS invoice
ON invoice."billingAccountId" = account."id"
WHERE account."isPrimary" = true
GROUP BY account."id", account."companyId", account."createdAt"
),
ranked_primary_accounts AS (
SELECT
"id",
ROW_NUMBER() OVER (
PARTITION BY "companyId"
ORDER BY open_balance DESC, invoice_count DESC, "createdAt" DESC, "id" DESC
) AS row_number
FROM account_scores
)
UPDATE "billing_accounts" AS account
SET "isPrimary" = false
FROM ranked_primary_accounts AS ranked
WHERE account."id" = ranked."id"
AND ranked.row_number > 1;
CREATE UNIQUE INDEX "billing_accounts_one_primary_per_company"
ON "billing_accounts" ("companyId")
WHERE "isPrimary" = true;
@@ -0,0 +1,12 @@
CREATE TABLE "platform_billing_settings" (
"id" TEXT NOT NULL DEFAULT 'default',
"taxRate" DOUBLE PRECISION NOT NULL DEFAULT 20,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedBy" TEXT,
CONSTRAINT "platform_billing_settings_pkey" PRIMARY KEY ("id")
);
INSERT INTO "platform_billing_settings" ("id", "taxRate", "updatedAt")
VALUES ('default', 20, NOW())
ON CONFLICT ("id") DO NOTHING;