Skip to main content

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_id context
  • if the transaction succeeds, the backend pushes a transaction_info to the merchant owner
  • on the merchant mobile:
    • transaction_info is audible
    • other notifications stay silent

Current Status

Status: LIVE

  • payment_service (port 8085) adalah satu-satunya ingest point untuk PSP webhook
  • payment.transactions di db_kesles_merchant_payment adalah sole source of truth transaksi (direct cutover; tidak ada dual-write ke merchant.transactions)
  • FCM push didispatch oleh payment_service via core_api (POST /internal/notifications/merchant-event); core_api yang resolve merchant → tokens lalu memanggil firebase_service
  • dashboard_api membaca recent transactions via HTTP ke payment_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_id
  • transaction_code
  • external_reference
  • payment_method = qris
  • payment_channel
  • gross_amount
  • transaction_status
  • transaction_at

Optional fields:

  • device_id
  • nmid
  • terminal_id
  • payer_reference
  • notes
  • metadata

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_id or device_id

Strongly recommended fields:

  • nmid
  • serial_number or terminal_code in metadata
  • payment_channel
  • source_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_id
  • transaction_code
  • external_reference
  • source_type
  • transaction_status
  • payment_method
  • payment_channel
  • device_id
  • nmid
  • gross_amount
  • mdr_rate
  • mdr_fee_amount
  • net_amount_after_mdr
  • transaction_at
  • settled_at
  • payer_reference
  • notes
  • metadata
  • terminal_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_id
  • terminal_code
  • terminal_type_code
  • manufacturer_name
  • product_model_name
  • device_id
  • serial_number
  • nmid
  • payment_routing_mode
  • payment_capabilities
  • status
  • last_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:

  1. the transaction has a merchant_id
  2. merchant_id maps to merchant.merchant_users
  3. the notification is sent to the merchant owner:
    • is_owner = TRUE
  4. 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

MethodPathAuthDeskripsi
POST/psp/v1/eventsHMAC-SHA256Webhook inbound dari PSP
GET/internal/transactionsX-Internal-API-KeyList transaksi — dipakai dashboard_api
GET/internal/qris-configX-Internal-API-KeyQRIS config singleton — dipakai dashboard_api

order_service internal endpoint

MethodPathAuthDeskripsi
POST/internal/orders/sales-orders/{id}/mark-paidX-Internal-API-KeyNotifikasi dari payment_service untuk Flow B

Notification Type Contract

For an incoming transaction, the payload type must remain:

  • type = transaction_info

Minimum contract:

  • type
  • title
  • body
  • screen
  • reference_id
  • merchant_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:

  1. deploy the latest backend
  2. reinstall or fully restart the merchant mobile app
  3. log in as the merchant owner
  4. ensure the FCM token is registered
  5. send a transaction_info test push
  6. verify:
    • notification appears
    • notification is audible
    • tapping the notification opens the transaction page
  7. send a news test push
  8. verify:
    • notification appears
    • notification is not audible

If transaction_info is still silent:

  • check the Android system notification settings
  • check the Kesles Transactions channel 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_code as the idempotency key
  • push only when the final status changes to success
  • idempotency middleware Redis-backed di payment_service (2026-05-26)
  • log:
    • merchant_id
    • transaction_code
    • user_id
    • push_token
    • sent_count

Open Follow-ups

  1. 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.
  2. Custom transaction sound — custom file .wav/.mp3 untuk channel transaksi Android jika default sound tidak memadai di device tertentu.
  3. Retention log outbounddata_processor cleanup_outbound_requests berjalan nightly; pastikan log payment webhook tidak terpotong sebelum debug window cukup.