feat(orders): read Uber trips, and reject the charge summary that duplicates them
ci / lint-test (push) Failing after 45s
ci / lint-test (push) Failing after 45s
Local rides are paid with credits (only overseas ones go on a card), so trips
belong to this slice and were simply never fetched — the Graph query searched
"order with Uber", the Eats subject. Captured 15 real messages from the mailbox
via a dry-run before touching anything, which found two defects that no amount
of reasoning about the template would have:
**Uber sends two mails per trip.** A "charge summary" when the ride ends, then
the real receipt when payment settles — same subject, same total. The summary
carries no tripReference, so order_reference fell back to `msg:<message-id>`
and I7 could not dedupe it against the receipt that follows. Every trip would
have been recorded twice. It says so itself ("This is not a payment receipt ...
You will receive a trip receipt when the payment is processed"), so it is now a
NotAReceiptError — 200 and silent, like every other expected non-receipt.
**Trip receipts label neither end of the journey.** Delivery receipts write
"1:20 pm - Pick-up"; trips print the time alone. The split regex put the time
into `label` and left `time` null. Time is now read properly, and a two-stop
trip is labelled Pick-up/Drop-off positionally — only where the receipt was
silent, so a template that does label its stops keeps its own wording.
Verified against all 15 captured messages: 7 trips recorded, 5 charge summaries
and 3 promotions skipped, 0 failures, no duplicate references. Two of the seven
are AUD credits-funded ($84.78 + $47.97) and would become transactions; the
five NZD ones are card-settled and correctly create provenance only (I5).
Fixtures ut-00 (local credits trip), ut-01 (overseas card trip) and ut-summary
(the charge summary) are captured mail, not written by hand.
This commit is contained in:
@@ -247,3 +247,59 @@ describe("Uber line items", () => {
|
||||
expect(p.flags).not.toContain("no_line_items_parsed");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Uber trips. Captured 2026-06 via a dry-run against the real mailbox after the
|
||||
* user pointed out that only *overseas* rides go on a card — local rides are
|
||||
* paid with credits, which puts them in the same class as delivery orders.
|
||||
*/
|
||||
describe("Uber trips", () => {
|
||||
const utMeta: Record<string, MessageMeta> = JSON.parse(
|
||||
readFileSync(resolve(dir, "ut-meta.json"), "utf-8")
|
||||
);
|
||||
const trip = (f: string) => parseOrderHTML(html(f), utMeta[f]);
|
||||
|
||||
it("a local trip is credits-funded", () => {
|
||||
const p = trip("ut-00");
|
||||
expect(p.platform).toBe("uber");
|
||||
expect(p.currency).toBe("AUD");
|
||||
expect(p.totals.total_charged).toBeCloseTo(84.78, 2);
|
||||
expect(p.payment.credits_amount).toBeCloseTo(84.78, 2);
|
||||
expect(p.payment.card_last4).toBeNull();
|
||||
expect(validateOrderTotals(p).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("an overseas trip is card-settled", () => {
|
||||
const p = trip("ut-01");
|
||||
expect(p.currency).toBe("NZD");
|
||||
expect(p.totals.total_charged).toBeCloseTo(55.51, 2);
|
||||
expect(p.payment.card_amount).toBeCloseTo(55.51, 2);
|
||||
expect(p.payment.card_last4).toBe("3893");
|
||||
});
|
||||
|
||||
it("labels the two ends of a trip, which the receipt does not", () => {
|
||||
// Delivery receipts write "1:20 pm - Pick-up"; trip receipts print the time
|
||||
// alone. The naive split put the time in `label` and left `time` null.
|
||||
const p = trip("ut-00");
|
||||
expect(p.route).toEqual([
|
||||
{
|
||||
label: "Pick-up",
|
||||
time: "7:32 pm",
|
||||
address: "Terminal 2, Melbourne Airport (MEL), Tullamarine VIC 3045, Australia",
|
||||
},
|
||||
{
|
||||
label: "Drop-off",
|
||||
time: "8:10 pm",
|
||||
address: "19 Lady Penrhyn Dr, Wyndham Vale VIC 3024, Australia",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects the charge summary Uber sends before the receipt", () => {
|
||||
// Uber sends two mails per trip with the same subject and the same total.
|
||||
// The first says "This is not a payment receipt" and carries no
|
||||
// tripReference, so order_reference would fall back to msg:<id> and I7
|
||||
// could not dedupe it — every trip would be recorded twice.
|
||||
expect(() => trip("ut-summary")).toThrow(NotAReceiptError);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user