fix(audit): the corte audit computed a book the app no longer shows
The script carried its own copy of the balance rules, and since 1.0.26 that copy was stale: it reported 631,078.98 MXN where the application reports -416,403.42. An audit that disagrees with production is worse than no audit, because its numbers look authoritative and diff cleanly against yesterday's run. It now mirrors NOT_CASH_JOURNAL and archiveIsHistorySql, keeps the floor-only figure as a labelled line so older runs still diff, and says plainly which line is the app's. Section B stops being an open defect list. EFECTIVO is a receipt book whose receipts are posted to the datos2 ledger by design, so those rows exist and always will; what matters is whether any of them still reaches a balance. That is now a counted assertion which must stay at 0, and it fails loudly if someone writes a balance query that forgets the exclusion. Section A grew the line that changes its recommendation. "Floor them, never carry" was written when the floorless group looked like utilities receipts whose charges were never migrated. Measured on production today: of the 99, ninety-five carry insurance-line cash and NONE carry utilities rows. The seguros EFECTIVO is that line's only ledger, so flooring them deletes receipts rather than removing a double count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+157
-12
@@ -15,14 +15,27 @@
|
||||
* is a raw lifetime sum. The platform only migrated the CURRENT-year
|
||||
* charge ledger (datos2); the per-year charge tables live in DreamHost
|
||||
* and were never staged. What survives before the cutover is therefore
|
||||
* the EFECTIVO cash journal — receipts with no matching charges — so
|
||||
* those sums read as the office owing money it does not owe.
|
||||
* a cash journal — receipts with no matching charges — so those sums read
|
||||
* as the office owing money it does not owe.
|
||||
*
|
||||
* B. DOUBLE-BOOKED 2026 RECEIPTS — one cash receipt recorded twice, once in
|
||||
* EFECTIVO with folio `N` and once in datos2 with reference `CN`. Both
|
||||
* rows are after the 2026-01-01 floor, so both count. The statement hides
|
||||
* them (STATEMENT_EXCLUDED_SOURCE_TABLES drops EFECTIVO); the balances
|
||||
* worklist, the movement browser and the /clientes/:id card do not.
|
||||
* READ THE COMPOSITION LINE BEFORE ACTING ON THIS. After the prior-period
|
||||
* import the group is mostly insurance-only customers whose rows come from
|
||||
* the seguros database's own EFECTIVO, which is that line's ONLY ledger.
|
||||
* Flooring those deletes receipts instead of removing a double count. The
|
||||
* "floor them, never carry" argument holds for the utilities rows alone.
|
||||
*
|
||||
* B. DOUBLE-BOOKED 2026 RECEIPTS — one cash receipt appearing twice, once in
|
||||
* EFECTIVO with folio `N` and once in datos2 with reference `CN`.
|
||||
*
|
||||
* This is NOT an office data-entry defect, which is what it looked like
|
||||
* while the pair count kept growing at ~40/month. EFECTIVO is the paper
|
||||
* receipt book and every receipt in it is POSTED to the datos2 ledger by
|
||||
* design — verified against the live legacy database, 296 of the 297
|
||||
* receipts written in 2026 carry a matching posting. Legacy summed the
|
||||
* ledger alone. The duplication was the migration flattening a journal and
|
||||
* its postings into one table, and since 1.0.26 the application drops the
|
||||
* journal from every balance. Section B now reports what the journal holds
|
||||
* and asserts that none of it still reaches a balance.
|
||||
*
|
||||
* The folio alone neither proves nor disproves a pair, so it is used as a
|
||||
* lead and never as the verdict. Folios are reused, so `C13483` can collide
|
||||
@@ -82,6 +95,44 @@ const BF_PREDICATE = `(
|
||||
AND t.legacySourceTable = 'datos2')
|
||||
)`;
|
||||
|
||||
/**
|
||||
* The rest of what a balance query drops, over and above the floor. Mirrors
|
||||
* NOT_CASH_JOURNAL and archiveIsHistorySql in billing.service.ts — an audit
|
||||
* that computes a different book than the application is worse than no audit,
|
||||
* because its numbers look authoritative and diff cleanly against yesterday's.
|
||||
*
|
||||
* The database qualifier is not decoration. `SEGUROS 16_be` keeps its own table
|
||||
* called EFECTIVO and that one is the insurance line's only ledger; matching on
|
||||
* the table name alone would report 55,444.95 USD of real receivables as
|
||||
* duplicate cash. See efectivo-is-a-journal-not-a-ledger.
|
||||
*/
|
||||
const NOT_CASH_JOURNAL = `(
|
||||
t.legacySourceDb IS NULL
|
||||
OR t.legacySourceDb <> 'UTILITIES'
|
||||
OR t.legacySourceTable IS NULL
|
||||
OR t.legacySourceTable NOT IN
|
||||
('EFECTIVO', 'EFECTIVO_BACKUP', 'EFECTIVO FM3', 'CHEQUE FM3', 'IVA 2015')
|
||||
)`;
|
||||
|
||||
/**
|
||||
* An imported period counts as history below the year start and is dropped at
|
||||
* or above it. Spelled as a positive OR: `NOT (col LIKE ... AND ...)` is NULL
|
||||
* for an app-captured row, which would silently drop every one.
|
||||
*
|
||||
* The bound is the running calendar year, matching currentYearStart() in the
|
||||
* application rather than the cutover — the app's current period is "this
|
||||
* year", whatever cut the data happens to reflect.
|
||||
*/
|
||||
const yearStart = `${new Date().getUTCFullYear()}-01-01`;
|
||||
const ARCHIVE_IS_HISTORY = `(
|
||||
t.legacySourceTable IS NULL
|
||||
OR t.legacySourceTable NOT LIKE 'datos2@%'
|
||||
OR t.transactionDate < '${yearStart}'
|
||||
)`;
|
||||
|
||||
/** Everything a balance drops apart from the floor itself. */
|
||||
const READ_SCOPE = `(${NOT_CASH_JOURNAL} AND ${ARCHIVE_IS_HISTORY})`;
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const limit = Number(arg(args, "--limit", "25"));
|
||||
@@ -127,9 +178,13 @@ async function main() {
|
||||
ROUND(SUM(CASE WHEN t.currency='MXN' THEN t.amount ELSE 0 END), 2) AS rawMxn,
|
||||
ROUND(SUM(CASE WHEN t.currency='USD' THEN t.amount ELSE 0 END), 2) AS rawUsd,
|
||||
ROUND(SUM(CASE WHEN t.currency='MXN' AND (b.floorDate IS NULL OR t.transactionDate >= b.floorDate)
|
||||
THEN t.amount ELSE 0 END), 2) AS todayMxn,
|
||||
THEN t.amount ELSE 0 END), 2) AS floorOnlyMxn,
|
||||
ROUND(SUM(CASE WHEN t.currency='USD' AND (b.floorDate IS NULL OR t.transactionDate >= b.floorDate)
|
||||
THEN t.amount ELSE 0 END), 2) AS todayUsd,
|
||||
THEN t.amount ELSE 0 END), 2) AS floorOnlyUsd,
|
||||
ROUND(SUM(CASE WHEN t.currency='MXN' AND (b.floorDate IS NULL OR t.transactionDate >= b.floorDate)
|
||||
AND ${READ_SCOPE} THEN t.amount ELSE 0 END), 2) AS todayMxn,
|
||||
ROUND(SUM(CASE WHEN t.currency='USD' AND (b.floorDate IS NULL OR t.transactionDate >= b.floorDate)
|
||||
AND ${READ_SCOPE} THEN t.amount ELSE 0 END), 2) AS todayUsd,
|
||||
ROUND(SUM(CASE WHEN t.currency='MXN' AND t.transactionDate >= ?
|
||||
THEN t.amount ELSE 0 END), 2) AS flooredMxn,
|
||||
ROUND(SUM(CASE WHEN t.currency='USD' AND t.transactionDate >= ?
|
||||
@@ -197,6 +252,39 @@ async function main() {
|
||||
cutover,
|
||||
);
|
||||
|
||||
// What the floorless population's balance is actually MADE OF.
|
||||
//
|
||||
// "Floor them, never carry" was written when this group looked like
|
||||
// utilities cash receipts whose charges were never migrated. It is not that
|
||||
// any more. After the prior-period import the group is 99 customers, and
|
||||
// almost all of them are insurance-only — their rows come from the seguros
|
||||
// database's own EFECTIVO, which is that line's ONLY ledger. Nothing posts
|
||||
// it a second time, so flooring it does not remove a double count, it
|
||||
// deletes receipts. Split the two so the remedy is chosen per population
|
||||
// rather than for the group.
|
||||
const [floorlessMix] = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
WITH nobf AS (
|
||||
SELECT c.id FROM customers c
|
||||
WHERE EXISTS (SELECT 1 FROM transactions t WHERE t.customerId = c.id AND t.voidedAt IS NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM transactions t LEFT JOIN type_transactions tt ON tt.id = t.typeId
|
||||
WHERE t.customerId = c.id AND t.voidedAt IS NULL AND ${BF_PREDICATE}
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
COUNT(DISTINCT CASE WHEN t.legacySourceDb = 'SEGUROS 16_be' THEN t.customerId END) AS insCusts,
|
||||
ROUND(SUM(CASE WHEN t.legacySourceDb = 'SEGUROS 16_be' AND t.currency='MXN'
|
||||
THEN t.amount ELSE 0 END), 2) AS insMxn,
|
||||
ROUND(SUM(CASE WHEN t.legacySourceDb = 'SEGUROS 16_be' AND t.currency='USD'
|
||||
THEN t.amount ELSE 0 END), 2) AS insUsd,
|
||||
COUNT(DISTINCT CASE WHEN t.legacySourceDb <> 'SEGUROS 16_be' THEN t.customerId END) AS utilCusts
|
||||
FROM transactions t JOIN nobf n ON n.id = t.customerId
|
||||
WHERE t.voidedAt IS NULL AND t.outstanding = 0 AND t.transactionDate < ?
|
||||
`,
|
||||
cutover,
|
||||
);
|
||||
|
||||
// ---- section B: double-booked receipts ---------------------------------
|
||||
const pairs = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
@@ -269,6 +357,34 @@ async function main() {
|
||||
`,
|
||||
);
|
||||
|
||||
// REGRESSION GUARD. Since 1.0.26 the application drops the whole cash
|
||||
// journal from every balance, so none of section B's rows should reach one
|
||||
// any more. This counts the ones that still do: it is 0 while the exclusion
|
||||
// holds, and goes non-zero the moment someone reintroduces a balance query
|
||||
// that forgets it. A defect list that cannot tell you whether the defect is
|
||||
// still live is just history.
|
||||
const [stillCounted] = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
WITH bfloor AS (
|
||||
SELECT t.customerId, MAX(t.transactionDate) AS floorDate
|
||||
FROM transactions t LEFT JOIN type_transactions tt ON tt.id = t.typeId
|
||||
WHERE t.voidedAt IS NULL AND ${BF_PREDICATE}
|
||||
GROUP BY t.customerId
|
||||
)
|
||||
SELECT COUNT(*) AS n,
|
||||
ROUND(SUM(CASE WHEN t.currency='MXN' THEN t.amount ELSE 0 END), 2) AS mxn,
|
||||
ROUND(SUM(CASE WHEN t.currency='USD' THEN t.amount ELSE 0 END), 2) AS usd
|
||||
FROM transactions t
|
||||
LEFT JOIN bfloor b ON b.customerId = t.customerId
|
||||
WHERE t.voidedAt IS NULL AND t.outstanding = 0
|
||||
AND t.legacySourceDb = 'UTILITIES'
|
||||
AND t.legacySourceTable IN
|
||||
('EFECTIVO', 'EFECTIVO_BACKUP', 'EFECTIVO FM3', 'CHEQUE FM3', 'IVA 2015')
|
||||
AND (b.floorDate IS NULL OR t.transactionDate >= b.floorDate)
|
||||
AND ${READ_SCOPE}
|
||||
`,
|
||||
);
|
||||
|
||||
// ---- CSV escapes -------------------------------------------------------
|
||||
if (args.includes("--csv-a")) return dumpCsv(floorless);
|
||||
if (args.includes("--csv-b")) return dumpCsv([...pairs, ...nearby]);
|
||||
@@ -276,8 +392,15 @@ async function main() {
|
||||
// ---- report ------------------------------------------------------------
|
||||
console.log("\nBOOK (voided and outstanding rows excluded)");
|
||||
console.log(` raw lifetime sum, no floor ${money(book.rawMxn)} MXN ${money(book.rawUsd)} USD`);
|
||||
console.log(` today (per-customer BF floor) ${money(book.todayMxn)} MXN ${money(book.todayUsd)} USD`);
|
||||
console.log(` BF floor only (pre-1.0.26) ${money(book.floorOnlyMxn)} MXN ${money(book.floorOnlyUsd)} USD`);
|
||||
console.log(` TODAY, as the app computes it ${money(book.todayMxn)} MXN ${money(book.todayUsd)} USD`);
|
||||
console.log(` flat floor at ${cutover} ${money(book.flooredMxn)} MXN ${money(book.flooredUsd)} USD`);
|
||||
console.log(
|
||||
" (the middle line is the floor alone, kept only so older runs of this\n" +
|
||||
" script still diff against something. The app has dropped the cash\n" +
|
||||
" journal and windowed the archives since 1.0.26; USD going to zero on\n" +
|
||||
" the utilities side is correct, that ledger is peso-denominated.)",
|
||||
);
|
||||
|
||||
const preRowsTotal = floorless.reduce((s, r) => s + d(r.preRows), 0);
|
||||
const wouldZero = floorless.filter((r) => d(r.postRows) === 0);
|
||||
@@ -291,6 +414,17 @@ async function main() {
|
||||
console.log(` balance moved by flooring: ${money(-deltaMxn)} MXN ${money(-deltaUsd)} USD`);
|
||||
console.log(` customers left with NO rows at all after the cut: ${wouldZero.length}` +
|
||||
` (their balance becomes 0 — an assertion, not a migrated figure)`);
|
||||
console.log(
|
||||
` of the ${floorless.length}: ${d(floorlessMix.insCusts)} carry insurance-line cash` +
|
||||
` (${d(floorlessMix.insMxn).toFixed(2)} MXN / ${d(floorlessMix.insUsd).toFixed(2)} USD)` +
|
||||
`, ${d(floorlessMix.utilCusts)} carry utilities rows`,
|
||||
);
|
||||
console.log(
|
||||
` READ THAT LINE BEFORE FLOORING ANYONE. The seguros EFECTIVO is that\n` +
|
||||
` line's only ledger — nothing posts it twice — so flooring those\n` +
|
||||
` customers deletes receipts rather than removing a double count.\n` +
|
||||
` The argument for flooring holds for the utilities rows alone.`,
|
||||
);
|
||||
console.log(
|
||||
`\n ${"customer".padEnd(30)} ${"pre".padStart(4)} ${"post".padStart(4)}` +
|
||||
` ${"today MXN".padStart(13)} ${"after MXN".padStart(13)} ${"first tx".padStart(10)}`,
|
||||
@@ -347,6 +481,16 @@ async function main() {
|
||||
console.log(` confirmed USD receipt posted to datos2 in MXN: ${converted.length}`);
|
||||
console.log(` datos2 C-refs with no EFECTIVO partner at all: ${d(unpaired.n)}`);
|
||||
console.log(` EFECTIVO side of the confirmed pairs: ${money(efecMxn)} MXN ${money(efecUsd)} USD`);
|
||||
console.log(
|
||||
` still reaching a balance after the 1.0.26 exclusion: ${d(stillCounted.n)} rows` +
|
||||
` (${d(stillCounted.mxn).toFixed(2)} MXN / ${d(stillCounted.usd).toFixed(2)} USD)` +
|
||||
`${d(stillCounted.n) === 0 ? " <- 0 is the passing value" : " <- REGRESSION"}`,
|
||||
);
|
||||
console.log(
|
||||
` The rows below still exist and always will; the ledger's own C<folio>\n` +
|
||||
` posting is the copy that counts. This section is now a record of what\n` +
|
||||
` the journal holds, not a list of money being double-counted.`,
|
||||
);
|
||||
console.log(
|
||||
`\n ${"customer".padEnd(28)} ${"datos2".padStart(10)} ${"efectivo".padStart(10)}` +
|
||||
` ${"datos2 amt".padStart(13)} ${"efectivo amt".padStart(13)} ref`,
|
||||
@@ -382,8 +526,9 @@ async function main() {
|
||||
"\nNOTE: nothing above has been changed. Section A is a proposal to move the\n" +
|
||||
"floor, not a carried-forward balance: the pre-cutover charge ledger was\n" +
|
||||
"never migrated, so no true opening balance can be computed from this\n" +
|
||||
"database. It exists in the DreamHost per-year tables. Section B is an\n" +
|
||||
"independent defect and does not need a corte to fix.",
|
||||
"database. It exists in the DreamHost per-year tables. Section B is no\n" +
|
||||
"longer an open defect — it was fixed read-side in 1.0.26 — and the line\n" +
|
||||
"that matters there is the regression count, which must stay at 0.",
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
|
||||
Reference in New Issue
Block a user