feat(recibos): split a receipt, sin fondos per receipt, keep a batch open
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m57s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m22s

Three asks from the 2026-10-08 meeting, all on the OCR receipt queue.

- Split: one receipt paid by several customers (411 + 523 sharing water
  3014057) posts one charge per part. statement_document_splits holds free
  amounts that must sum to the receipt in cents, checked on save and again
  at confirm; "partes iguales" fills them in. Each part's row carries
  captureRef <docId>#<n>, so the double-post guard still holds.
- Sin fondos per receipt: StatementDocument.outstanding, toggled in review.
  At confirm a line is outstanding if its receipt or the whole check is.
- Open batch: POST /statements/batches/:id/files appends scans, numbered
  after the existing pages; a COMPLETED batch reopens. The confirm card
  prefills the check the batch was last posted against.

Verified on dev against a real batch: 1,892.00 split 946/946 across two
customers, a sibling receipt posted outstanding, second confirm refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rmancinasandClaude Opus 5.5 committed 2026-10-12 19:20:20 -07:00
1 parent f655fcbb82
commit fd5df240bc
9 files changed
+756 -49

No files matched your search

+33 -2
View File
@@ -130,7 +130,8 @@ model Customer {
transactions Transaction[]
statementDocuments StatementDocument[]
policyOcrDocuments PolicyOcrDocument[] @relation("PolicyOcrDocumentCustomer")
statementSplits StatementDocumentSplit[]
policyOcrDocuments PolicyOcrDocument[] @relation("PolicyOcrDocumentCustomer")
emailNotificationLogs EmailNotificationLog[]
accountStatusHistory AccountStatusHistory[]
@@ -730,6 +731,26 @@ model StatementBatch {
}
/// One statement — one customer, one period — after splitting the batch.
/// One customer's share of a split receipt — a water bill two neighbours
/// share, an HOA fee spread across a building. Free amounts, no stored
/// percentages: the office splits unevenly as often as not.
model StatementDocumentSplit {
id String @id @default(uuid())
documentId String
document StatementDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
/// 1-based; also the `#n` in the posted row's captureRef.
sequence Int
customerId String
customer Customer @relation(fields: [customerId], references: [id])
amount Decimal @db.Decimal(12, 2)
postedTransactionId String? @unique
postedTransaction Transaction? @relation("StatementDocumentSplitTransaction", fields: [postedTransactionId], references: [id])
@@unique([documentId, sequence])
@@map("statement_document_splits")
}
model StatementDocument {
id String @id @default(uuid())
batchId String
@@ -775,6 +796,15 @@ model StatementDocument {
postedTransactionId String? @unique
postedTransaction Transaction? @relation(fields: [postedTransactionId], references: [id])
/// Sin fondos for this receipt alone: posted as outstanding even when the
/// rest of the check is funded.
outstanding Boolean @default(false)
/// When set, the receipt is paid for by several customers and posts one
/// charge per part instead of one to `matchedCustomer`. The parts always sum
/// to `extractedAmount`.
splits StatementDocumentSplit[]
createdAt DateTime @default(now())
@@unique([batchId, pageNumber])
@@ -853,7 +883,8 @@ model Transaction {
/// Set only on OCR-posted rows — the statement page this came from.
statementDocument StatementDocument?
policyOcrDocument PolicyOcrDocument? @relation("PolicyOcrDocumentTransaction")
statementSplit StatementDocumentSplit? @relation("StatementDocumentSplitTransaction")
policyOcrDocument PolicyOcrDocument? @relation("PolicyOcrDocumentTransaction")
@@unique([legacySourceDb, legacySourceTable, legacyId])
@@index([customerId, transactionDate])