Blockchain balance nie jest product balance
Wallet może mieć 10 000 USDC i nadal mówić bardzo mało o produkcie. Nie wie, czy środki należą do dziesięciu klientów, jednej faktury, treasury transfer, refund return czy operacyjnego top-up.
Wallet balance odpowiada „gdzie są aktywa?”. Internal ledger odpowiada „dlaczego produkt rozpoznaje tę wartość?”.
Po co jest internal ledger
Ledger tworzy trwałą biznesową interpretację external payment evidence. Provider order lub blockchain transaction dowodzi, że coś wydarzyło się poza bazą danych. Ledger zapisuje, co to zdarzenie oznacza wewnątrz produktu.
Przykładowe ledger events:
PAYMENT_ACCEPTEDCUSTOMER_CREDITEDPAYMENT_FEE_RECORDEDREFUND_INITIATEDREFUND_COMPLETEDMANUAL_ADJUSTMENTTREASURY_TRANSFER_RECORDED
Nie modeluj ledgera jako jednego mutowalnego balance
Operacja user.balance += amount traci reason, evidence i historię. Preferuj append-only entries i wyliczaj saldo z nich albo z projection, którą można odtworzyć.
Rekomendowane core entities
PaymentIntent
Oczekiwana płatność biznesowa: customer, invoice/order, amount, currency, expiry i allowed rail.
ExternalPayment
Evidence object: provider order ID lub blockchain tx hash, network, asset, amount, timestamps i external state.
LedgerEntry
Immutable business value event z debit/credit direction, amount, unit, reason code, referencją do PaymentIntent i ExternalPayment, idempotency key i timestampem.
BalanceProjection
Derived view dla wydajności. Projection można odbudować z ledger entries; nie powinno być jedynym dowodem.
SettlementRecord
Osobny obiekt reprezentujący provider settlement, withdrawal lub treasury movement. Customer payment może być valid zanim settlement się zakończy.
Idempotency należy do granicy ledgera
Duplicate callbacks i powtórne chain observations są normalne. Ten sam external event nie może wygenerować efektu ekonomicznego dwa razy. Zapisuj unique key, np. provider:event:id lub chain:txHash:logIndex:businessAction, i wymuszaj uniqueness transakcyjnie.
Credit i settlement to różne fakty
W managed flow provider może potwierdzić customer payment i produkt może credited order. EUR settlement może wydarzyć się później. Opóźnienie settlementu nie unieważnia płatności klienta. Rozdziel paymentAcceptanceState od settlementState.
Tak samo przy custom wallets: valid deposit może credited customer, a późniejszy treasury sweep może się nie udać. Sweep jest operacyjnym movementem, nie powodem do cofnięcia payment.
Refundy jako compensating entries
Refund nie usuwa pierwotnego credit. Twórz nowy refund lifecycle i nowe ledger entries. Dzięki temu zachowujesz pełne wyjaśnienie gross payment, fee, partial refunds i net retained value.
Multi-currency i jednostki stablecoin
Zdecyduj, w jakiej jednostce działa dany ledger. Dla B2B invoicing można utrzymać commercial ledger w EUR, a USDC payment evidence osobno. W native stablecoin produkcie ledger może być denominowany w USDC. Nie mieszaj jednostek w jednym balance bez jawnych conversion entries i rate evidence.
Proponowany ledger entry
| Pole | Cel |
|---|---|
| entryId | Immutable identifier |
| accountId | Customer, merchant, clearing lub fee account |
| direction | Debit / credit |
| amount + unit | Wartość w jednostce ledgera |
| reasonCode | PAYMENT, REFUND, FEE, ADJUSTMENT… |
| paymentIntentId | Kontekst biznesowy |
| externalReference | Provider order/event lub chain tx |
| idempotencyKey | Blokuje duplicate economic effect |
| createdAt | Audit ordering |
Reconciliation dowodzi poprawności ledgera
Internal ledger bez external reconciliation również może się rozjechać. Scheduled jobs powinny porównywać totals i entries z provider lub chain evidence. Proces opisuje Reconciliation płatności stablecoin.
Provider vs custom wallet
Przy CoinGate provider orders i ledger transactions są external evidence. Przy custom wallets external evidence stanowią blockchain transactions i chain observations. Internal ledger pozostaje koncepcyjnie taki sam.
Czego ledger nie powinien robić
- Nie przechowuje private keys.
- Nie wylicza customer credit bezpośrednio z wallet balance.
- Nie nadpisuje historii, żeby „naprawić” błąd.
- Nie skleja payment, treasury i settlement w jeden status.
- Nie udaje statutory accounting.
Ledger invariants
Produkcyjny ledger powinien wymuszać invariants w kodzie i — gdzie to możliwe — w bazie danych. Przykłady: external event może wygenerować dany economic effect tylko raz; posted entries są immutable; debits i credits używają znanej jednostki; adjustments referują wpis lub event, który korygują; payment nie może zostać credited przed spełnieniem acceptance policy.
Account structure
Nawet bez pełnego accounting general ledger jawne logical accounts upraszczają model. Typowe konta to customer available balance, merchant clearing, provider clearing, fees, refunds i treasury. Przesuwanie wartości między logicznymi kontami jest bardziej audytowalne niż update kilku niezależnych balance fields.
Concurrency i transactions
Payments są systemem współbieżnym. Dwa callbacki mogą przyjść jednocześnie albo callback może ścigać się z reconciliation job. Ledger posting powinien działać w database transaction z uniqueness constraints na idempotency keys. Jeśli aktualizujesz projection, rób to w tej samej transaction albo przez replayable event pipeline.
Pytania audytowe, na które ledger powinien odpowiadać
- Dlaczego ten customer został credited?
- Która external transaction utworzyła wpis?
- Czy payment był refunded, partially refunded lub adjusted?
- Które entries składają się na current product balance?
- Czy accepted payment został reconciled z settlementem?
Podsumowanie
Internal ledger jest granicą, która pozwala używać wielu payment rails bez utraty spójności biznesowej. Zbudowany wokół jawnych value events może później obsługiwać CoinGate, direct USDC deposits, cards i bank transfers przy tym samym core business model.