# QRIS Soundbox Platform Package

Paket ini berisi blueprint final v1 untuk platform merchant aggregator QRIS + soundbox universal.

## Isi paket
- 01-executive-blueprint.md
- 02-system-architecture.md
- 03-domain-modules.md
- 04-device-flows.md
- 05-api-contract-draft.md
- 06-mqtt-contract-draft.md
- 07-database-schema-draft.md
- 08-implementation-roadmap.md
- 09-screen-inventory.md
- 10-design-blueprint.md
- 11-low-fi-wireframes.md
- DEBIAN13_APP_SERVER_SETUP.md
- 18-mqtt-broker-mosquitto-debian13.md

## Tujuan
Dokumen ini dibuat supaya tim bisa langsung mulai:
- desain UI/UX
- breakdown engineering
- desain backend
- integrasi device
- cicil implementasi per fase

## Quick Start Implementasi (Lanjutan dari CODEx Handoff)

- Backend bootstrap Fase 1 sudah dibuat di `src/`.
- Fitur awal yang sudah aktif:
  - request context + `request_id` di middleware
  - error envelope (`code`, `message`, `details`, `request_id`, `timestamp`)
  - auth token minimal untuk endpoint admin
  - middleware idempotency untuk endpoint sensitif
- endpoint awal:
  - `GET /health`
  - `GET /health/deep`
  - `GET /admin/health` (dengan `Authorization: Bearer <token>`)
  - `GET /admin/health/deep`
  - `POST /admin/login`
  - `POST /admin/sample-idempotent`
  - `POST /admin/merchants`
  - `GET /admin/merchants`
  - `GET /admin/merchants/{id}`
  - `PATCH /admin/merchants/{id}`
  - `POST /admin/merchants/{merchantId}/outlets`
  - `POST /admin/merchants/{merchantId}/approve`
  - `POST /admin/merchants/{merchantId}/reject`
  - `GET /admin/outlets`
  - `GET /admin/outlets/{id}`
  - `POST /admin/outlets/{outletId}/terminals`
  - `GET /admin/terminals`
  - `GET /admin/terminals/{id}`
  - `POST /admin/devices`
  - `GET /admin/devices`
  - `GET /admin/devices/{id}`
  - `POST /admin/devices/{id}/credentials/rotate`
  - `POST /admin/devices/{id}/bind`
  - `POST /admin/devices/{id}/unbind`
  - `POST /admin/devices/{id}/commands`
  - `GET /admin/devices/{id}/commands`
  - `GET /admin/devices/{id}/commands/{commandId}`
  - `GET /admin/devices/{id}/notifications`
  - `GET /admin/devices/{id}/config`
  - `PATCH /admin/devices/{id}/config`
  - `GET /admin/devices/{id}/config/status`
  - `POST /admin/devices/{id}/config/retry-push`
  - `GET /admin/devices/{id}/mqtt-messages`
  - `GET /admin/audit-logs`
  - `GET /admin/ledger-entries`
  - `GET /admin/dashboard/summary`
    - Memuat settlement finance summary termasuk `settlement_adjustment_amount` dan `settlement_adjusted_paid_amount`.
  - `POST /admin/settlement-batches`
  - `GET /admin/settlement-batches`
  - `GET /admin/settlement-batches/{batchId}`
  - `GET /admin/settlement-batches/{batchId}/export.csv`
    - Query `format=bank_generic` tersedia untuk CSV upload bank generik; default tetap settlement report standard.
  - `POST /admin/settlement-batches/{batchId}/mark-paid`
  - `PATCH /admin/settlement-batches/{batchId}/reference`
  - `POST /admin/settlement-batches/{batchId}/adjustments`
    - Adjustment disimpan di tabel formal `settlement_batch_adjustments` dan ikut tampil di detail batch admin/merchant.
    - Metadata adjustment lama otomatis dibackfill ke tabel formal saat schema initialization berjalan.
  - `GET /admin/settlement-adjustments`
    - Report adjustment lintas settlement batch dengan filter `merchant_id`, `adjustment_type`, `from`, `to`, dan `limit`.
  - `GET /admin/settlement-adjustments/export.csv`
    - Export CSV untuk report adjustment settlement dengan filter yang sama.
  - `POST /admin/exports/settlement-adjustments`
    - Membuat job export CSV adjustment settlement secara async, dengan permission `settlement:export`.
  - `GET /admin/exports/{jobId}`
    - Melihat status job export.
  - `GET /admin/exports/{jobId}/download`
    - Download hasil CSV saat job sudah `completed`.
  - `POST /admin/settlement-batches/{batchId}/mark-failed`
  - `POST /admin/settlement-batches/{batchId}/cancel`
  - `POST /admin/settlement-batches/{batchId}/reprocess`
  - `GET /admin/reconciliation/settlement-batches`
  - `GET /admin/observability/summary`
  - `POST /merchant/login`
  - `GET /merchant/profile`
  - `GET /merchant/settlement-summary`
  - `GET /merchant/settlement-batches`
  - `GET /merchant/settlement-batches/{batchId}`
  - `GET /merchant/settlement-batches/{batchId}/export.csv`
    - Query `format=bank_generic` tersedia untuk CSV upload bank generik; default tetap settlement report standard.
  - `GET /admin/transactions`
  - `GET /admin/transactions/{transactionId}`
  - `POST /admin/transactions`
  - `POST /admin/transactions/expire-due`
  - `GET /admin/transactions/{transactionId}/events`
  - `POST /admin/transactions/{transactionId}/retry-notification`
  - `POST /admin/seed`
  - `POST /device/transactions/dynamic-qr`
  - `POST /device/mqtt/uplink/dynamic-qr/request`
  - `GET /device/config`
  - `POST /device/config/ack`

