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) | Verified — SSMPOSSDK 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:
- Accept Worldline card taps (NFC via Soft Space SDK)
- Take cash payments (API confirms cash rail)
- Show SAGIO QR for wallet pay when merchant has a wallet on profile
- Run POS menu flow (products → cart → checkout → kitchen ticket)
- 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 dev → master: 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)
- Add repo secrets (if not present):
FASSTAP_ACCESS_KEY,FASSTAP_SECRET_KEY,FASSTAP_UNIQUE_ID,FASSTAP_DEVELOPER_ID,FASSTAP_SSO_ID,FASSTAP_ENVIRONMENT - Add workflow job or run existing deploy script from CI with
CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID(already in repo secrets)
Verify:
Dev worker (optional):
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 master → Deploy workflow. Manual full redeploy:
- GitHub → Actions → Deploy → Run 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_allinput (not yet implemented).
3. Mint terminal credentials (per merchant / per APK build)¶
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.com → d365d71d-170d-4ad0-ba5e-ed549b33dc17. Re-mint if secrets were lost.
Merchant prerequisites:
- Row in
merchants+business_profiles - SAGIO pay:
smart_wallet_addressorembedded_wallet_address - POS menu: products for merchant in DB
- Optional:
fasstap_unique_id/fasstap_developer_idonmerchants(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/
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¶
- Install APK, unlock phone
- Settings → confirm Worldline SDK credentials: READY, Environment: UAT
- READY = API returned all four credential fields (not the same as SDK init success)
- Grant runtime permissions when first Card charge prompts
- Home → Card → amount → Tap to Pay → tap test card
Verified in session (local API):
Logcat filter:
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
masterandworkflow_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¶
- Pull
devormaster(same tip):git pull origin dev - Deploy Fasstap secrets to prod (section 1) — unblocks terminal Settings
- Re-run fasspay-config curl smoke test
- Mint or recover terminal API key →
local.properties→ build APK - Email Soft Space signing SHA-256 if not whitelisted
- Install on NFC device; run E2E table
- GitHub Deploy → Run workflow if dashboard/wallet need redeploy
- 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.