Account States & Rules
This page documents the SIL account state machine and the invariants every SIL command must respect. Understanding this section is mandatory before calling any of the financial commands.
Account states
Defined by SITAccountStatusEnum:
| State | Value | Meaning |
|---|---|---|
Active | 1 | Account is open and may be debited or credited |
Dormant | 2 | Account is inactive due to non-use; must be reactivated before posting |
PostNoDebit (PND) | 3 | Credits allowed, debits blocked |
PostNoCredit (PNC) | 4 | Debits allowed, credits blocked |
Closed | 5 | Account is closed; no posting of any kind |
Frozen | 6 | Account is administratively blocked (regulatory hold, fraud, etc.) |
State transition diagram
Only Block / Unblock are local mutations. Every other status change is
authoritative on the core banking system; SIL just mirrors them via
SITSyncAccountCommand.
"Active" rule
An account that is not Active cannot be transacted on.
This is enforced inside SITPostingService.EnsureAccountCanPost before any
balance is touched. The exact rules are:
| Status | Deposit / Credit | Withdrawal / Debit | Transfer (source) | Transfer (counterparty) |
|---|---|---|---|---|
Active | ✅ allowed | ✅ allowed (subject to balance) | ✅ allowed | ✅ allowed |
PostNoCredit | ❌ blocked | ✅ allowed | ✅ allowed | ❌ blocked |
PostNoDebit | ✅ allowed | ❌ blocked | ❌ blocked | ✅ allowed |
Dormant | ❌ blocked | ❌ blocked | ❌ blocked | ❌ blocked |
Frozen | ❌ blocked | ❌ blocked | ❌ blocked | ❌ blocked |
Closed | ❌ blocked | ❌ blocked | ❌ blocked | ❌ blocked |
A blocked state returns 409 with a message such as "Account is frozen."
or "Account is not active (status: Dormant).". No partial state is
written when the guard rejects a request.
Counterparty rule
For SITTransferCommand, the counterparty (receiving) account is also
guarded — it must not be Closed, Frozen, Dormant or PostNoCredit. A
counterparty in any of those states aborts the transfer before any balance
movement.
Limit rule (routes to approval)
Each account carries two SIL-only limits:
| Limit | Default | Effect when breached |
|---|---|---|
SITDebitLimit | configured per account | Debits above the limit are held for approval, not rejected |
SITCreditLimit | configured per account | Credits above the limit are held for approval, not rejected |
A breach is not an error. The transaction persists with
Status = Pending and ApprovalStatus = PendingApproval, with no balance
impact and no replay queued to core. A supervisor must call
SITApproveTransactionCommand (which applies the balance and queues
replay) or SITRejectTransactionCommand. See
Approval Workflow.
Insufficient funds
For debits and the source leg of transfers, AvailableBalance must be
greater than or equal to the requested amount. Insufficient funds returns
409 and is not routed to approval — the customer simply does not have
the money in the SIL cache.
Idempotency rule
Every financial command requires a unique transactionReference. The
reference is hashed against the request body and stored in
SITIdempotencyKey. Replays of the same reference with the same body
return the cached response. Replays with a different body for the same
reference are rejected as a programming error.
Uniqueness rules
| Constraint | Index | Effect |
|---|---|---|
| One account number per tenant | IX_SITAccount_Tenant_AccountNumber (unique) | A single account number cannot exist twice in SIL for the same tenant |
| One core customer id per provider per tenant | IX_SITCustomer_Tenant_Provider_Core (unique, filtered) | A core banking customer id cannot be duplicated within the same (tenant, CoreBankingType) pair. The filter WHERE CoreCustomerId IS NOT NULL allows multiple offline-onboarded customers. |
Block / Unblock semantics
SITBlockAccountCommand flips Active → Frozen.
SITUnblockAccountCommand flips Frozen → Active.
Both write a row to SITAccountStateChange with
Status = Pending. The hosted worker drains that table separately from the
financial queue and replays the state change to core when connectivity is
restored.