Transaction Notification Flow
This document describes the design of the incoming transaction path and merchant notification for Kesles Merchant, including usage from mobile QRIS and payment terminal devices like the Aisino Pro 161.
Goal
- a successful transaction from any payment source is recorded into
payment.transactions - that transaction always carries a
merchant_idcontext - if the transaction succeeds, the backend pushes a
transaction_infoto the merchant owner - on the merchant mobile:
transaction_infois audible- other notifications stay silent
Current Status
Status: LIVE
payment_service(port 8085) adalah satu-satunya ingest point untuk PSP webhookpayment.transactionsdidb_kesles_merchant_paymentadalah sole source of truth transaksi (direct cutover; tidak ada dual-write kemerchant.transactions)- FCM push didispatch oleh
payment_serviceviacore_api(POST/internal/notifications/merchant-event);core_apiyang resolve merchant → tokens lalu memanggilfirebase_service dashboard_apimembaca recent transactions via HTTP kepayment_service /internal/transactions(tidak query SQL langsung)
Two Transaction Flows
Flow A — Customer → Merchant (transaksi masuk)
Sumber: pelanggan bayar ke merchant via QRIS atau terminal fisik.
PSP (payment.kesles.com)
→ payment_service: POST /psp/v1/events (HMAC-SHA256)
payment_service:
1. Verify HMAC signature
2. INSERT payment.transactions (db_kesles_merchant_payment)
3. Resolve merchant_id (lookup by NMID via core_api)
4. POST /internal/notifications/merchant-event ke core_api (event_type=transaction_info)
core_api:
→ resolve merchant owner + active push tokens (notification.fcm_push_tokens)
→ POST /internal/fcm/send ke firebase_service (X-Internal-API-Key)
firebase_service:
→ Firebase FCM HTTP v1 → merchant device
→ mobile: transaction_info (audible channel)
Flow B — Merchant → Kesles beli produk (order payment)
Sumber: merchant melakukan pembayaran order produk Kesles (QRIS Plus, dll.) via PSP.
PSP (payment.kesles.com)
→ payment_service: POST /psp/v1/events (HMAC-SHA256)
payment_service:
1. Verify HMAC signature
2. INSERT payment.transactions
3. Deteksi konteks order produk (NMID match payment.qris_config = Flow B)
4. POST /internal/orders/sales-orders/{sales_order_id}/mark-paid ke order_service (X-Internal-API-Key)
order_service:
→ UPDATE orders.sales_order_payments (db_kesles_merchant_order, mig 004)
→ UPDATE sales_orders.status = 'payment_received'
Event Sources
1. Mobile QRIS
Characteristics:
- event source comes from the merchant QRIS payment flow
- minimally carries the merchant identity and amount
- device context can be empty if it is not a physical terminal
Minimum fields that must be available:
merchant_idtransaction_codeexternal_referencepayment_method = qrispayment_channelgross_amounttransaction_statustransaction_at
Optional fields:
device_idnmidterminal_idpayer_referencenotesmetadata
2. Terminal Payment Device
Example targets:
Aisino Pro 161(SN format: 11 digit pure numeric, mis.00087000668)- model yang skema rekam sebagai
Q161 Pro
Characteristics:
- the transaction may originate from the merchant's physical device
- device context is stronger and should always be carried
- comes from a PSP webhook callback
Minimum fields:
- all base transaction fields
terminal_idordevice_id
Strongly recommended fields:
nmidserial_numberorterminal_codein metadatapayment_channelsource_type
Tables That Are The Source Of Truth
payment.transactions (db_kesles_merchant_payment)
Sole source of truth untuk semua transaksi PSP.
Important columns:
merchant_idtransaction_codeexternal_referencesource_typetransaction_statuspayment_methodpayment_channeldevice_idnmidgross_amountmdr_ratemdr_fee_amountnet_amount_after_mdrtransaction_atsettled_atpayer_referencenotesmetadataterminal_id
payment.qris_config (db_kesles_merchant_payment)
Singleton konfigurasi QRIS korporat (NMID = ID9999999999999). Dibaca oleh dashboard_api via HTTP ke payment_service /internal/qris-config.
merchant.payment_terminals (db_kesles_merchant)
Source of truth untuk device terminal fisik merchant.
Important columns:
merchant_idterminal_codeterminal_type_codemanufacturer_nameproduct_model_namedevice_idserial_numbernmidpayment_routing_modepayment_capabilitiesstatuslast_seen_at
orders.sales_order_payments (db_kesles_merchant_order)
Payment lifecycle untuk order produk Kesles. Diisi oleh order_service saat menerima notifikasi mark-paid dari payment_service (Flow B).
Merchant → Notification Recipient User Relation
The user receiving the transaction push must not be determined from the phone as a business rule. The correct source of truth is:
- the transaction has a
merchant_id merchant_idmaps tomerchant.merchant_users- the notification is sent to the merchant owner:
is_owner = TRUE
- push tokens come from
notification.fcm_push_tokens
Send rules:
- send to every active push token of the merchant owner
- do not send to anonymous devices
- do not send if the user has no active token
Sequence Diagram (Flow A)
sequenceDiagram
participant PSP as payment.kesles.com (PSP)
participant PS as payment_service (8085)
participant DB as db_kesles_merchant_payment
participant CA as core_api (8080)
participant FS as firebase_service (8093)
participant M as Mobile Merchant
PSP->>PS: POST /psp/v1/events (HMAC)
PS->>PS: Verify HMAC signature
PS->>DB: INSERT payment.transactions
PS->>CA: POST /internal/notifications/merchant-event (X-Internal-API-Key)
CA->>CA: Resolve owner merchant + active push tokens
CA->>FS: POST /internal/fcm/send (X-Internal-API-Key)
FS-->>M: FCM transaction_info push
M->>M: Transaction channel audible
Sequence Diagram (Flow B)
sequenceDiagram
participant PSP as payment.kesles.com (PSP)
participant PS as payment_service (8085)
participant OS as order_service (8083)
participant DBO as db_kesles_merchant_order
PSP->>PS: POST /psp/v1/events (HMAC)
PS->>PS: Verify HMAC + detect order context
PS->>PS: INSERT payment.transactions
PS->>OS: POST /internal/orders/sales-orders/{id}/mark-paid (X-Internal-API-Key)
OS->>DBO: INSERT orders.sales_order_payments
OS->>DBO: UPDATE sales_orders.status = payment_received
Internal Endpoints
payment_service internal endpoints
| Method | Path | Auth | Deskripsi |
|---|---|---|---|
POST | /psp/v1/events | HMAC-SHA256 | Webhook inbound dari PSP |
GET | /internal/transactions | X-Internal-API-Key | List transaksi — dipakai dashboard_api |
GET | /internal/qris-config | X-Internal-API-Key | QRIS config singleton — dipakai dashboard_api |
order_service internal endpoint
| Method | Path | Auth | Deskripsi |
|---|---|---|---|
POST | /internal/orders/sales-orders/{id}/mark-paid | X-Internal-API-Key | Notifikasi dari payment_service untuk Flow B |
Notification Type Contract
For an incoming transaction, the payload type must remain:
type = transaction_info
Minimum contract:
typetitlebodyscreenreference_idmerchant_id
Default routing:
- screen:
transactions
Mobile Sound Rules
Current rules:
transaction_info→ transaction channel → audible- anything else → general channel → silent
The current implementation separates:
- Android transaction channel
- Android general silent channel
- APNs sound only for
transaction_info
Important Android note:
- an existing channel cannot be reconfigured after creation
- if a device created the channel with the old config, the app needs:
- a full restart
- or uninstall + reinstall
Notification Audio Test Checklist
Run the following in order:
- deploy the latest backend
- reinstall or fully restart the merchant mobile app
- log in as the merchant owner
- ensure the FCM token is registered
- send a
transaction_infotest push - verify:
- notification appears
- notification is audible
- tapping the notification opens the transaction page
- send a
newstest push - verify:
- notification appears
- notification is not audible
If transaction_info is still silent:
- check the Android system notification settings
- check the
Kesles Transactionschannel is not muted - check the device-vendor battery/background restriction policy
- consider a custom transaction sound file
Risks And Technical Notes
Main risks:
- duplicate push if the successful transaction is processed more than once
- race condition if the PSP sends the callback multiple times
- a terminal sending a transaction without a valid
merchant_id - old FCM tokens still active on some owner devices
Mitigations:
- use
transaction_codeas the idempotency key - push only when the final status changes to
success - idempotency middleware Redis-backed di
payment_service(2026-05-26) - log:
merchant_idtransaction_codeuser_idpush_tokensent_count
Open Follow-ups
- FCM event ke mobile untuk Flow B — saat ini tidak ada push ke merchant saat order mark-paid via Flow B; merchant hanya tahu via pull-to-refresh halaman pesanan.
- Custom transaction sound — custom file
.wav/.mp3untuk channel transaksi Android jika default sound tidak memadai di device tertentu. - Retention log outbound —
data_processor cleanup_outbound_requestsberjalan nightly; pastikan log payment webhook tidak terpotong sebelum debug window cukup.