Skip to content

Remote POS UAT

This runbook covers the first merchant and physical-device test in the isolated emali2-test environment. It contains test identifiers only; no access token, activation code, customer contact, provider credential or secret belongs in this document.

Prepared merchant fixture

Field Test value
Organization 131 — Emali UAT Workshop Organization 132605
Merchant store 577
Store alias 0000472
Country SZ
Currency SZL
Locale en-SZ
Time zone Africa/Mbabane
Fiscalization DISABLED
Customer-app provider EMALI_APP, priority 100

The Core projection contains one merchant store and seven active merchant-store staff assignments. Agent unit 578 and biller unit 579 are deliberately not POS stores. Re-running platform, catalog or customer projection sync is safe; the first platform sync is explicit and later refreshes run automatically.

The POS-owned test catalog contains:

Field Test value
Item ID 1000000
SKU UAT-WATER-500
Barcode 9900000000577
Description UAT Bottled Water 500 ml
Price SZL 10.00
Opening stock 25

Do not copy this item to a production tenant. It exists only to make the first sale, scan, refund and stock-reconciliation path deterministic.

Physical Android checkpoint — 15 July 2026

The standalone POS debug APK was installed on a Samsung SM-P619 test tablet running Android 14 (API 34) on arm64-v8a. The installed package is com.emali2.pos version 0.1.0; its APK matched the locally built artifact byte-for-byte with SHA-256 6d9b48af86cda66bf8b3046ac2c7a19dd7661bdeb8baef463d4f9fc51f04db0f. That hash identifies the historical physical checkpoint. The later printer-routing candidate added signed offline batch replay, manager-decision consumption, country-neutral tender serialization and truthful printer-delivery confidence. Its immutable URL remains available here with SHA-256 db8c2b5f775fb10134b94540ef647e0e758e383b961bb3abb897ebbfe487d10d. Both APKs are debug-signed, belong only on managed test hardware, and are not production artifacts.

The Android key-attestation audit candidate remains available at this immutable URL. It is 47,182,384 bytes and has SHA-256 c85bf2c3b03af30bc82ef9f78e92d63f6254018c445dd7c3cb9f8c29dc27e6ac. Its debug signing-certificate SHA-256 is 0d8500f4bb193f02d2367b1a410b14baf4e5301e318acdb31f8bb48637702835; production must use an authorized release signing identity instead. The overlay is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:c75829f79544d73a19461f4572fce821af8ba357dc0d2469836241372fd333cb. The matching POS service is pinned to ghcr.io/mainamartin/pos-service@sha256:3164864e6ece6982077705f864f3faa0ae2fd8c88f62951bd098b4e93543125c; Flyway advanced from v30 to v31, both workloads are Ready with zero restarts, and the public service health is UP. Attestation remains in AUDIT mode until a physical revoke/re-enroll validates a real TEE/StrongBox chain.

The terminal-revocation recovery candidate is now the latest managed-test installer and is also available at this immutable URL. Both latest and immutable paths are 46,768,675 bytes with SHA-256 eaaa2e18372a81a509508ac1f72188546e5d145ef0f9b099a7aa13bfefa2c5cd. The overlay is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:160b2c29eb56d73f9fb7b79da5b5db9147663b6182d614c2323204c7b211f7c7. The running download pod is Ready with zero restarts; both in-pod files match the source hash, the public URL reports the exact content length and byte-range support, and a public first-kilobyte range matches the local APK. The previous attestation URL remains byte-identical. The build passes 83 unit tests in both variants, debug lint, and debug/release assembly. It is not yet a physical acceptance result.

This exact printer-routing candidate was then installed in place on the same Samsung Android 14 tablet with application data preserved. Android reported a successful install and cold start, MainActivity became the resumed activity, and the post-launch process log contained no fatal exception. A later customer-payment UI candidate makes the cashier-visible distinction between a prompt that was sent and is waiting for approval and a payment that was captured. It has SHA-256 28c2cb691d51fbe40983dc7e78243ec143f9a219ca86acf2596e9712e6bcc48f, passes the expanded 53-test Android suite, and was installed in place on the same tablet with application data preserved. The installed base APK matched that hash byte-for-byte and MainActivity completed a cold start without an application crash. Publication is now complete: the image is ghcr.io/mainamartin/emali2-pos-apk:test-20260715-payment-prompt-ui1 at digest sha256:b599616cdc34b92f114159d88be56aa08a6ba4f0b663b66c5f291e487ea61fe6, and both the latest test installer and its immutable URL were downloaded after rollout and matched the same APK hash.

