Skip to main content

Transaction Flow

Dokumentasi lengkap alur transaksi di MStore Backend, mencakup state machine, offline-first capability, inventory movement, dan journal accounting.

🎯 Overview

Sistem transaksi MStore dirancang dengan prinsip:
  • βœ… State Machine untuk lifecycle management yang jelas
  • βœ… Offline-First untuk operasional tanpa internet (CASH only)
  • βœ… Multi-Payment support (CASH, QRIS, E-Wallet, VA, Credit Card)
  • βœ… Inventory Integration dengan automatic stock movement
  • βœ… Journal Accounting untuk setiap transaksi
  • βœ… Idempotent untuk retry safety

πŸ—οΈ Transaction State Machine

State Definitions

StateDescriptionAllowed Actions
draftTransaksi baru dibuat, belum submit paymentEdit, Delete, Submit Payment
pending_paymentMenunggu konfirmasi pembayaranVerify Payment, Cancel
paidPembayaran berhasil, belum sync accountingSync, Void
syncedSudah sync ke accounting (journal posted)Void (with reversal)
failedPembayaran gagal-
expiredPembayaran expired-
voidedTransaksi dibatalkan-

πŸ“‘ Transaction API Endpoints

1. Create Transaction (Draft)

Membuat transaksi baru dalam status draft.
curl -X POST https://api.mushola-store.com/api/v1/transactions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "branch_code": "BRN-MST-OSK00001",
    "cashier_id": 5,
    "items": [
      {
        "product_id": 1238,
        "quantity": 2,
        "unit_price": 15000,
        "discount": 0
      }
    ],
    "payment_method": "CASH",
    "customer_id": null,
    "notes": "Transaksi kasir"
  }'
Response:
{
  "code": 201,
  "data": {
    "id": 185,
    "transaction_code": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
    "status": "draft",
    "subtotal": 30000,
    "discount_total": 0,
    "tax_total": 3000,
    "grand_total": 33000,
    "payment_method": "CASH",
    "created_at": "2025-10-13T10:30:00Z"
  }
}

2. Submit Payment

Mengubah status dari draft β†’ pending_payment dan membuat payment request.
curl -X POST https://api.mushola-store.com/api/v1/transactions/185/submit-payment \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "payment_method": "QRIS",
    "amount": 33000
  }'
Response (QRIS):
{
  "code": 200,
  "data": {
    "transaction_id": 185,
    "transaction_code": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
    "status": "pending_payment",
    "payment": {
      "qr_url": "https://xendit.co/qr/abc123.png",
      "qr_string": "00020101021126...",
      "reference_id": "qr_abc123xyz",
      "expires_at": "2025-10-13T10:35:00Z"
    }
  }
}
Response (CASH):
{
  "code": 200,
  "data": {
    "transaction_id": 185,
    "transaction_code": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
    "status": "paid",
    "message": "CASH payment auto-verified"
  }
}

3. Verify Payment

Verifikasi pembayaran dari payment gateway (webhook atau manual check).
curl -X POST https://api/v1/transactions/185/verify-payment \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "payment_gateway_ref": "qr_abc123xyz"
  }'
Response:
{
  "code": 200,
  "data": {
    "transaction_id": 185,
    "status": "paid",
    "paid_at": "2025-10-13T10:32:15Z",
    "payment_method": "QRIS"
  }
}

4. Sync Transaction (Accounting)

Sync transaksi ke accounting system (post journal entries).
curl -X POST https://api/v1/transactions/185/sync \
  -H "Authorization: Bearer YOUR_TOKEN"
Response:
{
  "code": 200,
  "data": {
    "transaction_id": 185,
    "status": "synced",
    "journal_code": "JV-2025-10-000123",
    "journal_entries": [
      {
        "account_code": "1-10100",
        "account_name": "Kas",
        "debit": 33000,
        "credit": 0
      },
      {
        "account_code": "4-40100",
        "account_name": "Penjualan",
        "debit": 0,
        "credit": 30000
      },
      {
        "account_code": "2-20300",
        "account_name": "PPN Keluaran",
        "debit": 0,
        "credit": 3000
      }
    ],
    "synced_at": "2025-10-13T10:33:00Z"
  }
}

5. Void Transaction

Membatalkan transaksi (dengan reversal journal jika sudah synced).
curl -X POST https://api/v1/transactions/185/void \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "reason": "Customer request refund",
    "voided_by": 5
  }'
Response:
{
  "code": 200,
  "data": {
    "transaction_id": 185,
    "status": "voided",
    "reversal_journal_code": "JV-2025-10-000124",
    "voided_at": "2025-10-13T11:00:00Z"
  }
}

πŸ”„ Offline-First Flow

Untuk transaksi CASH, sistem mendukung offline-first: Lihat dokumentasi lengkap: Offline-First POS System Lihat dokumentasi lengkap: Offline-First Architecture (coming soon)

πŸ“¦ Inventory Integration

Setiap transaksi yang paid atau synced akan otomatis membuat inventory movement:

Inventory Movement Flow

Movement Record Example

{
  "movement_type": "SALES_OUT",
  "warehouse_id": 1,
  "inventory_id": 456,
  "quantity": -2.0,
  "unit": "PCS",
  "reference_type": "transaction",
  "reference_id": 185,
  "reference_code": "TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
  "notes": "Sales transaction",
  "created_at": "2025-10-13T10:32:15Z"
}

