Skip to main content

Story 4: Double-Entry Finance Engine

Overview​

FieldValue
Story IDNGE-14-4
Story Points21
SprintSprint 12-13
LanguageGo

Why Double-Entry Accounting?​

  • Audit Trail - Every transaction is traceable
  • Balance Verification - Debits always equal credits
  • Financial Reports - Trial balance, P&L, balance sheet
  • Compliance - Standard accounting practices

Core Principle​

Every financial transaction creates TWO entries:

  • One Debit entry
  • One Credit entry
  • They must always balance

Technical Implementation​

// finance_engine.go
func (e *FinanceEngine) CreateTransaction(ctx context.Context, txn *Transaction) error {
// Validate balance
totalDebit := decimal.Zero
totalCredit := decimal.Zero
for _, entry := range txn.Entries {
totalDebit = totalDebit.Add(entry.Debit)
totalCredit = totalCredit.Add(entry.Credit)
}

if !totalDebit.Equal(totalCredit) {
return fmt.Errorf("unbalanced: debit=%s, credit=%s", totalDebit, totalCredit)
}

// Insert with running balance
return e.db.WithTx(ctx, func(tx pgx.Tx) error {
// Insert transaction header
// Insert ledger entries with balance_after
return nil
})
}

// Invoice: Debit Receivable, Credit Income
func (e *FinanceEngine) RecordInvoice(ctx context.Context, invoice Invoice) error {
return e.CreateTransaction(ctx, &Transaction{
Entries: []LedgerEntry{
{AccountID: "1310", Debit: invoice.Amount}, // Receivable
{AccountID: "4100", Credit: invoice.Amount}, // Income
},
})
}

// Payment: Debit Cash/Bank, Credit Receivable
func (e *FinanceEngine) RecordPayment(ctx context.Context, payment Payment) error {
return e.CreateTransaction(ctx, &Transaction{
Entries: []LedgerEntry{
{AccountID: "1100", Debit: payment.Amount}, // Cash
{AccountID: "1310", Credit: payment.Amount}, // Receivable
},
})
}

Chart of Accounts​

CodeNameType
1000AssetsASSET
1100CashASSET
1200BankASSET
1310Student ReceivableASSET
4100Tuition IncomeREVENUE
4200Transport IncomeREVENUE
4400Late Fee IncomeREVENUE