The capability-aware successor was the preceding test installer. It persists the adapter-advertised CANCEL_PENDING_PAYMENT operation through Room v16 and never infers cancellation from WALLET. Its 54-test Android suite passes; Room 13-to-14, 14-to-15 and 15-to-16 migrations passed on the same physical tablet. The in-place install preserved application data, cold-started without a crash, and the installed base APK matched SHA-256 89d25273f0330d694ce649834c55b0cfafb20a78fd835e733068cbdb0a71f651. The image is ghcr.io/mainamartin/emali2-pos-apk:test-20260716-provider-capabilities1 at digest sha256:19c23e2b5f38d258033feee77573c4cc5908b41fe5b3fbb57166d9c4f69dd9c1. The latest test installer and capability-aware immutable URL both downloaded after rollout with the same application hash; the two earlier immutable URLs remain available.

The private-LAN printer build remains available as the preceding immutable installer. It adds a manually configured country-neutral raw-TCP ESC/POS adapter, rejects public destinations, bounds readiness and print connections, records successful one-way sends as unconfirmed, and does not try a second resolved address after a possibly partial write. All 60 unique Android unit tests pass in both debug and release variants; full debug lint and both APK builds pass. The same Samsung tablet accepted an in-place install with the matching debug signature, preserved application data, cold-started with a live process and logged no fatal app exception. The exact APK SHA-256 is 694361156e71472f9c1716693d7177f42fc1ba03557a15ca93be1fed58211a71. The image is digest-pinned at ghcr.io/mainamartin/emali2-pos-apk@sha256:8af98ae1d43a3929f63684995589b4b63d86714f9cb9022e4d034c0696cc535d. The network-printer immutable URL was downloaded after the USB rollout and still matched the exact APK hash; the earlier capability-aware immutable URL still returns 200.

The app completed a cold start and displayed its cashier/manager sign-in gate. Selecting Sign in securely reached auth.test.emali2.damplabs.com through the system secure browser, confirming remote OIDC/PKCE routing without entering credentials.

The physical checkpoint was refreshed on 16 July 2026 with the current network-printer build. The sign-in gate remained stable with no crash. Bluetooth was off and the Android Bluetooth manager listed no bonded printer, so receipt output was not claimed. Physical print acceptance requires a powered, paired test printer; customer-prompt acceptance additionally requires assigned merchant login, terminal enrollment and a dedicated test customer routing reference.

A previous USB-host build remains available at its immutable URL. It recognizes only a USB printer-class device/interface with a bulk OUT endpoint, uses an opaque local identity, requests temporary Android permission after operator selection, probes the claimed interface, chunks bulk output, records a complete write as unconfirmed, and blocks retry after partial output. All 68 unique tests pass in debug and release variants; lint and both APK builds pass. The Samsung tablet advertises USB host support, accepted the in-place build, cold-started without a fatal exception, and its pulled base APK matched SHA-256 9625f5756e6c63a87bb3ef9be231b914b2a4c0cc9bead4bfbc6214b136734f14. The image is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:50c40e2edf334b1bb18920b42f23a8efee9d6315ccbcf2687ee19ea543c574d4. The USB-printer immutable URL still matches that APK hash byte-for-byte. The network-printer immutable URL also remains available with its original hash.

The current cash-drawer audit candidate passes 86 unique unit tests in both debug and release variants, debug lint, both APK assemblies, five isolated Room migration tests, and a targeted installed-app upgrade test. It was installed with data-preserving replacement on the Samsung tablet, migrated the existing database from v17 to v18 while preserving all three store rows, started MainActivity, and kept the process running. Automatic drawer actions remain disabled because no physical drawer has been qualified. The latest test alias and immutable cash-drawer URL match local SHA-256 3a28a81476aa61253f704986a4e2851d123012817917d2d66f7ba94c7f579117. The artifact image is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:50786cb1c89b4b4cda4224f7a91ae77ffc50c09e7b99e941babd4d77da1b3d15; the download pod is Ready with zero restarts. The preceding revocation-recovery URL retains its original bytes.

The succeeding supervisor no-sale candidate passes 88 unit tests in both Android variants, debug lint, both APK assemblies, five physical Room migration tests and one targeted installed-app schema test. It requires online supervisor approval bound to the exact tenant/store/terminal/shift/cashier/action, expires after five minutes, records the zero-value server NO_SALE movement before hardware I/O, and makes the approval single-use. The server is live at Flyway v35 on ghcr.io/mainamartin/pos-service@sha256:181b7f4769fbca58f8649c9a425edd89db31c9d6a6591192eff58fb71cd42af1; health is UP, and the pod is Ready with zero restarts.

The live latest alias and immutable supervisor no-sale URL match SHA-256 3dcc2600a0c94f0086398df93a63a6f42d7c021a770511b5b5e03850cc04b224. The APK image is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:10c6ba0136f5ea455d0a03b6269b546995bb9bad322e9b67465b084cafabb980; the download pod is Ready with zero restarts and public Range requests return 206.