πŸ’° Journal Accounting Integration

Setiap transaksi yang di-sync akan membuat journal entries:

Journal Mapping Rules

Transaction TypeDebit AccountCredit Account
CASH Sales1-10100 (Kas)4-40100 (Penjualan)
QRIS Sales1-10200 (Bank)4-40100 (Penjualan)
Sales Tax1-10100/2002-20300 (PPN Keluaran)
COGS5-50100 (HPP)1-13100 (Persediaan)

Journal Entry Example

{
  "journal_code": "JV-2025-10-000123",
  "journal_date": "2025-10-13",
  "description": "Sales Transaction TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7",
  "entries": [
    {
      "account_code": "1-10100",
      "account_name": "Kas",
      "debit": 33000,
      "credit": 0
    },
    {
      "account_code": "4-40100",
      "account_name": "Penjualan",
      "debit": 0,
      "credit": 30000
    },
    {
      "account_code": "2-20300",
      "account_name": "PPN Keluaran",
      "debit": 0,
      "credit": 3000
    },
    {
      "account_code": "5-50100",
      "account_name": "Harga Pokok Penjualan",
      "debit": 20000,
      "credit": 0
    },
    {
      "account_code": "1-13100",
      "account_name": "Persediaan Barang Dagang",
      "debit": 0,
      "credit": 20000
    }
  ],
  "total_debit": 53000,
  "total_credit": 53000,
  "status": "posted"
}

πŸ§ͺ Testing Scenarios

Scenario 1: CASH Transaction (Happy Path)

test('should create and complete CASH transaction', async () => {
  // 1. Create transaction
  const tx = await createTransaction({
    branch_code: 'BRN-MST-OSK00001',
    payment_method: 'CASH',
    items: [{ product_id: 1238, quantity: 2 }]
  });
  expect(tx.status).toBe('draft');
  
  // 2. Submit payment (auto-verify for CASH)
  const paid = await submitPayment(tx.id, { payment_method: 'CASH' });
  expect(paid.status).toBe('paid');
  
  // 3. Sync to accounting
  const synced = await syncTransaction(tx.id);
  expect(synced.status).toBe('synced');
  expect(synced.journal_code).toBeDefined();
});

Scenario 2: QRIS Transaction with Webhook

test('should handle QRIS payment with webhook', async () => {
  // 1. Create transaction
  const tx = await createTransaction({
    payment_method: 'QRIS',
    items: [{ product_id: 1238, quantity: 2 }]
  });
  
  // 2. Submit payment (get QR code)
  const payment = await submitPayment(tx.id, { payment_method: 'QRIS' });
  expect(payment.status).toBe('pending_payment');
  expect(payment.payment.qr_url).toBeDefined();
  
  // 3. Simulate webhook from Xendit
  const webhook = await handleWebhook({
    id: payment.payment.reference_id,
    external_id: tx.transaction_code,
    status: 'PAID'
  });
  
  // 4. Verify transaction updated
  const updated = await getTransaction(tx.id);
  expect(updated.status).toBe('paid');
});

πŸ“Š Performance Metrics

MetricTargetNotes
Create Transaction< 200msDatabase insert
Submit Payment (CASH)< 300msAuto-verify + inventory
Submit Payment (QRIS)< 2sExternal API call
Verify Payment< 500msUpdate status + inventory
Sync Transaction< 1sJournal posting
Void Transaction< 1sReversal journal

πŸ”’ Security Considerations

1. Transaction Code Generation

// Format: TRX-{MERCHANT_CODE}-{BRANCH_CODE}-{YYMMDD}-{RANDOM_4}-{RANDOM_4}-{SEQ}
// Example: TRX-MC01-BRN-MST-OSK00001-251013-5UJW-6SWG-7

2. Idempotency

  • Gunakan idempotency_key untuk retry safety
  • Cek duplicate transaction_code sebelum insert
  • Webhook handler harus idempotent

3. Authorization

  • Cashier hanya bisa create/view transaksi di branch sendiri
  • Manager bisa void transaksi
  • Admin bisa view semua transaksi

πŸ’‘ Best Practices

DO βœ…

  • Selalu gunakan state machine untuk update status
  • Simpan snapshot payment request/response di tabel payments
  • Buat inventory movement setelah payment verified
  • Post journal entries setelah sync
  • Handle webhook idempotent
  • Log semua state transitions

DON’T ❌

  • Jangan skip state machine (direct DB update)
  • Jangan lupa create inventory movement
  • Jangan post journal sebelum payment verified
  • Jangan ignore webhook errors
  • Jangan hardcode account codes

πŸ†˜ Troubleshooting

Problem: Transaction Stuck in pending_payment

Symptoms: Status tidak update setelah payment success Solution:
# Manual verify payment
curl -X POST /api/v1/transactions/{id}/verify-payment \
  -d '{"payment_gateway_ref": "qr_abc123"}'

Problem: Journal Not Posted

Symptoms: Transaction status paid tapi belum ada journal Solution:
# Manual sync
curl -X POST /api/v1/transactions/{id}/sync

Problem: Inventory Not Reduced

Symptoms: Stock tidak berkurang setelah transaksi Solution:
  • Cek inventory_movements table
  • Verify warehouse stock calculation
  • Re-sync transaction jika perlu

Payment Gateway

Integrasi Xendit & Midtrans

Inventory Flow

Warehouse & stock management

Need Help? Contact backend team atau check GitHub Issues