Initial scaffold: unified customer/insurance/utilities platform
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.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@jorgecuadros/database",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "generated/client/index.js",
|
||||
"types": "generated/client/index.d.ts",
|
||||
"scripts": {
|
||||
"generate": "prisma generate",
|
||||
"migrate:dev": "prisma migrate dev",
|
||||
"migrate:deploy": "prisma migrate deploy",
|
||||
"studio": "prisma studio",
|
||||
"validate": "prisma validate",
|
||||
"format": "prisma format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user