The Gradle connected-test cleanup uninstalled the target package and removed the prior tablet sandbox after the initial update-in-place install. There was no restorable backup. The APK was reinstalled and a manual targeted instrumentation run left a fresh Room v18 database and launchable process in place. This run therefore proves current installation/schema compatibility but does not claim preservation of the prior three store rows. No printer or cash drawer was connected and no drawer pulse was sent.

The preceding country-neutral hardware-UAT build was rebuilt from source on 17 July with 76 unit tests passing in both debug and release variants, debug lint clean, and both APKs assembled. Its debug signing identity matches the installed UAT application. It was installed with Android's replace/data-preserving mode on the same Samsung tablet, started MainActivity, remained running as the top activity, and produced no fatal application event. The pulled installed APK from that checkpoint and the country-neutral immutable URL all match SHA-256 a700276eb60e4f57d76ff0b45729987e083add04762f1efdbb64e09decfa4c4f. The artifact image is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:f6c32767ddaa4b7d45db0fe1be4fa286db0e94c3bc7ed27c7592027aeb1a07cd. The sanitized read-only audit passed its Android/API, installed-application and hash baseline without emitting a device or peripheral identity. Bluetooth was disabled and it again observed no USB printer interface, external HID scanner or USB HID interface, so no physical peripheral is claimed by this checkpoint.

The same tablet received the current customer remote-test APK without clearing its existing application data. Its app-level service-PIN gate was respected; no PIN, customer identity or payment data was requested, bypassed or recorded.

The same tablet was updated in place after the foreground scan-intent adapter was added; the installed base APK again matched this exact hash and completed a cold start without clearing application data.

An isolated Android instrumentation test then registered the configured foreground receiver on this tablet, sent a package-targeted intent using the default action/category and DataWedge-compatible extras, and verified the exact barcode, symbology and validated-ready state. One device test passed. The test runner's normal cleanup removed its target package, so the same hash-verified APK was reinstalled and cold-started afterward. The tablet was not yet enrolled and contained no POS transaction state.

Two additional device tests passed for the camera fallback: the bundled model decoded a generated, checksum-valid EAN-13 without downloading a model, and CameraX confirmed the tablet's rear camera before permission plus a valid scan transitioned the process-only capability to ready. The final APK was reinstalled byte-for-byte and cold-started. Test-granted camera permission was then revoked, so UAT must exercise the real permission-on-use flow.

A further device test generated the enrollment key in Android Keystore, matched its public-key thumbprint, signed the canonical activation-proof payload and verified the signature from the exported public key. The same signature failed after the signed metadata was changed. This proves device-side key possession; it does not consume a merchant activation code or replace merchant-led UAT.

This is a pre-enrollment checkpoint, not physical-hardware certification. The tablet exposed only its built-in touch, stylus, key and audio input devices. No external HID scanner was attached, and no identifiable connected ESC/POS printer or cash drawer was available. Terminal enrollment, merchant sign-in, catalog sync, scan, print and drawer evidence therefore remain open.

The 16 July sanitized baseline audit independently confirmed Android 14/API 34, security patch 2025-05-01, USB host, Bluetooth, camera/autofocus, Wi-Fi and telephony capability, plus the exact installed APK hash. It emitted no ADB, USB, Bluetooth or input-device identity and read no customer data or app secret. Its four fake-device guard cases pass. The full current connected suite then passed six application tests and three Room migration tests on the tablet. The runner's cleanup removed the app; the published immutable APK was hash-verified, restored, and the read-only baseline passed again. Bluetooth remained disabled and the audit detected zero USB printer interfaces, USB HID interfaces and external keyboard devices, so this evidence does not change the open physical printer/scanner status.

Executable evidence harness

The preferred path is an interactive OAuth device login using the dedicated public emali2-pos-uat client. It has no client secret, service account, password grant, implicit grant, redirect URI or broad role scope. Keycloak shows the normal merchant sign-in in a browser; the harness polls for the short-lived access token in memory, verifies the issuer, client, pos-api audience and a merchant/operator role, and never prints or writes the token or refresh token. This follows the Keycloak device authorization flow.

python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-preflight.json \
  preflight

Open the displayed HTTPS verification link, enter its one-time user code if it is not already present, and sign in with the assigned merchant account. The harness never accepts a username or password. For an externally managed automation session, POS_UAT_ACCESS_TOKEN remains a fallback; use it only for a short-lived merchant token and never together with --device-login.

On 17 July, a real public-client device authorization completed with valid issuer, client, pos-api audience and merchant role, but the signed-in account belonged to organization 55. POS correctly returned 403 Store access denied for the organization 131 fixture before exposing bootstrap data or permitting any mutation. The mode-0600 evidence contains only the successful interactive authorization and API-health checks; it contains no token, subject, username, customer reference or agent identifier. This is positive tenant-isolation evidence, not merchant UAT completion. Enrollment must be retried with an administrator actively assigned to organization 131 and store 577.

