fix(billing): folio typos hid two double-booked receipts from the audit
Matching the datos2 `CN` reference against EFECTIVO folio `N` finds pairs only where both were keyed correctly. Jorge Cuadros Jr's account carries `C13647` against EFECTIVO folio `13649` — same day, same 3,500.00 MXN, one receipt — and POWERS carries `C135808` against `13508`. Folio matching alone calls both accounts clean, which is exactly backwards: an account used as a validator reporting a false negative is worse than no audit. A second pass now sweeps the C-refs the first pass left orphaned, on proximity alone (same customer, same three-day window), and both passes are judged by the same money rules. The folio is demoted to a lead: it can be wrong in either direction, so it never decides anything on its own. 278 confirmed pairs, up from 276 — 989,740.00 MXN and 88,392.00 USD on the EFECTIVO side. Of the 97 orphaned C-refs only 3 had any EFECTIVO row nearby, so the remaining 94 are datos2-only postings rather than misses. Rejections rise to 3. RAMIREZ, SUSANA pairs 3,320.00 MXN against 18,000.00 the next day; that is a partial application, not a duplicate, and it needs a human rather than a rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+60
-10
@@ -24,12 +24,21 @@
|
||||
* them (STATEMENT_EXCLUDED_SOURCE_TABLES drops EFECTIVO); the balances
|
||||
* worklist, the movement browser and the /clientes/:id card do not.
|
||||
*
|
||||
* The folio alone does NOT prove a pair. EFECTIVO folios restart and are
|
||||
* reused, so `C13483` can collide with an unrelated receipt. Every pair is
|
||||
* therefore corroborated on money as well: identical amount when both legs
|
||||
* are in the same currency, or an implied USD->MXN rate inside the band the
|
||||
* exchange_rates table actually observed that year. Pairs that fail are
|
||||
* reported separately and must not be counted as duplicated money.
|
||||
* 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
|
||||
* with an unrelated receipt; folios are also mistyped, so a genuine pair can
|
||||
* carry two different numbers. Detection therefore runs twice — once on the
|
||||
* `CN` cross-reference, once over the C-refs that pass left orphaned, this
|
||||
* time on proximity alone (same customer, within three days) — and BOTH
|
||||
* passes are then judged on the money: identical amount when the two legs
|
||||
* share a currency, or an implied USD->MXN rate inside the band the
|
||||
* exchange_rates table actually observed that year. Anything that fails is
|
||||
* reported apart and must not be counted as duplicated money.
|
||||
*
|
||||
* The second pass is not a refinement. Jorge Jr's own account carries
|
||||
* `C13647` against EFECTIVO folio `13649` — same day, same 3,500.00 — and
|
||||
* POWERS carries `C135808` against `13508`. Folio matching alone reports
|
||||
* both accounts as clean.
|
||||
*
|
||||
* node scripts/corte-audit.mjs # summary + both sections
|
||||
* node scripts/corte-audit.mjs --cutover 2026-01-01
|
||||
@@ -215,6 +224,37 @@ async function main() {
|
||||
`,
|
||||
);
|
||||
|
||||
// Pass two — the leads pass one could not follow. Same shape of row, judged
|
||||
// by the same money rules below, so a folio typo costs nothing.
|
||||
const nearby = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT d.id AS datos2Id, e.id AS efectivoId, c.name,
|
||||
DATE(d.transactionDate) AS datos2Date, DATE(e.transactionDate) AS efectivoDate,
|
||||
d.amount AS datos2Amount, d.currency AS datos2Currency,
|
||||
e.amount AS efectivoAmount, e.currency AS efectivoCurrency,
|
||||
d.reference AS datos2Ref, e.reference AS efectivoRef,
|
||||
fx.lo AS rateLo, fx.hi AS rateHi
|
||||
FROM transactions d
|
||||
JOIN transactions e
|
||||
ON e.customerId = d.customerId AND e.legacySourceTable = 'EFECTIVO'
|
||||
AND e.voidedAt IS NULL AND e.amount > 0
|
||||
AND ABS(DATEDIFF(e.transactionDate, d.transactionDate)) <= 3
|
||||
JOIN customers c ON c.id = d.customerId
|
||||
LEFT JOIN (
|
||||
SELECT YEAR(effectiveDate) AS y, MIN(rate) AS lo, MAX(rate) AS hi
|
||||
FROM exchange_rates GROUP BY YEAR(effectiveDate)
|
||||
) fx ON fx.y = YEAR(d.transactionDate)
|
||||
WHERE d.voidedAt IS NULL AND d.legacySourceTable = 'datos2'
|
||||
AND d.reference REGEXP '^C[0-9]+$'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM transactions x
|
||||
WHERE x.customerId = d.customerId AND x.legacySourceTable = 'EFECTIVO'
|
||||
AND x.voidedAt IS NULL AND x.reference = SUBSTRING(d.reference, 2)
|
||||
)
|
||||
ORDER BY d.transactionDate
|
||||
`,
|
||||
);
|
||||
|
||||
const [unpaired] = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT COUNT(*) AS n
|
||||
@@ -231,7 +271,7 @@ async function main() {
|
||||
|
||||
// ---- CSV escapes -------------------------------------------------------
|
||||
if (args.includes("--csv-a")) return dumpCsv(floorless);
|
||||
if (args.includes("--csv-b")) return dumpCsv(pairs);
|
||||
if (args.includes("--csv-b")) return dumpCsv([...pairs, ...nearby]);
|
||||
|
||||
// ---- report ------------------------------------------------------------
|
||||
console.log("\nBOOK (voided and outstanding rows excluded)");
|
||||
@@ -281,7 +321,15 @@ async function main() {
|
||||
? "confirmed"
|
||||
: "suspect";
|
||||
};
|
||||
for (const p of pairs) p.verdict = classify(p);
|
||||
for (const p of pairs) {
|
||||
p.pass = "folio";
|
||||
p.verdict = classify(p);
|
||||
}
|
||||
for (const p of nearby) {
|
||||
p.pass = "proximity";
|
||||
p.verdict = classify(p);
|
||||
}
|
||||
pairs.push(...nearby);
|
||||
|
||||
const confirmed = pairs.filter((p) => p.verdict === "confirmed");
|
||||
const suspect = pairs.filter((p) => p.verdict === "suspect");
|
||||
@@ -292,7 +340,9 @@ async function main() {
|
||||
const efecUsd = confirmed.reduce((s, p) => s + (p.efectivoCurrency === "USD" ? d(p.efectivoAmount) : 0), 0);
|
||||
|
||||
console.log(`\nB. DOUBLE-BOOKED RECEIPTS — ${confirmed.length} confirmed pairs across ${byCust.size} customers`);
|
||||
console.log(` folio matches examined: ${pairs.length} (confirmed ${confirmed.length}, rejected on money ${suspect.length})`);
|
||||
const byFolio = confirmed.filter((p) => p.pass === "folio").length;
|
||||
console.log(` candidates examined: ${pairs.length} (confirmed ${confirmed.length}, rejected on money ${suspect.length})`);
|
||||
console.log(` found by folio cross-reference: ${byFolio}, by proximity after a folio miss: ${confirmed.length - byFolio}`);
|
||||
console.log(` confirmed same-currency, amount equal to the cent: ${sameCur.length}`);
|
||||
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)}`);
|
||||
@@ -306,7 +356,7 @@ async function main() {
|
||||
` ${(p.name || "(sin nombre)").slice(0, 28).padEnd(28)}` +
|
||||
` ${day(p.datos2Date).padStart(10)} ${day(p.efectivoDate).padStart(10)}` +
|
||||
` ${money(p.datos2Amount)} ${p.datos2Currency}` +
|
||||
` ${money(p.efectivoAmount)} ${p.efectivoCurrency} ${p.datos2Ref}`,
|
||||
` ${money(p.efectivoAmount)} ${p.efectivoCurrency} ${p.datos2Ref}/${p.efectivoRef}`,
|
||||
);
|
||||
}
|
||||
if (confirmed.length > limit) console.log(` ... ${confirmed.length - limit} more (--csv-b)`);
|
||||
|
||||
Reference in New Issue
Block a user