Next.js + NestJS + Prisma (MySQL) monorepo replacing the legacy PHP internal app. Includes a session-based auth module with Argon2 password hashing and global input validation (replacing the old app's SQL injection and plaintext password comparison), the full target Prisma schema for customers/insurance/utilities/shared ledger/bank register, Docker Compose + Dockerfiles, and an Access-to-staging migration pipeline (migration/) already run against the real source databases. See PLAN.md and RESUME.md for the full architecture and session history.
505 lines
15 KiB
Plaintext
505 lines
15 KiB
Plaintext
// 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
|
|
}
|
|
|
|
enum UserRole {
|
|
ADMIN
|
|
STAFF
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Identity — the actual point of the project: one customer record shared by
|
|
// both business lines.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
model Customer {
|
|
id String @id @default(uuid())
|
|
name String
|
|
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)
|
|
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?
|
|
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?
|
|
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")
|
|
}
|