Preflight proves that the authenticated bootstrap resolves organization 131 and merchant store 577, rejects any leakage of agent 578 or biller 579, validates the SZ/SZL operating profile and generic provider capability, looks up the prepared barcode, and reads POS-owned reconciliation. The harness has a strict /api/v1/pos/ path allowlist and cannot call agent-withdraw or other Core business APIs.

The mutation harness must not impersonate an Android terminal: its Keystore key is intentionally non-exportable. Run Android sale and payment flows in the app. For repeatable API mutation tests, enroll a separate programmable HARDWARE_POS UAT terminal with its own permission-restricted P-256 key.

Generate that test-only key locally. The command refuses to overwrite a file, creates it with mode 0600, and prints only its public-key thumbprint:

python3 scripts/remote_pos_uat.py \
  generate-hardware-key \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem"

The preferred path lets the signed-in merchant administrator create a dedicated five-minute challenge for a stable lane such as UAT-API-1 and immediately consume it with the local key. The activation code remains in memory and is never displayed, accepted as an argument/environment variable, or written to a log, binding, or evidence file:

export POS_UAT_EXECUTE_MUTATIONS=CONFIRM_POS_TEST_MUTATIONS
python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-activation.json \
  activate-hardware-terminal \
  --create-activation \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem" \
  --binding-file "$HOME/.emali2/pos-uat-terminal.json"

The create request still requires an assigned ORG_ADMIN or UNIT_ADMIN; a regular merchant/operator receives 403, and the harness never weakens or bypasses that server gate. For a challenge created separately in Backoffice, omit --create-activation; the harness reads the code through a hidden prompt. Both paths require the matching signed proof. The binding file contains no private key or activation code. Use its terminal UUID for the transactional API portion:

export POS_UAT_EXECUTE_MUTATIONS=CONFIRM_POS_TEST_MUTATIONS
python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-cash.json \
  cash-flow \
  --terminal-id '<active programmable UAT terminal UUID>' \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem"

This opens and submits a dedicated shift, replays identical idempotency keys, captures a two-unit sale across two cash tender legs, returns one line at a time through two partial refunds, verifies original-capture-order allocation, stock reversal and exact reconciliation deltas. Every protected request is signed over its exact method, path, serialized body, timestamp, fresh nonce, terminal UUID and idempotency key. It attempts to close its shift after a failure and records sanitized cleanup identifiers if administrator recovery is required. It validates the server side of delayed/offline replay; Android airplane-mode queueing and physical reconnect remain device tests.

The live Core/POS release includes the authoritative EMALI_APP captured-payment refund endpoint and adapter, and bootstrap advertises REFUND_CAPTURED_PAYMENT. The evidence harness has a tested, opt-in electronic refund sequence, but no merchant-authorized live execution has been recorded yet. Mixed cash/electronic allocation remains covered by the PostgreSQL integration gate. Remote acceptance still requires the real customer-app run and separately sanitized customer/merchant balance evidence.

If a prompt enters MANUAL_REVIEW, do not initiate a replacement tender. An authorized merchant manager must open Operations → Customer payment exception review, independently match the amount, currency and provider record, then record either captured or failed with an opaque evidence reference and the provider's occurrence time. The confirmation must state that no provider request will be sent. After recording, verify the attempt and tender share the same terminal state; for capture, verify the sale/receipt and stock transition, and for failure verify that another tender is allowed only after the pending outcome has been cleared. Never place a customer phone number, payment account or access token in the evidence.

For a real customer-app prompt, keep the customer routing value out of shell history by using the dedicated environment variable. Choose the outcome the tester will perform in the customer app:

export POS_UAT_CUSTOMER_REFERENCE='<test customer routing reference>'
python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-prompt.json \
  prompt-flow \
  --terminal-id '<active programmable UAT terminal UUID>' \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem" \
  --expected-tender-status CAPTURED \
  --refund-after-capture \
  --verify-core-balances

Use --refund-after-capture only for the CAPTURED case and with a refund-authorized merchant manager. It creates a two-unit payment, requires the sale to be PAID, verifies the immutable original provider-tender routing, and submits the same signed refund body twice under one idempotency key. It waits for a completed one-unit refund, verifies the first distinct credit note, provider-tender allocation, one-unit stock restoration and exact store reconciliation, then repeats the process for the remaining unit. The final sale and tender must be REFUNDED, stock must equal its pre-sale quantity, two credit notes must exist, and reconciliation must show one gross/paid wallet sale plus the exact full refunded amount. This cleanup avoids leaving a paid UAT purchase behind. A rejected, timed-out or manual-review refund records only sanitized sale/refund recovery identifiers and prevents a false pass.

