Standalone POS Implementation Plan
Status: implementation in progress
Audit date: 25 July 2026
Active objective: make Emali2 POS independently buildable, deployable, operable, and data-owning while integrating with the Emali2 platform only through authenticated APIs and versioned events.
Verified implementation baseline through 17 July 2026:
- 25 July lost-terminal recovery delta: the standalone service and Backoffice
now expose an idempotent, administrator-only
recover-and-revokeworkflow for a lost or broken Android, browser or dedicated POS device. It deliberately requires no device proof when the enrolled private key is unavailable. The transaction locks the selected terminal and open shift, rejects stale selections, caps automatic recovery at 50 carts, voids only fully unpaidOPENsales with no pending, authorized or captured tender, submits the shift, seals the signed close-of-day pack, records audit history and revokes the terminal. Partial or uncertain electronic payment fails closed for explicit reconciliation. The flow is payment-provider, fiscal-adapter and country neutral; its PostgreSQL-backed orchestration and replay test passes, the full service run has 199 tests with 198 passing and the opt-in physical fixture skipped, Backoffice has 24 passing tests plus a clean type-check/build, the remote harness has 32 passing tests, and contract validation covers 138 OpenAPI operations and 25 events. The digest-pinned remote route then recovered a fresh Eswatini/SZL disposable terminal end to end: one unpaid sale becameVOIDED, its shift becameSUBMITTED, a signed close pack was sealed, the terminal becameREVOKED, identical replay succeeded, and database/IAM cleanup checks found no open shift, active UAT terminal, disposable user or disposable client; - 25 July remote cash and receipt delta: the isolated service is Ready on
sha256:e78c9bd147d9acb1133c64f036e3b0be6ce885ce3947e391c6d391509f94f046. A real Eswatini/SZL programmable terminal run passed administrator-approved activation, proof-bound replay-safe shift and sale creation, two exactly-once cash legs, receipt issue, two line-level partial refunds, stock restoration, reconciliation, zero-variance shift submission and terminal revocation. A real signed public receipt capability then passed authenticated link generation, anonymous minimized projection, privacy headers and the public UI route. The run used genericCUSTOMER_APP_PAYMENT/EMALI_APPconfiguration and selected neither M-PESA nor KRA. The cash failure discovered on the first attempt was a null legacy engagement-policy projection; it is now normalized to an empty country-neutral policy and covered by a focused unit regression. The stale unpaid test sale was recovered with a guarded audited void, then its management shift was submitted and terminal revoked. Disposable identities, client secrets, tokens, activation codes and terminal private keys were removed; sanitized evidence contains none of them; - canonical
com.emali2service and Android sources only; 58 stalecom.dukafiles removed; - Android debug and release APKs build with 76 unique unit tests passing in both variants and full debug lint clean; the physical Android 14 tablet also passes the server-style Ed25519 close-pack verification plus data-preserving Room migrations through v17. Room v16 persists the selected customer-app adapter's advertised pending-cancellation support and defaults existing stores to false; Room v17 caches refund headers and line allocations for repeatable partial returns. The print policy now proves that only a status-capable device/SDK may be recorded as printer-confirmed: generic Bluetooth SPP, USB bulk OUT and network raw-TCP acceptance remain
SUBMITTED_UNCONFIRMED, have no completion timestamp and are never silently retried. A transport router persists and enforces the selected adapter, combines discovery without cross-adapter failure, and keeps vendor SDKs out of checkout and spool code. A fail-closed built-in-printer boundary maps common readiness and sanitized failure states without importing a proprietary SDK; its eight tests prove that only an exact completion result becomesDEVICE_CONFIRMED, while an accepted request remains unconfirmed. Customer-app and mobile-money collection now tell the cashier whether a prompt was sent and is awaiting approval or the payment was captured, without naming a country-specific rail. Web contracts and packages type-check, and all focused web package tests pass. Cashier Web contributes six browser-proof tests, one provider-operation test and four return-planning tests; its Receipts page now provides an exact-quantity manager return tray, completed credit-note history and fail-closed preflight for every electronic tender leg; - 24 July Android delta: 86 unique tests pass in both debug and release variants, debug lint is clean, and both APKs assemble. Room v18 adds a non-customer drawer-action audit: automatic cash-sale/refund opening requires a successful one-shot qualification pulse plus explicit physical confirmation for the exact printer, inserts the action before I/O, hashes the business-event key, suppresses duplicates, records safe outcomes and never retries a crash-ambiguous action. Five isolated Room migration tests cover 13-to-14 through 17-to-18. A separate installed-app test migrated the Samsung tablet's existing database from v17 to v18, preserved all three store rows and verified the new audit table;
- 24 July supervisor no-sale delta: 88 Android tests pass in each build variant, lint is clean, and both APKs assemble. A cashier can request an online supervisor no-sale only for an open server-backed shift. The approved override is bound for five minutes to the same organization, store, terminal, shift, cashier and action; Flyway v35 makes the
SUPERVISOR:<event-id>authorization single-use. The service records an immutable zero-valueNO_SALEcash movement before Android enters the existing audited one-shot drawer path. An unqualified terminal still records the server action but sends no pulse. The Gradle connected-test runner subsequently removed the target package and its prior sandbox; the APK and a fresh Room v18 schema were restored manually, so this later checkpoint does not claim preservation of the earlier three-row tablet state; - 24 July digital-receipt delta: the standalone service creates an authenticated, store-authorized verification link for a paid or refunded receipt and exposes only a signed, read-only, privacy-minimized public view. The capability is an HMAC-authenticated opaque token; invalid tokens are rejected before a database query, links require HTTPS, and public success and error responses are non-cacheable, non-indexable and referrer-safe. Cashier Web can copy/open the link and includes it on printed receipts; Android can request and open it only after server synchronization and never persists the capability. The public contract excludes customer, cashier, terminal, organization, provider and external payment references. The full service run has 194 tests with 193 passing and the one opt-in physical fixture skipped; Cashier Web has 14 passing tests; all Android debug/release unit, lint and assembly gates pass; and contract validation covers 137 OpenAPI operations and 25 events;
- POS service has 168 current tests (167 passing and one explicitly opt-in physical-device attestation test skipped), including a clean PostgreSQL 16 migration through Flyway v32, country-neutral fiscal item profiles with non-Kenya persistence and a legacy KRA bridge, no-core startup, automatic locked platform-projection refresh, merchant-only store projection filtering, upstream tenant-identity validation, POS-owned catalog and inventory, customer and engagement, cashier-shift, Ed25519-signed close-of-day pack, country-neutral split-tender sale, branded legacy-tender normalization, line-allocated full and partial refunds, provider-neutral mixed cash/card refund dispatch, constrained operator reconciliation and atomic completion, immutable provider-statement ingestion with capture/refund matching and reference minimization, ordered offline batch replay, sanitized conflict persistence and resolution, provider routing, electronic-tender, reconciliation, store operating-profile, signed no-store terminal-enrollment challenges, durable per-request terminal proof and replay rejection, atomic open-shift revocation protection, Daraja, customer-app payment, OSCU and tenant-scoped card-terminal binding lifecycles, tenant isolation, idempotent replay, fail-closed legacy routes, protected local-route ownership, exact POS-browser CORS allowlist, card-terminal binding security, dedicated
pos-apiaudience enforcement, and cryptographically verified ES256/RS256 service-client assertions that contain no reusable secret. A database-backed two-lane checkout test starts with exactly one unit in an Eswatini/SZL store and proves concurrent cash collection cannot oversell it: one sale commits asPAID, the other receives a cashier-recoverable HTTP409conflict, stock reaches exactly zero, and the losing transaction leaves no tender, receipt, cash movement, stock movement, completed idempotency record or sale-completed event. Flyway v32 adds country-neutral, manager-only fiscalization resolution: an unknown provider write remains quarantined until a manager supplies an opaque portal, support, certified-report, or fiscal-device evidence reference; resolution is request-hash idempotent, never calls the provider, atomically updates the fiscal job and receipt or credit note, and records one audit plus a versioned fiscalization event. Customer-app tests cover immediate consumption, delayed consumption, every pending/approved/consumed/denied/expired/cancelled/unknown platform state, retryable platform failure, provider-authoritative cancellation, the capture-wins cancellation race, authoritative partial captured-payment refunds, deterministic provider rejection, and adapter-advertised operation discovery without country switches; - card-present and settlement reactors have 434 passing tests: 134 in the acquiring switch and 300 in the card-present service;
- committed platform client-secret fallbacks are removed and local ports are assigned to POS 8821 and card-present 8822;
- the browser auth package contains only POS OIDC, role, response-envelope and API-origin concerns; copied wallet, agent, reversal, customer, permissions and platform mock surfaces were removed;
- validated contracts cover 138 OpenAPI operations and 25 versioned POS event schemas, including strict POS-owned store operating profiles, bootstrap, shift, guarded lost-terminal recovery, signed close-of-day pack, ordered offline batch replay and manager conflict resolution, catalog and item images, stock, suppliers, goods receiving, transfers, physical counts, supervisor credentials and override decisions, country-neutral sales and tender events, signed public digital receipts, provider results, signed provider callbacks, fiscalization reconciliation, electronic-refund reconciliation, provider-statement reconciliation, store reconciliation, refunds, customers, engagement, and card-terminal binding;
- the remote evidence harness has 31 passing tests. It can authorize an assigned merchant through the dedicated public
emali2-pos-uatOAuth device client without a password or client secret, validates issuer/client/pos-apiaudience/merchant role, keeps the access token in memory, creates a permission-restricted P-256 key for a separately enrolled programmableHARDWARE_POSUAT terminal, and can let an assigned administrator create and consume a five-minute activation challenge entirely in memory without displaying or persisting its code. Regular merchants/operators still receive403; the harness does not bypass the administrator gate. It signs activation and every protected mutation with the same cross-language canonical vectors, and fails locally rather than attempting bearer-only terminal mutations. Its destructive recovery command requires an assigned terminal administrator, binds the exact proof-bearing identity session with a signed heartbeat, revokes the terminal, follows refresh-token rotation until the identity provider denies the session, and confirms the revoked terminal key receives the generic proof rejection without recording tokens or the opaque session identifier. Prompt UAT validates the generic wallet prompt type, amount and currency, follows only the configured provider's tender, requires a captured full-payment sale to bePAID, rejects an uncaptured paid sale, cleans up failed test sales, and excludes the customer routing reference from evidence. Its cancellation flow first requires the exact configured adapter to advertiseCANCEL_PENDING_PAYMENT, then waits for an immutable provider reference, requires the signed tender-cancellation route to execute, and proves both confirmed cancellation and capture-wins race outcomes. Android remains device-led because its Keystore key is non-exportable; - POS owns a PostgreSQL
posschema foundation with idempotency, inbox, outbox, audit, merchant/store/staff projection, terminal-enrollment, authoritative catalog, barcode, item-image metadata, store-price/stock, immutable stock-movement, supplier, goods-receipt/line, stock-transfer/line, physical-count/line, supervisor-credential/override, cashier-shift, cash-movement, sale, line, tender, receipt, refund/credit-note, sanitized offline-sync conflict, customer, customer-store membership, and versioned engagement tables; private image bytes live in POS-owned S3-compatible storage; - PostgreSQL identity separation is implemented and verified for both installation paths. Fresh initialization uses a separate bootstrap administrator Secret and creates an application-owned database/
posschema under a SCRAM role with no superuser, database-creation, role-creation, replication or bypass-RLS capability. On 17 July the guarded legacy transition completed remotely after a validated pre-migration dump: it created the separate administrator, recreated the application username least-privileged, transferred every database/schema/table/sequence/function owner, removed all application memberships and deleted the temporary transition role. The resumable migration now co-locates its isolated Job with PostgreSQL, permits only the exact database Service/32and port, waits for policy propagation, drains legacy sessions after disabling new logins, and handles table-owned sequences without a conflicting second owner change. Four resource guards, three orchestration guards, six recovery-readiness guards and the identity/linked-sequence scratch-PostgreSQL integration pass. The remote database is Ready at Flyway v30 with 52 tables; the application identity owns all POS relations and has none of the administrator capabilities; - the remote PostgreSQL 16 logical recovery rehearsal restores all 50 POS tables at Flyway v29 into a fresh temporary database, matches critical business-row checksums, column and stable schema-structure metadata, and sequence state, then verifies removal of the temporary database and dump. The runner now requires the authoritative
POS_DATABASE_NAMErather than PostgreSQL's bootstrap database, validates and shell-quotes its remote kubectl inputs, and passes six guards plus ShellCheck. The current dump is 212,830 bytes and the verified cycle completed in three seconds. This is same-pod logical-restore evidence only; enabled external backups, a remote isolated replacement-database restore from real off-cluster artifacts, and measured RPO/RTO remain pilot gates; - the provider-neutral off-cluster backup foundation is implemented and its immutable amd64 image is published. It creates a consistent custom-format dump, encrypts the dump and completion manifest with an age public recipient, requires verified external HTTPS, bucket versioning, default object-lock retention and S3 SSE, and verifies ciphertext metadata. The image also contains a strictly read-only storage readiness command that proves reachability, versioning, positive default retention, exact default encryption and a non-public ACL; its AWS mode additionally requires all four public-access-block controls. Six command-level tests prove fail-closed policy and zero storage writes, and five runner guards prove digest pinning, Secret-key-name-only inspection, DNS/external-HTTPS-only egress and resource cleanup. A fresh native ARM64 integration passes the auditor, encrypted backup and isolated restore through a TLS S3-compatible store and a separate PostgreSQL service with exact checksums and cleanup. The digest-pinned storage-audit manifest also passes the real cluster's server-side dry run with
resources_created=false. Both remote backup schedules referencetest-20260717-partial-refunds-v29-1but remain suspended with zero Jobs/pods and no external-backup credential Secret. External immutable storage, access-audit routing and a remote restore drill from real artifacts remain open; - a region-neutral AWS provisioning package now defines the preferred native-S3 path instead of leaving it as prose. Terraform creates Versioning plus Object Lock at bucket creation, default retention, public-access blocking, bucket-owner-enforced ownership, TLS-only access, SSE-KMS with a rotating customer-managed key, deletion guards, and separate archive-writer/recovery-reader policies. It creates no access keys and grants neither workload delete, retention-change nor governance-bypass permissions. The configuration formats and validates with Terraform 1.5.7 against pinned AWS provider 6.55.0; five static CI guards reject credential state, region locking and missing immutability controls. A guarded Secret reconciler validates a mode-0600 non-symlink bundle, requires distinct writer/reader and replication/recovery-database credentials, cryptographically matches the age recipient to its private identity, supports native AWS KMS or generic AES-256 S3-compatible storage, streams values only over SSH standard input with server-side apply, refuses create/rotation unless both schedules are suspended, and never changes the CronJobs. Nine guards prove no-network planning, separate confirmations, partial-state refusal and secret-free output/arguments. A live metadata-only SSH/Kubernetes audit confirms the two CronJobs are suspended and both dedicated external backup/recovery Secrets remain absent; the in-cluster SeaweedFS application store is not counted as off-cluster recovery. Actual AWS account/region approval, provisioning, access-audit routing, Secret injection and the remote drill remain open;
- the provider-neutral WAL/PITR foundation is implemented in separate immutable
backup and PostgreSQL images. Synchronous WAL archival returns success only
after age, SSE and object-lock verification; authenticated physical bases
record system identity and exact manifest WAL range and pass
pg_verifybackup; recovery accepts only an isolated data root and one time/LSN/name target. Local integration starts the recovered PostgreSQL 16 server, proves the named recovery target includes a committed post-base row and excludes a later row, verifies the original system identity, all 50 Flyway v29 tables and promotion, then rotates the restored role to a generated recovery-only password. TCP is forced through SCRAM from startup. The narrow trusted Unix-socket bootstrap is usable only by the non-root isolated PostgreSQL container, is atomically replaced with SCRAM after rotation, and passwordless socket access plus the source password are rejected before the application starts. The real standalone POS image starts with the rotated credential, reportsUP, and returns401from a protected route. A non-root sidecar generates a pod-local ephemeral RSA identity, serves only its JWKS, signs a five-minutepos-api/POS_SERVICEtoken and validates an authenticated merchant bootstrap200from the recovered projection; it removes the private key, authorization header and response before success and mounts no live OAuth or POS application Secret. The latest prepare-to-promotion sample is 15 seconds. Four backup, thirteen PITR, six remote-runner, and three dedicated-role guards pass. The runner propagates the recovery Secret's authoritative SSE mode and optional KMS key id to both restore containers, closing an AWS SSE-KMS failure path, and never mounts the live application Secret. The Secret carries the matching restored username/database plus a distinct drill-only password rather than the live database password. The corrected manifest passes the real cluster's server-side dry run with zero resources. The exact role SQL also passes creation, SCRAM physical-replication authentication, rotation, old-password rejection, no-membership and denied POS-table integration tests. A fail-closed remote runner creates a unique PVC/Pod, separate recovery identity mount, deny-ingress/external-HTTPS-only policy, expected identity/schema assertions, promotion wait, no-integration-profile POS smoke sidecar, and verified cleanup. A digest-pinned one-shot role provisioner authenticates only as the separate PostgreSQL administrator and creates two temporary DB-and-DNS-only NetworkPolicies; its Kubernetes server-side validation creates zero resources and it is a required predecessor to archiving activation. The remote Sunday base-backup CronJob remains suspended with zero Jobs/pods; the backup Secret is absent and the stock primary image, arguments and egress remain unchanged. The hardened PITR image is locally verified on native ARM64, builds as a loadable AMD64 image, and is published astest-20260717-authenticated-smoke1at digestsha256:4cdad5e814c035860b46266780aeac788893e26ece31bd9b9c19746730295d8f. Registry inspection proves Linux/AMD64, and the real-cluster server-side drill validation, including the authenticated sidecar, passes with zero resources; no remote workload was changed. A real external bucket, recovery-vault identity, live dedicated replication role, enabled/monitored archive command, execution of the implemented remote authenticated business smoke and measured remote RPO/RTO remain pilot gates; - typed platform, card-present, mobile-money, customer-app-payment, and fiscalization ports keep provider and jurisdiction rules outside the POS domain. Public APIs, events, web clients and Android emit only generic tender categories such as
MOBILE_MONEYandWALLET; provider brands live inproviderCodeand adapter configuration. Executable domain code depends only onMobileMoneyPortandFiscalizationPort; the deprecated eTIMS-named domain alias has been removed and adapter tests now compile against the generic fiscal boundary. A four-check CI guard rejects country/provider names in domain sources or generic POS events and proves the OSCU and Daraja implementations remain behind their respective generic ports. Adapter identity is now distinct from provider response codes: tenders retain stable identities such asMPESA_DARAJAorEMALI_APP, while raw result codes remain adapter-attempt evidence. The generic simulator proves a non-KenyanUGXrequest through the same port. Old storedMPESAandEMALItender values are decoded and normalized only for migration compatibility, never emitted by current clients. The opt-inmpesa-darajaprofile provides the Kenya-specific M-PESA implementation, while the certification-gatedetims-oscuprofile provides the Kenya-specific KRA OSCU v2.0 implementation. Other countries add adapters without changing tender categories, sale state, or POS database ownership. The country-neutral, capability-aware Flyway v30 release is the database baseline in test. On 17 July, the customer-app refund change was then rolled out as narrow, auditable overlays on the exact live images: Coresha256:91df9d195226774639d6aaba42d7e2a58242c06db1d6bb70bec8c8dc98d95d1d, POS servicesha256:24f1ff4b5d5249a1b18788106f4d3d61340936e03de7e547b033a9fe579fd88e, and Cashier Websha256:091690b777a38a443db5b3c3f3eb91606e14ae6c48fadf8f3a8fa8dd45800824. No schema, secret, Backoffice, Android or country-adapter profile changed. Core API/worker, POS and Cashier are Ready with zero restarts; POS health and Cashier return200, unauthenticated bootstrap remains401, an authenticated service probe reaches the new Core refund controller, and live bootstrap advertisesEMALI_APPREFUND_CAPTURED_PAYMENT. Kenya-specific OSCU and Daraja remain disabled. The current physically checked Android test overlay is pinned tosha256:f6c32767ddaa4b7d45db0fe1be4fa286db0e94c3bc7ed27c7592027aeb1a07cd; physical printer and merchant-authenticated captured-refund UAT remain open. - the backoffice and cashier web derive money labels, reports, loyalty economics, inventory prices/imports, tenders and chargeback currency from each store's ISO operating profile or the shift-locked currency. They no longer fabricate KES, SZL, USD, or a country locale when configuration is absent; price writes fail clearly until a currency is configured. For any store-bound fiscalization provider, Backoffice exposes the same neutral provider/classification/item/unit/tax profile and generic CSV columns; it no longer builds KRA field names or Kenya tax choices into the catalog UI. Operations now includes the manager-only fiscalization exception queue: no outcome is preselected, constrained evidence is mandatory, rejection cannot invent a provider reference, and both the warning and confirmation state that no document is resubmitted. Dedicated test-mode builds compile the test API and identity origins instead of production-mode values.
- the platform exposes read-only, role-scoped OAuth APIs for merchant, store, staff, catalog, customer, and engagement projections; POS persists those projections without platform database access;
- terminal activation, signed proof of possession of the submitted public key, key binding, heartbeat, revocation, and transactional enrollment outbox emission are implemented behind an opt-in, secret-backed feature flag. Invalid or tampered proofs fail generically before activation-code lookup or consumption.
- Android enrollment now adds optional server-verified hardware attestation without changing the generic terminal API for web or vendor devices. Flyway v31 stores only
NOT_PRESENT,PRESENT_UNVERIFIED, orVERIFIEDaudit evidence and trusted attributes; it never stores the certificate chain. The service verifies the submitted key, chain signatures and expiry, both published Android trust anchors, Google revocation status, the activation challenge, TEE/StrongBox security, generated SHA-256 signing authorization, locked verified boot, package name and signing-certificate digest. A connected Samsung SM-P619 produced a four-certificate, hardware-backed chain whose leaf matched the non-exportable Keystore key; the exact server verifier accepted that evidence against the live Google revocation list and configured package/signing identity. Bootstrap and Backoffice now expose only the normalized status, security level and verification time, never the chain, trust-anchor hash or a device identifier. Rollout remainsAUDIT; an authenticated activation/re-enrollment and verification of its persisted v31 result remain the gates forREQUIRED_ANDROID_HARDWARE. - Android now reconciles its persisted binding with the authenticated server bootstrap immediately after successful OIDC sign-in and during every 15-minute WorkManager cycle. A revoked or no-longer-assigned terminal UUID is deactivated locally without deleting offline transaction history, allowing a fresh activation to proceed. The client never adopts a replacement device merely because it occupies the same lane or has a similar terminal code; the opaque server UUID remains authoritative. Five focused unit tests cover retained identity, omission, explicit revocation/inactivity, legacy no-UUID matching, and replacement-device rejection.
- the separate card-present service is live at
card-api.test.emali2.damplabs.comwith its own PostgreSQL database, migrations, Secret, image, certificate and NetworkPolicy. It accepts the POS service's confidential OAuth token for a read-only lookup by opaque POS terminal UUID; there is no cross-database foreign key and no Core proxy. - the Android client now uses AppAuth Authorization Code with S256 PKCE, encrypted
AuthState, a public Keycloak client with no secret, an Android Keystore EC terminal key, a versioned activation proof, and per-request proof for protected terminal mutations signed by that non-exportable key. Cashier web now enrolls onlyWEBterminals, creates a non-exportable WebCrypto P-256 key, persists it through structured clone in IndexedDB, retains no activation code, signs the exact serialized mutation body, and refuses bearer-only or cross-platform terminal use. The server WEB proof test covers signed request acceptance, durable replay rejection, missing/tampered proof rejection and CORS preflight headers. Android has data-preserving Room migrations through v17. Room v13 persists the store country, currency, locale, timezone, fiscalization mode, preferred provider codes, and shift-locked currency. Room v14 preserves the exact canonical signed close pack after Android verifies its SHA-256, Ed25519 signature, current trust-anchor key id/public key, and store/terminal/session binding. Room v15 retains the sanitized server conflict id/outcome for blocked jobs, pulls manager retry/discard decisions for the enrolled terminal, and consumes each retry decision once. Room v16 stores adapter-advertised customer-app cancellation support and safely defaults legacy rows to disabled. Room v17 preserves authoritative refund and line-allocation state so the handheld can subtract already returned quantities and use cash-tender deltas for drawer expectation. Reconnect submits at most 50 dependency-ordered offline-safe mutations through an exact-body-signed batch, applies each authoritative item result, and keeps customer-app/mobile-money/card prompts online-only. The UI exposes only configured provider capabilities and only adapter-advertised operations. Reconnect, periodic sync, Home and Reports refresh unresolved electronic sales from the authoritative server. A vendor-neutral hardware module provides a transport router, control-safe ESC/POS encoder, Bluetooth SPP selection, explicit-permission USB printer-class discovery, private-LAN raw-TCP configuration, selectable 58/80 mm profiles, print test, marked reprints, explicit queue retry, a disabled-by-default non-retried drawer qualification pulse, rapid external HID barcode capture, and real battery/network/printer/scanner health published during enrollment and periodic sync. - Android money, receipt, shift and close-of-day rendering now require the enrolled store or shift currency and operating timezone. There is no deployment-wide currency or Nairobi timezone fallback. The current source build passes 83 unit tests in both variants, including terminal UUID reconciliation, transport routing, USB identity/re-enumeration/permission/probe/partial-write policy, private-address enforcement, no-resend-after-partial-network-write behavior, provider-neutral payment-prompt messaging, provider-operation bootstrap compatibility, device-confirmed versus one-way-transport print completion, and eight status-capable built-in-printer boundary cases; debug lint and both APK assemblies pass. The physically installed hardware-UAT APK and its immutable URL remain SHA-256
a700276eb60e4f57d76ff0b45729987e083add04762f1efdbb64e09decfa4c4f;MainActivityremained the top activity with a live process and no fatal launch event. The latest test alias now serves the separately verified terminal-revocation recovery APK with SHA-256eaaa2e18372a81a509508ac1f72188546e5d145ef0f9b099a7aa13bfefa2c5cd, pinned toghcr.io/mainamartin/emali2-pos-apk@sha256:160b2c29eb56d73f9fb7b79da5b5db9147663b6182d614c2323204c7b211f7c7; it is not yet physically accepted. The earlier attestation-audit, USB-host and network-printer URLs remain immutable and downloadable with their original hashes. The sanitized audit passes but correctly reports no connected printer or scanner, so physical peripheral and merchant customer-prompt validation remain open. - the adjacent customer withdrawal-quote path now converts missing, inactive or non-KYC agents into a stable
agent_unavailablecontract; customer web, Android and iOS map both the new contract and legacy raw 404 payloads to identifier-free guidance, and iOS cannot continue without a valid quote. A fresh focused verification on 17 July passes 36 Core tests, five customer-web tests, two focused Android test classes, and the preceding unsigned iOS simulator build. An authenticated live request using the reported unavailable agent returned only404 agent_unavailable; neither the identifier nor legacyAgent not founddetail appeared. The disposable customer-role probe client was deleted and a follow-up identity audit found zero probe clients. The deployed Core artifact at digestsha256:d7d55d4aa72065bfa8a5e9b8fd00cbafc0c8438610cbf2985c288b9681caf1abcontains the compiled exception, advice and endpoint-only last-mile sanitizer. The handler and filter share the immutable public message constant, customer USSD documentation contains no legacy identifier-bearing error, and CI runs the cross-repository customer-surface guard plus focused Core, customer-web and Android regression tests. The POS service-credential preflight still proves health, merchant-only bootstrap, country-neutral profile, provider capability, POS-owned catalog, reconciliation and absence of an agent-withdraw dependency without retaining a token, secret, password, customer reference or MSISDN. The Android test installer is published at the established HTTPS download aliases with SHA-25662f8dc4004b0067a9735abf738fc418800768b0951247cb6a404165d02ac063e. Signed TestFlight/App Store distribution remains an Apple-account gate. This remains a platform flow, not POS-owned data.
1. Executive decision
Emali2 POS should be treated as a separate product boundary, using the repositories that already exist:
| Repository | Ownership |
|---|---|
| emali2-pos-suite | POS backoffice, cashier web, Android client, POS API contracts, POS service, POS database migrations, hardware adapters, POS documentation and deployment |
| emali2-card-present | Card terminal estate, card authorization handoff, reversals, settlement, chargebacks and sponsor/acquirer adapters |
| emali2-backend-services | Platform organizations, organization units, users, wallet and merchant-payment rails, ledger, notifications and shared API gateway |
The existing emali2-pos-android copy and the POS code embedded in emali2-core remain migration sources only. They must not become competing long-term implementations.
The public API path remains stable at /api/v1/pos/** while the gateway changes its downstream target from emali2-core to the standalone POS service. This lets the Android and web clients migrate without an avoidable URL break.
2. What exists today
Healthy foundations
- The canonical POS suite Android project builds successfully.
- The Android design already uses Room, offline sale state, WorkManager and dependency-ordered replay.
- Focused embedded-core POS tests pass.
- The POS suite web packages type-check and all focused package tests pass; the country-neutral backoffice formatting and terminal-health suites currently contribute six tests.
- The POS suite backend builds and 170 current tests pass, with only the
opt-in physical-attestation fixture skipped when its private device evidence
is absent. Coverage includes a real
platform-httpapplication-context regression test, PostgreSQL migration through Flyway v32, neutral fiscal item storage and non-Kenya provider coverage, independence, automatic cross-replica-locked platform projection refresh, merchant-only store filtering, tenant-identity validation, store operating-profile validation, platform/customer/engagement projections, catalog/image/stock/supplier/receiving/transfer/physical-count/supervisor/reconciliation ownership, idempotency, audit/outbox, signed no-store terminal enrollment, durable request-proof verification/replay rejection, ordered offline batch replay with per-item outcomes, sanitized conflict lifecycle, branded legacy-tender normalization, Ed25519-signed close-of-day packs with tamper rejection, standalone split-tender sales, line-allocated partial refunds with repeated quantity and tender consumption, durable mixed cash/card refund reconciliation with constrained operator evidence, immutable provider-statement matching with raw-reference minimization, customer-app dispatch, authoritative pending-prompt cancellation and captured-payment refunds, stale-prompt manual-review quarantine without unsafe cash fallback, adapter-operation discovery and complete terminal-state mapping, Daraja initiation/query/callback behavior, OSCU request/response mapping, fiscalization durability, opaque card-terminal binding, fail-closed compatibility, route-security, CORS-origin coverage and dedicated API-audience rejection. - The separate card-present repository has a meaningful authorization, idempotency, reversal and settlement foundation; 434 tests pass across its reactor. Its test deployment owns a separate empty database, applies Flyway v1-v5 and accepts only the intended card/POS service and card-administration roles.
- The API gateway already defines routes for /api/v1/pos/ and /api/v1/card-present/.
- POS backoffice and cashier Keycloak clients and roles are already represented in setup scripts.
- DNS for the proposed test and POS hostnames already resolves to the current server.
Gaps and risks
- Every application-level platform proxy has been removed. The service owns platform reference projections, terminal enrollment, catalog bootstrap/list/lookup/import and writes, item images, stock movements, suppliers, goods receiving, inter-store transfers, physical counts, POS-specific supervisor credentials and override audit, customers, engagement, cashier shifts, manual cash-drawer movements, sale drafts, tender handoffs, cash completion, voids, receipts, line-allocated full and partial refunds, credit notes, and store reconciliation locally. Legacy terminal registration and payment resolution/payment fail locally with
410 Gone; merchant onboarding is called directly on the configured platform API origin. - Embedded customer/engagement mutation handlers still exist in emali2-core as migration/rollback sources, but the public POS customer and engagement routes now terminate in the standalone service. Those embedded writes must be disabled only after historical migration, remote parity, and rollback rehearsal.
- POS no longer imports platform Java implementations. Existing merchant, store and staff projections refresh automatically through the confidential platform API in bounded 15-minute batches. Only Core units explicitly typed
MERCHANTand their linked staff can enter the POS store projection; agent and biller units are excluded. The requested organization must also match the returned snapshot identity. A PostgreSQL advisory lock prevents duplicate scans across replicas, transient failures retry safely, and only exception classes are persisted or logged. Platform event-driven refresh remains a later latency optimization. - The web auth package is now a minimal POS-owned OIDC, role, response-envelope and API-origin client. Browser token storage remains JavaScript-accessible, so a backend-for-frontend security option should be evaluated before a higher-risk rollout.
- Android OIDC, Keystore enrollment, browser WebCrypto enrollment, server-verified activation proof, and per-request proof for protected Android, hardware-POS and WEB mutations are wired. A verified proof records only the opaque identity-session ID and subject against its terminal; no access or refresh token is stored. Terminal revocation atomically queues every bound session, calls Core's machine-only exact-session revocation boundary after commit, and retries durably with sanitized error codes until IAM confirms
REVOKEDorALREADY_ABSENT. Device attestation policy remains. - No static client secret may be included in Android or browser code. Native apps and SPAs are public OAuth clients.
- The test environment still uses a generated, dedicated POS service client secret. Live configuration keeps that transitional secret only in the POS Kubernetes Secret; the backoffice, cashier web and Android clients are public Authorization Code/PKCE clients with no secret. The POS service now also implements
private_key_jwtusing a mounted PKCS#8 key, 60-second audience-bound assertions, unique JWT ids and strict ES256/P-256 or RS256/RSA-2048+ validation. Six protocol tests verify both signatures, absence ofclient_secret, backward compatibility and weak/mixed configuration rejection. A guarded generator produces a 3072-bit RSA identity plus a public one-key JWKS carrying the exactkid, without printing private material. The secured operator workspace now holds a mode-0600RSA-3072 identity for key idpos-test-20260717-v1; its public-only JWKS passes the no-network plan, and its certificate expires in August 2027. A Keycloak 26.6-compatible reconciler has no-write planning, GET-only audit, separately confirmed apply and rollback paths. Its secure remote path explicitly enforces HTTPS/TLS 1.2 and works on Bash 3.2 instead of expanding an empty array underset -u; seven guards prove public-key-only upload, settings preservation, secure HTTPS audit, no secret read and zero-write audit. A separate guarded Kubernetes rollout now validates that the private key matches the public JWKS, pins the exact cluster context and running image digest, creates a one-key dedicated Secret only during confirmed apply, mounts it read-only as0440with pod group access, removes the shared-secret env entry, and carries a reverse patch that restores the transitional Secret reference. Its server dry run substitutes a non-key placeholder, so no private material crosses the network; both forward and reverse patches pass live server-side validation without changing the cluster, and nine local guards cover identity mismatch, unsafe key permissions, context/digest drift, rollback retention and confirmation. A live GET-only Keycloak audit still reports the expected pre-migration state:NOT_READY, client-secret authentication, no registered matching public key,private_key_absent=true,client_secret_read=falseandresources_changed=false. An isolated digest-pinned Keycloak 26.6.1 integration proves actual Admin REST registration, a real RS256 client-credentials token, no-write audit, rollback and reuse of the original secret. Actual identity-provider mutation, confirmed service rollout, sanitized signed-token preflight, retirement of the old secret and production managed-secret verification still require an approved credential-change window. - Catalog, stock, customer, engagement, cashier-shift, manual cash-movement, sale, tender, normalized provider-result, void, cash-refund, provider-statement import and fiscalization-resolution handlers now enforce request-hash idempotency. The optional Daraja adapter durably queues initiation once, keeps status queries retryable and hash-deduplicates callbacks. Unknown fiscal-provider writes are never automatically retried; the manager-only resolution API records an externally verified accepted or rejected outcome with opaque evidence, tenant isolation, immutable audit and event emission. Provider-side reversal and settlement execution still need equivalent adapter guarantees.
- Generic Android Bluetooth SPP, explicit-permission USB-host printer-class bulk OUT, and private-LAN raw-TCP ESC/POS printing, persisted transport-aware routing, selectable 58/80 mm profiles, a durable reprint spool, a guarded single-shot drawer qualification command, external HID keyboard-wedge scanning, configurable foreground scan intents, an offline on-device camera fallback, truthful printer/scanner readiness and periodic terminal-health reporting are implemented. The USB adapter ignores non-printer USB classes, persists no serial number, requires temporary Android permission, probes the interface, bounds chunked transfer and blocks retry after partial output. The network adapter rejects public destinations, performs bounded readiness/print connections, records one-way delivery as unconfirmed, and avoids a second address after a possibly partial write. Physical label/peripheral validation, exact-firmware SmartPOS SDK adapters, paper-out telemetry, and audited cash-event drawer triggering remain.
- The KRA OSCU v2.0 adapter, item/tax snapshots, monotonic invoice sequence, durable sale/credit-note jobs, printable fiscal data, read-only reconciliation API, unknown-outcome quarantine and provider-neutral operator resolution are implemented behind
etims-oscu. KRA sandbox credentials, integration/certification testing and production approval remain external gates; VSCU is not implemented. - The remote
emali2-testrollout is live. All four POS-owned hosts serve through a valid Let's Encrypt certificate, the three web applications return 200, the standalone API reportsUP, unauthenticated bootstrap returns 401, exact-origin CORS succeeds and an untrusted origin returns 403. The live Eswatini fixture now runs only the provider-neutralplatform-httpprofile and carries no M-PESA or KRA environment reference. Both base Kubernetes manifests enforce the same neutral default; dedicated Daraja and OSCU patches source endpoints and credentials from provider-specific Secrets, and both pass Kubernetes server-side dry-run validation without changing the cluster. The dedicated publicemali2-pos-uatclient accepts the OAuth device flow, has password/implicit/service-account grants disabled, scopes only the existing merchant/operator roles, and emits an explicitpos-apiaudience. The same access-token-only audience is reconciled for all five approved POS browser, native and machine clients, and the service requires it in addition to issuer, signature and token-time validation. A fresh service token passed all seven read-only UAT checks. A real merchant device authorization also passed issuer/client/audience/role validation, but the organization-55 account was correctly denied access to organization 131/store 577 with403; its mode-0600 evidence contains no token or identity. A signed same-realm probe token withoutpos-apireceived 401 and its temporary client was deleted and verified absent. The evidence harness is request-proof aware and can enroll a dedicated programmable test terminal only after an assigned administrator authorizes it; its preferred one-run path creates a five-minute challenge and consumes the code in memory without displaying or persisting it. Its 31-test gate drives a two-unit sale across two captured cash legs, returns one line at a time through repeated partial refunds, and verifies original-capture-order allocation, stock, idempotency, reconciliation and shift closure. It also has a terminal-administrator-only recovery flow that binds the proof-bearing OAuth session, revokes the terminal, follows refresh-token rotation until provider denial, and proves the revoked key receives a generic401, with no token or opaque session identifier in its evidence. Its opt-in customer-app path now requires the exact provider to advertiseREFUND_CAPTURED_PAYMENT, captures two units, proves a provider-confirmed one-unit partial refund, then refunds the remaining unit as exact-once cleanup while validating two distinct credit notes, immutable original-tender routing, stock and wallet reconciliation. A separate redirect-free Core reader uses distinct in-memory customer and merchant subjects and two exact self-scoped balance routes to assert capture, partial-refund and cleanup principal deltas without giving POS a balance scope or recording raw balances. The live customer-app adapter advertises authoritative captured refunds and Cashier Web serves the manager return tray. Correctly assigned organization-131 administrator authorization, signed terminal activation, cash/refund/receipt execution, terminal revocation and a valid public receipt are now complete. One real customer-approved/declined/expired execution and physical handheld printer/scanner acceptance remain before UAT sign-off. - The legacy main-branch SSH deploy has been removed because it imported shared platform configuration/secrets and did not assert a Kubernetes context. CI builds immutable commit-tagged images and verifies Android tests plus debug/release APKs. Remote apply is manual and fail-closed; the first audited rollout used dedicated POS secrets and immutable test tags. A POS-service ingress/egress policy permits only its own PostgreSQL/object storage, the versioned platform API, cluster DNS, external HTTPS, ingress and monitoring traffic; it has no route to platform databases.
- Core supports customer-targeted Emali merchant collection with push, in-app and websocket prompts plus registered-device approval and transactional wallet payment. The store-scoped internal prompt/status/cancellation/refund API and standalone POS
WALLET/EMALI_APPadapter are deployed. POS encrypts the customer routing reference at rest, durably dispatches an idempotent prompt, polls the authoritative authorization, and captures the tender only after Core reports the wallet payment as consumed. A store-scoped cancellation operation locks the same authorization row as payment consumption: onlyPENDINGorAPPROVEDcan becomeCANCELLED, while a payment that already becameCONSUMEDremains captured. Store-scoped submit/status endpoints now provide idempotent full or partial principal refunds. Core serializes concurrent refunds on the captured payment, enforces currency and cumulative captured-balance limits, debits the merchant settlement ledger, credits the customer wallet, and queues the matching Finance transfer when Finance is enabled. The POS adapter advertisesREFUND_CAPTURED_PAYMENT, preserves the original authorization reference for both submit and status calls, and fails closed into manual review when the provider write outcome is unknown. A provider prompt that remains pending past its advertised expiry or cannot be confirmed within the configured 30-minute ceiling is now quarantined locally for manual reconciliation; its tender remains pending, so the unknown outcome cannot unlock cash fallback. Cashier Web exposes returns only when the exact configured adapter advertises the operation. Core also converts unexpected payment-adapter exceptions into stable customer-safe guidance instead of copying provider or internal exception text into customer-visible state. The customer cash-out quote boundary separately maps unavailable, inactive or non-KYC agents to one stableagent_unavailableresponse without returning the submitted agent identifier. Android and iOS enforce the same mapping inside their quote API clients as well as at the presentation layer, so even a legacy identifier-bearing404cannot escape through a new caller; wallet, validation and session errors remain distinct. The focused native gate passes 12 tests, the iOS source contract and unsigned simulator build pass, and the 33-test Core quote/service/HTTP regression gate passes. The current remote-test Android customer installer iscom.emali2.superappversion0.1.1, SHA-25630457e29e0986aeace18c647a174667c578876e5c72ef0f1399ce3b48daba192, published at both latest and immutable aliases from imagesha256:3c027cafb5f325d1eaf31eb0cb4016c50b70d9f987a09833708143276febef05; the download pod is Ready with zero restarts and every customer alias matches the same APK. A merchant-authenticated, till-scoped Core balance route exposes only organization/store identity, ISO currency, posting balance and observation time; authorization requires both the merchant view permission and access to the exact till, while the separate customer route remains self-scoped. Core API is Ready with zero restarts onsha256:bb399aeb1c208355c3fc8d5cc969106589e7da53a30076080dc46d00366c5f01; its immediate rollback issha256:fb4c5884c44536cb9178b2b1b50605b65300400eaddfc44bfbe64f7010a1b7f4. The unchanged worker is Ready onsha256:c9678b8af58bd1e62584f83748444cdf5c128dd3d7d5fe500fb595ac85512397. POS is Ready with zero restarts onsha256:2ffe8c5fe62d4536d691af9ec4f6e53f6c6ffa376bde4dcf713b230a9592ca43; its immediate rollback issha256:ff2226eefc33ba2cf3760cf6b62ee5f278c221ff2b65c6417773f785b86b42f6. Merchant-user approval/denial/cancellation/refund UAT remains. - The numeric POS-to-card terminal coupling has been replaced at the service boundary. Card-present stores
posTerminalReferenceandstoreReferenceas opaque strings; POS resolvesGET /api/v1/pos/terminals/{id}/card-present-terminalonly after local store authorization. Thecard-boundary2test deployment isUP, rejects unauthenticated requests with401, rejects an unenrolled UUID with403, and reaches the empty card estate with the POS service token. A real mapped200plus certified terminal SDK/direct terminal-to-card payment handoff remains for merchant/device UAT; POS must not forward PAN, PIN, track data or EMV cryptograms.
24 July 2026 customer-boundary refresh: Core API is Ready with zero restarts on
the immutable overlay
sha256:f111be0494a57b30c903de0df67958412216bfdb01d69ee029c97a98d45b5eb0;
its immediate rollback is
sha256:bb399aeb1c208355c3fc8d5cc969106589e7da53a30076080dc46d00366c5f01.
An authenticated live replay of the reported unavailable-agent quote returned
only the stable agent_unavailable contract and omitted both the submitted
identifier and legacy backend detail. The disposable CUSTOMER-role probe
identity was deleted, and a follow-up realm query returned no probe identities.
The worker was not changed.
24 July 2026 merchant-IAM gate: the live realm was missing the
merchant_integration role declared by the repository setup, so it was
provisioned through Keycloak in Kubernetes and audited. Organization 55 was
then loaded through the normal platform-projection sync, producing active
foreign stores 381 and 495 without direct database writes. A disposable
confidential client with an exact azp, the pos-api access-token audience
and only the machine-integration business role could read its explicitly bound
organization 131/store 577, received 403 for organization 55/store 381,
could not be rebound across organizations (400), and lost access immediately
after revocation (403 with the same unexpired token). Both disposable
Keycloak clients were deleted and confirmed absent; the POS authorization row
remains inactive as an audit record. Local validation also passes 23 focused
store/database tests and the disposable Keycloak client-credentials integration
test without printing or retaining a secret.
3. Target architecture
flowchart LR
A["POS Android"] --> G["Public API gateway"]
W["POS Cashier Web"] --> G
B["POS Backoffice"] --> G
G --> P["Standalone POS service"]
P --> D[("POS PostgreSQL")]
P --> O["POS object storage"]
P --> Q["POS outbox and event bus"]
P --> I["Platform integration API"]
I --> C["Platform organizations and assignments"]
I --> M["Wallet and merchant payment rails"]
I --> N["Notification service"]
P -. "Opaque terminal mapping and status only" .-> CP["Card-present service"]
T["Certified payment terminal or SDK"] --> CP
CP --> CD[("Card-present PostgreSQL")]
CP --> ACQ["Licensed acquirer or sponsor"]
P --> FISC["Country fiscalization adapter"]
P --> PAY["Customer app or payment-provider adapter"]
Boundary rule
No POS process may read or write a platform database. No platform process may read or write the POS database. Identifiers from another bounded context are stored as opaque external references, never as database foreign keys across services.
Deployment rule
Each product must have:
- its own CI pipeline and container image;
- its own database credentials and migrations;
- its own runtime configuration and secrets;
- independent health, metrics, logs and alerts;
- explicit network access rules;
- versioned synchronous and event contracts.
4. Data ownership
POS owns
- merchant and store reference projections;
- staff assignment projections needed at a lane;
- product catalog, barcodes, prices, tax categories and store assortment;
- inventory balances and stock movements;
- terminals, device enrollment and terminal configuration;
- cashier shifts and cash drawer state;
- sales, lines, discounts and promotions;
- operational tenders and provider references;
- receipts and print jobs;
- refunds, voids and supervisor overrides;
- customer POS profiles, consent, loyalty and rewards;
- fiscalization requests, responses and status, with jurisdiction-specific attributes isolated behind adapters;
- idempotency records, inbox, outbox and audit trail;
- POS reconciliation and reporting read models.
Platform owns
- legal organizations and organization units;
- authoritative platform users and roles;
- wallets, till accounts and financial ledger entries;
- Emali merchant-payment transaction truth;
- common notifications and communication delivery;
- platform compliance and reporting that is not POS operational reporting.
Card-present owns
- acquirer merchant profile;
- certified card terminal registry and key state;
- opaque POS terminal and store references with no POS database foreign key;
- authorization, reversal and capture lifecycle;
- sponsor/acquirer references;
- settlement batches, chargebacks and acquiring reconciliation;
- card-payment idempotency.
POS must never receive or persist a PIN, full PAN, track data or EMV secret. It should receive only a token or handoff identifier, authorization outcome, network, masked last four digits and provider references.
5. Identity, clients and secrets
Human clients
Create dedicated public OIDC clients:
| Client | Flow | Secret |
|---|---|---|
| emali2-pos-android | Authorization Code with PKCE | None |
| pos-cashier-web | Authorization Code with PKCE | None |
| pos-backoffice | Authorization Code with PKCE | None |
| emali2-pos-uat | OAuth 2.0 Device Authorization Grant, interactive UAT only | None |
For the first test environment, use the existing identity provider at auth.test.emali2.damplabs.com/realms/emali2 so staff identities are not duplicated. If POS later requires a separate realm, use identity brokering or federation; never copy or synchronize passwords.
Every approved POS client receives the dedicated pos-api audience in access
tokens only. Realm roles answer what a caller may do; the audience separately
proves the token was issued for this API. The resource server rejects missing
or wrong audiences before controller authorization.
Terminal enrollment
- A merchant administrator creates a short-lived, single-use terminal activation code or QR.
- The Android device generates a non-exportable key pair in Android Keystore.
- The app signs a deterministic versioned payload covering the normalized activation code, public-key thumbprint, device metadata and capability declaration, then sends the payload fields, public key and signature.
- The POS service verifies the signature before activation-code lookup, validates the administrator grant and binds the device to a merchant, store and lane.
- The server returns the terminal identifier and configuration version and binds the submitted public key; it does not issue a reusable terminal secret.
- Every sensitive terminal request is bound to the enrolled device and cashier session.
- Device revocation immediately invalidates refresh credentials and terminal access.
Use hardware-backed key attestation or Play Integrity where the selected device supports Google services. Provide a documented fallback for dedicated SmartPOS devices without Google Play services.
Service integrations
Confidential machine clients may use client credentials. Prefer private-key JWT or mutual TLS when supported; a shared secret is acceptable for the first controlled test environment only if it is:
- stored only in Kubernetes Secret or a managed secret store;
- injected at runtime;
- scoped to the minimum API permissions;
- different in local, test and production;
- rotated and auditable;
- never committed, logged or returned to a browser or Android app.
The POS platform client implements both the transitional client_secret_post
mode and private_key_jwt. Private-key mode reads one PKCS#8 DER key from a
read-only mounted file at startup, accepts only ES256 with P-256 or RS256 with
RSA-2048+, and signs a 60-second assertion whose issuer and subject are the
client id and whose audience is the exact token URL. It sends the standard JWT
bearer assertion type and no client_secret; startup fails if both credential
types are configured. The guarded operator generator defaults to RSA-3072 and
exports only a certificate/public key for identity-provider registration. The
remote service remains on client_secret_post until its signed-JWT client and
mounted Secret are provisioned in an approved rotation window.
Recommended service clients and scopes:
| Client | Required scopes |
|---|---|
| pos-service | platform.organization.read, platform.unit.read, platform.assignment.read, platform.catalog.read, platform.customer.read, platform.payment.create, platform.payment.read, platform.payment.refund, platform.notification.send |
| platform-pos-sync | pos.reference.write, pos.sale.read, pos.reconciliation.read |
| pos-service to card-present | card.terminal.read; current test realm enforces the POS_SERVICE machine role |
| certified card terminal | card.payment.authorize, card.payment.capture, card.payment.reverse; prefer terminal certificate or private-key JWT |
| card-present-service | pos.card-payment.write, pos.card-payment.read only if normalized outcomes are pushed back to POS |
| emali2-pos-payment-adapter | pos.payment-result.write (pos_payment_result_writer realm role) |
| fiscalization-adapter | pos.fiscalization.read, pos.fiscalization.write |
6. API inventory
All mutation endpoints require an Idempotency-Key, a correlation identifier, authenticated merchant/store context, and an audit actor. The path names below are the target contract; existing compatible routes should be retained through adapters during migration.
Device and terminal API
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/pos/terminal-activations | Create a short-lived activation challenge |
| POST | /api/v1/pos/terminals/activate | Bind a device to a merchant store and lane |
| POST | /api/v1/pos/terminals/{id}/heartbeat | Publish app, OS, battery, network and hardware health |
| GET | /api/v1/pos/terminals/{id}/card-present-terminal | Resolve tenant-scoped card-terminal mapping and readiness by opaque POS UUID |
| GET | /api/v1/pos/terminals/{id}/configuration | Fetch versioned remote configuration |
| POST | /api/v1/pos/terminals/{id}/revoke | Revoke a lost or compromised device |
| GET | /api/v1/pos/bootstrap/me | Load cashier, store, terminal and policy bootstrap |
| GET | /api/v1/pos/stores/{storeId}/operating-profile | Read the tenant-scoped country, currency, timezone and provider configuration |
| PUT | /api/v1/pos/stores/{storeId}/operating-profile | Version-check and update the store profile using external secret references only |
Catalog and inventory API
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/pos/catalog/bootstrap | Full or incremental offline catalog snapshot |
| GET | /api/v1/pos/catalog/lookup | Barcode, SKU or PLU lookup |
| POST | /api/v1/pos/catalog/items | Create an organization product |
| PUT | /api/v1/pos/catalog/items/{id} | Update item, tax, barcode and selling configuration |
| GET | /api/v1/pos/stores/{storeId}/stock | Store stock view |
| POST | /api/v1/pos/stores/{storeId}/stock-movements | Receive, adjust, waste, transfer or count stock |
| GET | /api/v1/pos/inventory/suppliers | List POS-owned suppliers |
| POST | /api/v1/pos/inventory/suppliers | Idempotently create or update a supplier by code |
| GET | /api/v1/pos/inventory/receipts | List POS-owned goods receipts |
| POST | /api/v1/pos/inventory/receipts | Atomically record receipt lines and increase stock |
| GET | /api/v1/pos/inventory/transfers | List completed inter-store transfers |
| POST | /api/v1/pos/inventory/transfers | Atomically move stock between two stores |
| GET | /api/v1/pos/stores/{storeId}/stock-counts | List physical counts |
| POST | /api/v1/pos/stores/{storeId}/stock-counts | Open a count and snapshot expected balances |
| GET | /api/v1/pos/stores/{storeId}/stock-counts/{countId} | Load a count and its lines |
| POST | /api/v1/pos/stores/{storeId}/stock-counts/{countId}/submit | Atomically apply a complete, non-stale count |
| POST | /api/v1/pos/stores/{storeId}/stock-counts/{countId}/cancel | Cancel an open or stale count |
Shifts and cash API
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/pos/sessions/open | Open cashier shift and declare opening float |
| GET | /api/v1/pos/sessions/current | Recover the current terminal session |
| POST | /api/v1/pos/sessions/{id}/cash-movements | Paid-in, paid-out and supervisor no-sale drawer action |
| POST | /api/v1/pos/sessions/{id}/close | Submit counted cash and variance |
| POST | /api/v1/pos/sessions/{id}/close-and-submit | Finalize the close-of-day pack |
Sales, tender and receipt API
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/pos/sales | Create or replay a sale using a client-generated reference |
| GET | /api/v1/pos/sales/{id} | Read sale state |
| GET | /api/v1/pos/sales/by-reference/{reference} | Recover an idempotent sale |
| POST | /api/v1/pos/sales/{id}/park | Park an unpaid basket |
| POST | /api/v1/pos/sales/{id}/resume | Resume a parked basket |
| POST | /api/v1/pos/sales/{id}/collect | Add cash, wallet, mobile-money, QR, card-present, loyalty or another configured tender category |
| POST | /api/v1/pos/payment-provider-results | Apply a normalized provider result using a confidential service client |
| GET | /api/v1/pos/sales/{id}/tenders/{tenderId} | Poll authoritative tender state |
| POST | /api/v1/pos/sales/{id}/void | Void before financial completion |
| POST | /api/v1/pos/sales/{id}/refund | Manager-authorized line-level full or partial refund allocated across captured tenders; electronic legs require an authoritative provider refund adapter |
| GET | /api/v1/pos/sales/{id}/receipt | Obtain printable and digital receipt data |
| POST | /api/v1/pos/sales/{id}/receipt/deliver | Email or SMS a receipt with recorded consent |
Offline synchronization API
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/pos/sync/batches | Submit ordered offline mutations with per-item results |
| GET | /api/v1/pos/sync/changes | Pull server changes from a monotonic cursor |
| GET | /api/v1/pos/sync/conflicts | Sanitized store/terminal conflict work queue |
| POST | /api/v1/pos/sync/conflicts/{id}/resolve | Supervisor retry/discard decision with audit note |
The server must return a deterministic result for every replayed idempotency key. A batch is not all-or-nothing: each item gets accepted, already-applied, retryable or rejected status.
Backoffice, reporting and integration API
| Method | Path | Purpose |
|---|---|---|
| GET/PUT | /api/v1/pos/engagement/stores/{id} | Loyalty, rewards, promotions and tender policy |
| GET/POST | /api/v1/pos/customers | Search or create customer profile |
| GET | /api/v1/pos/reconciliation/stores/{id} | Tender and terminal reconciliation |
| GET/POST | /api/v1/pos/provider-settlement-reports | List or idempotently import normalized provider statements |
| GET | /api/v1/pos/provider-settlement-reports/{id} | Inspect safe capture/refund match evidence without raw provider references |
| GET | /api/v1/pos/reports/close-of-day | Server-generated close-of-day pack |
| GET | /api/v1/pos/reports/close-of-day/signing-key | Current Ed25519 close-pack verification key and key id |
| GET/POST | /api/v1/pos/webhook-subscriptions | Merchant ERP/ecommerce webhook management |
| POST | /api/v1/pos/webhooks/payment-providers/{providerCode} | Provider-specific callback routed through a normalized payment adapter |
| POST | /api/v1/pos/webhooks/mpesa | Legacy Daraja callback alias while that adapter is enabled |
| POST | /api/v1/pos/webhooks/card-present | Card-present result callback if asynchronous |
| POST | /api/v1/pos/webhooks/etims | eTIMS callback only where the selected integration uses one |
Platform integration API
POS consumes:
- GET /api/internal/v1/organizations/{id}
- GET /api/internal/v1/organizations/{id}/units
- GET /api/internal/v1/organizations/{id}/assignments
- GET /api/internal/v1/organizations/{id}/stores/{storeId}/catalog
- GET /api/internal/v1/organizations/{id}/stores/{storeId}/customers
- POST /api/internal/v1/merchant-payments
- GET /api/internal/v1/merchant-payments/{reference}
- POST /api/internal/v1/merchant-payments/{reference}/refund
- POST /api/internal/v1/organizations/{id}/stores/{storeId}/merchant-collection-requests
- GET /api/internal/v1/organizations/{id}/stores/{storeId}/merchant-collection-requests/{authorizationId}
- POST /api/internal/v1/notifications
These paths must be confirmed against the platform implementation and published as OpenAPI before POS code depends on them. POS must not consume platform JPA entities or Java implementation packages.
Events
POS publishes:
- pos.catalog.item.changed.v1
- pos.sale.created.v1
- pos.sale.completed.v1
- pos.sale.refunded.v1
- pos.shift.opened.v1
- pos.shift.cash-movement.recorded.v1
- pos.shift.closed.v1
- pos.inventory.changed.v1
- pos.inventory.supplier.changed.v1
- pos.inventory.goods-receipt.recorded.v1
- pos.inventory.stock-transfer.completed.v1
- pos.inventory.stock-count.updated.v1
- pos.receipt.generated.v1
- pos.tender.updated.v1
- pos.terminal.enrolled.v1
- pos.customer.created.v1
- pos.customer.updated.v1
- pos.engagement.updated.v1
- pos.fiscalization.updated.v1
Platform publishes:
- platform.organization.updated.v1
- platform.organization-unit.updated.v1
- platform.user-assignment.updated.v1
- platform.merchant-payment.updated.v1
Every event carries eventId, schemaVersion, occurredAt, correlationId, organizationId and source. Consumers use an inbox table for deduplication. Producers use a transactional outbox.
7. Remote test environment
Recommended public hosts
| Purpose | Test URL |
|---|---|
| POS backoffice | https://pos.test.emali2.damplabs.com |
| Cashier web | https://pos-cashier.test.emali2.damplabs.com |
| POS documentation | https://docs-pos.test.emali2.damplabs.com |
| Standalone POS API | https://pos-api.test.emali2.damplabs.com/api/v1/pos |
| Platform API | https://api.test.emali2.damplabs.com/api/v1 |
| Identity issuer | https://auth.test.emali2.damplabs.com/realms/emali2 |
Do not expose the POS database, RabbitMQ, Redis, card service, eTIMS credentials or service-to-service endpoints through public DNS.
The DNS names resolve, and the test cluster now runs dedicated POS PostgreSQL
and object-storage StatefulSets, ingress-restricting NetworkPolicies, dedicated
secret references, the standalone API, and the three POS web applications. The
existing emali2 issuer remains the staff identity authority.
Remote rollout verified on 15 July 2026 through root@damplabs.com and the
emali2-test namespace:
- all six relevant hostnames resolve to
72.62.20.92; emali2-pos-test-tlsis Ready and the served Let's Encrypt certificate has SANs for all four POS-owned hosts;- backoffice, cashier web, and docs return 200 without disabling TLS checks;
- the standalone API reports
UP, rejects unauthenticated bootstrap with 401, permits the exact POS origins, and rejects an untrusted origin with 403; - the
emali2-pos-serviceconfidential client obtains a token with client credentials, while its service account correctly receives 403 from the staff bootstrap because it has no merchant staff assignment; - PostgreSQL is at Flyway v24 and SeaweedFS created the
emali2-pos-item-imagesbucket; - the
emali2-pos-serviceclient is confidential with a generated 32-character secret, whilepos-backoffice,pos-cashier-web, andemali2-pos-androidare public clients with standard flow enabled, service accounts/direct grants disabled, and no secret; the Android redirect uses S256 PKCE; - a POS-service NetworkPolicy restricts ingress to the shared test namespace, Traefik and observability, and restricts egress to POS PostgreSQL/object storage, Core's API pods, cluster DNS, and external HTTPS;
- Core and the customer app contain the Emali mobile-approval payment path. The store-scoped internal contract, generic POS attempt table, encrypted customer-reference queue and reconciliation worker are implemented locally; the cluster has the dedicated encryption secret, and the images are deployed; customer-app tender UAT remains.
- A non-destructive POS-owned PostgreSQL logical dump/restore rehearsal passes against the remote test database: all 46 tables, Flyway v24, selected critical rows, stable schema structure and sequence state match, and a separate check confirms no temporary database or dump remains. This does not yet satisfy enabled external backup, remote isolated restore, application smoke, or RPO/RTO requirements.
- The immutable
pos-postgres-backup:test-20260715-pitr-recovery-start1image is published at digestsha256:f2fa8e5536c65527cc7813b23deb6172138dcef547dedca4d083db1d8d727bb5. It verifies both objects' SSE, ciphertext metadata, and applied object-lock mode/expiry in addition to the prior controls. Its encrypted-backup/isolated-restore integration drill and four fail-closed guard tests pass. The remote01:17 UTCCronJob and its two least-privilege NetworkPolicies are deployed, but the CronJob is suspended, has created no Job or pod, and has no backup credential Secret. External storage is therefore still unproven. - The immutable
pos-postgres-pitr:test-20260715-pitr-recovery-start1image is published at digestsha256:b4397541661b3098498766aa3bfee45749e65c75f5b71aa961f0c08623ce716a. Its local integration drill verifies a real-size WAL round trip, SCRAM-only replication access, physical base backup, exact manifest WAL range,pg_verifybackup, encrypted manifest-last commit, safe extraction, actual recovered-server startup, named-target row inclusion/exclusion, system identity, Flyway v24, promotion and cleanup. The measured local prepare-to-promotion sample is 35 seconds. Four PITR guard tests pass. A separate digest-pinned, short-lived role provisioner has three manifest guards and an exact-SQL integration test proving SCRAM physical-replication access, rotation, old-password rejection and no POS-table access. Its test cluster server-side dry run created zero resources; the live role was not created because the backup Secret is deliberately absent. The remote02:43 UTCSunday physical-base CronJob is installed suspended with zero Jobs/pods. The backup Secret is absent, and the primary PostgreSQL image, arguments and egress policy were deliberately not changed; remote PITR remains unproven.
The deployment intentionally enables only platform-http; Daraja and OSCU
remain disabled until dedicated sandbox credentials and external approvals are
supplied. On 24 July, the Core API received an API-only identity-session
overlay at sha256:bb399aeb1c208355c3fc8d5cc969106589e7da53a30076080dc46d00366c5f01,
with sha256:fb4c5884c44536cb9178b2b1b50605b65300400eaddfc44bfbe64f7010a1b7f4
as its immediate rollback. The worker stayed unchanged at
sha256:c9678b8af58bd1e62584f83748444cdf5c128dd3d7d5fe500fb595ac85512397.
POS is pinned to sha256:8bbbd10e3c760a5351089f6966afb7d79a64ab15f0be1e5d8071c2459ac5eef8,
with sha256:24f1ff4b5d5249a1b18788106f4d3d61340936e03de7e547b033a9fe579fd88e
as rollback. Docs are deployed from this same strict-built source; the prior
docs digest sha256:1b7158d2fdcd707112f54e6adc6025186ab07758206acf4e6fdff72932ec8ce8
remains available for rollback. Core API, POS and docs are Ready with zero restarts. The live POS
schema is at Flyway v30 with 52 tables; both new session tables are empty before
merchant UAT. The Core boundary returns 401 without credentials and
200 ALREADY_ABSENT for an in-memory POS service-token probe. The signed offline batch rejects unauthenticated callers with 401,
the service client can read the sanitized conflict queue, and the deployed
Backoffice exposes retry/discard controls without raw mutation payloads. The
close-and-submit transaction now seals one immutable
country-neutral close-of-day pack, and the authenticated verification-key API
returns the configured Ed25519 key id while unauthenticated access returns 401.
The deployed Backoffice retrieves the sealed pack and exposes its digest after
a manager closes a shift. Request proof is enabled for ANDROID,HARDWARE_POS,WEB with a
five-minute clock window. Browser WEB enrollment and a merchant-authenticated
lane transaction, shift submission and pack-verification transaction remain
remote UAT gates even though cross-language proof, CORS, signature-tamper and
replay tests pass.
The published current POS test installer, explicit version and latest alias
all match SHA-256
89d25273f0330d694ce649834c55b0cfafb20a78fd835e733068cbdb0a71f651.
Test organization 131 is now projected
with merchant store 577; its agent and biller units are excluded. The store
uses explicit SZ, SZL, en-SZ, Africa/Mbabane, disabled fiscalization and
the generic CUSTOMER_APP_PAYMENT capability through EMALI_APP. A POS-owned
SZL 10.00 test item and opening stock of 25 are ready. No terminal is enrolled,
so the next remote gate is a merchant administrator creating a short-lived code
in Backoffice and completing Android activation on the physical device. Backoffice
also exposes reasoned terminal revocation, preflights the current cashier shift,
and lets a manager close and submit it with a counted cash value and audit note.
The service atomically rejects revocation while a cashier shift is open. Terminal
activation creation now verifies that the requested organization owns the store;
heartbeat requires active store access; revocation requires store-management
access; and customer-only realm tokens cannot activate or report a terminal. A
merchant-user authenticated business E2E must then cover terminal activation,
catalog sync, sale, customer-app prompt, print, refund, offline replay, and
reconciliation.
A physical pre-enrollment checkpoint was refreshed on 16 July 2026 using a
Samsung SM-P619 tablet on Android 14/API 34. The installed com.emali2.pos
v0.1.0 capability-aware APK matched the built debug artifact at SHA-256
89d25273f0330d694ce649834c55b0cfafb20a78fd835e733068cbdb0a71f651,
cold-started successfully, and displayed the cashier/manager sign-in gate. The
remote identity host had already been reached through the secure PKCE browser
flow without entering credentials. Bluetooth was off and the Android Bluetooth
manager listed no bonded printer; there was also no external HID scanner or
cash drawer. This proves Android compatibility, data-preserving migration and
remote sign-in reachability only. It does not satisfy merchant enrollment,
customer-prompt UI or physical-peripheral exit criteria.
An Android Keystore instrumentation test also generated the terminal key,
matched its public-key thumbprint, verified a canonical activation signature,
and rejected that signature after signed metadata was tampered with. The exact
request-proof release APK was then reinstalled without clearing app data; a
second Keystore instrumentation test verified exact-body request signing,
fresh nonces and tamper rejection before the app completed another successful
cold start.
On 24 July 2026 the attestation-capable APK was installed in place on the same
tablet with application data preserved and no fatal launch event. A read-only
audit reported no terminal binding and an unauthenticated OIDC state, so no
activation was attempted through the PIN-locked application. A separate
pre-enrollment test generated a non-consuming challenge key and returned a
four-certificate hardware-backed chain. The server-side bridge then passed that
real chain through the production verifier. This proves physical cryptographic
compatibility but does not prove an authenticated activation transaction or
the resulting v31 database audit record.
Customer cash-out test data is a separate platform prerequisite. Remote smoke
tests must select an active agent from /api/v1/agents/directory and use the
returned canonical agentCode; they must not rely on a remembered or hardcoded
till. If till 0000424 is intentionally part of UAT, the platform seed must
verify that an active organization_units row has that exact till_number, is
linked through agent_id, and points to an ACTIVE, KYC-verified agent before a
withdrawal quote is attempted. An invalid or retired identifier now returns the
safe agent_unavailable contract, but the API does not redirect cash withdrawal
to a numerically similar agent.
Internal service names
- emali2-pos-service.emali2-test.svc.cluster.local
- emali2-card-present-service.emali2-test.svc.cluster.local
- PostgreSQL service
emali2-pos-postgres, databaseemali2_pos_test, with separate bootstrap-administrator and least-privilege POS application roles. The guarded 17 July transition completed with bounded API downtime and cleanup verification; the application role owns all POS objects and has no superuser, create-role, create-database, replication, bypass-RLS or inherited-role capability. The physical-replication role and external backup/recovery Secrets remain intentionally absent until off-cluster storage is approved - object-storage service
emali2-pos-object-storage, bucketemali2-pos-item-images, with POS-only access keys - RabbitMQ vhost
/emali2-pos-testwith POS-only credentials when event transport is enabled; it is not required by the initialplatform-httprollout
Allocate unique local ports before coding. Recommended initial registry:
| Runtime | Port |
|---|---|
| POS service | 8821 |
| Card-present service | 8822 |
| Reserved fiscalization adapter, if split later | 8823 |
Kubernetes services may map any container port, but local development and gateway defaults must no longer collide with policy on 8814 or ledger on 8815.
Remote environment gates
- valid TLS with no insecure-client bypass;
- health, readiness and metrics endpoints;
- POS service connected only to its own database;
- authentication and token audience validation;
- API-only gateway rate limiting and request-size limits: each Traefik gateway
instance groups requests by its trusted remote-address view, permits an
average 100 requests/second with a burst of 200, and rejects bodies over
10 MiB before forwarding. The ceiling matches Spring's multipart request
limit and remains above the service's 5 MiB catalog-image limit; a live
10 MiB + 1 byte request receives
413while ordinary protected traffic still reaches Spring. Static Backoffice, Cashier and Docs routes do not inherit the API middleware. A globally shared production quota requires Traefik's Redis-backed limiter or an upstream managed edge rather than pretending the per-instance test limiter is cluster-global; - network policy and egress allow-list;
- secret rotation without image rebuild;
- same-pod logical backup/restore rehearsal (verified), local encrypted off-cluster/PITR plus standalone application startup drills (verified), and enabled remote point-in-time recovery/authenticated business smoke (pending);
- structured logs with correlation IDs and no tokens, secrets or card data;
- Android build contains only test HTTPS endpoints;
- remote E2E covers sale, print, payment, refund, offline replay and reconciliation.
The 16 July external-storage catalog review rejected Railway buckets for POS
recovery because their documented feature set omits versioning, object lock,
server-side encryption and lifecycle configuration. Compatible candidates are
AWS S3 Cape Town (af-south-1) for the Africa-region pilot, or Backblaze B2 for
a low-volume test at a published starting rate of USD 6.95/TB/month with no
minimum storage duration. Both expose the versioning/Object Lock calls enforced
by the provider-neutral scripts; B2 requires separate data-location approval.
Either deployment must use encryption, public-access blocking, access logs and
separate archive/recovery identities. No provider resource or credential was
created during the audit.
8. Hardware architecture
Create a POS-owned hardware abstraction:
- PrinterAdapter
- BarcodeScannerAdapter
- CashDrawerAdapter
- PaymentTerminalAdapter
- DeviceHealthAdapter
Initial implementations:
- Fake adapters for automated tests.
- Android external HID keyboard-wedge, bundled offline camera and configurable vendor-intent adapters, followed by physical scan-engine qualification.
- Bluetooth, USB-host printer-class or private-LAN raw-TCP ESC-POS printer for generic testing through the implemented transport router.
- Sunmi printer/scanner SDK adapter.
- Ciontek or the selected regional handheld-vendor adapter.
- Countertop 80 mm printer and printer-driven RJ11/RJ12 drawer.
- Certified acquirer handoff adapter for card payment.
Vendor SDK code must remain in small provider modules. Checkout, receipt and synchronization code depend only on the interfaces. Print jobs need a local spool with status, retry, reprint audit and duplicate-receipt marking.
The Android client now persists the selected printer's transport and routes
discovery, readiness, print and guarded drawer operations through a shared
adapter registry. Generic Bluetooth is the first installed adapter. The CS30
vendor adapter remains a separate exact-firmware task: Ciontek provides the SDK
after a sample order, and the public CS30Pro guide is an older Android 10
reference, not a compatible-binary guarantee for the current Android 14 unit.
Only a status-capable adapter that proves job completion may claim
DEVICE_CONFIRMED.
Kenya hardware shortlist and test budget
Price audit: 17 July 2026. This is Kenya pilot procurement data, not a country-specific product dependency. Listed prices can change and may exclude VAT, delivery, paper and vendor integration support.
| Pilot option | Current listed price | Decision |
|---|---|---|
| Ciontek SmartPOS CS30 | KSh 23,000 | First integrated evaluation choice, conditional on signed-APK installation plus an ESC/P or supplied-SDK print demonstration on the exact local firmware |
| H10S Android 13, 2 GB/16 GB, built-in 58 mm printer | KSh 25,000 ex VAT | Modern-OS comparison choice; the advertised free SDK, status behavior, scanner option and third-party APK installation must be demonstrated before purchase |
| NB55 Android 12, 3 GB/16 GB, NFC, stated 1D/2D scanner and 58 mm printer | KSh 28,000 | Scanner-equipped comparison choice; require exact scan-engine, printer-status, SDK/broadcast and signed-APK evidence |
| Generic marketplace integrated-printer handheld | KSh 23,990–32,999 | Price comparison only; reject any unit whose OEM, firmware, SDK and warranty cannot be identified before payment |
| Generic Comstore 4 GB/32 GB terminal | KSh 25,000 | Comparison only until the seller identifies the OEM, Android build and printer/scanner SDK |
| Ciontek CS50 or 3 GB CS30G | KSh 26,000 | Second model only after the exact SKU and adapter contract are confirmed |
| SUNMI V2s | KSh 35,500–38,000 locally | Local listings conflict between Android 11/2 GB/16 GB and Android 7.1, while the current OEM T5940 standard specification is Android 12 Go/1 GB/8 GB; qualify the exact unit, not the family name |
| SUNMI V2 Pro | KSh 43,000 ex VAT | Current local listing has an integrated 58 mm printer but Android 7/7.1; reject for a new pilot despite the premium |
| SmartPOS H5 | KSh 19,999 | Compatibility test only; Android 8.1 and 1 GB/8 GB are too constrained for a new production pilot |
The lowest-risk integrated lab order is one conditional CS30, a KSh 5,000 YCP-58 fallback printer, a KSh 8,500 wired 1D/2D scanner and five rolls: approximately KSh 37,000 before delivery, SIM/data, OTG adapter and spare power. Reusing an existing Android phone with the YCP-58 and a KSh 4,000 wired 1D scanner reduces the peripheral-only proof to approximately KSh 9,000.
Before purchasing any handheld, require the vendor to demonstrate:
- installation of the signed Emali2 test APK, not only Play Store apps;
- USB debugging or managed APK deployment and a documented factory-reset path;
- printer SDK/AAR, sample code, supported paper width and paper-out status;
- camera or hardware scan intent output for EAN-8, EAN-13, Code 128 and QR;
- Android Keystore EC key generation and whether Google Play services are present;
- Wi-Fi, 4G, Bluetooth, charging and a full-shift battery test;
- local warranty, replacement lead time and availability of print heads, covers, chargers, batteries and paper.
An eTIMS ready product listing does not by itself certify Emali2's fiscal
integration. Likewise, a device described as PDQ cannot accept live cards for
Emali2 until a licensed acquirer supplies and approves the payment application,
keys, merchant provisioning and integration route.
The currently listed fixed-lane bundle is the in-stock KSh 14,500 ECO250 USB/serial/Ethernet printer, KSh 8,500 Deli wired 1D/2D scanner and KSh 6,500 drawer: approximately KSh 29,500 before the cashier computer/display, delivery and consumables. The KSh 9,500 CK710 is excluded because the current listing is out of stock. The detailed, actively maintained qualification matrix and seller acceptance test are in Hardware and Local Testing.
Hardware price sources:
- AndroidPOSKenya CS30
- Ciontek CS30 OEM specification
- Ciontek SDK/support FAQ
- Viva CS30 SDK integration page
- Public CS30Pro SDK guide (older Android 10 reference only)
- AndroidPOSKenya H5
- AndroidPOSKenya current catalogue
- AndroidPOSKenya YCP-58 printer
- TDK H10S Android 13 handheld
- Carl & Kyle NB55 Android 12 handheld
- Jumia integrated Android POS price comparison
- Nebula Comstore terminal
- Nebula current printer/scanner catalogue
- Nebula ECO250 printer
- Nebula CK710 stock listing
- SUNMI V2s OEM specification
- Kenya SUNMI V2s listing
- Kenya SUNMI V2 Pro listing
9. Kenya payments, tax and privacy
eTIMS
KRA provides system-to-system integration using:
- OSCU for invoicing systems that are always online;
- VSCU for bulk or intermittently connected invoicing systems.
The first pilot targets OSCU because the fiscal adapter runs in the online server environment; Android's ability to sell offline does not make the central tax adapter a VSCU workload. The implemented provider-neutral port leaves room for a later VSCU adapter where a merchant's invoicing server is genuinely intermittent or bulk-oriented. The current OSCU v2.0 slice uses the documented JSON sandbox endpoint and maps sale/credit receipt types, payment codes, KRA tax types A-E, the original accepted invoice for a credit note, and returned fiscal receipt data. Currency is deliberately restricted to KES in this slice. Receipts expose NOT_REQUIRED, PENDING, ACCEPTED, REJECTED, MANUAL_REVIEW or CANCELLED; an uncertain network result is MANUAL_REVIEW because blind retry can duplicate an accepted KRA invoice. No QR value is invented where the selected OSCU response does not supply one.
Do not promise production tax invoices until KRA integration testing, technical and administrative review, and self-integrator or third-party certification are completed.
M-PESA and other PSPs
Use Safaricom Daraja or a licensed PSP/acquirer sandbox. Daraja currently publishes Lipa na M-PESA Online, online-query, general transaction-status and reversal capabilities. Implement initiate, callback normalization, query/status, timeout, reversal/refund and reconciliation as one state machine. A browser redirect or callback alone never proves payment; the adapter must query or otherwise verify the provider server-to-server before posting the normalized result to POS.
Card payments
For the pilot, prefer a certified bank/PSP PDQ or SmartPOS application-to-application handoff. This keeps raw card and PIN data out of Emali2. Any solution that stores, processes, transmits or can affect cardholder data falls within PCI DSS scope.
Regulatory boundary
Integrating a licensed PSP or acquirer is materially simpler than operating an acquiring or payment service directly. Before production, obtain legal and compliance confirmation of Emali2's role under Kenya's National Payment System rules and the merchant/acquirer contracts.
Personal data
Customer lookup, loyalty, digital receipts, staff activity and device telemetry involve personal data. Implement:
- purpose and consent capture for marketing and digital receipts;
- collection minimization;
- retention and deletion rules;
- encryption in transit and at rest;
- merchant-level access isolation;
- customer export/correction/deletion workflows where legally applicable;
- breach response and audit records;
- processor/controller agreements and ODPC registration review.
10. Delivery phases
Phase 0 — Stabilize the canonical repositories
Progress: substantially complete. Duplicate sources, web runtime failures, local port conflicts, default credential fallbacks, the embedded-route OpenAPI inventory, remote test TLS/ingress rollout, and an optional signed-JWT service-client implementation are resolved. Remaining Phase 0 work is to archive the separate Android migration copy after parity, migrate the remote platform client from its transitional shared secret during an approved change window, and complete production credential rotation and secret-manager verification.
Work:
- declare emali2-pos-suite and emali2-card-present authoritative;
- remove the duplicate com.duka source trees after confirming the emali2 versions contain the intended behavior;
- move or archive the separate emali2-pos-android copy after parity;
- fix the frontend test runtime;
- make every canonical build and test pass;
- remove committed/default confidential credentials and rotate remote credentials;
- publish the service port registry;
- capture OpenAPI from current embedded POS endpoints.
Exit criteria:
- clean Android, web, POS service, card service and docs builds;
- no duplicate package trees;
- no production secret defaults;
- one documented source of truth for each component.
Verified 24 July 2026 delta: POS service now has 186 tests (185 passing and
one explicitly opt-in physical-device attestation test skipped), with a clean
PostgreSQL 16 migration through Flyway v34. V33 adds a country-neutral,
manager-only customer-payment resolution trail: an uncertain prompt stays
pending until a manager records an independently verified captured or failed
outcome with an opaque evidence reference and provider occurrence time. The
transition is tenant scoped, request-hash idempotent, audited, race safe and
never sends a provider prompt, query, cancellation or refund. V34 adds a
POS-owned merchant machine-client registry that binds exact OAuth azp client
identifiers to one organization and explicit active stores, fails closed on a
missing/mismatched/inactive binding, supports immediate revocation, and stores
no client secret or private key. The validated contract now covers 135 OpenAPI
operations and 25 versioned event schemas,
including the customer-payment queue and resolution mutation. Backoffice
tests, type checking and the test-mode production build pass with the new
manager workflow.
Flyway v35 adds delegated, country-neutral cash-movement authorization. A
cashier's supervisor approval must match the organization, store, terminal,
open shift, requesting cashier and action, and must be no more than five
minutes old. A partial unique index makes each SUPERVISOR:<event-id>
reference single-use while the existing idempotency key safely replays the
original response. Android records NO_SALE on the server before entering the
Room-backed one-shot drawer path. The unchanged 186-test backend suite passes,
including a database test for delegated success and reuse rejection. The live
test service is Ready with zero restarts at Flyway v35 on
sha256:181b7f4769fbca58f8649c9a425edd89db31c9d6a6591192eff58fb71cd42af1,
and public health is UP.
The remote test rollout is pinned to POS service
sha256:32810d32ddfe56e30e243029cea9fab34ff0407460723a00e9f1e685e45c0b31,
Backoffice
sha256:7b881b5aa822ae9ebb4da25027f16f59c552364260f48cfea3232882340d278c,
with the matching POS documentation published alongside them.
Flyway applied v34 remotely; the POS deployment is Ready with zero restarts,
public health is UP, and unauthenticated bootstrap, customer-payment queue
and integration-client requests return 401.
Recovery gates were also refreshed against v34. The live test database passed the non-destructive logical dump/restore rehearsal with 54 tables, matching critical-row, schema-object and sequence checksums, and zero residual database or dump artifacts. Native encrypted logical-backup and PITR integrations pass with age encryption, SSE, versioning/Object Lock simulation, manifest-last commit, a named recovery target, password rotation, source-password rejection, application health and authenticated bootstrap. Both current remote Kubernetes recovery manifests pass server-side dry-run with zero resources or writes. The real schedules remain safely suspended because the approved external immutable-storage account and split backup/recovery Secrets have not yet been provisioned.
The terminal-revocation recovery APK is now installed data-preservingly on the
physical Samsung SM-P619. The release hash and signing certificate matched
before replacement, Android reported a successful install, and the sanitized
post-install audit confirms the installed APK SHA-256
eaaa2e18372a81a509508ac1f72188546e5d145ef0f9b099a7aa13bfefa2c5cd
on Android 14/API 34. An explicit cold start returned Status: ok and left
the application process running. The device/application baseline passes; printer,
scanner, merchant activation, reboot/revocation and real customer-payment
acceptance remain open.
Phase 1 — Contracts and POS database
Progress: foundation implemented and verified. PostgreSQL/Flyway ownership through v32, including durable terminal-request nonce claims, proof-bound OIDC session bindings and revocation jobs, normalized attestation audit evidence, split-tender and partial-value refund allocations, refund merchandise/tax snapshots, sequential electronic-refund jobs, constrained operator refund and fiscalization reconciliation, immutable provider-settlement reports and reference-minimized match entries, immutable signed close-of-day packs, sanitized offline-sync conflicts, country-neutral fiscal item profiles, the integration-control, supplier/receiving/transfer/physical-count, item-image metadata, supervisor credential/override, fiscalization, generic provider, and store operating-profile tables, typed ports, simulator adapters, the opt-in real Daraja and OSCU adapters, 130 OpenAPI operations, 25 event schemas, contract checks, and PostgreSQL Testcontainers coverage are present. Catalog, image, stock, supplier, goods-receipt, stock-transfer, physical-count, supervisor, shift, close-pack, sale, offline-sync, provider-result, refund, provider-statement reconciliation, fiscalization, reconciliation, customer, and engagement repositories now build on this foundation.
The 24 July delta advances this foundation to Flyway v34 and 135 OpenAPI operations by adding the provider-neutral customer-payment manual-review queue, manager resolution mutation, and tenant/store-scoped merchant machine-client registry.
Work:
- add POS-owned PostgreSQL and Flyway migrations;
- add idempotency, inbox, outbox and audit tables;
- publish OpenAPI and event schemas in pos-api-contracts;
- create typed platform, card-present, mobile-money, customer-app-payment and fiscalization ports, with country/provider names confined to optional adapters;
- add contract tests and Testcontainers.
Exit criteria:
- POS service can start with platform core stopped;
- database migrations run from an empty database;
- integration ports can use simulators;
- no POS service dependency on emali2-core Java packages.
Phase 2 — Merchant/store projections and terminal enrollment
Progress: server foundation implemented and verified. The platform has a
role-scoped internal v1 API and POS uses client credentials to load merchant,
store and staff projections into its own tables. A local role-scoped
/api/v1/pos/bootstrap/me now returns only projected stores and active enrolled
terminals, including the external terminal UUID required by the standalone
shift API. Reconciliation can be invoked without shared-database access. Terminal activation uses a peppered HMAC,
single-use row lock, validated EC/RSA public key, a deterministic versioned
activation payload, server-verified key signature, key thumbprint, hashed hardware
identifier, heartbeat history, revocation and a transactional enrollment event.
Lost activation responses can be retried only with the same code and public key
before expiry; the existing terminal is returned without duplicating the outbox
event.
Existing tenant projections are also refreshed automatically through the
confidential platform client. Refresh is bounded, retryable and protected by a
PostgreSQL advisory lock so multiple POS replicas cannot concurrently replace
the same projection estate. A first-time merchant still requires an explicit
bootstrap sync; subsequent assignment and store changes no longer depend on an
operator remembering to refresh.
The feature is disabled unless a shared environment injects its activation
pepper. Android now uses an AppAuth public client with Authorization Code and
S256 PKCE, stores its OIDC state in encrypted app storage, creates a non-exportable
P-256 signing key in Android Keystore, signs the activation payload without
exporting the private key, consumes the single-use activation, stores the server
terminal UUID through an explicit Room upgrade, and publishes an initial
heartbeat. Protected Android and hardware-POS mutations now bind their exact
method, path, body digest, terminal, timestamp, nonce and idempotency key to a
signature from that enrolled key. The server enforces resource binding and a
five-minute clock window and atomically records hashed nonces in PostgreSQL so
replays fail across replicas and restarts. Cashier web now generates a
non-exportable P-256 key, retains it in IndexedDB, signs the same canonical
activation and request-proof formats, exposes only WEB terminals, and refuses
protected mutations without its matching binding. A restrictive self-script
CSP reduces the same-origin script surface. A successful proof binds the
provider's opaque identity-session claim to that exact terminal, and terminal
revocation durably terminates every bound session through Core without storing
access or refresh tokens. Web OIDC consolidation, attestation enforcement and
lower-latency event-driven projection updates remain open.
Work:
- bootstrap organization, stores and staff assignments through platform APIs (implemented);
- refresh existing projections on a locked schedule (implemented);
- add platform events for lower-latency updates while retaining scheduled reconciliation;
- consolidate remaining Web OIDC behavior;
- browser WebCrypto activation, exact-body request proof and CORS qualification (implemented);
- qualify device attestation policy.
Exit criteria:
- a new merchant can sign in, see assigned stores and activate a terminal without shared-database access;
- revoked terminals cannot refresh or transact.
Phase 3 — Catalog, inventory and read cutover
Progress: catalog read and initial write ownership implemented and verified. The platform exposes
a structurally allowlisted catalog, barcode, price and stock projection that
cannot include supervisor PIN or engagement fields. POS synchronizes it through
its confidential service client, replaces a store snapshot transactionally in
its own database, records synchronization state, and serves catalog
bootstrap and exact barcode lookup from the POS database. Store access is
checked against the token organization or a locally projected active staff
assignment; unsynchronized stores fail closed with 503. Android persists the
stock quantity through a data-preserving Room v7-to-v8 migration. PostgreSQL
integration tests cover price, stock, lookup and cross-store denial, and handler
mapping tests prove these reads no longer fall through to the core proxy. A
service-only organization backfill endpoint refreshes every active store,
continues past per-store failures, and returns deterministic SHA-256 parity
checksums over business fields from the platform snapshot and the persisted POS
read model. Flyway v5 promotes those projection tables to authoritative catalog,
barcode and store assortment/price/stock tables and adds an immutable stock
movement ledger. POS now creates and updates items and records opening balance,
adjustment and generic stock movements through tenant-scoped local handlers.
Every mutation requires Idempotency-Key, rejects changed replays, writes audit
and transactional outbox records, and protects POS-owned rows from later platform
snapshot overwrite. Flyway v14 adds tenant-bound suppliers, receipt headers and
receipt lines. Supplier upsert and goods receipt creation are idempotent, audited,
event-producing local handlers; a receipt and its RECEIPT stock movement commit
atomically. Flyway v15 adds tenant-bound transfer headers and lines; source and
destination stock movements, audit, and outbox events commit as one unit, with
deterministic store locking for reverse-transfer safety. Compatibility
/api/v1/org-inventory/** catalog, stock, supplier, receipt, transfer, and override-audit routes
terminate in the POS service instead of the proxy. The generic inventory proxy is removed. Flyway v16 adds count headers
and scoped lines. A count snapshots expected balances, accepts an exact complete
scope, rejects stale submissions after intervening inventory activity, and applies
its STOCK_COUNT movements atomically; open, submit, and cancel transitions are
idempotent, audited, and event-producing. Flyway v17 adds POS-owned image metadata
backed by private S3-compatible storage; list, duplicate lookup, and atomic import
also terminate locally. Flyway v18 adds POS-specific supervisor credentials,
throttled online authorization, durable approved/denied audit, and non-secret
events. Remaining work is scheduled/event-driven
synchronization and disabling the corresponding platform mutation endpoints
after remote parity and rollback gates pass.
Work:
- move product, catalog, barcode, price, stock and image ownership;
- backfill data from platform tables;
- compare old and new read results in shadow mode;
- route catalog and inventory reads to POS.
- route catalog and stock writes to POS with idempotency, audit and outbox;
- disable platform catalog/stock writes only after remote parity and rollback rehearsal.
Exit criteria:
- parity reports are clean for agreed stores;
- Android offline catalog bootstrap comes from POS;
- platform code no longer writes POS-owned inventory.
Phase 4 — Sales, shifts, receipts and cash cutover
Progress: cashier shifts, sales, cash collection and receipts are now POS-owned.
Flyway v6 creates UUID-based shifts and an immutable cash-movement ledger with
one-open-shift constraints for terminal and cashier. Flyway v7 adds UUID-based
sales, server-snapshotted lines, tenders and receipts. Sale creation derives
store, terminal and cashier from the open shift, revalidates catalog price and
stock, and never trusts client totals. Positive cash and electronic tender legs
can now settle part of the authoritative outstanding balance. The first partial
leg reserves the full basket stock once, captured legs advance the sale through
PARTIALLY_PAID, and only the final captured leg commits stock, completes the
sale, and issues a receipt. Cash legs update the drawer atomically. Flyway v22
allocates a full all-cash refund across every captured cash leg while retaining
the legacy first-tender reference. The normalized provider-result API gives
mobile-money, wallet, card-present and other electronic requests one active
tender at a time, binds a provider reference once, retains a reservation after
a failed retry when an earlier leg was captured, and records audit/schema-v1
events on every provider result. The endpoint accepts only OAuth
client-credential roles and applies the same request-hash idempotency policy.
Open sales block shift close; cash acceptance and voiding are rejected while an
electronic result is unresolved.
Cashier web now uses shift and enrolled-terminal UUIDs for sale reads/writes,
persists retry-stable create/collect/void/refund keys, and no longer labels pending
electronic requests as captured. Its receipt support surface derives available
line quantities only from completed refunds, displays credit notes, and blocks
unsupported provider legs before a manager submits the country-neutral return
contract. Android uses the same strict wire contract and
stable local sale/tender keys; Room v10 adds UUID remote tender and receipt
references without overwriting legacy numeric columns, while Room v11 adds an
independent durable receipt print spool and Room v12 persists provider
transaction receipts separately from request references. Flyway v8 adds
manager-authorized, idempotent full cash refunds with an open same-store shift,
optional atomic stock return, drawer reversal, credit note, audit and
pos.sale.refunded schema-v1 outbox event. Android sends the refund shift UUID and a
stable refund idempotency key, and refuses unverifiable local refund fallback.
Flyway v9 adds tenant-scoped customer, store-membership, and engagement projections.
The POS service synchronizes their structurally allowlisted payload through a
confidential platform client and serves customer lookup, local POS receipt activity, and catalog
engagement bootstrap without a platform database or customer-route proxy.
Flyway v10 promotes customers and engagement into explicit POS or platform ownership,
allocates collision-resistant local customer identifiers, and stores the full
versioned engagement workspace. Local create/update handlers require
Idempotency-Key, enforce tenant/store scope and identity uniqueness, preserve
POS-owned rows during later platform snapshots, audit every mutation, and emit
full pos.customer.created, pos.customer.updated, and pos.engagement.updated
schema-v1 envelopes. The strict customer/engagement OpenAPI contract rejects
unknown fields and documents all six read/write operations.
Customer search no longer depends on a legacy platform customer snapshot; a
new POS-owned customer is immediately searchable at a projected store while
the platform is unavailable.
Android reconnect replay now uses stable dependency phases, creation time, and
job UUID ordering; it recovers stale running jobs, stops the ordered prefix on a
transient failure, applies capped deterministic backoff, and quarantines
permanent/exhausted conflicts as blocked work. Historical customer/engagement
cutover, certified production-provider refund/reversal adapters, automated
statement delivery, partial-provider-outcome compensation and jurisdiction-specific
fiscal acceptance remain.
The server now accepts at most 50 ordered offline-safe mutations per signed batch,
returns accepted/already-applied/retryable/rejected status for every item, supports
same-batch shift and sale dependencies, and excludes electronic-payment initiation.
Rejected items create a sanitized conflict record containing hashes and operational
metadata rather than raw payloads or idempotency keys. Backoffice managers can request
retry or discard with an audited note; discarded mutation keys are blocked from later
execution and a successful replay closes the conflict automatically. Android now signs
and submits that batch, persists each authoritative per-item result, stores only the
sanitized conflict id/outcome, and consumes manager retry/discard decisions without
deleting unresolved local evidence. Final
shift submission now atomically seals a POS-owned country-neutral close pack:
the exact canonical JSON, SHA-256 digest, Secret-backed Ed25519 signature,
verification key id, audit record and outbox event persist together. Retrieval
re-verifies the configured trust anchor, payload digest and signature, and
tenant/cashier isolation is covered by PostgreSQL integration tests.
Android now consumes that final artifact after supervisor submission. The API
client fetches the current signing-key view, validates the exact UTF-8 digest
and Ed25519 signature, rejects key/payload or store/terminal/session mismatches,
and Room v14 caches the canonical payload and detached verification material.
Room v15 preserves the queued work while adding nullable server conflict metadata.
The local CSV/PDF generator is explicitly labeled provisional. JVM tamper/key
tests, the physical Android 14 crypto test, and the physical Room 13-to-14 and
14-to-15 migration tests pass.
Work:
- complete historical customer/engagement migration, parity checks, and embedded-write retirement;
- keep new public POS customer/engagement writes landing only in the POS database;
- emit events to platform reporting and reconciliation;
- preserve /api/v1/pos compatibility at the gateway;
Migration method:
- initial backfill with checksums and counts;
- outbox/CDC catch-up from the current source;
- shadow reads and reconciliation;
- short controlled write freeze;
- switch gateway writes to POS;
- verify balances, tender totals and receipt counts;
- retain a time-limited read-only rollback view.
Avoid application-level dual writes to two databases.
Exit criteria:
- core POS writes are disabled;
- cash sales work with platform core temporarily unavailable;
- recovery and replay never create duplicate sales or tenders.
Phase 5 — Electronic payments and card-present
Progress: the provider-neutral result-ingestion, capture and statement-reconciliation core is implemented.
It trusts POS-owned amount and currency rather than provider payload totals,
requires a confidential OAuth service role plus Idempotency-Key, prevents
provider-reference rebinding and terminal-state changes, reserves and releases
inventory, and emits pos.tender.updated alongside sale and receipt events. A
PostgreSQL integration tests cover a legacy branded mobile-money reservation
release and completion path, cash recovery, receipt generation and replay. The
optional Kenya-only mpesa-daraja adapter implements OAuth, STK Push initiation, status
query, encrypted durable dispatch, signed callback normalization, duplicate
callback handling and transaction-receipt reconciliation. Unknown initiation
outcomes and mismatched callback amounts enter manual review without a
duplicate prompt or cash fallback. Certified production refund/reversal
adapters remain; line-level full and partial allocation plus constrained
reconciliation are implemented. Immutable normalized provider-statement
imports now match captures and completed refunds by configured capability and
provider, amount and currency. Raw entry references are reduced to a digest and
display suffix, safe exceptions appear in Backoffice, and the event contains no
references. Provider-side clearing and settlement execution remain outside POS. The optional
Daraja flow accepts a Kenyan MSISDN only while online; no such format is imposed
on the country-neutral customer-app or mobile-money boundaries. Android refreshes unresolved
electronic sales after replay, on periodic work, and when operational summary
screens open, replacing local pending state with authoritative tender and
receipt state.
Work:
- Emali merchant-payment adapter;
- certified production refund/reversal adapters for each configured provider, including Daraja only where a Kenyan merchant enables it;
- card-present handoff to emali2-card-present;
- production automated statement delivery and partial-outcome compensation remain. Durable settlement import/matching, sequential refund jobs, the exception queue, and idempotent manager-confirmed success/rejection based on constrained portal, support or settlement evidence are implemented without exposing a blind provider-write retry.
Exit criteria:
- success, decline, timeout, duplicate callback, late approval and reversal scenarios pass;
- POS and provider totals reconcile by merchant, terminal, day and tender.
Phase 6 — eTIMS
Progress: local foundation implemented and verified. The etims-oscu profile
maps the OSCU v2.0 sale endpoint, catalog items carry all five KRA mapping
fields, sale lines retain tax-inclusive fiscal snapshots, and PostgreSQL owns
the numeric invoice sequence plus durable sale/credit-note jobs. Accepted KRA
receipt data is retained in structured and printable receipts. Unknown
transport outcomes, stale dispatches and response code 994 enter
MANUAL_REVIEW without blind retry. The read-only management queue is exposed
at /api/v1/pos/fiscalization/jobs. A separate manager-only resolve mutation
records an externally verified outcome idempotently, without calling the
provider, and retains opaque evidence in the audit trail. KRA-issued sandbox
credentials, certification execution and approval of the operating procedure
are still required before the profile is enabled remotely.
Work:
- register sandbox integrator/test taxpayer;
- complete KRA review of the implemented direct OSCU API path and any required certified security/key-management module;
- validate the implemented tax category, unit, item, sale and credit-note maps against the assigned sandbox taxpayer data; cancellation remains excluded;
- retain KRA receipt identifiers, internal data, signature and control-unit time; render a QR only if a certified response or library supplies one;
- execute KRA outage, overlap, rejection and credit-note certification cases;
- have KRA approve the implemented no-retry, operator-assisted reconciliation procedure and evidence types;
- complete KRA certification path.
Exit criteria:
- sandbox suite passes;
- tax invoice and refund states reconcile to POS;
- production enablement remains feature-flagged until certification.
Phase 7 — Hardware pilot
Progress: software print foundation implemented. core-hardware defines the
vendor-neutral printer contract, a transport router, and a bounded, control-safe
ESC/POS encoder. A generic built-in-printer bridge contract is also registered
fail-closed: it exposes no device without an installed vendor bridge, translates
ready/busy/paper-out/cover-open/offline state, sanitizes private SDK exceptions,
and grants device confirmation only for an exact completion result. Android
persists both the selected device and transport, can
combine discovery from multiple adapters without one failure hiding another,
and routes all printer operations without exposing vendor APIs to checkout or
the spool. It can select a paired Bluetooth SPP device, request temporary access
to a connected USB printer-class bulk OUT interface, or manually configure a
private-LAN raw-TCP endpoint; choose a 58/80 mm profile; print a test; create visibly
marked receipt reprints, persist Room v12 print jobs, recover interrupted jobs,
explicitly retry the queue, and send a separately guarded, non-retried drawer
qualification pulse. It also captures rapid external HID keyboard-wedge scans
and rejects slow ordinary typing. A bundled CameraX/ML Kit fallback decodes
common 1D/2D formats on-device, requests permission only from checkout, stores
no image, and remains not-ready until permission plus a valid decode. A configurable foreground
broadcast adapter now accepts exact vendor action/category/extra-key contracts,
validates bounded decoded payloads including GS1 separators, suppresses rapid
duplicate delivery, and remains not-ready until a valid test scan. Android
publishes real battery,
network, printer and scanner health at enrollment and during the 15-minute sync
cycle. The tenant-scoped bootstrap returns the latest health sample, and
backoffice distinguishes a recent heartbeat from active enrollment while
showing battery, network, printer, and scanner readiness. Operations also builds
a sanitized, store-scoped support queue: missing or stale heartbeat, low
battery, explicit offline network state, unavailable printer or scanner,
repeated or aged sync conflicts, manual-review payment/refund/fiscal outcomes,
and card-present unavailability are severity-ranked and link to their existing
workspaces. A diagnostic action never retries or resolves the underlying
operation. External paging and physical paper-out signals remain outstanding.
Unit tests plus debug and release builds pass. A physical Android 14
instrumentation test also proves
dynamic receiver registration, exact action/category matching, decoded-data
delivery, symbology delivery and ready-state transition. The current debug APK
also installs in place with application data preserved and cold-starts on a
physical Android 14/API 34 tablet without a fatal process error; the earlier
remote-routing checkpoint reached the remote PKCE identity host. Two
camera device tests prove the bundled decoder reads a checksum-valid EAN-13
without a model download and that the tablet exposes a rear CameraX provider;
the test permission was revoked after the hash-identical APK was reinstalled. A
separate device test proves Android Keystore signing of the canonical activation
payload and tamper rejection using the exported public key. A second device test
proves per-request signing against the enrolled terminal binding, nonce
uniqueness, exact-body verification, and tampered-body rejection.
The consolidated physical run passes six application instrumentation
tests, including the scan-intent, camera, activation-proof, request-proof and
signed close-pack paths. Five isolated Room migration tests now cover 13-to-14
through 17-to-18, and a separate installed-app test migrated the tablet's
existing database from v17 to v18 without resetting its three store rows. A
sanitized read-only readiness audit verifies the
expected installed APK and capability/count-only hardware state without
emitting device identifiers, customer data or app secrets; its four guard cases
run in CI. This is software and tablet-baseline evidence only.
No physical printer, integrated handheld printer SDK, optical label scan under
lane conditions, paper-out signal, or drawer behavior has
been certified yet.
Work:
- validate the implemented generic ESC-POS print and print spool on physical 58 mm and 80 mm devices;
- physical HID and integrated scan-intent diagnostics plus optical camera-label speed/lighting/damage tests;
- selected Sunmi/Ciontek handheld adapter;
- countertop 80 mm printer and cash drawer;
- physical network and USB-host ESC/POS qualification, paper-out/drawer telemetry and the implemented audited cash-event trigger;
- external alert delivery for the implemented in-product support diagnostics.
Exit criteria:
- receipt, reprint, paper-out, reconnect, barcode scan and drawer policies pass on physical devices;
- vendor SDK failure cannot block recovery of sale state.
Phase 8 — Remote UAT and pilot
Work:
- deploy the isolated test namespace and database;
- restore public POS hosts with valid TLS;
- seed a test merchant, store, catalog and staff;
- assert that agent and biller units cannot enter the merchant POS bootstrap; customer cash-out and agent-directory lookup remain separate platform flows;
- distribute signed Android test build;
- run remote E2E and reconciliation;
- retain the verified logical restore rehearsal and complete encrypted off-cluster backup, point-in-time recovery, security and support drills;
- pilot with one store and one or two lanes.
Exit criteria:
- no P0/P1 defects;
- daily close reconciles;
- offline/online recovery passes;
- support can revoke a device, inspect sync, reprint and trace a transaction end-to-end.
11. MVP and nice-to-haves
P0: pilot blockers
- secure login and terminal binding;
- products, barcodes, prices, tax and inventory;
- shifts, cash control and supervisor overrides;
- cash, customer-app, generic mobile-money and external/card handoff tenders;
- refunds, voids and split tender;
- offline cash sale and safe replay;
- 58 mm and 80 mm receipt printing;
- a market-selected fiscalization sandbox path, with KRA eTIMS/OSCU as the Kenyan reference adapter rather than a core dependency;
- idempotency, audit, observability, backup and reconciliation.
P1: high-value improvements
- email/SMS receipts with consent;
- promotions, loyalty and customer history;
- cycle-count scheduling, low-stock alerts, and receiving/transfer refinements such as purchase-order matching and in-transit approvals;
- Android Enterprise dedicated-device mode, remote terminal configuration, feature flags, managed app updates, and support-safe kiosk escape;
- remotely managed, vendor-specific signed intent profiles and dedicated-device policy on top of the implemented DataWedge-compatible contract and camera fallback;
- external alert delivery, escalation policy, and acknowledgement history for the implemented backoffice battery/network/printer/scanner and blocked-sync diagnostics;
- digital receipt QR lookup and GS1 Digital Link/2D-barcode readiness for product identity and future traceability;
- customer-facing display;
- accounting/ERP export and signed webhooks.
These priorities follow current platform guidance rather than vendor-specific POS marketing. Android recommends a local database as the source of truth for offline-first clients and persistent queued synchronization with WorkManager; Android Enterprise dedicated devices add allowlisted kiosk operation and managed updates. Zebra DataWedge demonstrates a vendor-neutral intent-output pattern for hardware scanning. GS1 Digital Link provides a standards-based route from GTINs and 2D barcodes to product and traceability information. References: Android offline-first, Android dedicated devices, Zebra DataWedge intent output, and GS1 Digital Link.
P2: expansion
- purchase orders and supplier workflows;
- ecommerce stock sync and BOPIS;
- label and shelf-edge printing;
- restaurant/kitchen routing;
- appointments, layaway and quotes;
- advanced fraud, shrinkage and refund analytics;
- multi-country tax/provider adapters.
12. Verification matrix
| Layer | Required verification |
|---|---|
| Contracts | OpenAPI compatibility, event schema compatibility, consumer-driven contract tests |
| Database | clean migration, upgrade migration, rollback rehearsal, backfill counts and checksums |
| Domain | sale/tender state-machine tests, refund rules, supervisor policy, tax rounding |
| Offline | dependency ordering, replay, duplicate submission, clock skew, catalog staleness, conflict resolution |
| Integrations | provider sandbox, callback signature, timeout, query, reversal and reconciliation |
| Security | role isolation, tenant isolation, token audience, device revocation, secret rotation, dependency and container scan |
| Hardware | scan, paper-out, reconnect, print spool, reprint marking, drawer policy, device reboot |
| Remote | TLS, authenticated E2E, metrics, logs, alerts, backup restore and external connectivity |
| Performance | catalog bootstrap size, checkout latency, peak lane concurrency and close-of-day report |
13. Goal completion criteria
The active goal is complete only when:
- the canonical POS suite is the only active POS client/service source;
- the POS service owns its database and has no platform database access;
- platform integration is through scoped OAuth APIs and versioned events;
- Android and SPAs contain no client secret;
- the remote test environment has valid TLS and passes authenticated E2E;
- physical printer/scanner testing passes;
- payment and fiscalization sandboxes reconcile;
- a controlled pilot store completes sale, refund, offline recovery and close-of-day.
14. Primary research references
- KRA eTIMS system-to-system integration: https://www.kra.go.ke/business/etims-electronic-tax-invoice-management-system/learn-about-etims/etims-system-to-system-integration
- KRA eTIMS overview: https://www.kra.go.ke/online-services/etims
- Safaricom Daraja developer portal: https://developer.safaricom.co.ke/
- OAuth 2.0 Security Best Current Practice, RFC 9700: https://www.ietf.org/rfc/rfc9700.html
- OAuth 2.0 for Native Apps, RFC 8252: https://www.rfc-editor.org/rfc/rfc8252
- Keycloak Server Administration Guide, service-account signed-JWT authentication: https://www.keycloak.org/docs/latest/server_admin/index.html
- Android Keystore: https://developer.android.com/privacy-and-security/keystore
- Android key attestation: https://developer.android.com/privacy-and-security/security-key-attestation
- Android Play Integrity: https://developer.android.com/google/play/integrity/overview
- PCI DSS: https://www.pcisecuritystandards.org/standards/pci-dss/
- PCI SSC document library: https://www.pcisecuritystandards.org/document_library/
- Kenya National Payment System Regulations: https://www.centralbank.go.ke/wp-content/uploads/2018/12/NPSRegulationsNew2014-1.pdf
- Kenya Data Protection Act: https://new.kenyalaw.org/akn/ke/act/2019/24/eng%402019-11-15
- ODPC registration guidance: https://www.odpc.go.ke/faqs/