Ledger & Transaction History
The ledger is the financial source of truth for every case. Every dollar that enters or leaves an escrow account is recorded here. It is append-only by convention — entries are never modified or deleted, only added.
Transaction Types
| Type | What It Represents |
|---|---|
DISBURSEMENT | Payment out of escrow — from an approved DR |
SCHEDULED PAYMENT | Payment out of escrow — from an SPC calendar item |
DEPOSIT | Incoming funds (wire, e-check, credit card) |
CREDIT | Manual account credit by an Admin |
DEBIT | Manual account debit by an Admin |
MANAGEMENT FEE | Administrative fee charged to the case |
E-CHECK FUNDING | Incoming ACH debit from an IP’s bank account |
Code:
LedgerTransaction.typeinseedtrust_flask/seedtrust/models/transactions.py
Transaction Status
stateDiagram-v2 [*] --> CREATED CREATED --> PENDING PENDING --> PROCESSING PROCESSING --> PAID PROCESSING --> RETURN PENDING --> WAITING-ON-ACH WAITING-ON-ACH --> PAID PENDING --> CC_PROCESSING CC_PROCESSING --> PAID PAID --> [*] RETURN --> [*] CREATED --> DELETED PENDING --> DELETED| Status | Meaning |
|---|---|
CREATED | Record exists but not yet active |
PENDING | Committed — funds are reserved but not yet sent |
PROCESSING | Being processed by the payment provider |
WAITING-ON-ACH | Queued for the next ACH batch run |
CC PROCESSING | Credit card payment in progress |
PAID | Successfully completed — funds moved |
PAID|CREDIT | Paid and credited back to escrow |
RETURN | Payment reversed by the receiving bank |
DELETED | Voided — should not have been created |
How Account Balance Works
The case account balance (Case.acct_balance) is a running total maintained in real time — not computed on demand from the ledger. It is updated whenever a transaction reaches PAID status:
- Credit transaction (deposit, refund):
acct_balance += amount - Debit transaction (disbursement, fee):
acct_balance -= amount
The post_transaction_balance field on each LedgerTransaction captures a point-in-time snapshot of the balance immediately after that transaction was applied. This is what powers the balance history view and audit trail.
Never compute a balance by summing transactions yourself. Use
Case.acct_balancefor the current balance. Usepost_transaction_balancefor historical balance at a given point.
The Append-Only Rule
The ledger is append-only by convention, not by database constraint. This means:
- Never update a ledger entry’s amount or type after it has been created
- Never delete a ledger entry to reverse a transaction — create a correcting entry instead
- Use
status = DELETEDonly for entries that were created in error and have not yet affected the balance - If a payment was sent incorrectly, create a new
CREDITorDEBITentry to offset it
Violating this rule creates balance discrepancies and breaks the audit trail.
Ledger Transfer (Between Cases)
A LedgerTransfer moves funds between two cases within SeedTrust. It creates two linked transactions:
- A DEBIT on the source case (
from_case_id) - A CREDIT on the destination case (
to_case_id)
Both transactions share a transfer_id foreign key so they can always be traced as a pair.
Transfer types:
INTERNAL— between two SeedTrust casesHUNTINGTON— via the Huntington APIEXTERNAL— to an outside bank instrument
Code:
LedgerTransferintransactions.py
Deposit Notifications
When an incoming wire transfer arrives, an Admin creates a DepositNotification record before the actual LedgerTransaction. This allows the team to track expected vs. received deposits and notify parties.
Key fields: amount, sender_name, sender_type, method, action_date, memo_details, internal_note, client_note.
Audit Trail
Every status change on a LedgerTransaction is logged automatically in LedgerTransactionActionLog via SQLAlchemy event listeners. The log captures:
- Who performed the action (
admin_id) - What the status changed from/to
- The full
old_dataandnew_dataas JSON - How long the transaction spent in the previous status
Do not write audit log entries manually — the event listeners handle this.
Key Relationships
LedgerTransaction├── case_id → Case├── compensation_id → Compensation (SPC items only)├── dr_batch_id → DisbursementRequest batch├── nacha_batch_id → NachaBatch (NACHA path)├── huntington_batch_id → HuntingtonBatch (Huntington path)├── transfer_id → LedgerTransfer (inter-case transfers)├── vendor_id → Vendor (vendor payments)└── admin_id → Admin (who created it)Gotchas for Developers
post_transaction_balance is set at payment time, not creation time. If you read it on a PENDING transaction, it is NULL. Only trust it once the transaction is PAID.
debit_credit and type are separate concerns. A DISBURSEMENT is always a DEBIT. But a DEPOSIT is a CREDIT. Always check debit_credit when computing balance impact — do not infer it from type.
The RETURN status is set by the bank, not by SeedTrust. When M&T or Huntington returns a payment, the status is updated via the reconciliation process. A RETURN means money that was sent has come back — the acct_balance must be adjusted accordingly.
Open Questions
1. PAID|CREDIT status
When exactly is PAID|CREDIT used vs. plain PAID? The distinction between a standard paid transaction and a credit-paid transaction is not clear from the code alone.
Balance recomputation:
Case.acct_balanceis recalculated by a function that runs on nearly every case interaction — it does not rely solely on the incremental update at payment time. If the balance becomes out of sync, triggering any case-touching action will recompute it. For a forced recalculation in an incident, check the function called on case save/update in the Flask models.