Run separate CAPTURED and FAILED cases for approve and decline/expiry; omit --refund-after-capture for the failure cases. The harness validates the returned generic WALLET prompt type, exact test amount and store currency. It follows only the tender whose provider code matches the store's configured customer-app adapter; an unrelated tender can never satisfy the test. A captured full-payment tender must leave the authoritative sale PAID before any refund, while a failed tender must not. Evidence includes provider code, sale/refund IDs, credit-note numbers and authoritative states, but never the token, customer routing reference, provider payment reference or provider transaction reference. An uncaptured test sale is voided before shift submission. The 26-test harness suite covers the cash, prompt, cancellation and electronic-refund evidence paths.

--verify-core-balances closes the principal-balance evidence gap without granting the POS service a Core balance scope. Supply an exact POS_UAT_PAYMENT_ACCOUNT_REFERENCE, a short-lived customer token in POS_UAT_CORE_CUSTOMER_ACCESS_TOKEN, and a different short-lived merchant token in POS_UAT_CORE_MERCHANT_ACCESS_TOKEN through a secret-safe process environment. The runner requires different JWT subjects and the expected customer/merchant roles. Its separate read-only Core client permits only the self-scoped customer balance route and the exact merchant-authorized till balance route, refuses redirects and non-test HTTPS origins, and keeps both tokens in memory.

The runner checks customer/merchant deltas of -20/+20 after capture, -10/+10 after the first refund, and 0/0 after full cleanup for the default two-unit fixture. Only those deltas and the ISO currency enter sanitized evidence; raw balances, till references, account identifiers, customer contact values and provider references do not. The merchant balance endpoint requires org.merchants.view plus access to the exact till and returns no ledger account ID or ledger owner reference. Redacted app/backoffice transaction-history screenshots remain useful human evidence, but they no longer substitute for the automated balance assertions.

To verify cashier-initiated cancellation, dispatch a fresh prompt and let the harness wait until Core has returned an immutable authorization reference:

python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-prompt-cancel.json \
  prompt-cancel-flow \
  --terminal-id '<active programmable UAT terminal UUID>' \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem"

The default expected outcome is FAILED, meaning the provider authoritatively cancelled the outstanding prompt. Use --expected-tender-status CAPTURED only for a deliberate race test where the customer completes payment as the cashier cancels. In that case the harness requires the sale to remain PAID; it never relabels the capture as cancelled. Before creating a shift or sale, the harness requires the exact configured customer-app provider in merchant bootstrap to advertise CANCEL_PENDING_PAYMENT; a country or provider without that adapter operation fails safely before any mutation. The cancellation request uses the enrolled terminal's signed request proof. Sale lookup, void cleanup and shift submission retain their endpoint-specific authorization rules, and the customer routing reference remains absent from evidence.

This file-backed key is for an isolated programmable test terminal only. A production hardware POS must use its vendor secure element, TPM or equivalent non-exportable key facility. After UAT, revoke the terminal in Backoffice and securely remove its local private-key and binding files.

Browser WEB terminal checkpoint

Cashier web uses a separate WEB enrollment and must never select an Android or dedicated hardware terminal. On the lane computer:

  1. Open POS Cashier Web, sign in as assigned merchant staff, and open Session.
  2. Select merchant store 577. While that computer is present, a merchant administrator creates a short-lived activation for a distinct lane such as UAT-WEB-1 in Backoffice.
  3. Enter the code only in Secure browser terminal. Do not paste it into chat, a shell command, a ticket or this runbook.
  4. Confirm the page reports Request proof ready and lists only WEB terminals. The private P-256 key is non-exportable and stored by structured clone in IndexedDB; the activation code is not stored.
  5. Open a dedicated shift and run one cash sale, collection, void/refund and close. Browser and server tests already cover exact-body signing, CORS, tamper rejection and replay rejection; this step supplies authenticated merchant/runtime evidence.
  6. Revoke UAT-WEB-1 after the checkpoint. If browser site data is cleared, revoke the now-unusable terminal before creating a replacement.

The WebCrypto key prevents export and binds requests to the enrolled lane, but same-origin script can ask the key to sign. The deployed cashier image therefore uses a self-script Content Security Policy. OIDC token isolation through a backend-for-frontend and managed-browser attestation remain later hardening options for higher-risk production estates.

1. Enroll one terminal

  1. Sign in to POS Backoffice as an ORG_ADMIN or UNIT_ADMIN assigned to store 577.
  2. Open Stores, locate the UAT store, then select Enroll terminal.
  3. Use a stable lane code such as UAT-LANE-1, add a device name, and create a ten-minute activation code while the Android device is in hand.
  4. On Android, open terminal setup and enter the code. The app creates its key in Android Keystore, signs a versioned proof over the code and normalized device metadata, and submits the public key, signed fields and signature.
  5. Close the code dialog after Android confirms activation. Never paste the code into chat, a ticket, source control or a shared document.
  6. Return to Stores after the first device sync. Confirm terminal identity, heartbeat, network, battery, printer and scanner state.

