Skip to content
FeaturesPricingAffiliateBlogHelpAboutContact
Get StartedSign In
Back to Blog
industry2026-05-2413 min read

HMRC saw single-row tax on my receipts — tax_breakdown_json BEACON gap (PR #654 VIII F5)

Marco (54) runs Princes Street Trattoria 70-cover family Italian in Edinburgh New Town corner of Princes Street meets Hanover Street. Two revenue streams: (1) sit-down hot Italian + drinks + alcohol (VAT 20%), (2) small retail counter cold takeaway sandwiches + bottled olive oil + jarred sauces + boxed panettone (VAT 0%) + bottled wine (VAT 20%). 18 months ago configured per-item tax_rate (PR #475 batch I). Mid-May Tuesday morning phone rang: **"Mr. Marco, HMRC compliance officer Sandra Whitmore. Randomized cross-check on last 90 days VAT returns. 12 receipt URLs texted to you, opening as a normal customer."** Marco opened first receipt (15 April £100.67 mixed lasagne+Chianti+olive oil+panettone order). Expected 2 VAT lines (VAT 20% £10.67 + VAT 0% £0.00). Saw: **SINGLE line VAT: £10.67**. Other 11 receipts same. Sandra: "48 hours, produce full breakdown or this becomes a positive finding + full audit." Engineering forensic 4-hour race: D1 query order_id — tax_breakdown_json column populated, 2-rate JSON correctly structured. Problem was receipt route apps/web-menu/src/app/api/orders/[id]/receipt/route.ts: `try { breakdown = JSON.parse(...); if (!Array.isArray(breakdown.rates) || breakdown.rates.length === 0) breakdown = null; } catch (e) { console.error("Failed to parse tax_breakdown_json", {...}); breakdown = null; }`. 90-day log grep "Failed to parse" → **0 entries**. Not firing. Engineering looked again carefully: JSON.parse wasn t throwing — JSON syntactically valid. But Array.isArray(breakdown.rates) returning FALSE because breakdown.rates had been serialized as Object (rate-string key map) instead of Array in some rows. PR #475 initial migration had written a subset with the map shape. Edge case path silently fell back to single-row variant — no console.error, no console.warn, just null. No log entry. **PR #654 batch VIII F5** 3-layer fix: **Layer 1 stable BEACON log prefix**: 3 distinct markers [BEACON:tax_breakdown_parse_rates_not_array] (Marco scenario, wrong shape), [BEACON:tax_breakdown_parse_rates_empty] (zero-entry edge case, elevated from warn to error), [BEACON:tax_breakdown_parse_exception] (truly malformed JSON). Each carries orderId + restaurantId + diagnostic field. **Layer 2 Cloudflare Logpush + Sentry alert rule**: prefixes pinned in alert rules; restaurant-level threshold > 5 occurrences/hour → PagerDuty incident → ops team page + proactive operator reach-out. **Layer 3 backfill batch**: tax_breakdown_json IS NOT NULL AND json_array_length(rates) IS NULL orders got one-off script (rates Object → Array conversion). ~847 orders affected platform-wide, spread across 18 restaurants, corrected within 30 minutes. 8 hours later Marco sent Sandra fresh 12 URLs, she opened — 2 VAT lines correctly rendered. "Cross-check passes, vendor display bug shipped fix, audit closed." Pattern: **for operator-visible compliance failure paths console.error alone is not enough — stable grep-able [BEACON:event_name] prefix required; log shipper / Sentry alert rules pin to prefix; ops team gets paged within minutes of first corrupt row.** Implementation checklist: (1) silent degradation unacceptable, every fallback emits BEACON; (2) BEACON prefix stable contract; (3) log shipper alert rule pin; (4) restaurant-level threshold PagerDuty; (5) backfill batch one-off script; (6) degradation tests (malformed JSON, empty array, wrong shape, null, undefined); (7) quarterly BEACON occurrence rate review. Bedri Bursa Inegol Koftecisi Maliye KDV audit same flow.

th

thMenu Team

thmenu.com

Found this helpful? Share it.