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:
- Open POS Cashier Web, sign in as assigned merchant staff, and open Session.
- Select merchant store
577. While that computer is present, a merchant administrator creates a short-lived activation for a distinct lane such asUAT-WEB-1in Backoffice. - Enter the code only in Secure browser terminal. Do not paste it into chat, a shell command, a ticket or this runbook.
- 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.
- 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.
- Revoke
UAT-WEB-1after 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
- Sign in to POS Backoffice as an
ORG_ADMINorUNIT_ADMINassigned to store577. - Open Stores, locate the UAT store, then select Enroll terminal.
- 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. - 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.
- Close the code dialog after Android confirms activation. Never paste the code into chat, a ticket, source control or a shared document.
- 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_integrationrealm 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 themerchant_integrationbusiness role and thepos-apiaccess-token audience; - an explicit
OPERATEbinding for organization 131/store 577 allowed catalog bootstrap for store 577; - the same token received
403for 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 received403for 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
9900000000577as 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-SZandAfrica/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.
- Open a shift with a documented cash float.
- Sell one
UAT-WATER-500for SZL 10.00 using cash. - Print the original receipt, then print a marked reprint.
- Verify stock moves from 25 to 24 and the sale appears in reconciliation.
- Refund the sale with the required supervisor approval and verify stock and tender totals reverse exactly once.
- Disable connectivity, sell one item for cash, and confirm it enters the offline queue.
- Restore connectivity and verify idempotent replay creates one server sale, one stock movement and one receipt outcome.
- 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.
- Select a test customer with an active registered app device and sufficient test-wallet balance.
- Start a new sale and choose the customer-app tender.
- Confirm the customer receives push/in-app/websocket notification.
- Approve inside the customer app.
- Verify POS remains pending until Core reports the authorization as consumed, then captures the tender exactly once.
- Exercise decline, expiry and abandoned prompt outcomes without capturing the tender.
- Attempt customer cash withdrawal with unavailable agent
0000424and verify that the customer sees only identifier-free unavailable-agent guidance; the raw agent code or backendAgent not found with identifierdetail 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:e78c9bd147d9acb1133c64f036e3b0be6ce885ce3947e391c6d391509f94f046ghcr.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.