Only a human merchant administrator can create the one-time code. The POS service account is intentionally unable to bypass this gate. A code can be used once, expires after at most 60 minutes, is hashed by the service, and is not stored by Backoffice. The private key never leaves Android Keystore. An invalid or tampered proof is rejected before code lookup or consumption and therefore does not reveal whether the code exists.

After activation, Android signs each protected operational request with its enrolled Keystore key; the programmable UAT terminal uses its separate local test key. Before continuing to sale UAT, confirm a normal heartbeat and shift open succeed. The security test gate must also show that replaying an already accepted proof, changing its body, using a stale timestamp, or binding it to another terminal receives generic 401 terminal_proof_invalid. Evidence must not include proof headers, bearer tokens, activation codes, hardware fingerprints, or private-key material. This proof is required for Android and hardware-POS clients in the test environment. An enrolled WEB terminal also uses proof of possession: Cashier Web generates a non-exportable P-256 key with WebCrypto, retains it in IndexedDB through structured clone, signs the exact serialized mutation body, and refuses bearer-only protected mutations.

To test recovery, select the revoke control beside the terminal in Backoffice. If its shift is still open, count the physical drawer, enter the counted cash and a specific administrator reason. A reachable device with no blocked cart may use the normal close-and-submit action followed by revocation. If the device is lost or broken and an abandoned unpaid cart blocks normal closure, select Recover and revoke terminal. The country-neutral management operation needs no device proof, is idempotent, and atomically voids only OPEN sales whose paid amount is zero and which have no pending, authorized or captured tender; it then submits the named shift, seals its signed close-of-day pack, audits the recovery and revokes the terminal. Any partial payment or uncertain electronic tender fails closed and must be reconciled explicitly. It never chooses a payment or fiscal adapter and has no country-specific behavior. Confirm it disappears from the active terminal estate and can no longer report a heartbeat. The service rejects revocation while a shift is open and locks the terminal during this check, so opening a shift cannot race with revocation. Confirm the existing cashier browser/app session can no longer refresh and must authenticate again on a newly enrolled terminal. Revocation queues every proof-bound identity session and retries safely if IAM is temporarily unavailable; it does not store access or refresh tokens. Secure or wipe the physical device after this check.

For a disposable programmable terminal, the repeatable API recovery check is:

export POS_UAT_EXECUTE_MUTATIONS=CONFIRM_POS_TEST_MUTATIONS
export POS_UAT_ACCESS_TOKEN='<assigned terminal-administrator test token>'
python3 scripts/remote_pos_uat.py \
  --evidence-file /tmp/pos-uat-terminal-recovery.json \
  terminal-recovery-flow \
  --terminal-id '<active programmable UAT terminal UUID>' \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem" \
  --opening-float 100.00 \
  --closing-cash 100.00

The command refuses a non-test hostname or non-administrator role, creates one unpaid cart through signed terminal mutations, invokes management recovery without device proof, verifies the exact void/shift/close-pack result and idempotent replay, then records only sanitized identifiers and evidence.

For the separately enrolled programmable HARDWARE_POS UAT terminal, the evidence harness can execute the same destructive recovery check end to end. Close its shift first and use only a terminal that may be permanently revoked:

export POS_UAT_EXECUTE_MUTATIONS=CONFIRM_POS_TEST_MUTATIONS
python3 scripts/remote_pos_uat.py \
  --device-login \
  --evidence-file /tmp/pos-uat-terminal-revocation.json \
  revoke-terminal-session \
  --terminal-id '<active programmable UAT terminal UUID>' \
  --terminal-key-file "$HOME/.emali2/pos-uat-terminal.pem" \
  --refresh-timeout-seconds 60

Sign in with an assigned ORG_ADMIN, UNIT_ADMIN or SYSTEM_ADMIN; the harness rejects other merchant roles before mutation. It first sends a signed heartbeat to bind the exact OAuth identity session to the terminal, commits terminal revocation, follows refresh-token rotation until the identity provider returns invalid_grant, then proves the revoked terminal key receives the generic 401 terminal_proof_invalid. The default 60-second window covers the durable revocation worker's first retry. Access tokens, refresh tokens and the opaque identity-session identifier remain in process memory only and are excluded from console and evidence output.

Merchant machine-client isolation gate

Verified 24 July 2026 in emali2-test:

  • the missing merchant_integration realm role was provisioned from the repository's idempotent Keycloak realm configuration;
  • organization 55 was loaded through the supported platform-projection sync, adding active foreign stores 381 and 495 alongside organization 131/store 577 without direct POS-database writes;
  • a confidential probe token contained its exact azp, only the merchant_integration business role and the pos-api access-token audience;
  • an explicit OPERATE binding for organization 131/store 577 allowed catalog bootstrap for store 577;
  • the same token received 403 for organization 55/store 381;
  • attempting to rebind the same client identifier from organization 131 to 55 returned 400;
  • revocation returned 200, and the same still-unexpired token immediately received 403 for store 577;
  • both disposable Keycloak clients were deleted and confirmed absent; neither client secret nor access token was printed or retained.

