Square round-trip: in-person sales on the same order board

Square round-trip: in-person sales on the same order board

The one-menu-database post described the happy path: website, Square catalog, and DoorDash reading the same menu rows in Kitchen POS. That story was true and incomplete.

It was true because every digital channel we cared about already flowed through Square as the programmable layer — catalog push, inventory 86, DoorDash order mirror, website checkout finalize. It was incomplete because the terminal still lived in a parallel universe. A walk-up sale on the Square register did not reliably appear on the Kitchen POS order board. A website ticket marked preparing in admin did not move Square’s fulfillment state. Buyer phone numbers that Square already knew did not always land in the row prep staff actually read.

June closed those gaps — not with a second database, but with round-trip sync on the same SQLite order model we use for web, DoorDash, and channel partners. This post is what “round-trip” means in production on a live ghost kitchen, what we still do not pretend to sync, and why the audit doc matters more than the marketing slide.

The authority model (who owns what)

Kitchen POS remains source of truth for menu, per-location inventory, and prep status on the line. Square remains downstream for catalog projection, card capture on website checkout, and DoorDash’s operational orders. DoorDash still pays DoorDash; Square still does not magically know our affiliate pickup windows.

That split is easy to draw on a whiteboard and easy to violate at 9:47 p.m. when a cook asks, “Why is this ticket in Square but not on the board?” Round-trip work is the discipline of naming which system owns which field — then writing the boring handlers that keep them aligned.

Direction one: Square in-person (SQUARE_POS) → Kitchen POS

Before mid-June, Square in-person sales (application_details.square_product = SQUARE_POS) were visible in Square Dashboard and invisible to the rest of the stack. For a ghost kitchen that is digital-first, that sounds like an edge case until you remember: catering pickups, partner window sales that settle on the terminal, and the occasional “just ring it up” moment when the website is slow.

Commit 625b02a added flowback for in-person Square orders: the cron path in square-sync-menu.php pulls SQUARE_POS sales via SearchOrders, imports them idempotently by square_order_id, and lands them on the same orders table as website and DoorDash mirrors. Notes carry Square in-person (SQUARE_POS) so staff can tell provenance at a glance.

The import includes buyer PII when Square exposes it — name, phone, email, delivery address fields mapped into columns and attribution_json. Partner orders and web orders already had customer identity expectations; in-person sales had been a hole. Filling it means the order board stops lying by omission.

Integration tests mock SearchOrders + Payments.retrieve so we do not need live Square to prove idempotency. Production still depends on cron cadence (~15 minutes on multihost) — same reliability class as DoorDash flowback, not millisecond webhooks.

Direction two: backfill buyer PII from Square Customers API

Import-on-pull is not enough for history. Orders that predate the column or missed a field during finalize should not require someone to open Square Dashboard and copy a phone number.

06af978 and 7ce7fd4 tightened PII backfill: when customer_name, customer_phone, or customer_email are empty, Kitchen POS can recover them from Square’s order payload and, when needed, the Customers API for profile phone/email. A dedicated customer_email column joined the schema so email is a first-class field — not buried only in JSON.

3b70545 and acd6bc9 fixed what staff actually see: pickup customer names on the order board and orders list. Data in SQLite is useless if the line cook sees “Guest.” The experiment fails if humans cannot trust the board.

CLI tools/backfill_order_customer_data_from_square.php supports historical runs — a data tool, not bootstrap code. We run it when Mark authorizes a slice, not on every deploy.

Kitchen POS admin — prep status changes here can push to Square fulfillment on paid website orders.
<strong>empanadaempire.us</strong> — website checkout path that finalizes into Square, then prep syncs back after payment.

Direction three: Kitchen POS prep status → Square fulfillment (website)

The painful report that triggered Task #1124: Mark moved a paid website order through prep in admin; Square Dashboard stayed static. The audit (docs/square-pos-sync-audit.md) was blunt — no PUT /v2/orders/{id} in production handlers; prep changes updated SQLite and the Ghost Kitchen Order Board only.

That was not flakiness. It was a missing feature dressed up as integration.

e177c73 shipped P0: kitchen_pos_square_sync_prep_status runs on admin prep dropdown changes and POST /api/update-order.php when the order is paid and has a square_checkout_order_id. POS pending / preparing / ready / completed map to Square OrderFulfillment.state best-effort — failures log, never block local prep.

Staff can live in Kitchen POS for the line and still see Square catch up for website Quick Pay orders. We document dual-field reality: website rows use square_checkout_order_id; DoorDash mirror rows use square_order_id. Conflating them breaks search filters.

The admin order detail page now sets expectations in copy, not folklore: if you change prep on a paid website order, Square should move — and if it does not, the sync log is the first place to look, not the webhook dashboard.

What the cron job actually does now

public/cli/square-sync-menu.php is the heartbeat. On each run it:

  1. Pulls DoorDash orders from Square (before any push — inventory race lessons documented in the integration plan).
  2. Pulls Square in-person (SQUARE_POS) orders into local mirrors.
  3. Pushes catalog and inventory assertions to Square.

Webhooks still accelerate DoorDash when Square delivers them; we do not bet service on webhook latency alone. Website payment finalize remains browser-redirect driven at checkout-complete.php — a known P1 gap if the buyer closes the tab early.

What we still do not sync (honest boundaries)

Round-trip is not omniscience. As of this writing:

  • DoorDash prep progression from Square COMPLETED does not auto-complete POS tickets (P2 in the audit).
  • Square → POS prep for non-DoorDash orders is not fully polled — staff authority is Kitchen POS for the line.
  • Payment state after checkout is one-shot at finalize, not a standing webhook reconciliation job.
  • Category rename → Square MENU_CATEGORY still has a documented gap in the mutation matrix.

Saying so in public is part of the field experiment. Operators should not assume sync exists because two logos appear on the same slide.

How this connects to the rest of the Empire stack

  • Website orders finalize into Square, then prep pushes back after e177c73.
  • DoorDash orders still enter via Square search/webhook; inventory decrements on import.
  • Channel partners hit the same board — affiliate pickup is not a CSV export.
  • In-person Square sales are no longer a blind spot on that board.

One menu database. One order table. More directions of truth than we had in May.

Why this mattered on a Tuesday night

Ghost kitchens do not get to choose a single screen. The line cook lives on the Kitchen POS order board and the Ghost Kitchen Order Board tablet. The owner checks Square for payouts. Marketing checks DoorDash for star counts. When those views disagree about whether an order exists or whether it is in prep, the argument is never about SQLite — it is about whether the experiment is trustworthy.

Round-trip sync is boring infrastructure until it is the difference between “we have a POS” and “we have one kitchen.” Empanada Empire is too small to staff a reconciliation desk. The software either keeps the views aligned or the views become someone’s unpaid second job.

Lessons for integrators

  1. Name the authority model before you demo “integration.” Prep status owned by POS must outbound to Square if staff use both UIs.
  2. Pull in-person and marketplace on the same cron discipline — different source_names, same idempotent import pattern.
  3. PII is a column problem and an API problem — backfill paths are ops tools, not one-off SQL.
  4. Audit docs are product — Task #1124’s write-up prevented us from “fixing” webhooks when the handler never existed.
  5. Best-effort outbound sync — log and continue; never block the cook because Square hiccuped.

Empanada Empire is still a Richardson ghost kitchen learning in public. The empanadas are real; the Square fees are real; the next gap we close will probably start with someone saying, “I thought that synced.”

Questions about Kitchen POS or Square wiring: contact us.