Skip to content

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

TypeWhat It Represents
DISBURSEMENTPayment out of escrow — from an approved DR
SCHEDULED PAYMENTPayment out of escrow — from an SPC calendar item
DEPOSITIncoming funds (wire, e-check, credit card)
CREDITManual account credit by an Admin
DEBITManual account debit by an Admin
MANAGEMENT FEEAdministrative fee charged to the case
E-CHECK FUNDINGIncoming ACH debit from an IP’s bank account

Code: LedgerTransaction.type in seedtrust_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
StatusMeaning
CREATEDRecord exists but not yet active
PENDINGCommitted — funds are reserved but not yet sent
PROCESSINGBeing processed by the payment provider
WAITING-ON-ACHQueued for the next ACH batch run
CC PROCESSINGCredit card payment in progress
PAIDSuccessfully completed — funds moved
PAID|CREDITPaid and credited back to escrow
RETURNPayment reversed by the receiving bank
DELETEDVoided — 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_balance for the current balance. Use post_transaction_balance for 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 = DELETED only for entries that were created in error and have not yet affected the balance
  • If a payment was sent incorrectly, create a new CREDIT or DEBIT entry 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 cases
  • HUNTINGTON — via the Huntington API
  • EXTERNAL — to an outside bank instrument

Code: LedgerTransfer in transactions.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_data and new_data as 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_balance is 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.