### Menjalankan lokal

```bash
npm install
cp .env.example .env
npm run db:migrate
npm run dev
npm run build && npm start
```

Schema migration memakai tabel `schema_migrations`. Migration awal `001_current_schema_bootstrap` membaseline schema saat ini; migration berikutnya bisa ditambahkan ke folder `migrations/` dengan format `NNN_description.sql` atau `NNN_description.mts`.

Untuk mode broker MQTT sungguhan, set `MQTT_PUBLISH_MODE=broker` di `.env` bersama `MQTT_BROKER_URL`, `MQTT_USERNAME`, `MQTT_PASSWORD`, dan `MQTT_CLIENT_ID`. Smoke test otomatis tetap override ke `MQTT_PUBLISH_MODE=simulator` agar tidak bergantung pada broker eksternal.

Dynamic QR expiry sweep berjalan otomatis bila `DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED=true`; interval dan batch size diatur lewat `DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS` dan `DYNAMIC_QR_EXPIRY_SWEEP_LIMIT`.

Finance light membuat ledger `gross_income`, `platform_fee`, dan `merchant_payable` saat transaksi menjadi `paid`. Rate fee default diatur lewat `FINANCE_PLATFORM_FEE_BPS`.

Settlement light membuat batch dari ledger `merchant_payable` yang belum dibatch, dikelompokkan per merchant, lalu admin dapat menandai batch sebagai `paid`.
UI `settlement-batch-management` sudah membaca batch real, menampilkan KPI settlement, membuka drawer detail batch, event history payout, generate batch, confirm paid dengan reference payout/note, mark failed/cancel dengan reason, reprocess failed/cancelled batch, dan download CSV payout report.
Admin dashboard overview juga menampilkan summary settlement/finance dari agregasi DB: pending payout, paid payout, total platform fee, jumlah batch, batch terbaru, dan download CSV payout report per batch.
UI `merchant-settlement-history` sudah membaca settlement batch milik merchant yang login, menampilkan pending/paid payout termasuk adjustment dan adjusted paid amount, membuka detail batch dengan payout event history, dan download CSV payout report merchant-scoped.

### Cleanup data smoke test

```bash
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:cleanup
```

Cleanup hanya menarget entitas smoke (`Smoke Merchant`, `PR-`, `DEV-`) agar data seed demo tidak ikut terhapus.

```bash
PORT=3100 ADMIN_TOKEN=admin-dev-token DEVICE_TOKEN=device-dev-token INTEGRATION_WEBHOOK_SECRET=dev-callback-secret PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:flow
```

Smoke flow akan melakukan create merchant/device/transaction + heartbeat + callback paid + verifikasi event/heartbeat/notification, duplicate callback, invalid signature, audit log, ledger finance light, settlement batch light, skenario terminal tanpa binding, dynamic QR API-direct, expiry sweep dynamic QR, dynamic QR MQTT, device config push/retry/status/ack, dan trace MQTT config ack.

### Smoke test end-to-end (bootstrap + flow + cleanup)

```bash
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:e2e
```

Perintah ini menjalankan:

