feat(polizas): OCR capture for insurance policy PDFs
Mirrors the utility statement intake on the insurance side: a policy_ocr batch/document pair of tables, a GMX parser, a matcher keyed on Policy.policyNumber, and a "Captura" screen under /polizas that proposes policy -> customer for staff to confirm. Lifts the OCR seam out of StatementsModule into its own OcrModule so PolicyOcrModule can inject OCR_PROVIDER without taking on the rest of the statement pipeline; StatementsModule now imports it and binds nothing itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `policy_ocr_batches` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`provider` VARCHAR(191) NOT NULL DEFAULT 'GMX',
|
||||
`status` ENUM('UPLOADED', 'PROCESSING', 'READY_FOR_REVIEW', 'COMPLETED', 'FAILED') NOT NULL DEFAULT 'UPLOADED',
|
||||
`uploadedById` VARCHAR(191) NOT NULL,
|
||||
`label` VARCHAR(191) NULL,
|
||||
`fileCount` INTEGER NOT NULL DEFAULT 0,
|
||||
`error` TEXT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`completedAt` DATETIME(3) NULL,
|
||||
|
||||
INDEX `policy_ocr_batches_status_createdAt_idx`(`status`, `createdAt`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `policy_ocr_documents` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`batchId` VARCHAR(191) NOT NULL,
|
||||
`pageNumber` INTEGER NOT NULL,
|
||||
`storageKey` VARCHAR(191) NOT NULL,
|
||||
`status` ENUM('PENDING_OCR', 'OCR_FAILED', 'NEEDS_REVIEW', 'MATCHED', 'CONFIRMED', 'POSTED', 'REJECTED') NOT NULL DEFAULT 'PENDING_OCR',
|
||||
`ocrRawText` TEXT NULL,
|
||||
`ocrConfidence` DECIMAL(4, 3) NULL,
|
||||
`provider` VARCHAR(191) NULL,
|
||||
`extractedPolicyNumber` VARCHAR(191) NULL,
|
||||
`extractedInsuredName` VARCHAR(191) NULL,
|
||||
`extractedAdditionalInsured` VARCHAR(191) NULL,
|
||||
`extractedAgentName` VARCHAR(191) NULL,
|
||||
`extractedLegalAddress` TEXT NULL,
|
||||
`extractedZip` VARCHAR(191) NULL,
|
||||
`extractedPolicyFrom` DATETIME(3) NULL,
|
||||
`extractedPolicyTo` DATETIME(3) NULL,
|
||||
`extractedPolicyDate` DATETIME(3) NULL,
|
||||
`extractedCurrency` VARCHAR(191) NULL,
|
||||
`extractedNetPremium` DECIMAL(12, 2) NULL,
|
||||
`extractedPolicyFee` DECIMAL(12, 2) NULL,
|
||||
`extractedBrokerFee` DECIMAL(12, 2) NULL,
|
||||
`extractedTotal` DECIMAL(12, 2) NULL,
|
||||
`extractedCoveragesJson` JSON NULL,
|
||||
`extractedPremiumPayment` VARCHAR(191) NULL,
|
||||
`matchedPolicyId` VARCHAR(191) NULL,
|
||||
`matchedCustomerId` VARCHAR(191) NULL,
|
||||
`matchCandidates` JSON NULL,
|
||||
`matchNote` VARCHAR(191) NULL,
|
||||
`reviewedById` VARCHAR(191) NULL,
|
||||
`reviewedAt` DATETIME(3) NULL,
|
||||
`createdPolicyId` VARCHAR(191) NULL,
|
||||
`postedTransactionId` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
UNIQUE INDEX `policy_ocr_documents_createdPolicyId_key`(`createdPolicyId`),
|
||||
UNIQUE INDEX `policy_ocr_documents_postedTransactionId_key`(`postedTransactionId`),
|
||||
INDEX `policy_ocr_documents_status_idx`(`status`),
|
||||
INDEX `policy_ocr_documents_matchedCustomerId_idx`(`matchedCustomerId`),
|
||||
UNIQUE INDEX `policy_ocr_documents_batchId_pageNumber_key`(`batchId`, `pageNumber`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_batches` ADD CONSTRAINT `policy_ocr_batches_uploadedById_fkey` FOREIGN KEY (`uploadedById`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_batchId_fkey` FOREIGN KEY (`batchId`) REFERENCES `policy_ocr_batches`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_matchedPolicyId_fkey` FOREIGN KEY (`matchedPolicyId`) REFERENCES `policies`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_matchedCustomerId_fkey` FOREIGN KEY (`matchedCustomerId`) REFERENCES `customers`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_reviewedById_fkey` FOREIGN KEY (`reviewedById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_createdPolicyId_fkey` FOREIGN KEY (`createdPolicyId`) REFERENCES `policies`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `policy_ocr_documents` ADD CONSTRAINT `policy_ocr_documents_postedTransactionId_fkey` FOREIGN KEY (`postedTransactionId`) REFERENCES `transactions`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -6,8 +6,8 @@
|
||||
// back to their Access original and the ETL can be re-run idempotently.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../generated/client"
|
||||
provider = "prisma-client-js"
|
||||
output = "../generated/client"
|
||||
// "native" covers local dev. The musl target is declared EXPLICITLY because
|
||||
// Prisma picks the engine by sniffing the build environment: the Docker build
|
||||
// stage has no openssl, so it detected plain "linux-musl", while the runtime
|
||||
@@ -78,42 +78,42 @@ enum UserRole {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Customer {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
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?
|
||||
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)
|
||||
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
|
||||
archivedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
legacyRefs CustomerLegacyRef[]
|
||||
properties Property[]
|
||||
@@ -122,6 +122,7 @@ model Customer {
|
||||
transactions Transaction[]
|
||||
|
||||
statementDocuments StatementDocument[]
|
||||
policyOcrDocuments PolicyOcrDocument[] @relation("PolicyOcrDocumentCustomer")
|
||||
|
||||
@@map("customers")
|
||||
}
|
||||
@@ -166,10 +167,10 @@ model PolicyType {
|
||||
/// one table with a policyType discriminator, instead of one Access table
|
||||
/// per line of business.
|
||||
model Policy {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
policyNumber String
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
policyTypeId String?
|
||||
policyType PolicyType? @relation(fields: [policyTypeId], references: [id])
|
||||
insuranceProviderId String?
|
||||
@@ -178,18 +179,18 @@ model Policy {
|
||||
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
|
||||
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)
|
||||
endorsement Boolean @default(false)
|
||||
liquidated Boolean @default(false)
|
||||
liquidationNumber String?
|
||||
liquidationDate DateTime?
|
||||
// Soft-delete marker (see Customer.archivedAt). Never hard-delete migrated
|
||||
@@ -198,8 +199,8 @@ model Policy {
|
||||
legacySourceDb String?
|
||||
legacySourceTable String?
|
||||
legacyId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
installments PolicyPaymentInstallment[]
|
||||
vehicles Vehicle[]
|
||||
@@ -210,6 +211,9 @@ model Policy {
|
||||
properties Property[]
|
||||
renewalNotices RenewalNotice[]
|
||||
|
||||
ocrMatchedDocuments PolicyOcrDocument[] @relation("PolicyOcrDocumentPolicy")
|
||||
ocrCreatedDocuments PolicyOcrDocument[] @relation("PolicyOcrDocumentCreatedPolicy")
|
||||
|
||||
@@unique([legacySourceDb, legacySourceTable, legacyId])
|
||||
@@index([policyNumber])
|
||||
@@map("policies")
|
||||
@@ -277,7 +281,7 @@ model Vehicle {
|
||||
licensePlate String?
|
||||
vinNumber String?
|
||||
stateCode String?
|
||||
notes String? @db.Text
|
||||
notes String? @db.Text
|
||||
// One legacy policy row can carry up to 3 vehicles, so they share a
|
||||
// legacyId (the source row number) — provenance is NOT unique per vehicle.
|
||||
// Sync rebuilds legacy vehicles by scoped delete + reinsert instead of upsert.
|
||||
@@ -304,9 +308,9 @@ model InsuredDriver {
|
||||
|
||||
/// From BENEF — already a clean child table in the source data.
|
||||
model PolicyBeneficiary {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
policyId String
|
||||
policy Policy @relation(fields: [policyId], references: [id])
|
||||
policy Policy @relation(fields: [policyId], references: [id])
|
||||
name String?
|
||||
address String?
|
||||
phone String?
|
||||
@@ -317,21 +321,21 @@ model PolicyBeneficiary {
|
||||
|
||||
/// 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
|
||||
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")
|
||||
}
|
||||
@@ -363,6 +367,115 @@ model PolicyDocument {
|
||||
@@map("policy_documents")
|
||||
}
|
||||
|
||||
/// Insurance OCR intake (mirrors statement_batches / statement_documents for
|
||||
/// the utility side). One upload session of policy PDFs from a provider
|
||||
/// portal (GMX, etc.) — the parser proposes policyNumber → existing Policy
|
||||
/// (or "new, pick customer"), staff confirms, and the system attaches the
|
||||
/// source PDF and optionally writes a premium Transaction.
|
||||
model PolicyOcrBatch {
|
||||
id String @id @default(uuid())
|
||||
/// Which insurance provider portal the batch came from. "GMX" today;
|
||||
/// future providers (AXA, GNP, …) extend the parser, not this table.
|
||||
provider String @default("GMX")
|
||||
status PolicyOcrBatchStatus @default(UPLOADED)
|
||||
uploadedById String
|
||||
uploadedBy User @relation("PolicyOcrBatchUploader", fields: [uploadedById], references: [id])
|
||||
label String?
|
||||
fileCount Int @default(0)
|
||||
/// Set when the pipeline fails as a whole (bad PDF, OCR binaries missing).
|
||||
error String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
|
||||
documents PolicyOcrDocument[]
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@map("policy_ocr_batches")
|
||||
}
|
||||
|
||||
enum PolicyOcrBatchStatus {
|
||||
UPLOADED
|
||||
PROCESSING
|
||||
READY_FOR_REVIEW
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
/// One parsed policy page — one Policy → one Customer (after staff confirms).
|
||||
model PolicyOcrDocument {
|
||||
id String @id @default(uuid())
|
||||
batchId String
|
||||
batch PolicyOcrBatch @relation(fields: [batchId], references: [id], onDelete: Cascade)
|
||||
pageNumber Int
|
||||
/// The rendered page image in object storage. Source PDF kept too on the
|
||||
/// batch (statement pattern) so re-running a corrected parser is possible.
|
||||
storageKey String
|
||||
status PolicyOcrDocumentStatus @default(PENDING_OCR)
|
||||
|
||||
ocrRawText String? @db.Text
|
||||
ocrConfidence Decimal? @db.Decimal(4, 3)
|
||||
/// Which parser claimed the page ("GMX" today).
|
||||
provider String?
|
||||
|
||||
// Extracted header fields, all staff-editable in review.
|
||||
extractedPolicyNumber String?
|
||||
extractedInsuredName String?
|
||||
extractedAdditionalInsured String?
|
||||
extractedAgentName String?
|
||||
extractedLegalAddress String? @db.Text
|
||||
extractedZip String?
|
||||
extractedPolicyFrom DateTime?
|
||||
extractedPolicyTo DateTime?
|
||||
extractedPolicyDate DateTime?
|
||||
extractedCurrency String?
|
||||
extractedNetPremium Decimal? @db.Decimal(12, 2)
|
||||
extractedPolicyFee Decimal? @db.Decimal(12, 2)
|
||||
extractedBrokerFee Decimal? @db.Decimal(12, 2)
|
||||
extractedTotal Decimal? @db.Decimal(12, 2)
|
||||
/// Per-coverage rows from the GMX "Material damages" / "Additional risk"
|
||||
/// tables — preserved verbatim so a missing premium receipt still leaves
|
||||
/// the coverages auditable.
|
||||
extractedCoveragesJson Json?
|
||||
extractedPremiumPayment String?
|
||||
|
||||
// Match by `Policy.policyNumber` → existing Policy / Customer.
|
||||
matchedPolicyId String?
|
||||
matchedPolicy Policy? @relation("PolicyOcrDocumentPolicy", fields: [matchedPolicyId], references: [id])
|
||||
matchedCustomerId String?
|
||||
matchedCustomer Customer? @relation("PolicyOcrDocumentCustomer", fields: [matchedCustomerId], references: [id])
|
||||
/// All policies carrying the same number, with their customer. One is
|
||||
/// normal; >1 means the policy number is shared across customers and a
|
||||
/// human must pick.
|
||||
matchCandidates Json?
|
||||
matchNote String?
|
||||
|
||||
reviewedById String?
|
||||
reviewedBy User? @relation("PolicyOcrDocumentReviewer", fields: [reviewedById], references: [id])
|
||||
reviewedAt DateTime?
|
||||
|
||||
createdPolicyId String? @unique
|
||||
createdPolicy Policy? @relation("PolicyOcrDocumentCreatedPolicy", fields: [createdPolicyId], references: [id])
|
||||
postedTransactionId String? @unique
|
||||
postedTransaction Transaction? @relation("PolicyOcrDocumentTransaction", fields: [postedTransactionId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([batchId, pageNumber])
|
||||
@@index([status])
|
||||
@@index([matchedCustomerId])
|
||||
@@map("policy_ocr_documents")
|
||||
}
|
||||
|
||||
enum PolicyOcrDocumentStatus {
|
||||
PENDING_OCR
|
||||
OCR_FAILED
|
||||
NEEDS_REVIEW
|
||||
MATCHED
|
||||
CONFIRMED
|
||||
POSTED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities domain
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -370,11 +483,11 @@ model PolicyDocument {
|
||||
/// 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())
|
||||
id String @id @default(uuid())
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
policyId String?
|
||||
policy Policy? @relation(fields: [policyId], references: [id])
|
||||
policy Policy? @relation(fields: [policyId], references: [id])
|
||||
addressLine1 String?
|
||||
addressLine2 String?
|
||||
phone1 String?
|
||||
@@ -393,15 +506,14 @@ model Property {
|
||||
archivedAt DateTime?
|
||||
legacySourceTable String?
|
||||
legacyId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
services PropertyService[]
|
||||
documents ServiceDocument[]
|
||||
trustAccount TrustAccount?
|
||||
|
||||
@@index([cadastralKey])
|
||||
|
||||
@@unique([legacySourceTable, legacyId])
|
||||
@@index([cadastralKey])
|
||||
@@map("properties")
|
||||
}
|
||||
|
||||
@@ -417,7 +529,7 @@ model PropertyService {
|
||||
route String?
|
||||
dueDay String?
|
||||
active Boolean @default(true)
|
||||
notes String? @db.Text
|
||||
notes String? @db.Text
|
||||
|
||||
statementDocuments StatementDocument[]
|
||||
|
||||
@@ -502,15 +614,15 @@ model StatementBatch {
|
||||
|
||||
/// One statement — one customer, one period — after splitting the batch.
|
||||
model StatementDocument {
|
||||
id String @id @default(uuid())
|
||||
batchId String
|
||||
batch StatementBatch @relation(fields: [batchId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(uuid())
|
||||
batchId String
|
||||
batch StatementBatch @relation(fields: [batchId], references: [id], onDelete: Cascade)
|
||||
/// 1-based page of the source PDF this was split from.
|
||||
pageNumber Int
|
||||
/// The rendered page image in object storage. The source PDF is kept too, so
|
||||
/// a reviewer can always see exactly what the parser read.
|
||||
storageKey String
|
||||
status StatementDocumentStatus @default(PENDING_OCR)
|
||||
status StatementDocumentStatus @default(PENDING_OCR)
|
||||
|
||||
/// Raw OCR text, kept even after a manual correction so a mismatch between
|
||||
/// what the machine read and what staff entered stays auditable.
|
||||
@@ -522,10 +634,10 @@ model StatementDocument {
|
||||
|
||||
// Extracted, then staff-corrected in place. `extractedAccountRef` is already
|
||||
// normalised for matching (CFE leading zeros stripped, Telnor LADA removed).
|
||||
extractedAccountRef String?
|
||||
extractedAmount Decimal? @db.Decimal(12, 2)
|
||||
extractedPeriod String?
|
||||
extractedDueDate DateTime?
|
||||
extractedAccountRef String?
|
||||
extractedAmount Decimal? @db.Decimal(12, 2)
|
||||
extractedPeriod String?
|
||||
extractedDueDate DateTime?
|
||||
/// Clave catastral when the statement prints one — a second key to match on
|
||||
/// when the account number is unreadable.
|
||||
extractedCadastralKey String?
|
||||
@@ -586,21 +698,21 @@ model TypeTransaction {
|
||||
/// 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())
|
||||
id String @id @default(uuid())
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
domain TransactionDomain
|
||||
typeId String?
|
||||
type TypeTransaction? @relation(fields: [typeId], references: [id])
|
||||
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)
|
||||
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)
|
||||
message String? @db.Text
|
||||
outstanding Boolean @default(false)
|
||||
/// How this row was captured. NULL = migrated from Access (the legacy*
|
||||
/// columns below say which table). Set explicitly on everything the app
|
||||
/// books, so an OCR-posted receipt is distinguishable from a hand-keyed one
|
||||
@@ -620,25 +732,26 @@ model Transaction {
|
||||
legacySourceDb String?
|
||||
legacySourceTable String?
|
||||
legacyId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
/// Set only on OCR-posted rows — the statement page this came from.
|
||||
statementDocument StatementDocument?
|
||||
policyOcrDocument PolicyOcrDocument? @relation("PolicyOcrDocumentTransaction")
|
||||
|
||||
@@unique([legacySourceDb, legacySourceTable, legacyId])
|
||||
@@index([customerId, transactionDate])
|
||||
// By-check reconciliation (billing.byCheck / the cheque-count report) looks
|
||||
// rows up by check number alone — the legacy EDITA CHEQUE COUNT lookup.
|
||||
@@index([checkNumber])
|
||||
// Drives the duplicate-post guard in BillingService.createBatch.
|
||||
@@index([captureRef])
|
||||
@@unique([legacySourceDb, legacySourceTable, legacyId])
|
||||
@@map("transactions")
|
||||
}
|
||||
|
||||
/// From TIPO HIST.
|
||||
model ExchangeRate {
|
||||
id String @id @default(uuid())
|
||||
rate Decimal @db.Decimal(10, 4)
|
||||
id String @id @default(uuid())
|
||||
rate Decimal @db.Decimal(10, 4)
|
||||
effectiveDate DateTime
|
||||
effectiveHour DateTime?
|
||||
|
||||
@@ -708,7 +821,7 @@ model BankTransaction {
|
||||
category BusinessLineCategory? @relation(fields: [categoryId], references: [id])
|
||||
cleared Boolean @default(false)
|
||||
transferred Boolean @default(false)
|
||||
notes String? @db.Text
|
||||
notes String? @db.Text
|
||||
amountInWords String?
|
||||
// Append + void (see Transaction.voidedAt): excluded from income/expense/net.
|
||||
voidedAt DateTime?
|
||||
@@ -745,6 +858,9 @@ model User {
|
||||
statementBatches StatementBatch[] @relation("StatementBatchUploader")
|
||||
statementsReviewed StatementDocument[] @relation("StatementDocumentReviewer")
|
||||
|
||||
policyOcrBatches PolicyOcrBatch[] @relation("PolicyOcrBatchUploader")
|
||||
policyOcrReviewed PolicyOcrDocument[] @relation("PolicyOcrDocumentReviewer")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
@@ -773,7 +889,7 @@ model EmailCampaign {
|
||||
id String @id @default(uuid())
|
||||
campaignName String
|
||||
subject String?
|
||||
body String? @db.Text
|
||||
body String? @db.Text
|
||||
status String @default("in_progress")
|
||||
emailSentCount Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -786,8 +902,8 @@ model EmailLog {
|
||||
customerId String?
|
||||
emailAddress String?
|
||||
emailType String?
|
||||
requestBody String? @db.Text
|
||||
responseBody String? @db.Text
|
||||
requestBody String? @db.Text
|
||||
responseBody String? @db.Text
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
@@map("email_log")
|
||||
@@ -813,16 +929,16 @@ enum OpsJobStatus {
|
||||
}
|
||||
|
||||
model OpsJob {
|
||||
id String @id @default(uuid())
|
||||
kind OpsJobKind
|
||||
status OpsJobStatus @default(RUNNING)
|
||||
id String @id @default(uuid())
|
||||
kind OpsJobKind
|
||||
status OpsJobStatus @default(RUNNING)
|
||||
// Combined stdout+stderr of the spawned process, appended as it runs.
|
||||
log String @db.LongText
|
||||
log String @db.LongText
|
||||
// Op-specific inputs (e.g. the backup filename a RESTORE targets). No FK on
|
||||
// createdById — the actor id is stored flat, like activity_logs' userId use.
|
||||
params Json?
|
||||
createdById String?
|
||||
startedAt DateTime @default(now())
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
|
||||
@@index([status])
|
||||
|
||||
Reference in New Issue
Block a user