Skip to main content

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:

StateValueMeaning
Active1Account is open and may be debited or credited
Dormant2Account is inactive due to non-use; must be reactivated before posting
PostNoDebit (PND)3Credits allowed, debits blocked
PostNoCredit (PNC)4Debits allowed, credits blocked
Closed5Account is closed; no posting of any kind
Frozen6Account is administratively blocked (regulatory hold, fraud, etc.)

State transition diagram

tip

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:

StatusDeposit / CreditWithdrawal / DebitTransfer (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:

LimitDefaultEffect when breached
SITDebitLimitconfigured per accountDebits above the limit are held for approval, not rejected
SITCreditLimitconfigured per accountCredits 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

ConstraintIndexEffect
One account number per tenantIX_SITAccount_Tenant_AccountNumber (unique)A single account number cannot exist twice in SIL for the same tenant
One core customer id per provider per tenantIX_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.