None of these were caught by 105 green tests, because the code they live in was barely tested and the HTTP path was not tested at all. 1. reconcilePendingOrders hardcoded category 'dining', so any order resolved through the deferred path booked as dining regardless of merchant — a Woolworths grocery order that parks and later reconciles was misfiled. That reintroduced, through the back door, exactly the misfiling resolveCategory() exists to prevent. Now calls it. 2. reconcileCardLeg never marked a statement line as consumed, so two orders on the same card inside the +/-4 day window both bound to the same charge and each booked its own credits remainder — double-counting spend. At 10-15 orders a month on one card that is not a corner case. Migration 0020 adds matched_transaction_id with a unique index; the matcher now excludes lines already claimed. 3. The ingest API returned HTTP 200 for every parse failure, and the Slack alert fires only on non-200. So the single most likely production failure — a provider template change breaking every order at once — was completely silent. Split into NotAReceiptError (promotions, delivery updates, refund and adjustment notices: 200, silent, expected traffic) and OrderParseError (it IS a receipt and would not parse: 422, alerts). Also: order_reference now anchors on Uber's own tripReference cell rather than 'first UUID in the document'. I had claimed to verify that the first UUID was always the order UUID; that check compared against zero samples and was vacuous. tripReference is present in all 29 captured receipts and, for ue-00, equals the UUID the PDF redirect resolves to. The positional fallback remains but only flags when there is genuine ambiguity. Adds the API route's first tests — auth gate and error taxonomy — plus anchoring regressions. 63 unit + 53 integration green on five consecutive runs; corpus holds at 63/65.
This commit is contained in:
@@ -42,9 +42,18 @@ export async function reconcileCardLeg(
|
||||
AND t.transaction_date BETWEEN $2::date - $4::int AND $2::date + $4::int
|
||||
AND (t.description ILIKE '%doordash%' OR t.description ILIKE '%uber%')
|
||||
AND t.amount <= $3::numeric + 0.02
|
||||
-- A statement line settles exactly one order. Without this, two orders
|
||||
-- on the same card inside the window both match the same charge and
|
||||
-- each books its own credits remainder — double-counting spend. At
|
||||
-- 10-15 orders a month on one card that is not a corner case.
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM expense_metadata em
|
||||
WHERE em.matched_transaction_id = t.id
|
||||
AND ($5::text IS NULL OR em.order_reference IS DISTINCT FROM $5::text)
|
||||
)
|
||||
ORDER BY abs(t.amount - $3::numeric), abs(t.transaction_date - $2::date)
|
||||
LIMIT 1`,
|
||||
[`%${last4}`, day, order.totals.total_charged, windowDays]
|
||||
[`%${last4}`, day, order.totals.total_charged, windowDays, order.order_reference || null]
|
||||
);
|
||||
|
||||
if (!row) return { cardAmount: null, matchedTransactionId: null };
|
||||
@@ -251,20 +260,29 @@ export async function reconcilePendingOrders(): Promise<{
|
||||
flags: [],
|
||||
};
|
||||
|
||||
const { cardAmount } = await reconcileCardLeg(probe);
|
||||
const { cardAmount, matchedTransactionId } = await reconcileCardLeg(probe);
|
||||
if (cardAmount === null) continue; // statement still hasn't arrived
|
||||
|
||||
const remainder = Number((total - cardAmount).toFixed(2));
|
||||
let txnId: number | null = null;
|
||||
|
||||
if (remainder > 0.02 && row.transaction_date >= CUTOVER_DATE) {
|
||||
// Category from the merchant, never hardcoded. Hardcoding 'dining' here
|
||||
// silently misfiled every grocery order that arrived with an unstated
|
||||
// split — reintroducing, through the deferred path, exactly the
|
||||
// misfiling resolveCategory() exists to prevent.
|
||||
const category = resolveCategory({
|
||||
merchant_name: row.merchant_normalized,
|
||||
platform: "doordash",
|
||||
} as ParsedOrder);
|
||||
|
||||
const txn = await queryRow<{ id: number }>(
|
||||
`INSERT INTO transactions (
|
||||
transaction_date, description, amount, amount_aud, category,
|
||||
payment_method, merchant_name, merchant_normalized, transaction_type, owner_id
|
||||
) VALUES ($1,$2,$3,$3,'dining','credits',$4,$4,'debit',NULL)
|
||||
) VALUES ($1,$2,$3,$3,$5,'credits',$4,$4,'debit',NULL)
|
||||
RETURNING id`,
|
||||
[row.transaction_date, `Order - ${row.merchant_normalized}`, remainder, row.merchant_normalized]
|
||||
[row.transaction_date, `Order - ${row.merchant_normalized}`, remainder, row.merchant_normalized, category]
|
||||
);
|
||||
txnId = txn!.id;
|
||||
created++;
|
||||
@@ -273,10 +291,11 @@ export async function reconcilePendingOrders(): Promise<{
|
||||
await queryRaw(
|
||||
`UPDATE expense_metadata
|
||||
SET transaction_id = COALESCE($2, transaction_id),
|
||||
matched_transaction_id = $4,
|
||||
reconciled_at = NOW(),
|
||||
flags = flags || $3::jsonb
|
||||
WHERE id = $1`,
|
||||
[row.id, txnId, JSON.stringify([`card_leg_${cardAmount.toFixed(2)}`])]
|
||||
[row.id, txnId, JSON.stringify([`card_leg_${cardAmount.toFixed(2)}`]), matchedTransactionId]
|
||||
);
|
||||
resolved++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user