Skip to content

POS Integration Flows

This page is the authoritative boundary map for the standalone POS. It separates retail operations from regulated money movement and shows how Emali POS, third-party POS systems, Emali Mobile Money, and external payment providers interact.

System boundary

flowchart LR
    subgraph Merchant["Merchant estate"]
        EPOS["Emali POS<br/>cashier, Android, backoffice"]
        TPOS["Third-party POS / ERP"]
        HW["Scanner, printer,<br/>cash drawer, card terminal"]
    end

    subgraph POS["Standalone emali2-pos"]
        API["POS API<br/>sales, stock, receipts"]
        DB[("POS PostgreSQL")]
        WORKER["Payment and outbox workers"]
        ADAPTERS["Provider-neutral adapters"]
    end

    subgraph Platform["Emali Mobile Money / Core"]
        CORE["Store-scoped merchant<br/>collection API"]
        WALLET["Customer wallet,<br/>approval and ledger"]
        APP["Customer mobile app"]
    end

    subgraph Providers["Country-specific providers"]
        MOBILE["Mobile-money adapter"]
        CARD["Card-present service"]
        FISCAL["Fiscalization adapter"]
    end

    EPOS --> API
    HW --> EPOS
    TPOS -->|"OAuth machine token + exact store binding"| API
    API --> DB
    DB --> WORKER
    WORKER --> ADAPTERS
    ADAPTERS -->|"EMALI_APP"| CORE
    CORE --> APP
    APP --> WALLET
    WALLET --> CORE
    ADAPTERS --> MOBILE
    ADAPTERS --> CARD
    ADAPTERS --> FISCAL

POS owns baskets, prices, stock reservations, tender state, receipts, shifts, terminal enrollment, and reconciliation. Emali Core owns customer identity, wallet approval, money movement, and ledger settlement. The systems share neither application code nor databases.

Country integrations are adapters selected by configuration. MobileMoneyPort and the fiscalization port are generic contracts; Daraja/M-PESA and KRA/eTIMS are optional Kenya implementations, not POS domain concepts.

Emali customer-app payment

The cashier starts a merchant collection, not an agent cash-out. POS must never call /api/v1/agents/withdraw/quote and does not accept an agentIdentifier in this flow.

sequenceDiagram
    autonumber
    actor Cashier
    participant POS as Emali POS
    participant Worker as POS payment worker
    participant Core as Emali Core
    participant App as Customer mobile app
    participant Ledger as Wallet ledger

    Cashier->>POS: Select EMALI_APP, amount, customer reference
    POS->>POS: Reserve stock and persist PENDING tender
    POS->>POS: Encrypt customer reference in durable attempt
    POS-->>Cashier: Prompt queued
    Worker->>Core: POST store-scoped merchant-collection request<br/>OAuth service token + opaque idempotency key
    Core->>Core: Verify issuer, audience, role, client and store binding
    Core-->>App: Push/in-app/websocket approval prompt
    App->>Core: Approve or deny with registered-device proof
    alt Approved and wallet payment commits
        Core->>Ledger: Atomic customer-to-merchant payment
        Ledger-->>Core: Committed
        Core-->>Worker: CONSUMED
        Worker->>POS: Mark tender CAPTURED and complete sale
        POS-->>Cashier: Receipt ready
    else Denied, expired or cancelled
        Core-->>Worker: DENIED / EXPIRED / CANCELLED
        Worker->>POS: Mark tender FAILED
        POS-->>Cashier: Offer another payment method
    else Outcome cannot be confirmed
        Worker->>POS: Move attempt to MANUAL_REVIEW
        POS-->>Cashier: Keep tender pending; block cash fallback
    end

Only Core status CONSUMED is a captured payment. APPROVED is not enough. POS polls with bounded backoff, removes the encrypted routing reference after dispatch, and keeps only a masked reference on the tender. A captured refund uses the original authorization UUID and the same organization/store scope.

The POS service token must satisfy all of these checks:

