// Unified customer / insurance / utilities data model. // See C:\Users\ricar\.claude\plans\logical-yawning-tome.md for the migration // plan this schema implements (source: UTILITIES.accdb, SEGUROS 16_be.mdb, // SCOTHIA.mdb). Every model that originates from a legacy Access table // carries legacySource* provenance columns so migrated rows are traceable // back to their Access original and the ETL can be re-run idempotently. generator client { provider = "prisma-client-js" output = "../generated/client" } datasource db { provider = "mysql" url = env("DATABASE_URL") } enum Currency { USD MXN } enum TransactionDomain { UTILITY INSURANCE TRUST } enum ServiceKind { WATER ELECTRIC GAS CABLE PROPERTY_TAX FEDERAL_ZONE ALARM OTHER } /// Ordered access tier (rank): ADMIN > MANAGER > STAFF > VIEWER. VIEWER is the /// read-only role; STAFF and above can write. Enforced by the API's ability /// matrix (apps/api/src/auth/abilities.ts), not by the enum itself. enum UserRole { ADMIN MANAGER STAFF VIEWER } // --------------------------------------------------------------------------- // Identity — the actual point of the project: one customer record shared by // both business lines. // --------------------------------------------------------------------------- model Customer { id String @id @default(uuid()) name String // Which legacy table `name` actually came from. Null = DATGRAL.NOMBRE, the // normal case. Anything else means DATGRAL's name was blank and the name was // recovered from a secondary table (see migration/transform_customers.py), // so staff can tell a reconstructed name from an original one. nameSource String? // True when `name` is the "(SIN NOMBRE)" placeholder. Denormalized so lists // can sort nameless records last — ordering by `name` alone puts them first, // since "(" sorts before every letter. nameMissing Boolean @default(false) addressLine1 String? addressLine2 String? city String? state String? zipCode String? country String? phone String? mobile String? fax String? email String? notes String? @db.Text identificationType String? identificationNumber String? identificationExpiration DateTime? customerSince DateTime? status Boolean @default(true) minimumBalance Decimal? @db.Decimal(12, 2) feeAmount Decimal? @db.Decimal(12, 2) preferredCurrency Currency @default(USD) // Soft-delete marker. Distinct from `status` (a legacy business flag): a // non-null archivedAt hides the row from default lists while preserving it // and its legacy provenance. Never hard-delete migrated data. archivedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt legacyRefs CustomerLegacyRef[] properties Property[] policies Policy[] vehicles Vehicle[] transactions Transaction[] @@map("customers") } /// Generalizes the old app's customer_mapping table: one row per legacy /// record folded into this customer, from either source system. model CustomerLegacyRef { id String @id @default(uuid()) customerId String customer Customer @relation(fields: [customerId], references: [id]) sourceSystem String // "utilities" | "insurance" sourceTable String // e.g. "DATGRAL", "COBRO3" legacyId String // stringified legacy id (source columns are often DOUBLE) createdAt DateTime @default(now()) @@unique([sourceSystem, sourceTable, legacyId]) @@map("customer_legacy_refs") } // --------------------------------------------------------------------------- // Insurance domain // --------------------------------------------------------------------------- model InsuranceProvider { id String @id @default(uuid()) name String @unique policies Policy[] @@map("insurance_providers") } model PolicyType { id String @id @default(uuid()) name String @unique shortDescription String? policies Policy[] @@map("policy_types") } /// Consolidates INCENDIO/MULT/M EMPR/all auto-table variants/LICENCIAS into /// one table with a policyType discriminator, instead of one Access table /// per line of business. model Policy { id String @id @default(uuid()) policyNumber String customerId String customer Customer @relation(fields: [customerId], references: [id]) policyTypeId String? policyType PolicyType? @relation(fields: [policyTypeId], references: [id]) insuranceProviderId String? insuranceProvider InsuranceProvider? @relation(fields: [insuranceProviderId], references: [id]) agentName String? policyDate DateTime? policyFrom DateTime? policyTo DateTime? coveragePeriodDays Int? @default(365) netPremium Decimal? @db.Decimal(12, 2) policyFee Decimal? @db.Decimal(12, 2) brokerFee Decimal? @db.Decimal(12, 2) commission Decimal? @db.Decimal(12, 2) total Decimal? @db.Decimal(12, 2) currency Currency @default(MXN) observations String? @db.Text notes String? @db.Text coveragesJson Json? endorsement Boolean @default(false) liquidated Boolean @default(false) liquidationNumber String? liquidationDate DateTime? // Soft-delete marker (see Customer.archivedAt). Never hard-delete migrated // policy data; archiving hides it from default lists. archivedAt DateTime? legacySourceDb String? legacySourceTable String? legacyId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt installments PolicyPaymentInstallment[] vehicles Vehicle[] insuredDrivers InsuredDriver[] beneficiaries PolicyBeneficiary[] claims Claim[] documents PolicyDocument[] properties Property[] @@unique([legacySourceDb, legacySourceTable, legacyId]) @@index([policyNumber]) @@map("policies") } /// Unpivots the 4 hardcoded payment-installment columns found on every /// legacy policy table (1ER PAGO/FECHA PAGO/NO CHEQUE, ...2, ...3, ...4). model PolicyPaymentInstallment { id String @id @default(uuid()) policyId String policy Policy @relation(fields: [policyId], references: [id]) sequence Int amount Decimal? @db.Decimal(12, 2) currency Currency @default(MXN) dueDate DateTime? paidDate DateTime? checkNumber String? isCash Boolean @default(false) @@map("policy_payment_installments") } /// Unpivots MCA2's 3 hardcoded vehicle slots (and the single-vehicle auto /// tables) into one row per vehicle. model Vehicle { id String @id @default(uuid()) customerId String? customer Customer? @relation(fields: [customerId], references: [id]) policyId String? policy Policy? @relation(fields: [policyId], references: [id]) make String? model String? modelYear String? bodyType String? engineNumber String? licensePlate String? vinNumber String? stateCode String? notes String? @db.Text legacySourceTable String? legacyId String? @@map("vehicles") } /// Unpivots the repeated named-insured/license columns in MCA2/LICENCIAS. model InsuredDriver { id String @id @default(uuid()) policyId String policy Policy @relation(fields: [policyId], references: [id]) fullName String? birthDate DateTime? sex String? occupation String? licenseNumber String? licenseState String? @@map("insured_drivers") } /// From BENEF — already a clean child table in the source data. model PolicyBeneficiary { id String @id @default(uuid()) policyId String policy Policy @relation(fields: [policyId], references: [id]) name String? address String? phone String? email String? @@map("policy_beneficiaries") } /// From DATOS (siniestros). model Claim { id String @id @default(uuid()) policyId String policy Policy @relation(fields: [policyId], references: [id]) claimType String? incidentDate DateTime? reportedDate DateTime? description String? @db.Text adjusterId String? adjuster Adjuster? @relation(fields: [adjusterId], references: [id]) claimedAmount Decimal? @db.Decimal(12, 2) settledAmount Decimal? @db.Decimal(12, 2) settlementDate DateTime? checkNumber String? resolved Boolean @default(false) resolutionNotes String? @db.Text @@map("claims") } /// From AJUSTADORES / AJUSTADORESATLAS. model Adjuster { id String @id @default(uuid()) company String? city String? name String? phone String? beeper String? claims Claim[] @@map("adjusters") } /// Extracted LONGBINARY blobs from the policy tables — file lives in object /// storage, only the pointer + type lives here. model PolicyDocument { id String @id @default(uuid()) policyId String policy Policy @relation(fields: [policyId], references: [id]) documentType String storageKey String originalColumn String? createdAt DateTime @default(now()) @@map("policy_documents") } // --------------------------------------------------------------------------- // Utilities domain // --------------------------------------------------------------------------- /// From DATMEX — shared with the insurance domain so a property can carry /// both a home-insurance policy and utility service enrollments. model Property { id String @id @default(uuid()) customerId String customer Customer @relation(fields: [customerId], references: [id]) policyId String? policy Policy? @relation(fields: [policyId], references: [id]) addressLine1 String? addressLine2 String? phone1 String? phone2 String? phone3 String? zone String? // Soft-delete marker (see Customer.archivedAt). archivedAt DateTime? legacySourceTable String? legacyId String? createdAt DateTime @default(now()) services PropertyService[] documents ServiceDocument[] trustAccount TrustAccount? @@map("properties") } /// Unpivots DATMEX's inline service columns and PROFILE's enrollment flags /// into one row per enrolled service per property. model PropertyService { id String @id @default(uuid()) propertyId String property Property @relation(fields: [propertyId], references: [id]) kind ServiceKind accountNumber String? meterNumber String? route String? dueDay String? active Boolean @default(true) notes String? @db.Text @@map("property_services") } model ServiceDocument { id String @id @default(uuid()) propertyId String property Property @relation(fields: [propertyId], references: [id]) documentType String storageKey String createdAt DateTime @default(now()) @@map("service_documents") } /// From TRUSTVENCE. model TrustAccount { id String @id @default(uuid()) propertyId String @unique property Property @relation(fields: [propertyId], references: [id]) bankName String? trustNumber String? bankFee Decimal? @db.Decimal(12, 2) dueDate1 DateTime? dueDate2 DateTime? @@map("trust_accounts") } // --------------------------------------------------------------------------- // Shared financial ledger — one office, one set of books. // --------------------------------------------------------------------------- /// ES/EN transaction-type lookup, carried over from the old schema. model TypeTransaction { id String @id @default(uuid()) nameEn String nameEs String? isService Boolean @default(false) transactions Transaction[] @@map("type_transactions") } /// Unifies utilities' EFECTIVO/EFECTIVO FM3/EFECTIVO_BACKUP/FEE ANUAL/ /// datos2/fee15/billing/CHEQUE FM3/IVA 2015 and insurance's EFECTIVO. model Transaction { id String @id @default(uuid()) customerId String customer Customer @relation(fields: [customerId], references: [id]) domain TransactionDomain typeId String? type TypeTransaction? @relation(fields: [typeId], references: [id]) transactionDate DateTime period String? reference String? amount Decimal @db.Decimal(12, 2) currency Currency @default(MXN) exchangeRate Decimal? @db.Decimal(10, 4) checkNumber String? message String? @db.Text outstanding Boolean @default(false) legacySourceDb String? legacySourceTable String? legacyId String? createdAt DateTime @default(now()) @@index([customerId, transactionDate]) @@map("transactions") } /// From TIPO HIST. model ExchangeRate { id String @id @default(uuid()) rate Decimal @db.Decimal(10, 4) effectiveDate DateTime effectiveHour DateTime? @@map("exchange_rates") } // --------------------------------------------------------------------------- // Company bank register (SCOTHIA.mdb) — the office's own operating account, // deliberately separate from customer-facing Transaction records. // --------------------------------------------------------------------------- /// From TABLA RAMODOS ("ramo" = line of business). model BusinessLineCategory { id String @id @default(uuid()) name String @unique bankTransactions BankTransaction[] @@map("business_line_categories") } /// Unifies SCOTHIA's DATOS E (egresos) / DATOS I (ingresos) into one /// signed-amount table: income positive, expense negative. model BankTransaction { id String @id @default(uuid()) transactionDate DateTime transactionType String? reference String? concept String? amount Decimal @db.Decimal(12, 2) categoryId String? category BusinessLineCategory? @relation(fields: [categoryId], references: [id]) cleared Boolean @default(false) transferred Boolean @default(false) notes String? @db.Text amountInWords String? legacySourceTable String? legacyId String? @@map("bank_transactions") } // --------------------------------------------------------------------------- // Admin / shared // --------------------------------------------------------------------------- model User { id String @id @default(uuid()) name String email String @unique passwordHash String role UserRole @default(STAFF) active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt activityLogs ActivityLog[] @@map("users") } model ActivityLog { id String @id @default(uuid()) userId String? user User? @relation(fields: [userId], references: [id]) event String level String message Json? createdAt DateTime @default(now()) @@map("activity_logs") } model EmailTemplate { id String @id @default(uuid()) name String subject String templateSource String @db.Text @@map("email_templates") } model EmailCampaign { id String @id @default(uuid()) campaignName String subject String? body String? @db.Text status String @default("in_progress") emailSentCount Int @default(0) createdAt DateTime @default(now()) @@map("email_campaigns") } model EmailLog { id String @id @default(uuid()) customerId String? emailAddress String? emailType String? requestBody String? @db.Text responseBody String? @db.Text sentAt DateTime @default(now()) @@map("email_log") }