The POS binding remains inactive as an audit record. Re-run the normal projection sync before this gate if the designated foreign merchant is not yet present; do not insert a synthetic store directly into the POS database.

2. Confirm the lane and hardware

  • Pair the selected Bluetooth receipt printer in Android system settings.
  • Select it under Settings → Receipt printer, then print a test receipt.
  • Render 9900000000577 as Code 128 (it is a deterministic test code, not a checksum-valid EAN-13), scan it with the external HID scanner and verify that the item is found without treating slow keyboard input as a barcode.
  • Select Scan with camera, grant permission when asked, scan the same Code 128 test label, and verify that it follows the same catalog/basket path and changes camera-scanner readiness only after the successful decode.
  • For an integrated scanner service, configure its exact action, optional category, decoded-data extra and symbology extra under Settings → Integrated scanner broadcast. Target com.emali2.pos, send a physical test scan, and confirm readiness changes from listening to validated before checkout.
  • Confirm the store shows SZL, en-SZ and Africa/Mbabane; the client must not substitute a Kenya locale, currency or provider.
  • Leave the device online until Backoffice reports a current heartbeat.

For an integrated-printer handheld, install the vendor adapter behind the existing printer interface before claiming printer readiness. The generic Bluetooth ESC/POS path does not imply that an internal printer exposes SPP.

3. Run the business flow

Use a merchant-assigned cashier identity and record the generated sale and receipt references in the UAT evidence sheet.

  1. Open a shift with a documented cash float.
  2. Sell one UAT-WATER-500 for SZL 10.00 using cash.
  3. Print the original receipt, then print a marked reprint.
  4. Verify stock moves from 25 to 24 and the sale appears in reconciliation.
  5. Refund the sale with the required supervisor approval and verify stock and tender totals reverse exactly once.
  6. Disable connectivity, sell one item for cash, and confirm it enters the offline queue.
  7. Restore connectivity and verify idempotent replay creates one server sale, one stock movement and one receipt outcome.
  8. Close the shift and reconcile expected cash, counted cash, electronic tenders, refunds and variance.

Customer-app and mobile-money prompts are online-only. They must never be queued for delayed offline dispatch because the customer could be charged after the checkout context has ended.

4. Customer mobile-app payment

The generic POS capability is CUSTOMER_APP_PAYMENT; EMALI_APP is the configured test provider. This is separate from the generic MOBILE_MONEY capability and from any country-specific mobile-money brand.

For an Android customer test device, the current remote-test installer is Emali 2.0 with SHA-256 62f8dc4004b0067a9735abf738fc418800768b0951247cb6a404165d02ac063e. Verify the hash before sideloading and use it only on a test device. This is the customer application, not the standalone POS cashier APK. The current iOS source is simulator-build verified, but a real iPhone test requires an authorized signed/TestFlight build.

  1. Select a test customer with an active registered app device and sufficient test-wallet balance.
  2. Start a new sale and choose the customer-app tender.
  3. Confirm the customer receives push/in-app/websocket notification.
  4. Approve inside the customer app.
  5. Verify POS remains pending until Core reports the authorization as consumed, then captures the tender exactly once.
  6. Exercise decline, expiry and abandoned prompt outcomes without capturing the tender.
  7. Attempt customer cash withdrawal with unavailable agent 0000424 and verify that the customer sees only identifier-free unavailable-agent guidance; the raw agent code or backend Agent not found with identifier detail must not appear in web, Android or iOS UI.

Verified 24 July 2026: an authenticated request through api.test.emali2.damplabs.com using the same unavailable agent, customer MSISDN, wallet and amount from the reported failure returned:

{
  "error": "agent_unavailable",
  "detail": "This agent is not currently available for cash withdrawal. Search for an active agent and try again."
}

The response was HTTP 404 but contained neither the submitted identifier nor the legacy not_found/Agent not found detail. The disposable CUSTOMER-role identity was removed after the check and a follow-up realm query returned no probe identities. Core API is Ready with zero restarts on the immutable customer-boundary overlay sha256:f111be0494a57b30c903de0df67958412216bfdb01d69ee029c97a98d45b5eb0; the unchanged worker was not rolled. The customer portal is deployed on three Ready, zero-restart replicas at sha256:811dedc6d83dacc832d6016e7574a299cfa0ceb8b0776f80655ce00d93284779. Its public production bundle contains the safe guidance and does not contain the legacy identifier-bearing message.

The prepared Eswatini store does not enable M-PESA/Daraja or KRA eTIMS/OSCU. Those are Kenyan provider adapters selected only for stores whose operating profile and credentials require them. Another country can bind different mobile-money and fiscalization adapters without changing sale, tender or receipt domain models.