Check Required value
Issuer exact configured Emali realm issuer
Audience core-internal-api
azp and client_id both present, equal, and the configured POS service client
Role POS_SERVICE from the approved POS client role source
Authorization active persisted client + organization + store binding

Raw customer references, till values, secrets, and idempotency values do not belong in resource identifiers or logs.

Third-party POS and ERP clients

A merchant may integrate an existing POS, ecommerce platform, warehouse system, or ERP with the standalone POS API. This is a separate direction from the POS-to-Core service integration.

sequenceDiagram
    autonumber
    participant Admin as Merchant administrator
    participant IAM as Identity provider
    participant Client as Third-party POS / ERP
    participant POS as Standalone POS API
    participant DB as POS database
    participant Events as POS outbox / events

    Admin->>IAM: Provision confidential client
    Admin->>IAM: Add pos-api audience and approved realm role
    Admin->>POS: Bind exact client ID to organization and stores
    Client->>IAM: client_credentials<br/>(prefer private_key_jwt)
    IAM-->>Client: JWT with matching azp + client_id
    Client->>POS: API request + JWT + idempotency key
    POS->>POS: Verify issuer, pos-api audience and realm role source
    POS->>DB: Verify active exact store binding
    POS->>DB: Apply store-scoped operation atomically
    POS->>Events: Publish safe operational event
    POS-->>Client: Stable response or safe error code

The token proves client identity; the POS-owned binding decides which merchant and stores that identity can access. There is no global-store fallback. Missing or mismatched azp/client_id, a role from an unrelated client, or an inactive binding fails closed. Terminal mutations additionally require the enrolled terminal's request signature.

Use stable API versions, an opaque Idempotency-Key for every mutation, and event/outbox delivery for downstream synchronization. Clients must not receive Keycloak secrets through POS APIs.

Other payment integrations

Every electronic provider returns a normalized POS result:

Provider family Typical initiation Captured evidence Important boundary
Emali customer app Core merchant-collection request Core CONSUMED Never use agent cash-out
External mobile money Country adapter prompt or merchant collection Provider query/callback confirmed M-PESA is one optional adapter
Card present Certified terminal/card service handoff Acquirer-approved transaction POS never handles PAN, PIN, track or EMV cryptograms
Cash Cashier confirmation Server-owned cash tender No external provider
Loyalty POS loyalty authorization POS ledger/points result Not platform money

A timeout is not a decline. When an external write may have succeeded but the answer is unknown, POS keeps the tender unresolved and sends it to manual reconciliation. It must not silently accept cash and risk charging twice.

Error isolation

The customer cash-out/agent quote API is a different product flow owned by the mobile-money clients and Core. A failure there must be mapped by those clients to a stable customer-safe state such as agent_unavailable; an upstream error body or submitted identifier must never be rendered directly.

As of the 26 July 2026 source audit, the standalone POS is isolated from that route and has a CI guard enforcing the boundary. Core/mobile commit c0ab1c6d245df43f63343422e491e1511dec5178 prepares the governed non-enumerating contract in an isolated worktree: public agent cash-out routes remain retired with 410 Gone, and Android/iOS no longer submit or display the legacy identifier-bearing request or raw response body. Its focused Core, Android, iOS contract, and iOS Simulator checks pass. That commit has not been merged, published, or deployed; it remains a Core/mobile release gate pending authoritative migration ordering. The currently deployed Core jar already contains the 410 tombstone, so the reported raw 404 came from an earlier or different request path rather than the current public controller.

Deployment-facing endpoints

Surface Test hostname
Backoffice pos.test.emali2.damplabs.com
Cashier pos-cashier.test.emali2.damplabs.com
API pos-api.test.emali2.damplabs.com
Docs docs-pos.test.emali2.damplabs.com
Receipts receipts.test.emali2.damplabs.com

Production uses the corresponding *.pos.damplabs.com hosts only after production database, object storage, identity clients, secrets, certificates, and rollback evidence are provisioned.