█████████████ █████████
████████████ ███████████
██████████ █████████████
███████ ███████████████
█████████████████████████████████
████████████████ ████████
██████████████ ████████
███████████ ████████
█████████ ████████
Plug-and-play merchant payment layer, powered by DOKU.
Accept payments via QRIS, Virtual Account, and more — with built-in balance tracking, settlement reconciliation, and disbursement. No manual ledger wiring required.
This package is built for DOKU and DOKU only. Account creation, balance inquiry, bank account validation, withdrawals, and settlement reconciliation are all implemented against DOKU APIs and CSV formats. It is not designed to be payment-gateway-agnostic.
- Records product sales as immutable double-entry ledger entries
- Tracks seller balances across two buckets:
PENDING(captured, not yet settled) andAVAILABLE(settled, withdrawable) - Reconciles DOKU settlement CSVs — matches CSV rows to product transactions, applies fee adjustments, and moves balances from
PENDING→AVAILABLE - Handles seller withdrawals via DOKU sub-account payout
- Transfers platform fees to the platform sub-account after settlement
- No top-up / balance loading — seller balances only grow through settled product transactions. There is no API to credit a seller's balance directly.
- No payment gateway abstraction — all payment, sub-account, and disbursement operations are wired to DOKU APIs only.
ledger/
├── ledger.go # LedgerClient — all public operations
├── domain/ # Pure domain types and business rules
│ ├── account.go # Account (Seller, Platform, PaymentGateway)
│ ├── product_transaction.go # ProductTransaction + FeeBreakdown
│ ├── ledger_entry.go # LedgerEntry (immutable), factory functions
│ ├── fee_config.go # FeeConfig, FeeCalculator
│ ├── settlement_csv.go # DOKU settlement CSV parser
│ ├── settlement_batch.go
│ └── settlement_item.go
├── repo/ # Repository interfaces + PostgreSQL implementations
├── docs/ # Architecture docs and reconciliation flow diagrams
└── analytics/ # Read-side analytics queries
Ledger entries are insert-only — no row is ever updated or deleted. Balances are always derived by summing entries.
Two fee models control who bears the DOKU gateway fee:
| Model | Customer pays | Seller receives | DOKU fee borne by |
|---|---|---|---|
GATEWAY_ON_CUSTOMER |
SellerPrice + PlatformFee + DokuFee | SellerPrice (100%) | Customer |
GATEWAY_ON_SELLER |
SellerPrice + PlatformFee | SellerPrice − DokuFee | Seller |
Subscription transactions typically use GATEWAY_ON_SELLER with PlatformFee = 0.
import (
"github.com/21strive/ledger"
"github.com/21strive/doku/app/usecases"
)
dokuClient := usecases.NewDokuUseCase(...)
client := ledger.NewLedgerClient(db, dokuClient, logger, awsConfig)// Register a seller account (also provisions a DOKU sub-account)
account, err := client.CreateAccount(ctx, sellerID, email, name, domain.CurrencyIDR)
// Look up by seller ID
account, err := client.GetAccountBySellerID(ctx, sellerID)GeneratePayment creates a product payment between a buyer and a seller. It calculates fees, calls the DOKU payment API, and saves a ProductTransaction + PaymentRequest atomically.
resp, err := client.GeneratePayment(ctx, &ledger.GeneratePaymentRequest{
SellerAccountID: "seller-uuid",
BuyerAccountID: "buyer-uuid",
BuyerName: "Jane Doe",
BuyerEmail: "jane@example.com",
ProductID: "prod-123",
ProductType: "PHOTO",
SellerPrice: 100000, // in smallest currency unit (e.g. IDR cents)
Currency: "IDR",
PaymentChannel: "QRIS",
FeeModel: ledger.FeeModelGatewayOnCustomer,
Metadata: map[string]any{"title": "Sunset Photo"},
})
// resp.PaymentURL — redirect buyer here to complete payment
// resp.TotalCharged — what buyer will pay
// resp.SellerNetAmount — what seller will receive after settlementTwo convenience wrappers set the fee model explicitly:
// Customer pays all fees (seller receives 100% of SellerPrice)
resp, err := client.GeneratePaymentGatewayOnCustomer(ctx, req)
// Seller absorbs the gateway fee (customer pays SellerPrice + PlatformFee only)
resp, err := client.GeneratePaymentGatewayOnSeller(ctx, req)GenerateSubscriptionPayment creates a platform subscription payment. There is no seller — the platform receives all net proceeds. The buyer selects the payment channel via the DOKU Checkout page.
resp, err := client.GenerateSubscriptionPayment(ctx, &ledger.GenerateSubscriptionPaymentRequest{
BuyerAccountID: "buyer-uuid",
BuyerName: "Jane Doe",
BuyerEmail: "jane@example.com",
ProductID: "plan-pro",
SubscriptionPrice: 99000,
Currency: "IDR",
Metadata: map[string]any{"plan": "pro", "duration_days": 30},
})
// Fee model is always GATEWAY_ON_SELLER: buyer pays SubscriptionPrice, platform absorbs DOKU fee.After a buyer completes payment, DOKU sends a notification. Pass the raw request to HandlePaymentSuccess — it validates the notification, marks the ProductTransaction as completed, and writes the PENDING ledger entries for the seller, platform, and DOKU accounts.
err := client.HandlePaymentSuccess(ctx, dokuNotificationRequest)Preview the full fee breakdown before creating a payment:
// With explicit fee model
resp, err := client.CalculateFeesWithModel(ctx, 100000, "QRIS", "IDR", domain.FeeModelGatewayOnCustomer)
// resp.FeeBreakdown — full breakdown (SellerPrice, PlatformFee, DokuFee, TotalCharged, SellerNetAmount)
// resp.CheapestPaymentChannel — channel with the lowest DOKU fee for the same seller price
// List all supported payment channels and their fee config
configs, err := client.GetPaymentChannelFeeConfigs(ctx)Seller balances are derived entirely from ledger entries — never stored as a mutable field. There are two balance buckets:
| Bucket | When it grows | When it shrinks |
|---|---|---|
PENDING |
After HandlePaymentSuccess |
After ProcessReconciliation |
AVAILABLE |
After ProcessReconciliation |
After Withdraw |
There is no top-up. The only way to increase a seller's balance is through a completed + settled product sale.
// Read merchant balance
balance, err := client.GetAllBalancesBySellerID(ctx, sellerID)
// balance.PendingBalance — captured, awaiting settlement CSV
// balance.AvailableBalance — settled, withdrawable
// View pending and settled transactions
earnings, err := client.GetEarnings(ctx, sellerID, cursor, 20, "DESC")Settlement is triggered by uploading the DOKU settlement CSV. The reconciliation moves balances from PENDING → AVAILABLE for every matched seller.
resp, err := client.ProcessReconciliation(ctx, &ledger.ReconciliationRequest{
CSVReader: file,
ReportFileName: "settlement-20260504.csv",
UploadedBy: "admin@company.com",
SettlementDate: time.Now(),
})// Validate destination bank account first
valid, err := client.ValidateBankAccount(ctx, &ledger.ValidateBankAccountRequest{
BankCode: "BCA",
AccountNumber: "1234567890",
})
// Disburse from AVAILABLE balance to external bank
resp, err := client.Withdraw(ctx, sellerID, &ledger.WithdrawRequest{
AccountID: account.UUID,
Amount: 500000,
BankCode: "BCA",
AccountNumber: "1234567890",
AccountName: "John Doe",
})
// Paginated disbursement history
history, err := client.GetDisbursements(ctx, sellerID, cursor, 20, "DESC")// Get pre-signed S3 URLs for photo uploads (valid for 15 minutes)
ktpURL, err := client.GetPhotoKTPPresignedURL(ctx, sellerID, bucketName, "image/jpeg")
selfieURL, err := client.GetPhotoKYCSelfiePresignedURL(ctx, sellerID, bucketName, "image/jpeg")
// After buyer uploads, submit verification
verification, err := client.SubmitVerification(ctx, bucketName, ledger.SubmitVerificationRequest{
AccountUUID: account.UUID,
SellerID: sellerID,
IdentityID: "3271012345678901",
Fullname: "John Doe",
BirthDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
KTPPhotoExt: "jpeg",
SelfiePhotoExt: "jpeg",
})The reconciliation process:
- Parses the CSV (DOKU-specific format with 9 metadata rows + data rows)
- Matches each CSV row to a
ProductTransactionby invoice number - Detects fee mismatches (
ActualDokuFeefrom CSV vsExpectedDokuFeerecorded at payment time) - Applies
FEE_ADJUSTMENTentries when reconcilable; blocks when not - Writes settlement ledger entries atomically:
SETTLEMENT_CLEAR(debit PENDING) +SETTLEMENT_NET(credit AVAILABLE)
See docs/104-fee-mismatch-reconciliation.md for full fee mismatch rules.
Requires PostgreSQL. Schema is in schema.sql (not included in this package — managed by the host application).
Key tables: accounts, product_transactions, ledger_entries, journals, settlement_batches, settlement_items, fee_configs.
| Operation | DOKU API |
|---|---|
CreateAccount |
Create sub-account |
CreatePlatformAccount |
Create sub-account |
ValidateBankAccount |
Bank account inquiry + token |
Withdraw |
Send payout to sub-account |
ProcessPlatformFeeTransfer |
Transfer between sub-accounts |
GetBalance |
Get sub-account balance |
ProcessReconciliation |
Parses DOKU settlement CSV format |