- cleanup data smoke
- start server lokal di port 3100
- wait sampai `/health` aktif
- jalankan flow smoke lengkap
- hentikan server setelah selesai

### Load test awal

Jalankan backend lokal lalu eksekusi:

```bash
PORT=3120 LOAD_CALLBACKS=30 LOAD_HEARTBEATS=60 LOAD_DYNAMIC_QR=30 LOAD_READS=30 LOAD_CONCURRENCY=10 npm run load:test
```

Script ini membuat data load test sementara, menjalankan burst transaction create, callback QRIS paid, heartbeat device, dynamic QR API, dan observability read, lalu membersihkan merchant/transaksi load test.

Baseline lokal level 2 terakhir: 1610 request, 0 error, durasi 4955.45 ms, throughput perkiraan 324.9 req/s. p95 lokal: transaction create 183.56 ms, callback paid 129.03 ms, heartbeat 90.4 ms, dynamic QR 71.57 ms, observability summary 230.96 ms.

### Smoke test broker MQTT sungguhan

```bash
npm run smoke:mqtt-real
```

Perintah ini membaca `.env`, menjalankan app lokal dengan `MQTT_PUBLISH_MODE=broker`, subscribe ke broker `devices/+/downlink/#`, lalu memverifikasi publish real untuk payment success, config push, dan dynamic QR response. Test akan membersihkan data merchant/transaksi/MQTT trace yang dibuat khusus smoke.

Smoke broker real terakhir lulus terhadap `mqtts://broker.bizone.id:8883`: broker connect ok, subscribe `devices/+/downlink/#`, menerima payment success, config push, dynamic QR response, dan cleanup data smoke.

### Async export worker

Export adjustment settlement memakai worker internal yang aktif default lewat `EXPORT_WORKER_ENABLED=true`. Endpoint create job mengembalikan status `pending`, worker mengambil job secara berkala, lalu client dapat polling `GET /admin/exports/{jobId}` sampai `completed` dan download dari `download_url`.

UI `admin-reconciliation-management` sudah memakai flow async ini untuk tombol download adjustment report.

Konfigurasi utama:

- `EXPORT_WORKER_INTERVAL_MS`
- `EXPORT_WORKER_BATCH_SIZE`
- `EXPORT_JOB_STALE_RUNNING_MS`
- `EXPORT_SETTLEMENT_ADJUSTMENT_MAX_ROWS`
- `EXPORT_STORAGE_DIR`
- `EXPORT_RETENTION_DAYS`

Status worker dan count job tampil di `/admin/observability/summary`.
Hasil export baru disimpan sebagai file di `EXPORT_STORAGE_DIR`; metadata job tetap di database. Worker juga membersihkan hasil export yang melewati `EXPORT_RETENTION_DAYS`.

### Provisioning credential MQTT device

```bash
npm run mqtt:provision-device -- --base-url http://127.0.0.1:3000 --device-id <device-id>
```

Perintah ini rotate credential device lewat API admin dan menampilkan command `mosquitto_passwd` untuk broker. Username MQTT device sama dengan `device_id` agar cocok dengan ACL Mosquitto `devices/%u/...`.

Validasi/template ACL Mosquitto:

```bash
npm run mqtt:check-acl -- --print-template
npm run mqtt:check-acl -- --file /etc/mosquitto/acl
```

Smoke ACL memakai dua username device test:

```bash
MQTT_TEST_DEVICE_A_USERNAME=<device-a-id> \
MQTT_TEST_DEVICE_A_PASSWORD=<secret-a> \
MQTT_TEST_DEVICE_B_USERNAME=<device-b-id> \
npm run smoke:mqtt-acl
```

UI device technical detail juga menyediakan tombol `Rotate Credential` dan modal one-time secret untuk operator.

### Backup dan restore readiness

```bash
npm run backup:production -- --out ./backups --include-mosquitto
npm run restore:plan -- --backup ./backups/<file>.dump
npm run restore:validate
```

`backup:production` membuat dump Postgres format custom dan opsional menyalin file Mosquitto passwd/ACL. `restore:plan` default hanya menampilkan command restore; tambahkan `--execute` hanya pada database restore yang memang sudah disiapkan. `restore:validate` menjalankan migration idempotent, health check, admin deep health, dan cek tabel kunci setelah restore.

### Load test staging profile

```bash
npm run load:test:staging
```

