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;
+534 -186
View File
@@ -134,6 +134,81 @@ enum BillingPaymentAttemptStatus {
PARTIALLY_REFUNDED
}
enum SubscriptionCollectionMethod {
STRIPE
BANK_TRANSFER
CHECK
}
enum BillingPaymentChannel {
ONLINE
OFFLINE
}
enum ManualPaymentMethod {
BANK_TRANSFER
CHECK
}
enum ManualPaymentSubmissionStatus {
DRAFT
SUBMITTED
UNDER_REVIEW
APPROVED
REJECTED
}
enum ManualPaymentDocumentKind {
BANK_TRANSFER_RECEIPT
CHECK_COPY
OTHER_SUPPORTING_EVIDENCE
}
enum ManualPaymentDocumentScanStatus {
UPLOADED
SCANNING
CLEAN
QUARANTINED
SCAN_FAILED
}
enum CollectionsCaseStatus {
SCHEDULED
PRE_DUE
GRACE_PERIOD
RESOLVED
SUSPENDED
}
enum CollectionsCallTaskType {
PRE_EXPIRY_48H_CALL
PAYMENT_PROMISE_FOLLOW_UP
}
enum CollectionsCallTaskStatus {
OPEN
COMPLETED
CANCELLED
}
enum CollectionsCallOutcome {
CONTACTED
NO_ANSWER
PAYMENT_PROMISED
ISSUE_ESCALATED
}
enum CollectionsOverrideType {
PAYMENT_DISPUTE
MANUAL_EXTENSION
}
enum CollectionsOverrideStatus {
ACTIVE
REVOKED
EXPIRED
}
enum BillingRefundStatus {
PENDING
SUCCEEDED
@@ -441,6 +516,17 @@ enum NotificationType {
REFUND_PROCESSED
NEW_OFFER_FROM_SAVED_COMPANY
REVIEW_REQUEST
SUBSCRIPTION_PAYMENT_DUE_14D
SUBSCRIPTION_PAYMENT_DUE_7D
SUBSCRIPTION_PAYMENT_DUE_48H
SUBSCRIPTION_PAYMENT_DUE_24H
SUBSCRIPTION_GRACE_DAILY
SUBSCRIPTION_GRACE_FINAL
COLLECTIONS_CALL_REQUIRED
MANUAL_PAYMENT_EVIDENCE_SUBMITTED
SUBSCRIPTION_PAYMENT_CONFIRMED
MANUAL_PAYMENT_EVIDENCE_REJECTED
COLLECTIONS_OVERRIDE_CHANGED
}
enum FeedbackCategory {
@@ -492,6 +578,8 @@ enum NotificationStatus {
enum NotificationRecipientType {
EMPLOYEE
RENTER
BILLING_CONTACT
ADMIN
}
enum NotificationDeliveryStatus {
@@ -529,27 +617,32 @@ model Company {
status CompanyStatus @default(PENDING)
subscriptionPaymentRef String?
subscription Subscription?
billingAccounts BillingAccount[]
billingInvoices BillingInvoice[]
brand BrandSettings?
employees Employee[]
vehicles Vehicle[]
offers Offer[]
reservations Reservation[]
customers Customer[]
rentalPayments RentalPayment[]
subscriptionInvoices SubscriptionInvoice[]
contractSettings ContractSettings?
accountingSettings AccountingSettings?
insurancePolicies InsurancePolicy[]
pricingRules PricingRule[]
notifications Notification[] @relation("CompanyNotifications")
notificationEvents NotificationEvent[]
notificationDefaults CompanyNotificationPreference[]
complaints Complaint[]
companyMenuItems CompanyMenuItem[]
apiKeys CompanyApiKey[]
subscription Subscription?
billingAccounts BillingAccount[]
billingInvoices BillingInvoice[]
brand BrandSettings?
employees Employee[]
vehicles Vehicle[]
offers Offer[]
reservations Reservation[]
customers Customer[]
rentalPayments RentalPayment[]
subscriptionInvoices SubscriptionInvoice[]
contractSettings ContractSettings?
accountingSettings AccountingSettings?
insurancePolicies InsurancePolicy[]
pricingRules PricingRule[]
notifications Notification[] @relation("CompanyNotifications")
notificationEvents NotificationEvent[]
notificationDefaults CompanyNotificationPreference[]
billingContacts BillingContact[]
manualPaymentSubmissions ManualPaymentSubmission[]
manualPaymentDocuments ManualPaymentDocument[]
collectionsCases CollectionsCase[]
collectionsEvents CollectionsEvent[]
complaints Complaint[]
companyMenuItems CompanyMenuItem[]
apiKeys CompanyApiKey[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -597,6 +690,7 @@ model Subscription {
maxRetryCount Int @default(5)
invoices SubscriptionInvoice[]
billingInvoices BillingInvoice[]
collectionsCases CollectionsCase[]
events SubscriptionEvent[]
createdAt DateTime @default(now())
@@ -623,28 +717,28 @@ model SubscriptionEvent {
}
model SubscriptionInvoice {
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
subscriptionId String
subscription Subscription @relation(fields: [subscriptionId], references: [id])
requestedPlan Plan?
requestedBillingPeriod BillingPeriod?
providerInvoiceId String?
amount Int
currency String @default("MAD")
status InvoiceStatus
amanpayTransactionId String? @unique
paypalCaptureId String? @unique
stripeCheckoutSessionId String? @unique
paymentProvider PaymentProvider @default(AMANPAY)
billingInvoiceId String? @unique
billingInvoice BillingInvoice? @relation(fields: [billingInvoiceId], references: [id])
dueAt DateTime?
paidAt DateTime?
failedAt DateTime?
voidedAt DateTime?
attempts PaymentAttempt[]
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
subscriptionId String
subscription Subscription @relation(fields: [subscriptionId], references: [id])
requestedPlan Plan?
requestedBillingPeriod BillingPeriod?
providerInvoiceId String?
amount Int
currency String @default("MAD")
status InvoiceStatus
amanpayTransactionId String? @unique
paypalCaptureId String? @unique
stripeCheckoutSessionId String? @unique
paymentProvider PaymentProvider @default(AMANPAY)
billingInvoiceId String? @unique
billingInvoice BillingInvoice? @relation(fields: [billingInvoiceId], references: [id])
dueAt DateTime?
paidAt DateTime?
failedAt DateTime?
voidedAt DateTime?
attempts PaymentAttempt[]
createdAt DateTime @default(now())
@@ -653,35 +747,44 @@ model SubscriptionInvoice {
}
model BillingAccount {
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
isPrimary Boolean @default(true)
legalName String
billingEmail String
preferredLanguage String @default("en")
billingAddress Json?
taxId String?
taxExempt Boolean @default(false)
defaultCurrency String @default("MAD")
defaultPaymentMethodId String?
defaultPaymentMethod BillingPaymentMethod? @relation("BillingAccountDefaultPaymentMethod", fields: [defaultPaymentMethodId], references: [id])
invoiceTerms BillingInvoiceTerms @default(DUE_ON_RECEIPT)
netTermsDays Int @default(0)
providerCustomerId String?
dunningPaused Boolean @default(false)
dunningPausedAt DateTime?
dunningPausedBy String?
metadata Json @default("{}")
paymentMethods BillingPaymentMethod[] @relation("BillingAccountPaymentMethods")
invoices BillingInvoice[]
paymentIntents BillingPaymentIntent[]
paymentAttempts BillingPaymentAttempt[]
creditBalances BillingCreditBalance[]
creditLedgerEntries BillingCreditLedgerEntry[]
creditNotes BillingCreditNote[]
refunds BillingRefund[]
events BillingEvent[]
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
isPrimary Boolean @default(true)
legalName String
billingEmail String
preferredLanguage String @default("en")
timezone String @default("Africa/Casablanca")
reminderLocalTime String @default("09:00")
enabledCommunicationLocales String[] @default(["en"])
defaultCommunicationLocale String @default("en")
collectionsOwnerAdminId String?
collectionsOwnerAdmin AdminUser? @relation("BillingAccountCollectionsOwner", fields: [collectionsOwnerAdminId], references: [id], onDelete: SetNull)
billingAddress Json?
taxId String?
taxExempt Boolean @default(false)
defaultCurrency String @default("MAD")
defaultPaymentMethodId String?
defaultPaymentMethod BillingPaymentMethod? @relation("BillingAccountDefaultPaymentMethod", fields: [defaultPaymentMethodId], references: [id])
invoiceTerms BillingInvoiceTerms @default(DUE_ON_RECEIPT)
netTermsDays Int @default(0)
providerCustomerId String?
dunningPaused Boolean @default(false)
dunningPausedAt DateTime?
dunningPausedBy String?
metadata Json @default("{}")
paymentMethods BillingPaymentMethod[] @relation("BillingAccountPaymentMethods")
invoices BillingInvoice[]
paymentIntents BillingPaymentIntent[]
paymentAttempts BillingPaymentAttempt[]
creditBalances BillingCreditBalance[]
creditLedgerEntries BillingCreditLedgerEntry[]
creditNotes BillingCreditNote[]
refunds BillingRefund[]
events BillingEvent[]
billingContacts BillingContact[]
manualPaymentSubmissions ManualPaymentSubmission[]
collectionsCases CollectionsCase[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -691,6 +794,31 @@ model BillingAccount {
@@map("billing_accounts")
}
model BillingContact {
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
employeeId String?
employee Employee? @relation(fields: [employeeId], references: [id], onDelete: SetNull)
email String
locale String?
isPrimary Boolean @default(false)
receivePaymentNotices Boolean @default(true)
isActive Boolean @default(true)
verifiedAt DateTime?
notificationRecipients NotificationRecipient[]
callTasks CollectionsCallTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([billingAccountId, email])
@@index([companyId, isActive])
@@index([employeeId])
@@map("billing_contacts")
}
model BillingPaymentMethod {
id String @id @default(cuid())
billingAccountId String
@@ -717,26 +845,26 @@ model BillingPaymentMethod {
}
model BillingInvoice {
id String @id @default(cuid())
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
subscriptionId String?
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
legacySubscriptionInvoice SubscriptionInvoice?
invoiceNumber String? @unique
invoiceSequence Int? @unique
invoiceNumber String? @unique
invoiceSequence Int? @unique
invoiceType BillingInvoiceType
status BillingInvoiceStatus @default(DRAFT)
currency String @default("MAD")
subtotalAmount Int @default(0)
discountAmount Int @default(0)
creditAmount Int @default(0)
taxAmount Int @default(0)
totalAmount Int @default(0)
amountPaid Int @default(0)
amountDue Int @default(0)
status BillingInvoiceStatus @default(DRAFT)
currency String @default("MAD")
subtotalAmount Int @default(0)
discountAmount Int @default(0)
creditAmount Int @default(0)
taxAmount Int @default(0)
totalAmount Int @default(0)
amountPaid Int @default(0)
amountDue Int @default(0)
invoiceDate DateTime?
dueAt DateTime?
finalizedAt DateTime?
@@ -748,9 +876,14 @@ model BillingInvoice {
billingAddress Json?
providerInvoiceId String?
paymentProvider PaymentProvider?
isSubscriptionBlocking Boolean @default(false)
collectionMethod SubscriptionCollectionMethod @default(STRIPE)
requestedPlan Plan?
requestedBillingPeriod BillingPeriod?
checkoutIdempotencyKey String?
renewalKey String? @unique
isSubscriptionBlocking Boolean @default(false)
adminReason String?
metadata Json @default("{}")
metadata Json @default("{}")
createdByAdminId String?
lineItems BillingInvoiceLineItem[]
paymentIntents BillingPaymentIntent[]
@@ -759,14 +892,18 @@ model BillingInvoice {
creditNotes BillingCreditNote[]
refunds BillingRefund[]
events BillingEvent[]
manualPaymentSubmissions ManualPaymentSubmission[]
collectionsCase CollectionsCase?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
BillingCreditLedgerEntry BillingCreditLedgerEntry[]
@@unique([billingAccountId, checkoutIdempotencyKey])
@@index([billingAccountId])
@@index([companyId])
@@index([subscriptionId])
@@index([collectionMethod, status, dueAt])
@@map("billing_invoices")
}
@@ -816,32 +953,217 @@ model BillingPaymentIntent {
}
model BillingPaymentAttempt {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
paymentIntentId String?
paymentIntent BillingPaymentIntent? @relation(fields: [paymentIntentId], references: [id])
paymentMethodId String?
paymentMethod BillingPaymentMethod? @relation(fields: [paymentMethodId], references: [id])
providerPaymentId String?
status BillingPaymentAttemptStatus
amount Int
currency String @default("MAD")
failureCode String?
failureMessage String?
attemptedAt DateTime
metadata Json @default("{}")
refunds BillingRefund[]
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
paymentIntentId String?
paymentIntent BillingPaymentIntent? @relation(fields: [paymentIntentId], references: [id])
paymentMethodId String?
paymentMethod BillingPaymentMethod? @relation(fields: [paymentMethodId], references: [id])
providerPaymentId String?
channel BillingPaymentChannel @default(ONLINE)
manualMethod ManualPaymentMethod?
externalReference String?
normalizedExternalReference String?
receivedAt DateTime?
confirmedAt DateTime?
confirmedByAdminId String?
confirmedByAdmin AdminUser? @relation("ManualPaymentConfirmingAdmin", fields: [confirmedByAdminId], references: [id], onDelete: SetNull)
idempotencyKey String?
note String?
status BillingPaymentAttemptStatus
amount Int
currency String @default("MAD")
failureCode String?
failureMessage String?
attemptedAt DateTime
metadata Json @default("{}")
refunds BillingRefund[]
manualPaymentSubmission ManualPaymentSubmission?
resolvedCollectionsCase CollectionsCase? @relation("CollectionsResolutionPayment")
createdAt DateTime @default(now())
@@unique([billingAccountId, idempotencyKey])
@@unique([billingAccountId, manualMethod, normalizedExternalReference])
@@index([invoiceId])
@@index([billingAccountId])
@@map("billing_payment_attempts")
}
model ManualPaymentSubmission {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
method ManualPaymentMethod
submittedReference String
normalizedSubmittedReference String
status ManualPaymentSubmissionStatus @default(DRAFT)
submittedByEmployeeId String
submittedByEmployee Employee @relation(fields: [submittedByEmployeeId], references: [id])
submittedAt DateTime?
reviewedByAdminId String?
reviewedByAdmin AdminUser? @relation("ManualPaymentReviewingAdmin", fields: [reviewedByAdminId], references: [id], onDelete: SetNull)
reviewedAt DateTime?
rejectionReason String?
idempotencyKey String
paymentAttemptId String? @unique
paymentAttempt BillingPaymentAttempt? @relation(fields: [paymentAttemptId], references: [id], onDelete: SetNull)
documents ManualPaymentDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([billingAccountId, idempotencyKey])
@@index([invoiceId, status])
@@index([status, submittedAt])
@@map("manual_payment_submissions")
}
model ManualPaymentDocument {
id String @id @default(cuid())
submissionId String
submission ManualPaymentSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
invoiceId String
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
kind ManualPaymentDocumentKind
storageKey String @unique
originalFilename String
detectedMimeType String
detectedExtension String
byteSize Int
sha256 String
scanStatus ManualPaymentDocumentScanStatus @default(UPLOADED)
scannerResultCode String?
uploadedByEmployeeId String
uploadedByEmployee Employee @relation(fields: [uploadedByEmployeeId], references: [id])
uploadedAt DateTime @default(now())
deletedAt DateTime?
@@unique([submissionId, sha256])
@@index([invoiceId])
@@index([companyId])
@@index([scanStatus, uploadedAt])
@@map("manual_payment_documents")
}
model CollectionsCase {
id String @id @default(cuid())
invoiceId String @unique
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
subscriptionId String
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
status CollectionsCaseStatus @default(SCHEDULED)
originalExpirationAt DateTime
reminder14At DateTime
reminder7At DateTime
reminder48At DateTime
reminder24At DateTime
graceStartedAt DateTime?
finalSuspensionAt DateTime
resolvedAt DateTime?
suspendedAt DateTime?
nextActionAt DateTime?
collectionsOwnerAdminId String?
collectionsOwnerAdmin AdminUser? @relation("CollectionsCaseOwner", fields: [collectionsOwnerAdminId], references: [id], onDelete: SetNull)
resolutionPaymentAttemptId String? @unique
resolutionPaymentAttempt BillingPaymentAttempt? @relation("CollectionsResolutionPayment", fields: [resolutionPaymentAttemptId], references: [id], onDelete: SetNull)
processingLeaseUntil DateTime?
processingBy String?
version Int @default(0)
tasks CollectionsCallTask[]
overrides CollectionsOverride[]
events CollectionsEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([subscriptionId, originalExpirationAt])
@@index([status, nextActionAt])
@@map("collections_cases")
}
model CollectionsCallTask {
id String @id @default(cuid())
collectionsCaseId String
collectionsCase CollectionsCase @relation(fields: [collectionsCaseId], references: [id], onDelete: Cascade)
taskType CollectionsCallTaskType
assignedAdminId String
assignedAdmin AdminUser @relation("CollectionsTaskAssignee", fields: [assignedAdminId], references: [id])
billingContactId String?
billingContact BillingContact? @relation(fields: [billingContactId], references: [id], onDelete: SetNull)
dueAt DateTime
status CollectionsCallTaskStatus @default(OPEN)
outcome CollectionsCallOutcome?
note String?
promisedPaymentAt DateTime?
nextFollowUpAt DateTime?
completedByAdminId String?
completedByAdmin AdminUser? @relation("CollectionsTaskCompleter", fields: [completedByAdminId], references: [id], onDelete: SetNull)
completedAt DateTime?
cancellationReason String?
companyDefaultLocale String
contactLocale String
customerScript String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([collectionsCaseId, taskType])
@@index([assignedAdminId, status, dueAt])
@@map("collections_call_tasks")
}
model CollectionsOverride {
id String @id @default(cuid())
collectionsCaseId String
collectionsCase CollectionsCase @relation(fields: [collectionsCaseId], references: [id], onDelete: Cascade)
type CollectionsOverrideType
status CollectionsOverrideStatus @default(ACTIVE)
reason String
expiresAt DateTime
revisedSuspensionAt DateTime?
pauseSuspension Boolean @default(true)
pauseNotifications Boolean @default(false)
createdByAdminId String
createdByAdmin AdminUser @relation("CollectionsOverrideCreator", fields: [createdByAdminId], references: [id])
revokedByAdminId String?
revokedByAdmin AdminUser? @relation("CollectionsOverrideRevoker", fields: [revokedByAdminId], references: [id], onDelete: SetNull)
revokedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([collectionsCaseId, status, expiresAt])
@@map("collections_overrides")
}
model CollectionsEvent {
id String @id @default(cuid())
collectionsCaseId String
collectionsCase CollectionsCase @relation(fields: [collectionsCaseId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
eventType String
idempotencyKey String
scheduledFor DateTime?
occurredAt DateTime @default(now())
actorType String @default("system")
actorId String?
payload Json @default("{}")
@@unique([collectionsCaseId, idempotencyKey])
@@index([companyId, occurredAt])
@@map("collections_events")
}
model BillingCreditBalance {
id String @id @default(cuid())
billingAccountId String
@@ -1008,8 +1330,8 @@ model BrandSettings {
paypalEmail String?
paypalMerchantId String?
paymentMethodsEnabled PaymentProvider[]
isListedOnCarplace Boolean @default(true)
carplaceRating Float?
isListedOnCarplace Boolean @default(true)
carplaceRating Float?
homePageConfig Json? @map("home_page_config")
menuConfig Json? @map("menu_config")
@@ -1036,10 +1358,13 @@ model Employee {
preferredLanguage String @default("en")
isActive Boolean @default(true)
notifications Notification[] @relation("EmployeeNotifications")
notificationRecipients NotificationRecipient[]
notificationPreferences NotificationPreference[]
recordedRentalPayments RentalPayment[] @relation("RecordedRentalPayments")
notifications Notification[] @relation("EmployeeNotifications")
notificationRecipients NotificationRecipient[]
notificationPreferences NotificationPreference[]
recordedRentalPayments RentalPayment[] @relation("RecordedRentalPayments")
billingContacts BillingContact[]
manualPaymentSubmissions ManualPaymentSubmission[]
manualPaymentDocuments ManualPaymentDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1073,10 +1398,10 @@ model Vehicle {
allowDifferentDropoff Boolean @default(false)
dropoffLocations String[] @default([])
reservations Reservation[]
maintenance MaintenanceLog[]
offerVehicles OfferVehicle[]
calendarBlocks VehicleCalendarBlock[]
reservations Reservation[]
maintenance MaintenanceLog[]
offerVehicles OfferVehicle[]
calendarBlocks VehicleCalendarBlock[]
pricingConfiguration VehiclePricingConfiguration?
priceHistory VehiclePriceHistory[]
@@ -1116,11 +1441,11 @@ model VehiclePricingConfiguration {
}
model VehiclePricingRule {
id String @id @default(cuid())
id String @id @default(cuid())
configurationId String
configuration VehiclePricingConfiguration @relation(fields: [configurationId], references: [id], onDelete: Cascade)
name String
ruleType VehiclePricingRuleType @default(DATE_RANGE)
ruleType VehiclePricingRuleType @default(DATE_RANGE)
startDate DateTime
endDate DateTime
dailyRate Int?
@@ -1131,8 +1456,8 @@ model VehiclePricingRule {
maxDailyRate Int?
automaticAdjustmentPct Int?
priceAdjustment Int?
isActive Boolean @default(true)
sortOrder Int @default(0)
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1143,12 +1468,12 @@ model VehiclePricingRule {
}
model VehiclePriceHistory {
id String @id @default(cuid())
id String @id @default(cuid())
configurationId String
configuration VehiclePricingConfiguration @relation(fields: [configurationId], references: [id], onDelete: Cascade)
vehicleId String
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
source VehiclePriceChangeSource @default(CONFIG_UPDATE)
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
source VehiclePriceChangeSource @default(CONFIG_UPDATE)
changedByEmployeeId String?
previousDailyRate Int?
nextDailyRate Int?
@@ -1156,7 +1481,7 @@ model VehiclePriceHistory {
nextWeeklyRate Int?
note String?
effectiveFrom DateTime?
createdAt DateTime @default(now())
createdAt DateTime @default(now())
@@index([vehicleId, createdAt])
@@index([configurationId, createdAt])
@@ -1282,9 +1607,9 @@ model Customer {
licenseApprovedAt DateTime?
licenseApprovalNote String?
reservations Reservation[]
complaints Complaint[]
reviewOptOut Boolean @default(false)
reservations Reservation[]
complaints Complaint[]
reviewOptOut Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1309,7 +1634,7 @@ model Reservation {
promoCodeUsed String?
vehicleCategory VehicleCategory?
source BookingSource @default(DASHBOARD)
carplaceRef String?
carplaceRef String?
status ReservationStatus @default(DRAFT)
startDate DateTime
endDate DateTime
@@ -1342,11 +1667,11 @@ model Reservation {
reviewPaused Boolean @default(false)
insurances ReservationInsurance[]
insuranceTotal Int @default(0)
insuranceTotal Int @default(0)
additionalDrivers AdditionalDriver[]
additionalDriverTotal Int @default(0)
additionalDriverTotal Int @default(0)
pricingRulesApplied Json?
pricingRulesTotal Int @default(0)
pricingRulesTotal Int @default(0)
damageReports DamageReport[]
rentalPayments RentalPayment[]
inspections DamageInspection[]
@@ -1402,7 +1727,6 @@ model CarplaceFunnelEvent {
@@map("carplace_funnel_events")
}
model WebhookEvent {
id String @id @default(cuid())
provider String
@@ -1420,50 +1744,50 @@ model WebhookEvent {
}
model RentalPayment {
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
reservationId String
reservation Reservation @relation(fields: [reservationId], references: [id])
amount Int
currency String @default("MAD")
status PaymentStatus @default(PENDING)
type PaymentType @default(CHARGE)
paymentProvider PaymentProvider
amanpayTransactionId String? @unique
paypalCaptureId String? @unique
stripeCheckoutSessionId String? @unique
stripePaymentIntentId String? @unique
paymentMethod String?
reference String?
note String?
receivedAt DateTime?
recordedByEmployeeId String?
recordedByEmployee Employee? @relation("RecordedRentalPayments", fields: [recordedByEmployeeId], references: [id])
idempotencyKey String?
paidAt DateTime?
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
reservationId String
reservation Reservation @relation(fields: [reservationId], references: [id])
amount Int
currency String @default("MAD")
status PaymentStatus @default(PENDING)
type PaymentType @default(CHARGE)
paymentProvider PaymentProvider
amanpayTransactionId String? @unique
paypalCaptureId String? @unique
stripeCheckoutSessionId String? @unique
stripePaymentIntentId String? @unique
paymentMethod String?
reference String?
note String?
receivedAt DateTime?
recordedByEmployeeId String?
recordedByEmployee Employee? @relation("RecordedRentalPayments", fields: [recordedByEmployeeId], references: [id])
idempotencyKey String?
paidAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([companyId, idempotencyKey])
@@index([companyId])
@@index([reservationId])
@@unique([companyId, idempotencyKey])
@@map("rental_payments")
}
model Review {
id String @id @default(cuid())
reservationId String @unique
reservation Reservation @relation(fields: [reservationId], references: [id])
id String @id @default(cuid())
reservationId String @unique
reservation Reservation @relation(fields: [reservationId], references: [id])
renterId String?
renter Renter? @relation(fields: [renterId], references: [id])
renter Renter? @relation(fields: [renterId], references: [id])
companyId String
overallRating Int
vehicleRating Int?
serviceRating Int?
comment String?
isPublished Boolean @default(true)
isPublished Boolean @default(true)
companyReply String?
companyRepliedAt DateTime?
category FeedbackCategory?
@@ -1476,29 +1800,29 @@ model Review {
}
model Complaint {
id String @id @default(cuid())
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
reservationId String?
reservation Reservation? @relation(fields: [reservationId], references: [id])
reservation Reservation? @relation(fields: [reservationId], references: [id])
reviewId String?
review Review? @relation(fields: [reviewId], references: [id])
review Review? @relation(fields: [reviewId], references: [id])
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
customer Customer? @relation(fields: [customerId], references: [id])
severity ComplaintSeverity @default(LEVEL_1)
status ComplaintStatus @default(OPEN)
category FeedbackCategory
subject String
description String?
resolution String?
notes String?
assignedTo String?
resolvedAt DateTime?
resolvedBy String?
severity ComplaintSeverity @default(LEVEL_1)
status ComplaintStatus @default(OPEN)
category FeedbackCategory
subject String
description String?
resolution String?
notes String?
assignedTo String?
resolvedAt DateTime?
resolvedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([companyId])
@@index([reservationId])
@@ -1538,12 +1862,12 @@ model Notification {
}
model NotificationEvent {
id String @id @default(cuid())
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
type NotificationType
templateKey String?
locale String @default("en")
locale String @default("en")
title String
body String
data Json?
@@ -1552,7 +1876,7 @@ model NotificationEvent {
idempotencyKey String
recipients NotificationRecipient[]
outboxEntries NotificationOutbox[]
createdAt DateTime @default(now())
createdAt DateTime @default(now())
@@unique([companyId, idempotencyKey])
@@index([companyId, type, createdAt])
@@ -1568,6 +1892,10 @@ model NotificationRecipient {
employee Employee? @relation(fields: [employeeId], references: [id], onDelete: Cascade)
renterId String?
renter Renter? @relation(fields: [renterId], references: [id], onDelete: Cascade)
billingContactId String?
billingContact BillingContact? @relation(fields: [billingContactId], references: [id], onDelete: Cascade)
adminUserId String?
adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
readAt DateTime?
archivedAt DateTime?
deliveries NotificationDelivery[]
@@ -1575,6 +1903,8 @@ model NotificationRecipient {
@@index([employeeId, readAt, createdAt])
@@index([renterId, readAt, createdAt])
@@index([billingContactId, readAt, createdAt])
@@index([adminUserId, readAt, createdAt])
@@index([notificationEventId])
@@map("notification_recipients")
}
@@ -1953,9 +2283,19 @@ model AdminUser {
passwordResetToken String? @unique
passwordResetExpiresAt DateTime?
auditLogs AuditLog[]
permissions AdminPermission[]
recoveryCodes AdminRecoveryCode[]
auditLogs AuditLog[]
permissions AdminPermission[]
recoveryCodes AdminRecoveryCode[]
preferredLocale String @default("en")
ownedBillingAccounts BillingAccount[] @relation("BillingAccountCollectionsOwner")
confirmedManualPayments BillingPaymentAttempt[] @relation("ManualPaymentConfirmingAdmin")
reviewedManualSubmissions ManualPaymentSubmission[] @relation("ManualPaymentReviewingAdmin")
ownedCollectionsCases CollectionsCase[] @relation("CollectionsCaseOwner")
assignedCollectionsTasks CollectionsCallTask[] @relation("CollectionsTaskAssignee")
completedCollectionsTasks CollectionsCallTask[] @relation("CollectionsTaskCompleter")
createdCollectionsOverrides CollectionsOverride[] @relation("CollectionsOverrideCreator")
revokedCollectionsOverrides CollectionsOverride[] @relation("CollectionsOverrideRevoker")
notificationRecipients NotificationRecipient[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1974,7 +2314,6 @@ model AdminPermission {
@@map("admin_permissions")
}
model AdminRecoveryCode {
id String @id @default(cuid())
adminUserId String
@@ -2022,6 +2361,15 @@ model PricingConfig {
@@map("pricing_configs")
}
model PlatformBillingSettings {
id String @id @default("default")
taxRate Float @default(20)
updatedAt DateTime @updatedAt
updatedBy String?
@@map("platform_billing_settings")
}
model PlanFeature {
id String @id @default(cuid())
plan Plan
+13 -1
View File
@@ -25,10 +25,21 @@ export type NotificationType =
| 'REFUND_PROCESSED'
| 'NEW_OFFER_FROM_SAVED_COMPANY'
| 'REVIEW_REQUEST'
| 'SUBSCRIPTION_PAYMENT_DUE_14D'
| 'SUBSCRIPTION_PAYMENT_DUE_7D'
| 'SUBSCRIPTION_PAYMENT_DUE_48H'
| 'SUBSCRIPTION_PAYMENT_DUE_24H'
| 'SUBSCRIPTION_GRACE_DAILY'
| 'SUBSCRIPTION_GRACE_FINAL'
| 'COLLECTIONS_CALL_REQUIRED'
| 'MANUAL_PAYMENT_EVIDENCE_SUBMITTED'
| 'SUBSCRIPTION_PAYMENT_CONFIRMED'
| 'MANUAL_PAYMENT_EVIDENCE_REJECTED'
| 'COLLECTIONS_OVERRIDE_CHANGED'
export type NotificationChannel = 'EMAIL' | 'SMS' | 'WHATSAPP' | 'IN_APP' | 'PUSH'
export type NotificationDeliveryStatus = 'PENDING' | 'QUEUED' | 'SENT' | 'DELIVERED' | 'FAILED' | 'SKIPPED' | 'DEAD_LETTER'
export type NotificationRecipientType = 'EMPLOYEE' | 'RENTER'
export type NotificationRecipientType = 'EMPLOYEE' | 'RENTER' | 'BILLING_CONTACT' | 'ADMIN'
export interface Company {
id: string
@@ -61,6 +72,7 @@ export interface AdminUser {
isActive: boolean
totpEnabled?: boolean
totpSecret?: string | null
preferredLocale?: string
}
export interface InsurancePolicy {
+13 -1
View File
@@ -25,10 +25,21 @@ export type NotificationType =
| 'REFUND_PROCESSED'
| 'NEW_OFFER_FROM_SAVED_COMPANY'
| 'REVIEW_REQUEST'
| 'SUBSCRIPTION_PAYMENT_DUE_14D'
| 'SUBSCRIPTION_PAYMENT_DUE_7D'
| 'SUBSCRIPTION_PAYMENT_DUE_48H'
| 'SUBSCRIPTION_PAYMENT_DUE_24H'
| 'SUBSCRIPTION_GRACE_DAILY'
| 'SUBSCRIPTION_GRACE_FINAL'
| 'COLLECTIONS_CALL_REQUIRED'
| 'MANUAL_PAYMENT_EVIDENCE_SUBMITTED'
| 'SUBSCRIPTION_PAYMENT_CONFIRMED'
| 'MANUAL_PAYMENT_EVIDENCE_REJECTED'
| 'COLLECTIONS_OVERRIDE_CHANGED'
export type NotificationChannel = 'EMAIL' | 'SMS' | 'WHATSAPP' | 'IN_APP' | 'PUSH'
export type NotificationDeliveryStatus = 'PENDING' | 'QUEUED' | 'SENT' | 'DELIVERED' | 'FAILED' | 'SKIPPED' | 'DEAD_LETTER'
export type NotificationRecipientType = 'EMPLOYEE' | 'RENTER'
export type NotificationRecipientType = 'EMPLOYEE' | 'RENTER' | 'BILLING_CONTACT' | 'ADMIN'
export interface Company {
id: string
@@ -61,6 +72,7 @@ export interface AdminUser {
isActive: boolean
totpEnabled?: boolean
totpSecret?: string | null
preferredLocale?: string
}
export interface InsurancePolicy {