Taryana
Zero-Variance SettlementFinTech & Automated Billing
TESTER READY • LIVE ENGINE

Multi-Gateway Webhook & Double-Entry Reconcile

Idempotent Webhook Processing & Fault-Tolerant Ledger Outbox

Pengolahan webhook multi-gateway (Midtrans, Xendit, Bank Direct VA) dengan transactional outbox pattern dan double-entry ledger untuk rekonsiliasi bebas selisih.

Diagram Alur Data & Arsitektur Sistem

Visual Interaktif

Arsitektur Idempotent Webhook & Zero-Variance Settlement

Distributed lock Redlock dengan transactional outbox untuk menjamin integritas ledger

client
External PG

Payment Gateway

Webhook Callback (Midtrans/Xendit)

POST /webhook
guard
Crypto SHA256

HMAC Guard

Signature Verification

Acquire lock
cache
Lock TTL 2s

Redis Redlock

Distributed Mutex (<1ms)

Commit ledger tx
db
Zero-Variance

PostgreSQL Ledger

Atomic Double-Entry Tx

Poll pending events
service
Reliable Push

Outbox Dispatcher

CDC Event to Unblock Queue

Problem & Latar Belakang Kebutuhan

Saat siklus penagihan bulanan tanggal 1–5, puluhan ribu callback webhook pembayaran masuk serentak dari berbagai gateway. Gangguan jaringan atau retry agresif dari gateway sering menyebabkan race condition, double-crediting saldo, serta selisih antara pencatatan billing internal dan mutasi bank pihak ketiga.

Implementasi Reconcile Controller NestJS & Transactional Outbox

Setiap webhook diverifikasi keabsahan signature HMAC-nya, lalu dilakukan penguncian terdistribusi menggunakan Redis Redlock berbasis hash payload dan invoice ID. Jika lock berhasil, mutasi saldo dicatat ke tabel double-entry ledger PostgreSQL dalam satu transaksi atomik bersama event di tabel transactional outbox. Background worker NestJS membaca outbox untuk memicu unblock subscriber secara deterministik.

Aspek Skema Double-Entry Ledger PostgreSQL

Tabel PostgreSQL ledger_entries mencatat mutasi berpasangan (Debit: Gateway Clearing Account, Credit: Accounts Receivable Customer). Transaksi menggunakan level isolation READ COMMITTED dengan locking SELECT FOR UPDATE pada row invoice pelanggan.

Keamanan, Idempotensi & Kepatuhan SLA

Validasi signature HMAC-SHA256 pada header webhook, distributed lock per invoice ID dengan TTL 2 detik, serta pencatatan audit log transaksi finansial yang immutable (append-only).

NestJS Webhook Handler: Redis Redlock & Transactional OutboxNestJS (TypeScript) / PostgreSQL
// Idempotent Multi-Gateway Webhook Controller & Outbox Pattern di NestJS
@Controller('billing/webhooks')
@UseGuards(HmacSignatureGuard)
export class BillingWebhookController {
  constructor(
    private readonly redlockService: RedlockService,
    @InjectDataSource() private readonly dataSource: DataSource,
  ) {}

  @Post(':gateway')
  async handleGatewayWebhook(
    @Param('gateway') gateway: string,
    @Body() payload: PaymentWebhookDto,
    @Res() res: Response,
  ) {
    const lockKey = `lock:webhook:${payload.invoiceId}`;

    // 1. Acquire Distributed Mutex via Redis Redlock (TTL 2000ms)
    let lock: Lock;
    try {
      lock = await this.redlockService.acquire([lockKey], 2000);
    } catch {
      // Gracefully absorb duplicate retry callbacks
      return res.status(HttpStatus.ACCEPTED).json({ status: 'duplicate_event_absorbed' });
    }

    try {
      // 2. Transaksi Atomik: Settle Invoice + Double-Entry Ledger + Transactional Outbox
      await this.dataSource.transaction('READ COMMITTED', async (manager) => {
        const invoice = await manager.createQueryBuilder(Invoice, 'inv')
          .setLock('pessimistic_write')
          .where('inv.id = :id', { id: payload.invoiceId })
          .getOne();

        if (invoice?.status === 'SETTLED') return;

        await manager.update(Invoice, payload.invoiceId, {
          status: 'SETTLED',
          settledAt: new Date(),
        });

        await manager.insert(LedgerEntry, {
          invoiceId: payload.invoiceId,
          debitAccount: `GATEWAY_CLEARING_${gateway.toUpperCase()}`,
          creditAccount: 'AR_CUSTOMER',
          amount: payload.amount,
        });

        await manager.insert(OutboxEvent, {
          topic: 'billing.invoice.settled',
          payload: JSON.stringify(payload),
          status: 'PENDING',
        });
      });

      return res.status(HttpStatus.OK).json({ status: 'settled' });
    } finally {
      await lock.release().catch(() => null);
    }
  }
}

Live Simulator & Execution Sandbox

Interactive State Tester
Execution Telemetry Console
HTTP 200 OK
POST/api/v1/network/coverage/check
> Guard Evaluation Trace:

PostGIS GiST R-Tree ST_DWithin query executed in 3.8ms. Matched ODP-FATMAWATI-42 (84.2m).

> Database & State Result:

Estimated optical attenuation: -17.4 dBm. Port 4 of 8 available. Redis lock acquired with 24h TTL.

Audit Trail PostgreSQL & Redis (2 events)Auto-Appended
[10:14:02]SETTLEMENT(Double-Entry Worker)•WEBHOOK_INVOICE_SETTLED

HMAC-SHA256 verified. Redis Redlock acquired. Invoice INV-2026-0982 settled with zero ledger variance.

SUCCESS
[10:22:15]POSTGIS(Coverage Engine (NestJS))•ODP_PORT_RESERVED

ST_DWithin R-Tree matched ODP in 4.1ms (dist: 84.2m). Optical loss -17.4 dBm. Splitter port locked with 24h Redis TTL.

SUCCESS