Profile staging menjalankan transaction create, QRIS callback, heartbeat, dynamic QR, observability read, dan async export job. Set `BASE_URL`, token, dan env database staging sebelum menjalankan terhadap environment target.
Hasil staging disimpan otomatis ke folder `reports/` melalui `LOAD_REPORT_FILE`.

### UI QA ringan

```bash
npm run ui:qa
```

Checker ini memvalidasi inline script halaman operasional, memastikan export reconciliation memakai async job, dan memastikan navigasi placeholder di halaman operasional utama sudah dibersihkan.

### Rate limiting dan security polish

Rate limit in-memory aktif default untuk login admin/merchant, admin write routes, device routes, dan integration callback routes.
Login admin dan merchant juga dicatat ke audit log dengan action `admin.login.success`, `admin.login.failed`, `merchant.login.success`, dan `merchant.login.failed`.
Halaman `/ui/admin-system-audit-logs` sudah membaca audit log real, menyediakan preset login events, filter action/entity/date, pencarian client-side, KPI login success/failed, dan drawer JSON payload.

Konfigurasi utama:

- `RATE_LIMIT_ENABLED`
- `RATE_LIMIT_LOGIN_WINDOW_MS`
- `RATE_LIMIT_LOGIN_MAX`
- `RATE_LIMIT_DEVICE_WINDOW_MS`
- `RATE_LIMIT_DEVICE_MAX`
- `RATE_LIMIT_ADMIN_WRITE_WINDOW_MS`
- `RATE_LIMIT_ADMIN_WRITE_MAX`
- `TRUST_PROXY`
- `JSON_BODY_LIMIT`

Runbook operasional utama: `OPERATIONAL_RUNBOOK.md`. Checklist pilot: `PILOT_EXECUTION_CHECKLIST.md`. Catatan export storage: `EXPORT_STORAGE_READINESS.md`.

### Membuat admin user production

```bash
npm run admin:create-user -- \
  --email finance@example.com \
  --name "Finance Ops" \
  --role finance \
  --password "ganti-dengan-password-kuat"
```

Role yang tersedia: `admin`, `finance`, `ops`, `support`, dan `viewer`. Untuk update password user yang sudah ada, tambahkan `--rotate-password`.

### Membuat merchant user production

```bash
npm run merchant:create-user -- \
  --merchant <merchant-id-atau-code> \
  --email owner@merchant.com \
  --name "Merchant Owner" \
  --role owner \
  --password "ganti-dengan-password-kuat"
```

Role merchant yang tersedia: `owner`, `finance`, `ops`, dan `viewer`. Untuk update password user yang sudah ada, tambahkan `--rotate-password`.

### Device API auth

Endpoint `/device/*` mendukung credential per-device:

```bash
curl -X POST http://127.0.0.1:3000/device/heartbeat \
  -H "Content-Type: application/json" \
  -H "X-Device-Id: <device-id>" \
  -H "X-Device-Secret: <one-time-secret-yang-dipasang-di-device>" \
  -d '{"device_id":"<device-id>","timestamp":"2026-05-28T00:00:00.000Z","network_strength":88,"battery_level":77}'
```

Credential hanya boleh dipakai untuk `device_id` yang sama. `Authorization: Bearer DEVICE_TOKEN` masih tersedia sebagai fallback/dev compatibility.

Untuk production, fallback token global bisa dimatikan:

```env
DEVICE_AUTH_ALLOW_LEGACY_TOKEN=false
```

### Endpoint device lain

- `POST /device/commands/ack`

### Quick screen preview
- `GET /ui` => katalog halaman UI dari seluruh `design/*`.
- `GET /ui/:page` => buka halaman berdasarkan slug (contoh: `/ui/admin-login`, `/ui/admin-dashboard-overview`, `/ui/merchant-login`).

Status lanjutan: Fase 1 core flow sudah tercakup smoke e2e. Fase 2 sudah aktif untuk capability resolver, dynamic QR API-direct, dynamic QR MQTT via outbox, dan device config push/status/retry/ack.

Catatan Fase 2 ops: endpoint daftar/detail device admin juga mengirim `health_summary` (`status`, `score`, `age_seconds`, `reasons`) untuk membantu triage device. UI device registry dan device technical detail sudah menampilkan health summary, config drift, dan retry config push.

Catatan credential device: `POST /admin/devices/{id}/credentials/rotate` menerbitkan MQTT username/password satu kali, menyimpan fingerprint secret di database, dan payload device biasa hanya menampilkan ringkasan credential tanpa secret.
