A billing export that arrives by email is a delayed delivery, not the return value of a button click. Consider a design scenario: two stores request October exports, a worker receives a CSV later, and the job accepts it because the filename contains October. The file can be real while the association is wrong.
I work on BrowserAct. While adapting our Shopify billing guide for developers, the useful engineering question was how to preserve request identity across that delay. This article proposes an application-side review gate, with synthetic Python checks. It does not report access to a Shopify store, mailbox integration, a Shopify API call, or a tested invoice Bot.
Keep the supplier behavior separate from your contract
Our Shopify source guide, checked against the linked documentation on November 9, distinguishes an individual bill PDF or CSV from a billing-history CSV sent to the account email. The history export is not a ZIP of individual PDFs. A statement of charges answers another reporting question.
That distinction determines the consumer. A history CSV can enter a ledger-validation queue. It cannot satisfy a PDF-archive request merely because the export completed. Here we handle only the asynchronous history CSV association. We do not parse Shopify CSV columns, download bill PDFs, or assert document completeness.
Create an internal request before requesting an export
Persist a request reference, stable store reference, requested interval and output kind. Use an internal store identifier rather than a display name that can change. Keep the operational recipient separately in access-controlled configuration; do not put mailbox addresses in general task logs.
In this example intervals are half-open: start is included, end is excluded. That is our application convention, not a claim about Shopify date-picker semantics. An adapter must review how the actual selected dates map to that interval before storing them. A current-page export also needs a reviewed page manifest; do not squeeze it into a date-range contract without that evidence.
The request reference below is generated by our application. Shopify is not claimed to echo it in an email, filename, CSV column or API response. A reviewer associates a candidate delivery with that reference only after inspecting its provenance and scope. If that association cannot be supported, leave the delivery unassigned.
Refuse ambiguity before validating the file
The gate accepts a reviewed delivery receipt, not a raw email. It requires exactly one pending request for the same store, interval and output kind. Two overlapping attempts with identical scope remain ambiguous even if somebody copies one request reference onto the receipt. Resolve or cancel the redundant attempt explicitly before continuing.
from datetime import date def route_delivery(request, delivery, pending): keys = ("store_ref", "start", "end", "kind") try: start = date.fromisoformat(request["start"]) end = date.fromisoformat(request["end"]) if start >= end or request["kind"] != "billing_history_csv": return "invalid_request" if not request["request_ref"] or not request["store_ref"]: return "invalid_request" peers = [p for p in pending if all(p.get(k) == request[k] for k in keys)] if len(peers) != 1 or peers[0].get("request_ref") != request["request_ref"]: return "ambiguous_request" if delivery.get("reviewed") is not True or not delivery.get("review_evidence"): return "needs_review" if any(delivery.get(k) != request[k] for k in keys): return "scope_mismatch" if delivery.get("request_ref") != request["request_ref"]: return "request_mismatch" if not delivery.get("artifact_ref") or not delivery.get("sha256"): return "artifact_missing" digest = delivery["sha256"] if not isinstance(digest, str) or not re_full_sha256(digest): return "artifact_missing" return "ready_for_csv_validation" except (KeyError, TypeError, ValueError): return "invalid_request" def re_full_sha256(value): return len(value) == 64 and all(c in "0123456789abcdef" for c in value) The evidence reference points to the upstream review record. A boolean alone is insufficient, but neither field proves the review was truthful. The trusted adapter must verify the sender and delivery route under its own policy, inspect the attachment safely, establish store and range, and retain the evidence. A subject line or apparent sender is not authentication.
Likewise, artifact_ref and sha256 are assertions from that adapter. This function checks their presence and digest syntax; it does not read bytes, calculate a digest, inspect a CSV, or prove the object still exists. The next worker must open the retained object and recompute the digest before schema and content validation. A filename extension cannot replace those checks.
Make the next action explicit
| Observed condition | Routing result | Next action |
|---|---|---|
| One pending scope, reviewed matching delivery | ready_for_csv_validation | Validate retained bytes and CSV contract |
| Two pending requests with identical scope | ambiguous_request | Resolve attempt ownership |
| Delivery belongs to another store or interval | scope_mismatch | Quarantine association for review |
| Scope matches but internal request differs | request_mismatch | Reconcile the review record |
| Review missing or evidence absent | needs_review | Inspect provenance and scope |
| Artifact reference or digest missing | artifact_missing | Recover a verifiable retained artifact |
A successful result only schedules CSV validation. It does not mark the export reconciled, the PDFs archived, or the finance task complete. For missing mail, keep the original request pending with an observation timestamp. Use a bounded operational follow-up policy; a timeout does not prove Shopify has no bills, and blind retries can create more indistinguishable deliveries.
Test the association before building the mailbox worker
The local synthetic checks cover another store, another interval, another output kind, mismatched request references, duplicate pending attempts, absent review evidence, strict boolean handling, missing artifact references, malformed digests and invalid date ranges. They exercise only these routing decisions. No supplier request or real attachment is involved.
Production persistence needs an additional guarantee: reserve the chosen request and record the delivery association in one transaction, with a uniqueness rule for the accepted attempt. This pure function cannot prevent two concurrent workers from accepting the same candidate after separate reads. Keep an append-only observation record when the same bytes arrive twice; content deduplication must not silently erase the second delivery event.
For a browser-assisted collector, use the same receipt boundary after an authorized operator requests the native export. BrowserAct would still need a separately tested store adapter and delivery path. The engineering gain here is a specific handoff: an asynchronous CSV becomes eligible for validation only after its association is reviewed, while the original PDF collection request remains independently open.

