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
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:
+538
@@ -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);
|
||||
+12
@@ -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 $$;
|
||||
+27
@@ -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');
|
||||
+39
@@ -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;
|
||||
+12
@@ -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;
|
||||
Reference in New Issue
Block a user