Documentation Index Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
Payment Gateway Integration
Dokumentasi lengkap integrasi payment gateway di MStore Backend dengan support multi-provider (Xendit & Midtrans).
π― Overview
Sistem payment gateway MStore mendukung:
β
Multi-Provider : Xendit (primary), Midtrans (secondary)
β
Multi-Channel : QRIS, E-Wallet, Virtual Account, Credit Card
β
Webhook Integration : Real-time payment notification
β
Offline-First : Sync payment saat online kembali
β
Idempotent : Retry-safe operations
β
Audit Trail : Full payment lifecycle tracking
ποΈ Architecture
π³ Supported Payment Methods
Xendit (Primary Provider)
Method Channel Status Notes QRIS QR_CODE β
Production Dynamic QR via QR Codes API GoPay EWALLET β
Production E-Wallet Payment API OVO EWALLET β
Production E-Wallet Payment API DANA EWALLET β
Production E-Wallet Payment API ShopeePay EWALLET β
Production E-Wallet Payment API VA BCA VIRTUAL_ACCOUNT β
Production Virtual Account API VA BNI VIRTUAL_ACCOUNT β
Production Virtual Account API VA BRI VIRTUAL_ACCOUNT β
Production Virtual Account API VA Mandiri VIRTUAL_ACCOUNT β
Production Virtual Account API Credit Card CARDS β
Production Cards API
Midtrans (Secondary Provider)
Method Channel Status Notes QRIS QR_CODE π§ Development Snap API GoPay EWALLET π§ Development Snap API Credit Card CARDS π§ Development Snap API
π‘ Payment API Endpoints
1. Create Payment Request
Membuat payment request untuk transaksi.
cURL - QRIS
cURL - E-Wallet
JavaScript
curl -X POST https://api.mushola-store.com/api/v1/pos/payments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"order_code": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
"method": "qris",
"amount": 33000,
"currency": "IDR",
"customer_email": "[email protected] ",
"success_url": "https://pos.mushola-store.com/payment/success",
"failure_url": "https://pos.mushola-store.com/payment/failed"
}'
Response (QRIS):
{
"code" : 200 ,
"data" : {
"qr_url" : "https://api.xendit.co/qr_codes/qr_abc123xyz.png" ,
"qr_string" : "00020101021126..." ,
"reference_id" : "qr_abc123xyz" ,
"transaction_id" : "qr_abc123xyz" ,
"external_id" : "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7" ,
"status" : "pending" ,
"order_code" : "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7" ,
"method" : "qris" ,
"amount" : 33000 ,
"currency" : "IDR" ,
"channel_code" : "QR_CODE" ,
"expires_at" : "2025-10-13T10:35:00Z" ,
"extra" : {
"provider" : "xendit" ,
"qr_string" : "00020101021126..."
}
}
}
Response (E-Wallet):
{
"code" : 200 ,
"data" : {
"checkout_url" : "https://ewallet.gopay.com/checkout/abc123" ,
"reference_id" : "ewc_abc123xyz" ,
"transaction_id" : "ewc_abc123xyz" ,
"external_id" : "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7" ,
"status" : "pending" ,
"order_code" : "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7" ,
"method" : "gopay" ,
"amount" : 33000 ,
"currency" : "IDR" ,
"channel_code" : "EWALLET" ,
"expires_at" : "2025-10-13T10:35:00Z"
}
}
2. Get Payment Status
Mengecek status payment request.
curl -X GET https://api.mushola-store.com/api/v1/pos/payments/qr_abc123xyz \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"code" : 200 ,
"data" : {
"reference_id" : "qr_abc123xyz" ,
"status" : "paid" ,
"amount" : 33000 ,
"currency" : "IDR" ,
"paid_at" : "2025-10-13T10:32:15Z" ,
"payment_method" : "QRIS" ,
"channel_code" : "QR_CODE"
}
}
3. Webhook Handler
Endpoint untuk menerima notifikasi dari payment provider.
curl -X POST https://api.mushola-store.com/api/v1/webhook/xendit \
-H "Content-Type: application/json" \
-H "X-Callback-Token: YOUR_WEBHOOK_TOKEN" \
-d '{
"id": "qr_abc123xyz",
"external_id": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
"status": "PAID",
"amount": 33000,
"paid_at": "2025-10-13T10:32:15Z"
}'
Response:
{
"code" : 200 ,
"message" : "Webhook processed successfully"
}
4. Simulate Payment (Testing)
Endpoint untuk simulasi pembayaran di environment testing.
curl -X POST https://api.mushola-store.com/api/v1/pos/payments/qr_abc123xyz/simulate \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"amount": 33000
}'
Response:
{
"code" : 200 ,
"data" : {
"status" : "SUCCEEDED" ,
"message" : "Payment simulated successfully"
}
}
π§ Implementation Details
Xendit Integration
1. QR Codes API (QRIS)
// xendit_service.go
func ( c * XenditClient ) CreateQRCode ( ctx context . Context , req dto . CreatePaymentRequest ) ( * dto . CreatePaymentResponse , error ) {
payload := xenditQRReq {
ReferenceID : req . OrderCode ,
Type : "DYNAMIC" ,
Currency : "IDR" ,
Amount : req . Amount ,
Metadata : map [ string ] any { "external_order_code" : req . OrderCode },
}
url := c . BaseURL + "/qr_codes"
var qr xenditQRResp
resp , err := c . Resty . R ().
SetContext ( ctx ).
SetBasicAuth ( c . SecretKey , "" ).
SetHeader ( "Content-Type" , "application/json" ).
SetBody ( payload ).
SetResult ( & qr ).
Post ( url )
if err != nil {
return nil , err
}
return & dto . CreatePaymentResponse {
QRURL : qr . ImageURL ,
ReferenceID : qr . ID ,
Status : "pending" ,
Extra : map [ string ] any {
"provider" : "xendit" ,
"qr_string" : qr . QrString ,
},
}, nil
}
2. Payment Request API (E-Wallet, VA, Cards)
func ( c * XenditClient ) CreatePaymentRequest ( ctx context . Context , req dto . CreatePaymentRequest ) ( * dto . CreatePaymentResponse , error ) {
// Map method -> channel_code
channelCode := ""
switch req . Method {
case "qris" :
channelCode = "QRIS"
case "gopay" , "ovo" , "dana" , "shopeepay" :
channelCode = "EWALLET"
case "va_bca" , "va_bni" , "va_bri" , "va_mandiri" :
channelCode = "VIRTUAL_ACCOUNT"
case "credit_card" :
channelCode = "CARDS"
}
payload := xenditPaymentReq {
ReferenceID : req . OrderCode ,
Amount : req . Amount ,
Currency : "IDR" ,
ChannelCode : channelCode ,
ChannelProperties : buildChannelProperties ( req ),
Metadata : map [ string ] any { "order_code" : req . OrderCode },
}
url := c . BaseURL + "/payment_requests"
var pr xenditPaymentResp
resp , err := c . Resty . R ().
SetContext ( ctx ).
SetBasicAuth ( c . SecretKey , "" ).
SetHeader ( "Content-Type" : "application/json" ).
SetBody ( payload ).
SetResult ( & pr ).
Post ( url )
return mapToPaymentResponse ( pr ), err
}
3. Webhook Handler
func ( s * service ) HandleNotification ( ctx context . Context , payload map [ string ] any ) error {
invoiceID := payload [ "id" ].( string )
externalID := payload [ "external_id" ].( string )
status := payload [ "status" ].( string )
// Map status Xendit -> internal
statusMapped := mapXenditStatus ( status )
// Update payment record
merchID , branchID , txID , _ := s . trxService . ResolveByTransactionCode ( ctx , externalID )
_ = s . payRepo . UpsertFromWebhook ( ctx , invoiceID , externalID , statusMapped , payload , merchID , branchID , txID )
// Update transaction status via state machine
if txID != nil && statusMapped == "paid" {
verifyPayload := transaction . PayloadVerifyPayment {
PaymentGatewayRef : invoiceID ,
}
_ , err := s . trxService . VerifyPayment ( ctx , int64 ( * txID ), verifyPayload )
if err != nil {
// Log error tapi jangan fail webhook
log . Printf ( "[webhook] failed to verify payment: %v " , err )
}
}
return nil
}
Payment Model (Database)
// payment_model.go
type Payment struct {
ID uint `gorm:"primaryKey"`
MerchantID uint `gorm:"not null"`
BranchID * uint
TransactionID * uint
PaymentCode * string
ReferenceID * string
IdempotencyKey * string
Provider string `gorm:"not null"` // xendit, midtrans
Channel * string // QR_CODE, EWALLET, VIRTUAL_ACCOUNT, CARDS
Method * string // qris, gopay, ovo, dana, va_bca, etc
Amount float64 `gorm:"type:decimal(18,2)"`
Currency string `gorm:"not null"`
Status string `gorm:"not null"` // pending, success, failed, expired, canceled
ExternalPaymentID * string
ExternalInvoiceID * string
QRString * string
PaidAt * time . Time
ExpiresAt * time . Time
LastErrorCode * string
LastErrorMessage * string
WebhookStatus * string
WebhookReceivedAt * time . Time
IsOffline bool
DeviceID * string
OfflineReference * string
SyncStatus string // pending, synced, failed
SyncAttempts * int
LastSyncAt * time . Time
RequestPayload datatypes . JSON
ResponsePayload datatypes . JSON
CallbackPayload datatypes . JSON
CreatedAt * time . Time
UpdatedAt * time . Time
DeletedAt gorm . DeletedAt
}
π Security
1. Webhook Verification
// Verify Xendit webhook signature
func verifyXenditWebhook ( payload [] byte , signature string , token string ) bool {
mac := hmac . New ( sha256 . New , [] byte ( token ))
mac . Write ( payload )
expectedSignature := hex . EncodeToString ( mac . Sum ( nil ))
return hmac . Equal ([] byte ( signature ), [] byte ( expectedSignature ))
}
2. API Key Management
# Environment variables
XENDIT_SECRET_KEY = xnd_development_...
XENDIT_WEBHOOK_TOKEN = your_webhook_verification_token
MIDTRANS_SERVER_KEY = SB-Mid-server-...
MIDTRANS_CLIENT_KEY = SB-Mid-client-...
3. Idempotency
// Generate idempotency key
idempotencyKey := fmt . Sprintf ( " %s - %s - %d " ,
merchantID ,
transactionCode ,
time . Now (). Unix ())
π§ͺ Testing
Unit Test Example
func TestCreateQRCode ( t * testing . T ) {
client := NewXenditClient ( "test_key" , "https://api.xendit.co" )
req := dto . CreatePaymentRequest {
OrderCode : "TRX-TEST-001" ,
Method : "qris" ,
Amount : 50000 ,
Currency : "IDR" ,
}
resp , err := client . CreateQRCode ( context . Background (), req )
assert . NoError ( t , err )
assert . NotEmpty ( t , resp . QRURL )
assert . Equal ( t , "pending" , resp . Status )
}
Integration Test with Godog
Feature : Payment Gateway Integration
Scenario : Create QRIS payment
Given I have a transaction "TRX-TEST-001" with amount 50000
When I create a QRIS payment request
Then I should receive a QR code URL
And payment status should be "pending"
Scenario : Handle payment webhook
Given I have a pending payment "qr_abc123"
When Xendit sends a webhook with status "PAID"
Then payment status should be updated to "success"
And transaction status should be "paid"
π Monitoring
Payment Metrics
// Metrics to track
type PaymentMetrics struct {
TotalRequests int64
SuccessCount int64
FailedCount int64
PendingCount int64
AverageResponseTime time . Duration
WebhookReceived int64
WebhookFailed int64
}
Grafana Dashboard
Payment Success Rate : success_count / total_requests * 100
Payment Latency : P50, P95, P99 response time
Webhook Delivery : Success vs Failed
Provider Availability : Uptime per provider
π‘ Best Practices
Simpan full request/response payload untuk audit
Gunakan idempotency key untuk retry safety
Verify webhook signature sebelum process
Handle webhook idempotent (cek duplicate)
Set proper timeout untuk external API calls
Monitor payment success rate per provider
Implement circuit breaker untuk provider failures
DONβT β
Jangan hardcode API keys di source code
Jangan skip webhook verification
Jangan block webhook response (process async)
Jangan expose payment details di client-side
Jangan retry webhook infinitely
Jangan ignore payment expiration
π Troubleshooting
Problem: Payment Stuck in Pending
Symptoms : Status tidak update setelah customer bayar
Solution :
Check webhook logs di Xendit dashboard
Verify webhook URL accessible dari internet
Manual check payment status via API
Re-send webhook dari Xendit dashboard
Problem: QR Code Not Generated
Symptoms : Error saat create QR code
Solution :
Verify Xendit API key valid
Check amount minimum (Rp 1.500)
Verify currency = βIDRβ
Check Xendit API status
Problem: Webhook Signature Invalid
Symptoms : Webhook rejected dengan error signature
Solution :
Verify webhook token di environment
Check payload format (raw body)
Verify HMAC calculation
Check Xendit webhook settings
Transaction Flow Alur transaksi lengkap dengan state machine dan offline-first
Xendit API Reference Official Xendit documentation
Midtrans API Reference Official Midtrans documentation