diff --git a/eea-foh.json b/eea-foh.json
index 4cbe238..23f35fc 100644
--- a/eea-foh.json
+++ b/eea-foh.json
@@ -1,13 +1,13 @@
{
"name": "Front of House for Event Espresso",
"slug": "eea-foh",
- "version": "0.22.1",
- "download_url": "https://gitea.mulligan.casa/ee-extensions/foh-updates/raw/branch/main/eea-foh/eea-foh-0.22.1.zip",
+ "version": "0.22.2",
+ "download_url": "https://gitea.mulligan.casa/ee-extensions/foh-updates/raw/branch/main/eea-foh/eea-foh-0.22.2.zip",
"homepage": "https://gitea.mulligan.casa/ee-extensions/eea-foh",
"requires": "6.4",
"tested": "7.0",
"requires_php": "8.1",
- "last_updated": "2026-06-23 18:38:00",
+ "last_updated": "2026-06-23 19:04:23",
"icons": {
"1x": "https://gitea.mulligan.casa/ee-extensions/foh-updates/raw/branch/main/eea-foh/icon-128x128.png",
"2x": "https://gitea.mulligan.casa/ee-extensions/foh-updates/raw/branch/main/eea-foh/icon-256x256.png"
@@ -23,7 +23,10 @@
"registration"
],
"sections": {
- "description": "
Front-of-house event check-in for Event Espresso 4/5: tablet check-in stations
\n(installable PWA, zero WP chrome), device trust with admin approval handshake,
\nclerk attribution via badge codes, and a full append-only audit trail — plus a
\nwp-admin \"default mode\" page that makes it a drop-in replacement for the official
\n(stalled) EE Barcode Scanner add-on.
\nDesign contract: docs/SPEC.md. Read it before touching code.
\nStatus
\nPre-release scaffold (0.1.0). Schema and module skeleton in place; see the spec for
\nthe build plan.
\nRequirements
\n\n- WordPress ≥ 6.4 (multisite supported, per-site activation)
\n- PHP ≥ 8.1
\n- Event Espresso ≥ 4.10 (tested against 5.0.17.p)
\n- Optional:
eea-ticketing (enables camera/QR scanning of printed tickets) \n
\nDeploy
\nCopy the plugin directory into wp-content/plugins/ — built front-end assets are
\ncommitted under assets/app/; nothing to build or install on the host. Activate
\nper-site. Deactivate eea-barcode-scanner first; this replaces it.
",
- "changelog": "[0.22.1] - 2026-06-23
\nChanged
\n\n- Release now flows through the shared manifest pipeline; the \"View details\" modal carries a description, changelog, and tags. No functional change to the plugin.
\n
\n[0.22.0] - 2026-06-23
\nAdded
\n\n- Color-coded plugin icon, published to the update channel by the release workflow.
\n
\nChanged
\n\n- Declared WordPress 7.0 compatibility (Tested up to: 7.0).
\n
\n[0.21.2] — 2026-06-22
\nChanged
\n\n- Updates delegated to the Manifest Updater plugin. FOH no longer bundles its
\n own self-hosted updater (Support/Updater.php removed + its instantiation). It
\n now declares an Update URI header pointing at its foh-updates channel manifest
\n with the #manifest-updater.json trigger, so the network-active Manifest
\n Updater plugin surfaces + installs updates via WordPress's native update path.
\n Functionally identical update delivery; the point is that FOH can now run
\nsite-active instead of network-active — which removes the stray Return/
\n Settlement payment-method rows on non-desk subsites at the source (the cause of
\n the recurring \"automatically deactivated\" notice). Manifest Updater must be
\n network-activated to receive update notifications. \n
\n[0.21.1] — 2026-06-22
\nFixed
\n\n- The recurring \"payment method Return/Settlement was automatically deactivated\"
\n admin notice — root cause, surfaced on multisite. FOH is network-active (so its
\n self-hosted updater runs network-wide), and it eagerly auto-activated its two
\n internal payment methods (Return, Settlement) on EVERY subsite via
\nSchema::install, then proactively reactivated them on every admin/cron load.
\n On subsites that never run a front desk, those stray active rows had no registered
\n type in some request contexts (e.g. a network plugin update), so EE auto-deactivated
\n them and filed a sticky per-site notice — and the proactive reactivation flipped
\n them back on for EE to deactivate again, regenerating the notice in a loop. (Earlier
\n patches also cleared the notice in the wrong site's context on a network.) \n
\nThe lifecycle is now use-driven, matching how EE treats every other payment
\n method: the Return/Settlement rows are created/reactivated lazily on first actual
\n use (paymentMethodId()), which only happens on the site that records a
\n return/settlement — so no stray rows are planted elsewhere. The eager Schema::install
\n activation and the proactive reactivation are removed; the money path is unchanged
\n (it already self-heals the row on use). The PMT types are still registered on
\n every load (that is what stops EE deactivating the real active row on the desk site).
\n The stale notice is now purged through EE's own notice collection (the
\n...PersistentAdminNoticeManager__registerNotices seam), which works in any
\n admin/network-admin context and on any site — fixing the multisite mis-targeting of
\n the previous attempts. New EePersistentNotice::suppressAutoDeactivated().
\n[0.21.0] — 2026-06-22
\nAdded
\n\n- Per-method payment options end-to-end (payment-methods config Phase 3).
\n Settings ▸ Payment Methods now configures, per method: a display-label
\n override (all methods), require a check number (check), and a card
\n surcharge (card). The station honours all three — resolved label, check#-required
\n capture, and surcharge display + charge. \n- Card surcharge as a REAL fee line item (the money model). A configured card
\n surcharge is added to the transaction as a genuine, non-taxable EE fee line item
\n when the card payment is taken: the total rises by it, it prints on the receipt,
\n and it lands in EE's reports. The station launches the Collect reader with
\nbase + surcharge; the server records the gateway-verified captured amount and
\n adds the matching surcharge line (LIN_code = foh-srcg-{ref}, idempotent on
\n retry), clearing the surcharge-inclusive balance to zero. A divergence between the
\n charged amount and the configured surcharge is audited (card_surcharge_mismatch);
\n a surcharge that cannot be applied fails the record loudly rather than overpaying. \n
\nChanged
\n\n- Card retired from FOH's built-in registry; now registered by the Collect
\n add-on through eea_foh_register_payment_methods. FOH no longer carries a
\n transitional card built-in or reads eea_foh_collect_enabled. recordPayment
\n resolves card via the registry filter (which foh-collect registers unconditionally
\n at load), so the money path still resolves card on a pure REST request. Requires
\n eea-foh-collect 0.2.0+ (install it alongside this release — it supplies the card
\n registration). FOH now owns enable/disable + label + surcharge for every method,
\n including card, in one Settings section. \ncash_refunds_enabled forward-migrated and dropped (EEA_FOH_SCHEMA_VERSION
\n 18→19). On upgrade, the legacy event-wide cash-refund setting is migrated into
\npayment_methods.cash.options.allow_refunds (idempotent, gated on the pre-upgrade
\n version) and the legacy key is removed from all reads. An existing install keeps
\n its cash-refund setting; a fresh install defaults off. \n
\nHardening (adversarial money-path review)
\n\n- Surcharge is clamped to the configured amount so a concurrent payment that
\n races the balance toward zero (or a wrong amount on the reader) can't inflate it
\n into a phantom fee — any excess capture surfaces honestly as an overpaid txn. \n- A failed surcharge line-item write now fails the record loudly instead of
\n recording a payment that would overpay the transaction. \n- Server-side policy enforcement: a direct request can no longer record a method
\n the admin disabled (the UI already hid it); idempotent retries are unaffected. \n- Surcharge values are capped (percent ≤ 100, flat ≤ $999.99) so a typo can't
\n become a runaway fee. \n- Saving settings while the Collect add-on is deactivated no longer wipes the
\n saved card surcharge/label config. \n- The cash-refund forward-migration is gated on its own completion flag, so a
\n transient write failure retries instead of silently losing the setting. \n- A loud admin notice fires if the Collect add-on is active but older than 0.2.0
\n (which would otherwise let a card charge succeed at the reader and fail to record). \n
\nMigration / compatibility
\n\n- Behaviour-preserving by default. An untouched install offers exactly the same
\n methods as before (card included when Collect was enabled), check numbers stay
\n required unless an admin opts out, and the cash-refund setting is carried forward.
\n Surcharge is off until configured. The money path is unchanged except for the
\n surcharge line, which only applies when a card surcharge is configured. Sequence
\n the install: eea-foh-collect 0.2.0 first, then eea-foh 0.21.0, so card never
\n has a gap. \n
\n[0.20.1] — 2026-06-22
\nFixed
\n\n- Recurring \"payment method was automatically deactivated\" admin notice for the
\n Return and Settlement payment methods. The methods were being reactivated
\n correctly (returns/settlements kept working), but the stale persistent notice
\n kept reappearing. Root cause: EE's PersistentAdminNoticeManager loads every
\n notice into an in-memory collection and re-saves that whole collection to the
\nee_pers_admin_notices option on shutdown — so the prior fix's raw
\nupdate_option() delete was overwritten every request. The notice is now
\n dismissed through EE's own collection (dismissNotice(..., purge: true)),
\n which the shutdown save then removes for good, and the dismissal runs whenever
\n the PMD row is already healthy (not only during a reactivation). New
\nSupport/EePersistentNotice helper centralises the purge (incl. the
\nsanitize_key() collection-id vs raw option-key gotcha). Cosmetic-only,
\n best-effort, never touches the money path. \n
\n[0.20.0] — 2026-06-22
\nAdded
\n\n- Device hardware capabilities (payment-methods config Phase 2 — the capability
\n layer). Each device can now declare which payment hardware it has via a
\nHardware block in the device editor: card reader and cash drawer
\n (extensible). A payment method whose required hardware is unchecked on a device
\n is hidden on that station's point of sale — card needs a card reader, cash needs
\n a cash drawer, check needs neither. This completes capability ∩ policy ∩\n authority: PaymentsService::availableMethods / methodDescriptors now also
\n intersect the acting device's capabilities. \n
\nChanged
\n\n- New
DEV_capabilities column on the devices table (EEA_FOH_SCHEMA_VERSION
\n 17→18; the column is added by the existing idempotent dbDelta upgrade on next
\n load — additive, non-destructive). \n
\nMigration / compatibility
\n\n- Behaviour-preserving by default. Capability is visibility only — it never
\n weakens role authority (the can_take_payment / can_cash_refund caps stay the
\n gate) and never touches the money path (recordPayment and EE payment-method
\n resolution are unchanged). A device with no stored capabilities reads as
\nall-true, so every existing device offers exactly the methods it did at
\n 0.19.0 until an admin unchecks a box. Admin (Mode A) and actorless resolution
\n skip the capability layer entirely. \n
\n[0.19.0] — 2026-06-22
\nAdded
\n\n- Payment-methods configuration (Phase 1). A new data-driven method registry
\n replaces the hardcoded ['cash','check','card'] list: cash and check are built-in,
\n and additional methods can be contributed through the new
\neea_foh_register_payment_methods filter (card is the first such method, bridged in
\n during this phase pending foh-collect's own registration in a later phase). \n- Settings ▸ Payment Methods. One row per registered method — enable/disable cash
\n and check, set the cash-refund policy (the former \"Allow Cash Refunds\" toggle now
\n lives here as the Cash method's option), and a read-only Card row showing its status
\n (card-present is still provided by the Collect for Stripe add-on's own toggle). \n- Per-profile payment-method override. An access profile can now inherit the global
\n enabled set (default) or override it to a restricted subset — it can switch a method
\n off for that station but never enable one disabled globally
\n (PRO_scope.payment_methods = { inherit, methods[] }). \n
\nChanged
\n\nPaymentsService::availableMethods / methodDescriptors / resolvePaymentMethod now
\n iterate the registry and apply the policy (global ⊕ profile) ∩ role-authority
\n intersection instead of a hardcoded list. The money path is unchanged:
\nrecordPayment(slug) and EE payment-method resolution semantics are preserved, and
\n role capabilities (can_take_payment / can_cash_refund) remain the authority. \n
\nMigration / compatibility
\n\n- Behaviour-preserving by default. An untouched install behaves exactly as 0.18.x —
\n all methods enabled, card offered when Collect is enabled, cash refunds keyed off the
\n migrated payment_methods.cash.options.allow_refunds. \n- Non-destructive read-through migration. The legacy
cash_refunds_enabled and
\n foh-collect's eea_foh_collect_enabled options stay readable; the new
\npayment_methods structure is derived on read from them (plus registry defaults)
\n until an admin saves the new section, at which point cash/check become authoritative.
\n Card stays read-through to its own plugin's gate. No schema change. \n
\n[0.18.2] — 2026-06-22
\nSecurity
\n\n- Server-side ticket-category scope on the sell write path.
SellService::createSale
\n now re-resolves the acting profile's allowed ticket ids (the same eea_foh_scope_tickets
\n path the catalog read uses) and refuses any line whose ticket is out of scope. Previously
\n only the READ path (catalog) was scoped, so a clerk restricted to one category could POST a
\n ticket id from another category on the same event. Fail-closed; admin (no profile) and
\n unrestricted profiles are unaffected. \n
\nFixed
\n\n- Check-number dedup scoped to the transaction. A check number is only deduped within its
\n own transaction now, so the same check number legitimately reused across different
\n transactions (different payers/visits) is no longer rejected as a duplicate. Card charge-id
\n dedup stays global (replay protection). \n- Return payment fails loud on an unresolved Return PMD instead of silently writing an
\n unclassifiable PMD_ID=0 onto a live money row (mirrors the settlement guard). \n- Customer-credit reversal is now confirmed + loud.
reverseCarrierPayment returns a hard
\n failure when the payment delete does not take, and both CustomerCreditFlow call sites
\n (settle-store-credit, pay-with-credit) capture that result and leave a loud reconciliation
\n trail — no more silent \"nothing changed\" on a transaction left wrongly paid. \n- Incident report is honest.
POST /station/incident returns recorded:false when the
\n audit insert fails (was always true). \n- Badge scan feedback. A no-match scan plays the denied tone + a \"Badge not found\" toast; a
\n multi-match scan plays the already tone + a \"Multiple matches — select one\" toast (single-match
\n auto-action unchanged). \n- PWA won't reload mid-incident.
isAppBusy now also blocks on an open
\n.incident-modal-overlay so a pending \"Card charged — NOT recorded\" dialog can't be dismissed
\n by an idle/update reload. \n- Toast timer cancels a prior dismissal so a rapid second toast isn't clipped early.
\n- Service worker no longer evicts the companion PWA's cache. The activate-cleanup is
\n scoped to FOH's own eea-foh- cache prefix — caches.keys() is per-origin, so the prior
\n blanket deletion was wiping the attendee-facing companion app's eea-ec-* offline caches on
\n every FOH update (both apps share the subsite origin). \n
\nChanged
\n\n- Device User-Agent is sanitized at enroll; the
tendered arg on apply-payment and the incident
\n args are declared in the REST schema (incident kind validated at the schema layer); the
\n Enroll Links admin page routes its action off the query string only (matches the other admin
\n pages). \n
\n[0.18.1] — 2026-06-22
\nAdded
\n\n- Check payments at the desk. FOH now activates Event Espresso's built-in Check payment method in
\n the admin scope (non-disruptively — it adds admin scope without removing any existing public-cart
\n scope), so a clerk can take a check tender and the payment carries a real, classifiable PMD_ID. The
\ncheck descriptor + the client check-number prompt were already wired; this makes Check available. \n
\nSecurity
\n\n- Server-side event-scope enforcement on every transaction/registration-bound endpoint
\n (PaymentController: paymentInfo, applyPayment, completeTransaction, settle, payCredit, emailReceipt;
\n ReturnController: reverse-sale, exchange, returnable-items). A station clerk can now only act on a
\n transaction/registration whose event is within their profile scope — closes an IDOR where a
\n sequential id from another event drove a money/inventory/PII action. Fail-closed; Mode A admin
\n unscoped. \n- Clerk/manager code hardening. Codes widened from 6-digit (10^6) to 10-digit numeric (10^10) and
\n a per-device brute-force throttle (escalating lockout, loud audit) now guards clerk login, assist
\n login, and the force-checkin override. A remote \"Clear lockout\" action on the Devices admin page
\n lets a manager unstick a locked tablet. The attempted override code is no longer written to the
\n audit log in plaintext. \n
\n[0.18.0] — 2026-06-22
\nSecurity
\n\n- Card-present payments are now verified against the gateway before approval. A
card payment
\n previously recorded as PAID from a forgeable redirect-return URL with no gateway check. applyPayment
\n now fails closed: it requires a verifier (the eea-foh-collect companion) to confirm the charge
\n succeeded at Stripe and reports the gateway's true captured amount — which is recorded in place of the
\n client-supplied figure. A transient gateway-unreachable result is distinguished from a real decline so
\n the clerk reconciles rather than re-charging. \n
\nFixed
\n\n- Idempotency on every first-touch money write. create-sale, exchange, cash apply-payment,
\n pay-with-credit, and both settle paths now thread a stable idempotency key so a lost-response retry on
\n flaky Wi-Fi can no longer double-apply (double sale/charge/credit-debit). create-sale anchors on a txn
\n extra-meta key; exchange threads it through the replacement sale. \n- Audit failures are loud.
AuditRepo::log() now returns a status and error_logs the action + DB
\n error + row on a failed insert, so a lost audit row in the loud-failure channel can never vanish. \n
\n[0.17.2] — 2026-06-21
\nFixed
\n\n- Return / Settlement payment methods now reactivate themselves. Event Espresso auto-deactivates
\n a payment method on any request where its add-on isn't loaded — which includes the brief
\n deactivate→reactivate swap of a plugin update (and multisite/network contexts where FOH isn't
\n active). Previously the rows stayed off until someone re-activated them by hand, breaking returns
\n and settlements silently. FOH now detects the orphaned-inactive row on the next admin/cron load and
\n at money-path use-time, reactivates it (admin-scope only), and clears EE's stale \"automatically
\n deactivated\" notice. No manual step after an update, ever. \n
\n[0.17.1] — 2026-06-21
\nAdded
\n\n- Walk-up Return. A card-less entry to the Return / Exchange screen (a \"↩ Return\" button on the
\n check-in screen) for a buyer who purchased earlier and came back with no registration to look up. \n
\nFixed
\n\n- The sales process now verifies the transaction's TRUE status before completing. Exiting the
\n payment screen no longer falsely shows \"Sale Complete\" or auto-checks-in an unpaid sale — checkout
\n re-queries Event Espresso for the real paid/balance state and shows an honest \"Payment Not
\n Completed\" result when nothing was collected. Returns/exchanges apply the same check and flag an
\n unresolved refund or balance rather than passing silently. Fail-safe: an unverifiable status is
\n treated as unpaid, never as done. \n
\n[0.17.0] — 2026-06-21
\nAdded
\n\n- Admin-configurable branding. Four operator-set images on the Front of House Settings
\n page (WP media picker): receipt logo, app icon, splash, and station background. The app icon
\n overrides the PWA icon/manifest; the receipt logo is rasterized onto thermal receipts
\n (resilient — a missing/failed logo never blocks the money path). \n- Bluetooth badge printing in wp-admin. A \"Print badges over Bluetooth\" page on the Clerks
\n screen prints each active clerk's badge (CODE128 + QR) over a thermal printer ONE at a time,
\n with a Next button so the operator can tear between prints. Built as a self-contained admin
\n bundle so the station bundle is unchanged. \n
\n[0.16.2] — 2026-06-20
\nFixed
\n\n- Service worker REST namespace
eea-checkin/v1 → eea-foh/v1 so the never-cache-API guard
\n matches the live namespace (the old pattern was a dead branch). No behavior change on prod. \n
\n[0.16.1] — 2026-06-20
\nChanged
\n\n- Self-hosted update channel + repository URLs moved to the
ee-extensions org.
\n The in-plugin updater now reads the org-owned public manifest; no change on the
\n running site beyond the source of updates. \n
\nFixed
\n\n- Removed a hard-coded external domain from the receipt printer's barcode/QR test
\n page; it now prints a neutral placeholder URL. \n
\n[0.16.0] — 2026-06-19
\nChanged
\n\n- Taking payment is now a lookup filter, not a separate mode. \"Take Payment\" was
\n never its own surface — it was the registration search rendered as payment cards. It's
\n now a segmented filter on the lookup screen: All · Needs check-in · Needs payment
\n (shown when the profile enables payment and the clerk can take payment). The \"Needs
\n payment\" lens narrows to registrations with an outstanding transaction and renders
\n payment cards (+ POS scan-to-pay); \"All\" is the prior check-in behavior; \"Needs
\n check-in\" is a new lens. Removes the empty-page confusion of the old mode. Sell remains
\n its own mode. No schema or server change. \n
\n[0.15.0] — 2026-06-19
\nClerk badge printing + scan-to-login (camera or wedge). No schema change (v16).
\nAdded
\n\n- Printable clerk badges. The Receipt Printer modal gains \"Print clerk badges\" —
\n prints a badge per active clerk carrying their login code as both a CODE128 barcode
\n (hardware wedge scanners) and a QR (camera). Backed by a manager-gated
\nGET /station/clerk-badges. (Clerk codes are attribution codes, not secrets — treat a
\n printed badge as a physical login key.) \n- Camera scan-to-login. The clerk-login screen and the assist-login modal can scan a
\n badge with the device camera (reusing the check-in scanner) — a decode fills the code
\n and submits. Wedge-scanner login already worked; this adds the camera path for stations
\n with no wedge scanner. \n- Per-device camera configuration. Device Settings → Cameras: detect cameras and pick
\n which camera each scanner uses (login vs check-in may differ), plus an \"auto-start
\n scanner on login\" toggle. Cameras are enumerated when the device is approved. \n
\nChanged
\n\n- The camera scanner is centrally torn down on every view transition and handed off
\n between scanners (check-in ⇄ assist login) so only one holds the camera at a time;
\n html5-qrcode stays lazy-loaded (no initial-bundle weight). \n
\n[0.14.2] — 2026-06-18
\nAdded
\n\n- Receipt Printer configuration modal. Device Settings → Receipt printer →
\nConfigure › opens a dedicated modal (driver selector, Bluetooth pair/connect +
\n status, tear-feed) with on-device test buttons — \"Test receipt\" (a sample receipt
\n through the real encoder) and \"Test barcode\" (CODE128 + QR) — so a clerk can prove the
\n printer without ringing up a sale. Replaces the inline drawer controls; being a real
\n.modal-overlay it's also immune to the focus-reclaim issue by construction. \n
\n[0.14.1] — 2026-06-18
\nFixed
\n\n- Settings drawer form controls were unusable on touch devices. The station's
\n focus-reclaim (which keeps the barcode-wedge scanner alive) only recognized
\n.modal-overlay as a blocking overlay, not the Settings drawer (.drawer-backdrop)
\n or the payment-incident modal. Tapping the receipt-printer <select> (or the tear-feed
\n input) got focus yanked back to the search box ~10ms later — the dropdown wouldn't
\n open and the soft keyboard popped over the drawer. isModalOpen() now recognizes the
\n drawer and the incident modal, so their controls work. \n
\n[0.14.0] — 2026-06-18
\nBluetooth thermal receipt printing, templated header/footer, itemized receipts, and
\nloud failure handling for the card-present path. No schema change (v16).
\nAdded
\n\n- Bluetooth thermal receipt printing. A Web Bluetooth (BLE GATT → ESC/POS) driver
\n prints receipts directly from the station PWA to a 58mm thermal printer (Munbyn IMP001
\n class) — pair once, reconnect-on-demand at print time, never a false success on a dead
\n link. Select \"Bluetooth (thermal)\" in Device Settings → Connect. Requires HTTPS. \n- Templated receipt header/footer. Plain-text header/footer with
{token}
\n substitution, authored globally in Check-In Settings with a per-access-profile override.
\n The server ships the raw template + token values; the client substitutes in one pass.
\n The transaction body (totals) is always generated, never templated. \n- Itemized receipts. Receipt body carries line items (tickets + taxes) with 32-col
\n money alignment, derived from the EE line-item tree. \n- Configurable tear feed. Per-device trailing line-feed count for the manual tear.
\n- Loud charged-but-unrecorded handling. When a Collect card charges but the record
\n against the transaction fails, the station no longer swallows it: it retries, then
\n escalates across every channel — structured log, persisted incident, a printed manager
\n error slip (both our IDs and Stripe's chargeId/amount/status), a POST /station/incident
\n audit beacon, and a blocking manager modal (retry / reprint / acknowledge). \n
\nChanged
\n\n- The ESC/POS layer (
escpos.ts) is now generic primitives + a composeDocument frame;
\n each print context (transaction receipt, error slip) composes its own body. \n
\n[0.13.0] — 2026-06-18
\nFOH returns, refunds & customer credit — the v1.0.0-blocking foundation. Full design
\nin docs/RETURNS-CREDIT-IMPLAN.md.
\nAdded
\n\n- Real transaction for returns & exchanges. The return/exchange path now creates one
\n real EE_Transaction (POST /station/exchange): replacement items are real ticket
\n purchases (variation pricing + stock decrement, sold-out rolls the whole thing back),
\n returned goods are recorded as a typed +Return payment (a first-class EE_PMT_Return
\n encoding {ticket_id, option_id, qty}), and the server computes the net. Restock fires on
\n approval via the existing inventory hook. Replaces the old transaction-less store-credit
\n IOU that returned no transaction id. \n- Settle credit owed (cash refund / forfeit). When a return/exchange overpays a
\n transaction, the Transaction screen offers a settlement: Cash refund (money out of the
\n drawer — gated on a new can_cash_refund role cap and an event-wide \"cash refunds
\n enabled\" toggle) or Forfeit (the books-balancing \"keep it\"). Each records a typed
\n−payment clamped to what's owed, idempotency-keyed, stamped with the settle type +
\n acting clerk. \n- Store credit + pay-with-credit (requires the new eea-customer-credit plugin).
\n- Store credit settle: the \"Store credit\" button on credit-owed opens a dedicated
\n customer lookup/create screen (email-keyed; echoes name + current balance), then parks
\n the owed credit on the customer's account. \n- Pay with store credit: a \"Store credit\" payment-method button on a balance-due
\n transaction looks up the customer, shows their available balance, and pays toward the
\n balance — refused (never clamped) when the account can't cover it. \n- Both go through the plugin's secure seam (the balance move is derived server-side from
\n the real payment, one move per payment); checkin keeps a one-way dependency and hides
\n the affordances entirely when the plugin is absent. \n- Suppress tax/fees on exchanges (per access profile, default on). A like-for-like
\n exchange nets a return against the replacement, so EE re-charging full tax on the
\n replacement double-taxes the swap. Exchanges now mark the replacement non-taxable (taxed on
\n the net of $0); pure sales and pure returns are unaffected. Toggle on the profile to
\n restore per-item tax. \n
\nFixed
\n\n- \"No route was found matching the URL\" on settling a return. The return path was
\n transaction-less, so the client settled on an undefined transaction id and 404'd. Returns
\n now create a real transaction with a real id. \n- Exchanges double-taxed a like-for-like swap (the patron was left owing the tax/fee on a
\n net-zero exchange). See the exchange tax suppression above. \n
\nNotes
\n\n- Schema v15 (Return payment method) → v16 (
can_cash_refund cap + Settlement method). \n- The FOH app stops calculating money client-side: \"owed\" and balances are read consistently
\n from the server-reported remaining balance. \n
\n[0.12.3] — 2026-06-16
\nAdded
\n\n- Automatic PWA updates (safe for a live station). The app now polls for new
\n service-worker versions (every 5 min + on returning to the foreground) and auto-applies
\n an update — but only when the station isn't busy (no open modal/transaction/checkout, no
\n assist mode, no pending card redirect, empty search, empty cart), via foreground-return or
\n ~60s-idle triggers, plus a tappable \"Update ready ↻\" pill for on-demand refresh. Reload-
\n loop guarded. After this build is on a device, future deploys self-apply — no manual reopen. \n
\nFixed
\n\n- A–Z search bar no longer leaks into Sell mode. It's now part of the registration-search
\n surface, so it only shows in Check-In / Take-Payment (where the search exists), not in the
\n Sell/POS grid. \n
\n[0.12.2] — 2026-06-15
\nChanged
\n\n- Card is strictly full-remaining. The card/redirect method ignores any edited partial
\n amount and always charges the entire balance (so a card payment always settles the txn).
\n Partial/split payments are taken cash/check-first (in-screen, leaving the balance open),
\n then card clears the rest. True arbitrary splits across methods can come later. \n
\n[0.12.1] — 2026-06-15
\nAdded
\n\n- Cash tender + change. Taking a cash payment now prompts for the amount given with a
\n live change-due readout and Square-style quick-tender buttons (Exact + common bills).
\nPAY_amount stays the applied charge; the tendered and change are recorded on the
\nEE_Payment (PAY_extra_accntng human-readable + PAY_details structured cents) and shown
\n on the detail-modal payments list. The success screen calls out the change to hand back. \n
\n[0.12.0] — 2026-06-15
\nAdded — card-present payments (pluggable; Collect verified on-device)
\n\n- Card-present via a pluggable provider model. Core stays provider-agnostic: a
\nredirect-kind payment-method descriptor carries its own launch template + return
\n param-mapping + record_method, so the PWA drives the deep-link launch and the
\n/checkin/payment-result return entirely from the descriptor — zero provider code in
\n core/PWA. Cash/check remain the only core built-ins. \neea-checkin-collect addon plugin (separate, scoped): registers the card method via
\neea_checkin_payment_methods with the device-confirmed Collect-for-Stripe contract
\n (status/chargeId/finalAmount, GET) and owns its own enable toggle via the new
\neea_checkin_settings_form_extra / eea_checkin_settings_saved hooks. A future Square
\n provider is an addon of the same shape — no core/PWA changes. \n- On-demand receipt:
POST /station/email-receipt fires EE's own EED_Messages::payment()
\n (the same branded receipt EE's processor sends); the success screen offers an \"Email
\n receipt\" action that captures an email on the spot when the walk-up didn't give one. \n- Validated end-to-end on the real device: M2 reader, Collect's https callback contract, and
\n the WebAPK in-scope return landing in the installed PWA (no assetlinks.json needed). \n
\nChanged
\n\n- The
eea_checkin_payment_methods hardener now preserves record_method/launch/return
\n for addon-provided descriptors and drops any malformed redirect descriptor. \n
\n[0.11.0] — 2026-06-15
\nAdded — Sell / POS mode (front-desk sales)
\n\n- Sell mode (third station mode; per-profile
modes includes sell ∩ clerk
\ncan_take_payment): a Square-style item grid (category tabs derived from ticket
\n class — Admission / Meals / Apparel / …) + a persistent cart (quantities, running
\n total) → native registrant-questions step → POST /station/create-sale →
\n the shared Transaction screen (cash/check) → check-in of the new registrations →
\nFinish. Tablet-first, enterprise styling, all four themes. \n- Server:
SellService + SellController with GET /station/ticket-catalog,
\nGET /station/ticket-questions, POST /station/create-sale. create-sale builds the
\n transaction + line-item tree + per-item attendee/registration/answers through EE's own
\n models (admin/service context — so selling after sale-end / sold-out is a non-issue),
\n with automatic tax, RAP status, and full transactional rollback on any
\n failure (no orphaned rows). Attendee fields mapped via EE's
\nsystem_question_to_attendee_field_mapping(). Sold-out / sale-ended tickets are shown
\n but flagged (still sellable in admin context). \n- Native questions render with the new
eea_checkin_registration_questions filter,
\n so ticket-class question-hiding (-cond) applies in Sell exactly as in SPCO. Non-input
\n EE pricing types (monetary/pricing) are excluded. \n
\nChanged — core is ticket-native; ticket classes moved to the companion
\n\n- Core
eea-checkin no longer knows about ticket classes (a concept added by the
\n third-party eea-ticket-classes plugin). The profile \"Ticket Class Filter\" (PCRE) is
\n replaced by an EE-native ticket multi-select (PRO_scope.tickets; none = all), and
\n search/check-in/catalog scope by ticket IDs. Four hooks let the companion layer classes
\n back on: eea_checkin_scope_tickets, eea_checkin_ticket_catalog,
\neea_checkin_profile_form_extra, eea_checkin_profile_saved. eea-ticket-classes now
\n owns the class-filter config (its own option), class-based scoping, and catalog
\n categories — alongside the question-hiding it already did. No Round-Up specifics in core. \n
\nNote
\n\n- Card-present (Collect) payment in Sell is intentionally deferred — it reuses the
\n existing pluggable registry, and the card method stays disabled until the Collect device
\n round-trip is confirmed. Cash/check work now. \n
\n[0.10.0] — 2026-06-15
\nAdded
\n\n- Assistive Login (schema v14): a role capability
ROL_can_assist lets a permitted
\n clerk temporarily log in over an active clerk on a device — a stacked session, not a
\n replacement. The base clerk session is preserved untouched and resumes on exit (its idle
\n timer revived). Endpoints POST /station/assist-login + /station/assist-exit
\n (SES_kind/SES_parent_id); assist-mode chrome = a contrasting viewport border, an
\n \"Assisting as {name}\" indicator, and a bottom-left Exit Assist button; UI state is
\n preserved across entry/exit. Gated on can_assist and the assisting clerk's station
\n scope (audited). Plugin option assist_invalidates_other_devices (default off) ends the
\n assisting clerk's other-device sessions on assist-login. \n- Per-device themes + manager Device Settings: four CSS-only themes via
data-theme
\n (Corporate (System) — default, follows OS light/dark — plus Dark Pro, Light, System),
\n persisted per-device, chosen from a manager-only Device Settings panel in the menu. \n- Q&A + payments in the details modal: the role-gated detail now renders the registrant's
\n questions & answers (EE answers via the eea_checkin_registration_answers hook) and a
\n transaction payments breakdown. \n
\nChanged
\n\n- Enterprise visual refinement: calmer, more spacious commercial look — glows replaced by
\n crisp borders + subtle elevation, tabular-num money, tighter type/spacing scale, blurred
\n modal backdrops, :focus-visible rings, full prefers-reduced-motion support. \n- A–Z search bar: letters-only (A–Z, no digits) for fast last-name lookup, ⌫ retained, and
\n a bottom ⌨ key that drops into a transient keyboard (for email/address) which
\n auto-reverts to the A–Z bar on input blur. \n
\n[0.9.0] — 2026-06-15
\nAdded — station modes, in-person payments, corrected check-in model (SPEC §16)
\n\n- Station modes (§16.5): the station is now modal — Check-In and Take Payment —
\n with a top-bar mode switcher (per-mode accent so the active mode is unmistakable). Mode
\n availability = a per-profile modes allow-list ∩ clerk capability (schema v12;
\n Take-Payment requires can_take_payment). \n- Pluggable payment methods (§16.4): server registry exposed via the
\neea_checkin_payment_methods filter producing hardened descriptors
\n{slug,label,kind,available,ordering,capture?}; the client dispatches by kind
\n (instant cash/check, redirect card). New method = new descriptor, no core change. \n- Shared Transaction screen +
POST /station/apply-payment /
\nGET …/payment-info: take cash/check now; Collect card is scaffolded (descriptor +
\n/checkin/payment-result shell route) but available:false behind the
\neea_checkin_collect_enabled option until the M2 device round-trip is confirmed. \nPOST /station/complete-transaction: finalizes an incomplete, zero-balance
\n transaction via EE's own update_status_based_on_total_paid() +
\nupdate_transaction_and_registrations_after_checkout_or_payment() (guarded, idempotent). \n- Q&A + payments in the details modal (§15.11): the role-gated detail endpoint now
\n carries the registrant's questions & answers (via EE_Registration::answers() →
\nEE_Answer::pretty_value()) behind the eea_checkin_registration_answers filter
\n (so ticket-class question-hiding wires in at the enrichment layer, not core), plus a
\n transaction payments breakdown and balance/complete. \n
\nChanged — the visibility ↔ action model (corrected; general EE, not Round-Up-specific)
\n\n- Visibility = registration status, configured per profile via
PRO_checkin_statuses
\n (schema v13; default RAP = approved-only). Drives the search status-filter; the
\n existing Force-Check-In covers any visible non-approved card. Approved-only visibility
\n structurally hides abandoned/pending carts with zero event-specific code. \n- Action = the owning transaction's completeness, not its balance. Cards carry
\ntxn_status + txn_complete; the check-in payment gate and the Take-Payment affordance
\n fire on txn_id > 0 && !txn_complete (an incomplete transaction needs handling even at a
\n $0 balance). The gate is role-governed client-side workflow (managers override; payment-
\n capable clerks resolve inline; others get a manager-override prompt). \n
\nPolished
\n\n- Full visual pass to a refined POS look: pill mode switcher with per-mode glow, richer
\n result cards + chips, the Transaction screen centerpiece (tabular-nums balance, method-
\n button states, a rewarding \"Paid in full ✓\" success), blurred modal backdrops,
\n:focus-visible rings, and comprehensive prefers-reduced-motion support. \n- A–Z search bar refined: letters-only (A–Z, no digits) for fast last-name lookup,
\n ⌫ retained, and a bottom ⌨ key that drops into a transient keyboard mode (for
\n email/address) which auto-reverts to the A–Z bar on input blur. \n
\n[0.8.0] — 2026-06-15
\nAdded — in-person payment foundation (SPEC §16.1; POS epic begins)
\n\n- Record-payment service (
PaymentsService::recordPayment): records an EE_Payment
\n and finalizes via EE's own EE_Payment_Processor::update_txn_based_on_payment — applies
\n to registrations, sets paid, recomputes transaction status, toggles RPP→RAP, fires the
\n standard hooks/emails. Idempotent on the Stripe charge id / check number. The same path
\n EE's admin \"Apply Payment\" uses; the landing pad for cash/check and Collect card. \nPOST /station/apply-payment + GET /station/transaction/{id}/payment-info
\n (balance + which methods resolve to an active EE payment method). Gated by a new role
\n capability can_take_payment (schema v11; manager seeded on, clerk off). \n- Verified on the dev EE install: an unpaid transaction → check payment →
TCM/paid,
\n idempotent re-run no-ops. cash/check resolve to EE's offline methods; card→
\nStripe_Onsite (live only). \n- Not yet wired: the PWA take-payment UI and the Collect deep-link/return — next.
\n
\n[0.7.2] — 2026-06-14
\nAdded
\n\n- Role-gated registration details modal (new role flag
ROL_can_view_details,
\n schema v10): a clerk whose role permits it can tap a result row to open a modal with
\n the fuller record (contact, ticket, status, group/transaction, check-in history, linked
\n context). Server-enforced on the detail endpoint (a real PII gate, 403 otherwise),
\n not just hidden UI. New-install manager gets it; clerk does not. \n- A–Z search bar: an opt-in toggle (small/portrait screens) swaps the soft keyboard
\n for a vertical right-edge A–Z scrubber (tap to append, one ⌫ to delete); choice is
\n remembered. Wedge/camera scan unaffected. \n
\nChanged
\n\n- PIN pad no longer raises the Android soft keyboard (
inputmode=\"none\") — the
\n on-screen keypad, wedge scanner, and paste still work. \n
\nFixed
\n\n- Manager action-bypass (v0.7.1) and the details flag now take effect on the first
\n clerk login, not only after a reload (handleClerkLoggedIn wasn't carrying the role
\n capability flags into the station view). \n
\n[0.7.1] — 2026-06-14
\nChanged
\n\n- Managers override the per-station action gate. A clerk whose role has
\ncan_override may lookup / check-in / check-out / undo on any station, regardless
\n of the profile's allowed actions (Actor::allows() bypass). The clerk-login payload
\n now carries can_override, and the station shows the full action set (incl. Check Out /
\n Undo) when a manager is signed in. The existing force-checkin override is unchanged. \n
\n[0.7.0] — 2026-06-14
\nAdded — linked registrations (SPEC §15.10)
\n\n- Reverse-lookup of related registrations (schema v9,
eea_checkin_reg_links):
\n finding one person can surface their related regs as extra, independently-checkable
\n rows — so a meals clerk searching the eater's name finds the meal that's stamped with
\n the buyer's name, and a group purchase surfaces its transaction-mates. \n- Deterministic where it can be, imported where it can't: transaction siblings are
\n live (WHERE TXN_ID = ?, per-profile \"Surface transaction siblings\" toggle); the
\n subtle cross-name/cross-order meal→eater resolution is imported from the offline
\n reconciliation resolver (no PHP re-derivation, no divergence) via
\nwp eea-checkin import-reg-links <csv> [--relation] [--replace] (refs accept REG_ID or
\n REG_code). \neea_checkin_search_results filter (the \"results mutator\") + ResultCard.linked
\n ({via_reg_id, relation, flag?, note?}); the station renders linked rows subordinate,
\n with a caution treatment for flag:'caution' (e.g. a meal with no admission). \n
\n[0.6.3] — 2026-06-14
\nAdded
\n\n- Pluggable row enrichment: an
eea_checkin_search_row filter
\n ($card, EE_Registration $reg, ['dtt_id'=>int]) lets a site/add-on push
\n display-ready items onto $card['extra'] ([{label, value, tone?}], hardened
\n by normalizeExtra()); the station renders them as compact chips under each
\n result. Core stays event-agnostic. \n- Example enricher (
examples/roundup-2026-enrichment.php, a standalone
\n mu-plugin — NOT part of core): surfaces this event's merch sizes (QST 22-25) and
\n meal plan (QST 12, d:N decode) on the row. \n
\n[0.6.2] — 2026-06-14
\nChanged
\n\n- Clerk timeout combine rule (SPEC §15.3): the effective timeout is now the
\nlowest-absolute magnitude across role / station / per-clerk, and zero-tolerance
\n if any contributor is negative. 0/blank = never (a no-op, excluded). A negative's
\n magnitude is an idle cap layered on zero-tolerance: -30 = logout on focus-loss AND a
\n 30-min idle cap; -0 = focus-loss logout only. Resolved shape is {ztol, minutes}, so
\n a station can run BOTH the idle countdown and instant focus-loss logout. \n
\nFixed
\n\n- Profile edit corrupted the validity window → device lockout. Saving a profile with
\n no start/expiry wrote 0000-00-00 00:00:00 (the edit path coerced null through a %s
\n placeholder), which read back as \"expired in year 0\" → ec_profile_expired locked the
\n bound device. update() now writes real NULL; reads normalize any zero-date to null
\n (so an already-corrupted profile un-expires itself on read — re-save to clean the row). \n- \"Copy\" on enrollment-link launch URLs failed on plain HTTP (the Clipboard API is
\n undefined outside secure contexts) — now uses execCommand('copy') with a Clipboard-API
\n fallback and a Ctrl+C prompt. \n
\n[0.6.1] — 2026-06-14
\nAdded — clerk → station scoping (SPEC §15.9)
\n\n- Clerk → access-profile scoping (schema v8): a role carries an allowed-stations set
\n (none = all); a clerk may narrow it (intersection). A clerk can't sign into — or keep
\n operating — a station they're not cleared for (ec_clerk_not_authorized, audited
\nclerk_denied); enforced at clerk-login and re-checked per request. Role + clerk
\n multiselects in admin; opt-in (ships unrestricted). \n
\nAdded — custom roles & stateful clerk sessions (SPEC §15)
\n\n- Configurable roles (
eea_checkin_roles, schema v7): per-role timeout, multi-login
\n policy, and \"can authorize a forced check-in\" capability. New Clerk Roles admin page;
\n seeds clerk (60 min) and manager (2 min, can-override). Clerks remain custom
\n PIN/badge identities — deliberately NOT WP users (keeps volunteers off the MU user
\n table and out of wp-admin). \n- Stateful clerk sessions (
eea_checkin_clerk_sessions): clerk tokens are now v2,
\n session-bound; every clerk request validates a live session row (one indexed read).
\n Enables force-logout. \n- Bump / force-logout: log a clerk out of one device, or everywhere; a live
\n \"Logged-in clerks\" roster on the Devices page; revoking a clerk ends all their sessions.
\n A new login on a shared tablet ends the previous operator's session there. \n- Effective timeout = strictest wins across role, station, and optional per-clerk
\n override. Sentinel encoding: N minutes, 0 = never, negative (incl. -0) =
\n zero-tolerance (logout on any focus loss). 12-hour hard cap removed. \n- Station idle/focus lifecycle: a confirmation modal (clerk nickname + live countdown,
\n Continue / Log out) on idle-past-¼-timeout, blur, or refocus; never mode skips it;
\n zero-tolerance logs out instantly on blur/sleep/refocus. clerk-heartbeat endpoint
\n slides the session; tokens refresh on heartbeat and check-in. \n
\nChanged
\n\n- A clerk login is now always required to operate a station (search + check-in). The
\n per-profile \"Clerk Required\" toggle is removed; PRO_clerk_timeout is relabeled
\n \"Clerk timeout (station cap)\" and feeds the effective-timeout calculation. \n- Manager-override / badge \"MANAGER\" tag is driven by the role's can-override capability
\n instead of a hardcoded manager role. \n
\nFixed
\n\n- New error states
ec_clerk_bumped (session force-ended) and ec_clerk_expired (idle
\n timeout) drop the station to clerk-login with distinct copy. \n
\n[0.5.1] — 2026-06-14
\nSecurity
\n\n- Closes a bootstrap privilege-escalation hole. Through v0.4 the profile token
\n was shared (one per profile), posted as a QR, and doubled as the operating
\n credential; combined with on-screen (public) device UUIDs and a device-secret that
\n auto-issued to whoever polled /session first, a leaked low-value token + a glimpsed
\n UUID could claim a higher-value bound device's secret during the pre-secret window.
\n v0.5 removes the coupling at the root (see below). Verified end-to-end on the dev
\n stack: a foreign enroll token + a victim UUID with no/wrong nonce now gets
\nec_bad_register_nonce and no secret. \n- Secret is now the sole operating credential. Neither a profile token nor an
\n enroll token can drive a station request. Operating endpoints require a valid
\nX-EEA-Checkin-Secret; an enroll token can no longer even read a live station's
\n config once a key has been claimed (ec_secret_required). \n- Removed the
setSecretHash() repo method — resetting a key now goes through
\nrequireReenroll(), which clears the nonce and returns the device to Pending so a
\n key can never be silently re-auto-issued into a reopened window. \n
\nAdded
\n\n- First-class enrollment links (Schema v6). A new \"Enrollment Links\" admin page
\n manages enrollment credentials decoupled from profiles: label, optional suggested
\n profile (pre-fills the approval binding), optional expiry, revoke/delete, and a
\n re-displayable QR (/checkin/#e=<token>). Deleting a link never breaks already-
\n approved devices — they operate by their secret. \n- Nonce-sealed secret claim.
register-device returns a one-time register_nonce
\n only on first registration (a repeat register for a known UUID is a touch — no new
\n nonce, the anti-hijack property). The device-secret is then issued only to the holder
\n of that nonce, atomically (UPDATE … WHERE DEV_secret_hash IS NULL), so a racing
\n second caller is shut out. \n
\nChanged
\n\n- Profiles page drops every launch-link affordance (the profile token is now dormant);
\n there is nothing to copy, show, or regenerate there. \n- \"Reset device key\" → \"Reset & require re-enrollment\": clears the secret and nonce,
\n returns the device to Pending, preserves the binding. \n- Station PWA boots from
#e= enrollment links, captures the nonce and secret, and
\n authenticates secret-only thereafter; new screens for ec_bad_enroll,
\n re-enroll-needed, ec_secret_required, unbound-device, and the dead #t= link. \n- Device approval is now a modal that requires a nickname and a station/location
\n before approving — no more approve-then-it-does-nothing unbound devices. \n
\nFixed
\n\n- Station no longer shows \"Offline — waiting for connection\" for server-side search
\n responses (ec_validation query-too-short, ec_out_of_scope, ec_not_found). The
\n offline banner is now reserved for genuine network failures; HTTP error responses
\n render inline (empty results / a toast). Pressing Enter also respects the profile's
\n minimum search length instead of firing a too-short query. \n
\n[0.4.0] — 2026-06-11
\nAdded
\n\n- Approve-to-anywhere (bootstrap-only): approving a device bound to any profile
\n brings it up as that station on its next poll — the binding is authoritative during
\n the enrollment window (no secret issued yet) and after an admin key reset. Once a
\n device holds its secret, token-only requests can no longer inherit the binding
\n (UUIDs are visible on-screen; the secret is the device credential). \n- Persistent launch links: profile tokens stored encrypted at rest
\n (libsodium, key derived from wp-config salts) — \"Show Launch Link\" re-displays
\n the URL + QR anytime; each view audited. Pre-existing profiles offer
\n regenerate-to-view. Schema v5. \n- Approval dropdown warns when binding a device to a different profile than it
\n enrolled under; Devices list gains Last Clerk column and Last Active rename. \n
\nFixed
\n\n- Waiting-room poll no longer swallows auth errors (stuck \"Pending Approval\").
\n- Service worker cache is version-stamped at serve time — plugin updates now
\n actually reach installed devices (ignoreSearch removed, no-cache on sw.js). \n- Token rotation confirmation states exactly which devices are and aren't affected.
\n
\n[0.3.0] — 2026-06-11
\nAdded
\n\n- Device-secret auth: the launch-URL token is now an enrollment ticket. On first
\n approved session each device receives a one-time-delivered secret and authenticates
\n with it thereafter; profile comes from the device's server-side binding. Installed
\n homescreen PWAs survive token rotation; reassigning a bound device is zero-touch.
\n \"Reset device key\" admin action; navigator.storage.persist() at boot. \n- Distinct failure states:
ec_token_rotated (detected via previous-hash
\n tracking), ec_profile_revoked, ec_profile_expired, ec_profile_not_started —
\n each with its own station screen saying what to do, replacing flattened
\nec_bad_token. \n- Schema v4 (
DEV_secret_hash, PRO_token_hash_prev), reactivation-free. \n
\n[0.2.0] — 2026-06-11
\nAdded
\n\n- Ticket-class scoping: profiles limit stations to tickets whose
TKT_class
\n tokens match PCRE patterns (gate=attendee, merch=apparel-, meals=meal-);
\n enforced in both search and check-in; save-time regex validation. \n- Datetime pinning:
current mode resolves the active datetime by clock —
\n stations show no picker. \n- Cascade check-in: optional per-profile; checking in consumes all datetimes
\n the ticket grants (full-event regs check in once). Checkout never cascades. \n- Device→profile binding: devices may be pinned to one profile at approval or
\n via Devices→Edit; mismatched tokens are rejected and the station explains itself. \n- Clerk roles + manager override: force check-in of non-approved registrations
\n requires a manager — directly, or via a manager-badge override scan recorded in
\n the audit row alongside the acting clerk. MANAGER tag on badges. \n- Bulk clerk creation (all-or-nothing batches,
Name, CODE, manager lines) and
\nbulk badge printing (card grid with cut lines). \n
\nChanged
\n\n- Admin mutations moved to
load-{page} hooks (proper redirect-after-POST). \n- Clerk sign-in: visible text input with paste + wedge-scan + keypad.
\n- Schema v3 (
DEV_profile, CLK_role) via reactivation-free migrations. \n
\nRemoved
\n\n- Station checked-in counter (and its per-poll SQL count).
\n
\n[0.1.0] — 2026-06-11
\nAdded
\n\n- Design spec (docs/SPEC.md): data model, REST API, auth flows, station PWA, admin UI.
\n- Foundation: profiles/devices/clerks/audit tables, opaque-token auth, device
\n approval handshake, HMAC clerk sessions. \n- REST layer (
eea-checkin/v1), ranked 5-field search, explicit-direction check-in
\n over EE models, append-only audit log. \n- Station PWA (fullscreen manifest, service worker, audio cues, camera scanning),
\n wp-admin management pages, Mode A drop-in admin station. \n- Docker dev stack with EE core + seeder.
\n
"
+ "description": "Front of House for Event Espresso runs the box office of a live event from a tablet. Each device is trusted by an administrator, every action is attributed to the clerk who took it, and the whole shift is captured in an append-only audit trail. It is an Event Espresso add-on — it reads and writes through Event Espresso's own models, so a sale, a payment, or a return at the door is the same real transaction it would be in wp-admin.
\nThe station runs as an installable PWA with no WordPress chrome: a clerk signs in with a badge, the tablet shows a fast registration search or a sales grid, and the door keeps moving.
\nWhat a station can do
\n\n- Check-in. Ranked name/email search, camera or hardware-wedge barcode scanning, cascade check-in across an event's datetimes, check-out, undo, and a manager-authorized force check-in for non-approved registrations. A segmented filter (All / Needs check-in / Needs payment) narrows the line to exactly who needs attention.
\n- Sell / POS. A category-tabbed item grid with per-ticket variations and quantity steppers, a persistent running-total cart, registrant questions, then a real Event Espresso transaction with automatic tax and full rollback if anything fails — no orphaned rows.
\n- In-person payments. Cash (with amount-tendered entry and live change-due), check, and card-present through a pluggable provider add-on. A shared transaction screen carries a role-governed payment gate so only permitted clerks can take money.
\n- Returns, exchanges, and customer credit. Returns and exchanges settle as real Event Espresso transactions — replacement items are genuine ticket purchases, returned goods are a typed return payment, and the server computes the net. Overpayments resolve to a cash refund (role- and event-gated), a forfeit, or store credit parked on the customer's account (via a companion credit plugin).
\n- Bluetooth thermal receipts. Print itemized receipts straight from the tablet to a 58mm thermal printer over Web Bluetooth — templated header and footer with token substitution, money-aligned line items, and a configurable tear feed. On-device test buttons prove the printer without ringing up a sale.
\n- Clerk badges + scan-to-login. Print a badge per clerk carrying a CODE128 barcode and a QR; a clerk signs in by scanning that badge with the device camera or a hardware wedge scanner, or by typing a code.
\n- Manager assist. A permitted manager can log in over an active clerk for a single task — a stacked, temporary session that preserves the base clerk's session and resumes it on exit.
\n- Per-device themes. Four CSS themes (a system-following default plus dark, light, and high-contrast variants), chosen per device by a manager.
\n
\nBuilt for the front desk
\n\n- Device trust. A device enrolls from an enrollment link, an admin approves it with a nickname and station, and the device then operates on a one-time-delivered secret — the operating credential, not a shared token.
\n- Clerks, not WP users. Clerks are lightweight badge/PIN identities with configurable roles, idle and focus-loss timeouts, multi-login policy, and station scoping — volunteers never touch the WordPress user table or wp-admin.
\n- Generic and reusable. Core stays event-agnostic and ticket-native. Filters let companion plugins layer on ticket categories, row enrichment, payment providers, and customer credit without forking the core.
\n- Multisite-ready. Activate per site on a multisite network.
\n
\nFull documentation and architecture notes live in the bundled README.md.
",
+ "installation": "\n- Install and activate Event Espresso 5 on the site first — this plugin is an Event Espresso add-on and does nothing without it.
\n- Upload the plugin to
wp-content/plugins/ and activate it (activate per site on multisite). \n- Open the Front of House admin menu and set up your Access Profiles, Clerk Roles, Clerks, and Settings (including receipt header/footer).
\n- Create an Enrollment Link, open it on a tablet, install the PWA, and approve the device under Devices.
\n- The site should be served over HTTPS — the camera scanner and Bluetooth receipt printing require a secure context.
\n
",
+ "faq": "Does this require Event Espresso?
\nYes. It is an Event Espresso add-on and is tested against Event Espresso 5. Sales, payments, check-ins, and returns are written through Event Espresso's own models, so they are identical to what wp-admin would produce.
\nWhy does it need HTTPS?
\nThe camera-based barcode/QR scanner (camera access) and Bluetooth thermal printing (Web Bluetooth) only work in a secure browser context. Serve the site over HTTPS and the station works on tablets and phones.
\nHow do clerks log in?
\nA clerk signs in to a station with a badge — scanned by the device camera, read by a hardware wedge scanner, or typed in as a code. Clerks are not WordPress users; they are managed under Front of House → Clerks and governed by configurable roles. Treat a printed badge as a physical login key.
\nWhat happens when the network drops?
\nThe station distinguishes a genuine network failure (an offline banner) from a server-side response such as a too-short search or an out-of-scope result (shown inline). When the card-present path charges but recording the payment fails, the station does not swallow it: it retries, prints a manager error slip, records an audit incident, and shows a blocking manager modal.
\nCan I add card payments or store credit?
\nYes. Cash and check are built in. Card-present payments come from a pluggable provider add-on, and store credit / pay-with-credit come from a companion customer-credit plugin. When those companions are absent, the station hides the affordances entirely.
\nDoes it work on multisite?
\nYes. Activate it per site on a multisite network.
",
+ "screenshots": "\n- Check-in station — registration search with the All / Needs check-in / Needs payment filter.
\n- Sell / POS — category-tabbed item grid with variations and a running-total cart.
\n- Transaction screen — cash tender with live change-due and the payment-method buttons.
\n- Returns & exchanges — net settlement with cash refund, forfeit, and store-credit options.
\n- Bluetooth receipt printer configuration with on-device test buttons.
\n- Clerk badge sheet — CODE128 + QR for scan-to-login.
\n- wp-admin — the Front of House admin section (profiles, devices, clerks, roles, audit).
\n
",
+ "changelog": "0.22.2
\n* Update-details sections now come from readme.txt (WP-native); base/config renamed to .mu-manifest.yaml + .mu-release.yaml. No functional change.
\n0.22.1
\n* Release now flows through the shared manifest pipeline; the \"View details\" modal carries a description, changelog, and tags. No functional change to the plugin.
\n0.22.0
\n* Added a color-coded plugin icon (delivered through the update channel) and declared WordPress 7.0 compatibility. No functional change.
\nThe full history is in CHANGELOG.md bundled with the plugin.
\n0.21.2
\n* Updates are now handled by the Manifest Updater plugin (via the standard \"Update URI\" header) instead of a bundled updater. No change to how Front of House works; this lets it run on a single site instead of network-wide. Requires the Manifest Updater plugin to be network-activated to receive update notifications.
\n0.21.1
\n* Fixed for real (multisite): the \"payment method Return/Settlement was automatically deactivated\" admin notice that kept coming back. Root cause was that Front of House auto-activated its internal Return/Settlement methods on every site in the network and then kept reactivating them, which Event Espresso re-deactivated in a loop. Those methods are now created only on the site that actually records a return/settlement (the desk site), and the stale notice is cleared in every admin context. No effect on how returns/settlements work.
\n0.21.0
\n* Per-method payment options. Settings ▸ Payment Methods now lets you set, per method, a custom button label, whether a check number is required, and (for card) a surcharge. A card surcharge is added to the sale as a real fee line item — it raises the total, prints on the receipt, and is recorded in Event Espresso — so the customer is actually charged it. Card payments include the surcharge in the amount charged on the reader.
\n* Card is now provided through the Collect for Stripe add-on's registration with Front of House, and Front of House owns enabling/disabling and labelling every method (including card) in one place. Update the Collect add-on to 0.2.0 alongside this release.
\n* The old \"Allow cash refunds\" setting is carried forward into the new Payment Methods section automatically; no reconfiguration needed.
\n0.20.1
\n* Fixed a \"payment method was automatically deactivated\" admin notice that kept reappearing for the Return and Settlement payment methods. The methods themselves were working; the stale warning is now cleared properly (the previous fix was being undone by Event Espresso's own notice save).
\n0.20.0
\n* Per-device payment hardware. Each device now has a \"Hardware\" block (card reader, cash drawer); unchecking one hides that payment method on that station's point of sale — card needs a card reader, cash needs a cash drawer. Visibility only: it never changes who may take payment, and nothing changes on existing devices (every device is treated as fully equipped until you say otherwise).
\n0.19.0
\n* A single home for payment methods. A new \"Payment Methods\" settings section turns cash, check, and card on or off and sets cash-refund policy in one place; an access profile can restrict which methods a given station offers. Nothing changes on an existing install — every method stays enabled exactly as before until you adjust it. Under the hood the method set is now data-driven (a registry) so future methods can plug in cleanly.
\n0.18.1
\n* Check payments at the desk (activates Event Espresso's Check method in the admin scope). Server-side event-scope enforcement on every payment/return endpoint (a clerk can only act on their own event's transactions). Clerk/manager codes widened to 10 digits with a per-device brute-force lockout, plus a remote \"Clear lockout\" admin action.
\n0.18.0
\n* Card-present payments are verified against Stripe before approval (no more trusting the return URL). Every first-touch money write (sales, exchanges, cash, credit, settlements) is now idempotent against a lost-response retry. Audit-log failures are no longer silent.
\n0.17.2
\n* The Return and Settlement payment methods now reactivate themselves automatically after a plugin update — no manual step on the Payment Methods screen, ever.
\n0.17.1
\n* Walk-up Return (card-less return/exchange). Sales and returns now verify the transaction's true status in Event Espresso before completing — no more false \"Sale Complete\" on an unpaid exit.
\n0.17.0
\n* Admin-configurable branding (receipt logo, app icon, splash, background) and Bluetooth badge printing in wp-admin (one badge at a time, tear between).
\n0.16.2
\n* Service worker REST namespace fix (eea-checkin/v1 -> eea-foh/v1); no behavior change.
\n0.16.1
\n* Update channel + repository URLs moved to the ee-extensions org. Removed a hard-coded external domain from the receipt printer's barcode/QR test page.
\n0.16.0
\n* Taking payment is now a lookup filter (All / Needs check-in / Needs payment), not a separate mode. Sell remains its own mode. No schema or server change.
\n0.15.0
\n* Printable clerk badges plus camera and hardware-wedge scan-to-login, with per-device camera configuration.
\n0.14.x
\n* Bluetooth thermal receipt printing with templated header/footer, itemized line items, and configurable tear feed; a dedicated receipt-printer configuration modal with on-device test buttons; loud \"charged but unrecorded\" incident handling for the card-present path.
\n0.13.0
\n* Returns, exchanges, refunds, and customer credit settled as real Event Espresso transactions, with cash-refund/forfeit/store-credit settlement and exchange tax suppression.
\n0.12.0
\n* Card-present payments via a pluggable, provider-agnostic descriptor model; cash tender with change; on-demand emailed receipts.
\n0.11.0
\n* Sell / POS mode — front-desk sales through a category grid and persistent cart, building real Event Espresso transactions with automatic tax and full rollback.
\n0.9.0 - 0.10.0
\n* Station modes, pluggable in-person payments, the corrected visibility-vs-action check-in model, assistive (stacked) manager login, per-device themes, and a Q&A + payments details modal.
\n0.5.0 - 0.7.x
\n* Device-secret authentication and nonce-sealed enrollment, first-class enrollment links, configurable clerk roles and stateful sessions, clerk-to-station scoping, linked-registration reverse lookup, and a role-gated registration details modal.
\n0.1.0 - 0.4.0
\n* Foundation: profiles/devices/clerks/audit data model, REST layer, ranked search, check-in over Event Espresso models, the device approval handshake, and the station PWA.
"
}
}
diff --git a/eea-foh/eea-foh-0.22.1.zip b/eea-foh/eea-foh-0.22.2.zip
similarity index 89%
rename from eea-foh/eea-foh-0.22.1.zip
rename to eea-foh/eea-foh-0.22.2.zip
index 2f3ad7e..6257b88 100644
Binary files a/eea-foh/eea-foh-0.22.1.zip and b/eea-foh/eea-foh-0.22.2.zip differ