Skip to content

Merchant Terminal Launch Handoff

Audience: Engineer taking this to UAT / demo / production
As of: 2026-05-21 (Phase 1 updates 2026-06)
Branches: master and dev aligned at 126efa7d (merge PR #206); active work on feat/phase-1-roadmap
App: Native Android terminal — apps/terminal (io.sagio.merchant)
Context: BNZ / Worldline Fasstap (Soft Space SDK v3.6.5.0) + Sagio POS, cash, and SAGIO wallet pay. Phase 1 focus: stable traditional card (no Web3).

Phase 1 additions (post-UAT, prod keys): explicit QR rail + dynamic hosted checkout fallback, enhanced ESC/POS with status parse + customer receipts on success, atomic confirms + classified error boundaries + structured logging (SAGIO-vs-Worldline), AML data map + API summary endpoint + export script, dashboard real create/cancel wiring, prod CI env injection, reversibility comments. See plan in session and main repo docs/ops/aml-data-map-phase1.md.


Executive summary

Item Status
Code on master Merchant terminal + merchant-app API merged (PR #204)
Prod API auth (/merchant-app/auth) Working (Neon full_name schema fix, PR #203)
Prod API deploy (Workers) Deployed (Deploy run 26205761768)
Prod Fasstap secrets (Cloudflare) Deployed (nzsagio@gmail.com / sagio-api-prod, refreshed 2026-05-22 via wrangler)
Dev Fasstap secrets Deployed (sagio-api-dev, all six FASSTAP_* vars, 2026-05-22)
Prod fasspay-config Working (live smoke 2026-05-22: UAT, hosts, CA cert, Play project 757874674469)
SDK init on device (debug APK) VerifiedSSMPOSSDK v3.6.5.0 init OK (local API via adb reverse; prod credentials now available)
End-to-end card tap on prod Not verified — rebuild APK against prod, Settings → Refresh, then card tap
Dashboard / wallet Pages redeploy Skipped on last deploy (path filters); use manual workflow run

Resolved (2026-05-21): Fasstap secrets on prod + API fix so buildFasspayConfig() reads process.env after Worker env injection (import-time config.fasspay was always empty on Cloudflare).


Goal

Ship a working merchant terminal that can:

  1. Accept Worldline card taps (NFC via Soft Space SDK)
  2. Take cash payments (API confirms cash rail)
  3. Show SAGIO QR for wallet pay when merchant has a wallet on profile
  4. Run POS menu flow (products → cart → checkout → kitchen ticket)
  5. Pull Fasstap credentials from Sagio API — merchant never types Worldline config

What shipped to master (2026-05-21)

PR What
#203 merchantAppService: merchants.full_name auth query (fixes prod column "name" does not exist)
#204 Full devmaster: terminal app, POS/cash rails, Fasstap config API, docs
#205 Dashboard order API tests → https://api.sagio.io/api
#206 CI: Node 22 for Wrangler; workflow_dispatch on Deploy workflow

Android (on dev after next merge): canCharge allows Card rail when NFC is on without pre-granting SDK permissions (permissions requested during first SSMPOSSDK.init()).


Architecture (30-second version)

Android terminal (apps/terminal)
  → POST /merchant-app/auth          (SAGIO_API_KEY + SECRET baked into APK)
  → GET  /merchant-app/fasspay-config (Bearer token)
  → Soft Space SDK init (attestation + keyloading to fasspay.com)

Card tap: SDK → Worldline → POST payment-request → POST confirm-worldline
Cash:     terminal → POST payment-request → POST confirm-cash
SAGIO pay: terminal → payment-request + QR on processing screen
POS:      GET products → POST order → complete-payment (cash/card/SAGIO)

Full diagrams: 004-fasstap-visual-architecture.md
Config reference: 003-fasstap-configuration.md
Card / attestation FAQ: merchant-terminal-card-attestation.md
Investigation log (READY + attestation fail, HSM key): merchant-terminal-attestation-investigation.md
Soft Space cert whitelist: softspace-play-integrity-whitelist.md


Production smoke tests (run these first)

Auth (should pass today)

curl -sS -X POST "https://api.sagio.io/api/merchant-app/auth" \
  -H "Content-Type: application/json" \
  -d '{"apiKey":"<SAGIO_API_KEY>","apiSecret":"<SAGIO_API_SECRET>","deviceId":"handoff-test"}'
# Expect: success true, merchantName populated

Fasstap config (fails until secrets deployed)

TOKEN="<accessToken from auth>"
curl -sS "https://api.sagio.io/api/merchant-app/fasspay-config" \
  -H "Authorization: Bearer $TOKEN"
# Today: success false, "Fasstap configuration missing: accessKey, secretKey, uniqueId, developerId"
# After secrets deploy: success true, environment UAT, 11 fields

Payment request (after secrets; needs merchant JWT)

curl -sS -X POST "https://api.sagio.io/api/payment-requests" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":"0.01","currency":"NZD","paymentRail":"WORLDLINE","description":"Card payment","referenceNumber":"handoff-001"}'
# Expect: success true (recipient_address + created_by fix is on master)

Access you need before starting

Item Where / who
Soft Space integrator accessKey / secretKey Soft Space onboarding — store in apps/api/.env.local, never in APK or git
Soft Space merchant portal MUID → FASSTAP_UNIQUE_ID, company SSO ID → FASSTAP_DEVELOPER_ID
Neon DATABASE_URL For migration + minting keys
Sagio Cloudflare account Wrangler or API token Account ID used by CI: secrets.CLOUDFLARE_ACCOUNT_ID (not personal Gmail account)
Android SDK + JDK 17 Gradle toolchain auto-downloads JDK 17
Test merchant account Must exist in Neon with products if testing POS
NFC-capable Android phone minSdk 29, arm64 recommended
Kitchen printer (optional) Network ESC/POS on TCP 9100

UAT values (in team .env.local — rotate if Soft Space reissues):

FASSTAP_ACCESS_KEY=<from Soft Space>
FASSTAP_SECRET_KEY=<from Soft Space>
FASSTAP_UNIQUE_ID=nydtubnqssautomationtester4
FASSTAP_DEVELOPER_ID=yjOItT3r3MKqRZE
FASSTAP_SSO_ID=yjOItT3r3MKqRZE
FASSTAP_ENVIRONMENT=UAT

Attestation host (https://mpos-uat.fasspay.com:9001/v2), cert pinning, CA cert, and Google Play project number 757874674469 (Soft Space's) are API defaults in apps/api/src/config/index.ts.


Launch checklist (in order)

1. Deploy Fasstap secrets to production worker (done 2026-05-21)

Local .env.local does not affect api.sagio.io. Values must be Cloudflare Worker secrets on sagio-api-prod.

Option A — Wrangler (Sagio account)

cd apps/api
wrangler login   # MUST be Sagio org account, not personal Gmail
bun run deploy:fasstap-secrets -- .env.local
# No second arg = production worker (sagio-api-prod)

Option B — GitHub Actions (recommended if local Wrangler is wrong account)

  1. Add repo secrets (if not present): FASSTAP_ACCESS_KEY, FASSTAP_SECRET_KEY, FASSTAP_UNIQUE_ID, FASSTAP_DEVELOPER_ID, FASSTAP_SSO_ID, FASSTAP_ENVIRONMENT
  2. Add workflow job or run existing deploy script from CI with CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID (already in repo secrets)

Verify:

# fasspay-config smoke test (see above) → success true

Dev worker (optional):

bun run deploy:fasstap-secrets -- .env.local sagio-api-dev
bun run deploy:dev

2. API backend (mostly done on prod)

Migration (once per Neon env if not applied):

-- apps/api/src/migrations/add_merchant_fasstap_columns.sql
-- merchants.fasstap_unique_id, merchants.fasstap_developer_id

Worker code is deployed via push to masterDeploy workflow. Manual full redeploy:

  • GitHub → ActionsDeployRun workflow (branch master)
  • Deploys apps whose paths changed on last push; to force all apps, touch a file under each app or extend workflow with deploy_all input (not yet implemented).

3. Mint terminal credentials (per merchant / per APK build)

cd apps/api
bun run mint-merchant-key -- --email <merchant@example.com>

Output is one-time — copy SAGIO_API_KEY and SAGIO_API_SECRET immediately into apps/terminal/local.properties (gitignored).

UAT merchant used in testing: chriskwon0@gmail.comd365d71d-170d-4ad0-ba5e-ed549b33dc17. Re-mint if secrets were lost.

Merchant prerequisites:

  • Row in merchants + business_profiles
  • SAGIO pay: smart_wallet_address or embedded_wallet_address
  • POS menu: products for merchant in DB
  • Optional: fasstap_unique_id / fasstap_developer_id on merchants (else global env vars)

4. Build and install APK

cd apps/terminal
cp local.properties.example local.properties
# sdk.dir=/path/to/Android/sdk
# SAGIO_API_KEY=...
# SAGIO_API_SECRET=...
# Optional for local API testing:
# SAGIO_API_BASE_URL=http://127.0.0.1:8787/api/
./gradlew :app:assembleDebug
# apps/terminal/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk

Local API on device:

bun run dev:api
adb reverse tcp:8787 tcp:8787
adb install -r app/build/outputs/apk/debug/app-arm64-v8a-debug.apk

Default prod API URL in app: https://api.sagio.io/api/ (Settings can override; build-time SAGIO_API_BASE_URL in local.properties overrides Retrofit base URL).


5. Soft Space / Play Integrity

Whitelist package io.sagio.merchant and signing cert SHA-256 for Soft Space GCP project 757874674469.

Debug keystore:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Without cert whitelist: SDK may fail on device even when Settings shows READY.


6. Device bootstrap

  1. Install APK, unlock phone
  2. Settings → confirm Worldline SDK credentials: READY, Environment: UAT
  3. READY = API returned all four credential fields (not the same as SDK init success)
  4. Grant runtime permissions when first Card charge prompts
  5. Home → Card → amount → Tap to Pay → tap test card

Verified in session (local API):

D FasstapManager: SDK initialized: version=3.6.5.0, cotsId=<device-id>

Logcat filter:

adb logcat | rg -i "FasstapManager|SDK initialized|Failed to initialize|OkHttp|merchant-app"

Settings UI: READY vs SDK initialized

Signal Meaning
Settings READY GET /fasspay-config succeeded; access/secret/uniqueId/developerId stored locally
Settings MISSING Auth failed, secrets missing on worker, or network error
Logcat SDK initialized: version=3.6.5.0 SSMPOSSDK.init() succeeded on device
Charge cancelled after init Often missing runtime permissions — grant and tap again

E2E test plan

# Flow Steps Pass criteria
0 Prod API curl auth + fasspay-config Auth OK; fasspay OK after secrets
1 SDK bootstrap Settings → Refresh Worldline READY, env UAT
2 Card payment Card rail → amount → Charge → tap SDK completes; confirm-worldline in API
3 Cash Cash rail → amount → Charge Payment request + confirm-cash
4 SAGIO QR SAGIO rail (wallet on profile) QR on processing screen
5 POS menu Menu → cart → checkout Order + payment completes
6 Kitchen ticket Printer IP in Settings ESC/POS on :9100
7 Settlement / lookup ⋮ menu No crash
8 Config refresh Settings → Refresh Re-pull after secret rotation

Merchant-app API routes on master:

Method Path
POST /merchant-app/auth
POST /merchant-app/refresh
GET /merchant-app/profile
GET /merchant-app/fasspay-config
GET /merchant-app/products
POST /merchant-app/orders
POST /merchant-app/orders/:orderId/complete-payment
POST /payment-requests
POST /payment-requests/:txnId/confirm-worldline
POST /payment-requests/:txnId/confirm-cash

OpenAPI: apps/api/public/openapi.json


Key code locations

Purpose Path
Fasstap env defaults apps/api/src/config/index.ts
Merchant app service apps/api/src/services/merchantAppService.ts
Routes apps/api/src/routes/index.ts
Secrets deploy script apps/api/src/scripts/deployFasstapSecrets.sh
Mint terminal key apps/api/src/scripts/mintMerchantKey.ts
Android SDK init apps/terminal/app/src/main/java/io/sagio/merchant/sdk/FasstapManager.kt
Config fetch apps/terminal/app/src/main/java/io/sagio/merchant/data/SettingsRepository.kt
Retrofit base URL apps/terminal/app/src/main/java/io/sagio/merchant/network/NetworkModule.kt
Build-time API creds apps/terminal/app/build.gradle.kts + local.properties
POS UI apps/terminal/app/src/main/java/io/sagio/merchant/ui/screens/PosMenuScreen.kt
CI deploy .github/workflows/deploy.yml

Legacy Capacitor App: The old Capacitor-based hybrid app (apps/merchant) has been deleted. Use apps/terminal exclusively for the merchant terminal.


CI / deploy notes

  • Deploy workflow runs on push to master and workflow_dispatch
  • Deploy jobs use Node 22 (Wrangler 4.x requirement)
  • Path filters: only changed apps deploy; API-only pushes skip dashboard/wallet
  • Failed deploy history: PR #204 (dashboard tests), PR #205 (Node 20) — fixed in #205/#206

Troubleshooting

See also merchant-terminal-card-attestation.md for “attestation failed on card charge” and config beyond SAGIO_API_KEY.

Symptom Likely cause Fix
Prod auth column "name" does not exist Old worker Fixed on master; confirm Deploy ran
fasspay-config missing on prod (secrets set) Import-time config.fasspay empty on Workers Fixed in PR #207 (already hot-deployed)
fasspay-config missing on prod (no secrets) Fasstap secrets not on Cloudflare deploy:fasstap-secrets with Sagio account
Wrangler Authentication error 10000 Wrong Cloudflare account Login to Sagio account or use GH secrets + CI
Settings MISSING on prod Same as above Deploy secrets; Refresh in app
SDK init fails, Settings READY Play Integrity / cert Send SHA-256 to Soft Space
Payment Failed: "Missing parameters, please perform refresh token" startTransaction() called without Soft Space refreshToken() after init() Fixed on dev: FasstapManager.ensureTokenRefreshed() before first tap (not Sagio JWT refresh)
Charge button disabled at $0 Expected Enter amount > 0
Charge disabled with amount (old APK) canCharge required perms before init Apply stash fix or update PaymentViewModel.kt
Payment request 500 on prod Was recipient_address / created_by Fixed on master
SAGIO pay hidden No wallet on merchant Set wallet columns on merchant
POS menu empty No products Seed products in dashboard/DB

Suggested day-1 sequence

  1. Pull dev or master (same tip): git pull origin dev
  2. Deploy Fasstap secrets to prod (section 1) — unblocks terminal Settings
  3. Re-run fasspay-config curl smoke test
  4. Mint or recover terminal API key → local.properties → build APK
  5. Email Soft Space signing SHA-256 if not whitelisted
  6. Install on NFC device; run E2E table
  7. GitHub Deploy → Run workflow if dashboard/wallet need redeploy
  8. Before demo: prod fasspay-config OK + one card tap + one cash minimum

Contacts / external

  • Soft Space (Worldline NZ): MUID, SSO ID, integrator keys, cert whitelist
  • Specs: docs/specs/tap2pay/ in repo

Git

git fetch origin
git checkout dev && git pull origin dev
# master is at 126efa7d; dev should match after pull

Prior force-push note on old dev may still affect teammates with stale branches — use git reset --hard origin/dev if history looks wrong.