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:
+39
-3
@@ -191,17 +191,39 @@ function parseUberRoute(html: string): RoutePoint[] {
|
||||
if (!addrM) continue;
|
||||
|
||||
const address = collapse(decodeEntities(stripTags(addrM[1])));
|
||||
// "1:20 pm - Pick-up" — time and label share one cell.
|
||||
// Delivery receipts share one cell between time and label — "1:20 pm -
|
||||
// Pick-up". Trip receipts print the time alone, with no label at all, so
|
||||
// the naive split put the time in `label` and left `time` null. Position
|
||||
// carries the meaning there: first stop is where the ride began.
|
||||
const combined = collapse(decodeEntities(stripTags(rawLabel)));
|
||||
const split = combined.match(/^(.*?)\s+-\s+(.*)$/);
|
||||
const time = split ? split[1] : null;
|
||||
const label = split ? split[2] : combined;
|
||||
let time: string | null;
|
||||
let label: string;
|
||||
if (split) {
|
||||
time = split[1];
|
||||
label = split[2];
|
||||
} else if (/^\d{1,2}:\d{2}\s*(am|pm)?$/i.test(combined)) {
|
||||
time = combined;
|
||||
label = ""; // filled in positionally below — the receipt gives none
|
||||
} else {
|
||||
time = null;
|
||||
label = combined;
|
||||
}
|
||||
|
||||
const key = `${label}|${time}|${address}`;
|
||||
if (!address || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
points.push({ label, time, address });
|
||||
}
|
||||
|
||||
// A trip receipt labels neither end. Position is the only thing that says
|
||||
// which is which, and for a two-stop trip it says it unambiguously. Only
|
||||
// filled where the receipt itself was silent, so a future template that does
|
||||
// label its stops keeps its own wording.
|
||||
if (points.length === 2 && points.every((p) => !p.label)) {
|
||||
points[0].label = "Pick-up";
|
||||
points[1].label = "Drop-off";
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
@@ -357,6 +379,20 @@ export function parseOrderHTML(html: string, meta: MessageMeta): ParsedOrder {
|
||||
);
|
||||
}
|
||||
|
||||
// Uber sends TWO mails per trip with the same subject and the same total: a
|
||||
// "charge summary" when the trip ends, then the real receipt once payment
|
||||
// settles. The summary says so itself — "This is not a payment receipt ...
|
||||
// You will receive a trip receipt when the payment is processed with payment
|
||||
// information" — and it carries no tripReference, so order_reference would
|
||||
// fall back to `msg:<message-id>` and I7 could not dedupe it against the
|
||||
// receipt that follows. Every trip would be recorded twice.
|
||||
if (/This is not a payment receipt|This is your charge summary/i.test(text)) {
|
||||
throw new NotAReceiptError(
|
||||
"charge summary, not a payment receipt — the real receipt follows",
|
||||
meta.messageId
|
||||
);
|
||||
}
|
||||
|
||||
const platform = detectPlatform(meta, text);
|
||||
const merchant_name = parseMerchant(platform, meta, text);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user