5. Evidence and exit gate

Capture sanitized evidence for terminal health, scan, original receipt, reprint, online sale, refund, offline replay, customer-app payment, shift close and reconciliation. Evidence must omit tokens, activation codes, customer contact details and payment credentials.

The first UAT gate passes only when:

  • terminal enrollment, open-shift revocation protection, completed identity-session revocation and failed refresh on the revoked terminal;
  • one physical printer and scanner pass reconnect and retry tests;
  • online cash sale, refund and offline replay reconcile exactly once;
  • customer-app approve, decline and expiry outcomes are authoritative;
  • support can trace the sale without database access;
  • no Kenya-specific default appears for this Eswatini store.

Digital receipt and signed cash-flow checkpoint — 25 July 2026

The standalone POS service and Cashier Web are deployed and Ready at:

  • ghcr.io/mainamartin/pos-service@sha256:e78c9bd147d9acb1133c64f036e3b0be6ce885ce3947e391c6d391509f94f046
  • ghcr.io/mainamartin/pos-cashier-web@sha256:4c27e22d9f19c75834518b9eb0d558ced75a128ff7e2c6e3e83ea6a4601f9711

https://receipts.test.emali2.damplabs.com/r/{token} resolves to the public receipt UI and has a valid Let's Encrypt certificate covering the receipts hostname. An anonymous invalid capability returns the same generic HTTP 404 whether it is malformed, tampered, expired or unavailable. Live headers are Cache-Control: no-store, Referrer-Policy: no-referrer, X-Robots-Tag: noindex, nofollow and X-Content-Type-Options: nosniff; the body contains no receipt UUID, customer, cashier, terminal, organization, provider, external reference, exception, stack, trace or SQL detail.

The live happy path is now verified against a real remote receipt. A disposable SYSTEM_ADMIN UAT identity enrolled a dedicated HARDWARE_POS terminal for organization 131/store 577, and every terminal mutation was signed with its permission-restricted P-256 key. The run opened and replayed a shift, created and replayed a two-unit sale, captured two cash legs exactly once, issued one receipt, restored stock through two line-accurate partial refunds, reconciled the deltas, submitted the shift with zero variance and revoked the terminal. The store profile was SZ/SZL, en-SZ, Africa/Mbabane, CUSTOMER_APP_PAYMENT/EMALI_APP, with fiscalization disabled; neither M-PESA nor KRA was selected or required.

The authenticated digital-link route generated a signed public capability for that receipt. An anonymous lookup returned the minimized REFUNDED/SZL projection with the required privacy headers, and its https://receipts.test.emali2.damplabs.com/r/{token} UI route returned 200. The token, bearer credential, activation code and private key were not written to evidence. Sanitized mode-0600 evidence is retained locally as:

  • /private/tmp/emali2-pos-uat-activation-20260724212939-14536.json
  • /private/tmp/emali2-pos-uat-cash-20260724212939-14536.json

The first attempted cash UAT exposed a null legacy engagement-policy map. The service now treats an absent policy as an empty country-neutral policy and has a focused regression test. The failed test sale had no tender or paid amount; the product recovery API now covers that exact lost/broken-device case without database intervention by guarded-voiding only eligible unpaid carts, submitting the shift, sealing its close pack and revoking the terminal in one idempotent transaction. All disposable Keycloak users and clients were deleted.

The product recovery route was deployed on 25 July at service digest sha256:0a1b0639d2dd192474fbc403eec057afad08b7771cf8e5fd1f95e26a04eaf50a. A fresh disposable SYSTEM_ADMIN enrolled terminal bf6dd43c-41ad-459c-8bea-0dcf72ef9e38, opened shift 64a0ea2e-9003-45c8-a89c-82aa00d3eda8 with SZL 100.00, and created unpaid sale e54a28ee-5054-4e20-9352-24434f887a7d. The management API voided exactly that sale, submitted the shift, sealed close-pack digest 918560238f3de4137038482e28d525948a39cb5d473576e39d5b1fcf2acaf076, revoked the terminal and replayed the identical request safely. A direct post-run database check returned zero open shifts, zero active UAT-* terminals, terminal REVOKED and sale VOIDED; Keycloak contained zero disposable pos-uat-* users or clients. Permission-restricted evidence is:

  • /private/tmp/emali2-pos-uat-activation-20260724215948-39741.json
  • /private/tmp/emali2-pos-uat-recovery-20260724215948-39741.json

The Android debug test installer is available at the immutable digital-receipts URL and the established latest alias. Both server-side files match SHA-256 1a85926762dc0748e37afd00752327898ae8129c430c34082e37bda58f7de53e; the artifact image is pinned to ghcr.io/mainamartin/emali2-pos-apk@sha256:06b86c885c43bab8d1e681ade0240889d2235f948c6221c463b80417c169ae4d. This is debug-signed remote-test software and is not a production release.