Fowlboys Ops — Xero Integration Flows
Living reference for how data moves between Xero and the ops platform. One section per integration flow: a header, a plain-English explanation, and a diagram. (This is a real internal doc — lightly trimmed for sharing.)
Card-Charge → Xero Invoice Auto-Attachlive behind flag
We pay SaaS subscriptions and some utilities with virtual debit cards. Every charge eventually becomes a
bill in Xero that the bookkeeper keys by hand, and the receipt is supposed to be attached
to that bill. The manual pain is finding each vendor's invoice and attaching it. This flow automates
exactly that: a daily cron finds card-paid bills that have no receipt attached, searches our invoice inboxes
for the matching PDF, and attaches it to the bill via Xero's Attachments API. Matching keys on
vendor → email sender, a card anchor, and a ±4-day window — not amount, because Xero
stores MYR while the receipts are in USD with no stored FX rate. Exactly-one confident match auto-attaches;
anything ambiguous (zero, multiple, or low-confidence) is parked as needs_review for the operator.
flowchart TD CRON["⏱ Daily cron 23:30 UTC
/api/cron/swipey-doc-attach"] --> FLAG{"swipey_doc_attach_enabled?"} FLAG -->|false| NOOP["No-op (ships inert)"] FLAG -->|true| ENUM["Enumerate card-paid Xero BILLS
with no receipt · since 2026-01-01"] ENUM --> LOOP{"for each bill"} GM[("📧 Invoice inboxes (read-only)")] -.-> SEARCH LOOP --> SEARCH["Search inboxes
vendor→sender · card anchor · ±4 days"] SEARCH --> SCORE{"confidence"} SCORE -->|"HIGH — exactly 1 match"| DL["Download invoice PDF"] DL --> STORE["Store in receipts bucket"] STORE --> ATTACH["PUT attach PDF to the Xero BILL
scope: accounting.attachments"] ATTACH --> XERO[("📒 Xero bill
now has its receipt")] ATTACH --> OKROW[("reconciliation row
state = attached")] SCORE -->|"LOW · 0 · many"| REVIEW[("reconciliation row
state = needs_review")] REVIEW --> DASH["/app/finance/swipey
review-assist dashboard"]
Dry-run: the HIGH path records the would-attach intent (state=attached, no storage path) but skips the download, bucket write, and Xero PUT entirely. Idempotent: only rows with a real stored receipt count as done, so a later run never double-attaches and a dry-run never blocks the first live run.
Xero Bill → Telegram Maker/Checker Payment Approvalsibling flow
The other Xero integration: when a bill is marked AUTHORISED in Xero, a card is posted into the
per-entity INVOICES topic of the finance Telegram forum. A maker enters the payment on the bank
portal and picks the paying bank account; a checker approves; on confirmation the system writes
the payment back to Xero via POST /Payments. Reminders chase stalled approvals — so a bill can't
quietly sit unpaid.
flowchart LR AUTH["Xero bill = AUTHORISED"] --> CARD["Telegram card
finance forum · INVOICES topic"] CARD --> MAKER["Maker: enter on bank portal
+ pick paying account"] MAKER --> CHECKER["Checker: approve"] CHECKER --> PAY["POST /Payments → Xero"] PAY --> PAID["bill marked paid + reconciled"]