Uber Cash IS credits, and the payment line had become unreadable in the
newer layout: "Payments Uber Cash 10/17/25 8:50 PM A$54.87" carries a
timestamp between the label and the amount, and writes the currency as a
prefix. Both defeated the pattern, so credits_amount stayed null and the
order was filed as card-settled — sent looking for a card leg that does
not exist, found nothing, and left as an orphan with no transaction and
no card tail to match on. 118 captured messages sit in that state, and
every one of them is pre-cutover, so reading them correctly means I1
skips them rather than storing enrichment that points at nothing.
The card leg had the same blind spot, hidden behind the first: the gap
between the mask and the amount was [^\d]{0,40}, which a timestamp
breaks. While BOTH legs were unreadable a mixed payment still looked
consistent — the order read as card-settled for the full total. Fixing
only credits reads half an order, which validateOrderTotals correctly
refuses. Found exactly that way: four messages that validated before
began failing "payments sum to 34.92 but receipt states 43.60".
Legs are summed rather than taken first, because one order can be charged
in instalments and an instrument can carry no mask at all (PayPal). But a
leg that already equals the stated total IS the payment, not an
instalment: a Dubai trip prints an AED 17.67 authorisation and then the
AED 577.83 settled charge, and adding the hold overstates the trip. A
mixed credits+card order is unaffected — neither leg equals the total
there, which is why it needs summing.
A/B over all 776 captures: 468 parsed by both, zero change to any amount,
currency or existing card tail, 0 lost, 118 credits figures newly read.
Mutation-tested: dropping the exact-leg rule fails 2 tests, dropping the
Uber Cash read fails 7.
This commit is contained in:
@@ -412,3 +412,79 @@ describe("credits-funded orders are orders", () => {
|
||||
expect(validateOrderTotals(o, notCredits).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Uber Cash is credits, not a card", () => {
|
||||
it("reads a payment line carrying a timestamp and a prefixed currency", () => {
|
||||
// "Payments Uber Cash 10/17/25 8:50 PM A$54.87". The old pattern allowed
|
||||
// neither the timestamp nor the A$ prefix, so credits_amount stayed null
|
||||
// and the order was filed as card-settled — sent looking for a card leg
|
||||
// that does not exist, and left as an orphan with nothing to match on.
|
||||
const o = parseOrderHTML(
|
||||
html("ue-aud-prefix"),
|
||||
meta({
|
||||
subject: "Your Friday evening order with Uber Eats",
|
||||
sender: "Uber Receipts <noreply@uber.com>",
|
||||
})
|
||||
);
|
||||
expect(o.payment.credits_amount).toBe(54.87);
|
||||
expect(o.payment.card_last4).toBeNull();
|
||||
expect(o.payment.ambiguous).toBe(false);
|
||||
});
|
||||
|
||||
it("still reads the plain form, where the timestamp follows the amount", () => {
|
||||
// "Uber Cash $25.33 22/7/26 1:41 pm" — the older layout the widened
|
||||
// pattern must not break.
|
||||
const o = parseOrderHTML(
|
||||
html("ue-00"),
|
||||
meta({
|
||||
subject: "Your order with Uber Eats",
|
||||
sender: "Uber Receipts <noreply@uber.com>",
|
||||
})
|
||||
);
|
||||
expect(o.payment.credits_amount).toBe(25.33);
|
||||
});
|
||||
|
||||
it("still reads the card leg of a mixed payment", () => {
|
||||
// "Uber Cash $1.17 ... Westpac ••••8032 $15.33" — the credits half must
|
||||
// not swallow the card half.
|
||||
const o = parseOrderHTML(
|
||||
html("ue-mixed"),
|
||||
meta({ subject: "Your order with Uber Eats", sender: "Uber Receipts <noreply@uber.com>" })
|
||||
);
|
||||
expect(o.payment.credits_amount).toBe(1.17);
|
||||
expect(o.payment.card_last4).toBe("8032");
|
||||
});
|
||||
});
|
||||
|
||||
describe("payment legs", () => {
|
||||
const trip = (f: string) =>
|
||||
parseOrderHTML(
|
||||
html(f),
|
||||
meta({
|
||||
subject: "Your Wednesday afternoon trip with Uber",
|
||||
sender: "Uber Receipts <noreply@uber.com>",
|
||||
})
|
||||
);
|
||||
|
||||
it("does not add a superseded authorisation to the settled charge", () => {
|
||||
// "Citi Prestige ••••0253 AED 17.67" then the same card "AED 577.83",
|
||||
// against a stated total of 577.83. The first is a hold, not a part
|
||||
// payment; adding it overstates the trip by the held amount.
|
||||
const o = trip("ut-reauth");
|
||||
expect(o.totals.total_charged).toBe(577.83);
|
||||
expect(o.payment.card_amount).toBe(577.83);
|
||||
expect(o.payment.card_last4).toBe("0253");
|
||||
expect(validateOrderTotals(o, html("ut-reauth")).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("adds the legs of a genuinely split payment", () => {
|
||||
// "PayPal - <email> A$78.41" + "Uber Cash A$6.85" = 85.26. Neither leg
|
||||
// equals the total, so both are real and both must be counted — and the
|
||||
// PayPal leg carries no card mask to anchor on.
|
||||
const o = trip("ut-paypal");
|
||||
expect(o.totals.total_charged).toBe(85.26);
|
||||
expect(o.payment.credits_amount).toBe(6.85);
|
||||
expect(o.payment.card_amount).toBe(78.41);
|
||||
expect(validateOrderTotals(o, html("ut-paypal")).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user