Caricash Nova Platform
Home
Home
  1. Internal
  • Default module
    • Clients
      • PBAC for Customer Authentication
      • Create Clients
    • Internal
      • Accounts, Transactioins and Ledger Implementatioin
      • Ensure real-time balance guarantees
      • Web App Scaffold
      • Database Migrations Guide
      • Microservices
      • Service Implementation
      • TO-DO
      • Authentication & Authorization
    • Customers
      • Onboarding
  • Release Schedule
    • Agency Operations
      • Agent APIs Specs
        • auth
          • POST /v1/agent/auth/login
          • POST /v1/agent/auth/logout
          • POST /v1/agent/auth/refresh
          • POST /v1/agent/auth/device-bind
          • POST /v1/agent/auth/otp/request
          • POST /v1/agent/auth/otp/verify
        • agent
          • GET /v1/agent/me
          • PATCH /v1/agent/me
          • GET /v1/agent/outlet
          • GET /v1/agent/capabilities
        • kyc
          • POST /v1/kyc/customers
          • POST /v1/kyc/customers/{customer_id}/upgrade
          • POST /v1/kyc/customers/{customer_id}/rekcy
          • GET /v1/kyc/customers/{customer_id}/status
        • transactions
          • POST /v1/txns/cashin
          • POST /v1/txns/cashout
          • POST /v1/txns/p2p/assist
          • GET /v1/txns/{txn_id}
          • POST /v1/txns/{txn_id}/reverse
        • wallets
          • GET /v1/wallets/{wallet_id}/balance
          • GET /v1/wallets/{wallet_id}/transactions
        • float
          • GET /v1/float
          • POST /v1/float/topup
          • POST /v1/float/redeem
          • GET /v1/float/instructions/{instruction_id}
        • commissions
          • GET /v1/agents/{agent_id}/commissions
          • POST /v1/agents/{agent_id}/commissions/payouts/preview
          • POST /v1/agents/{agent_id}/commissions/payouts/accept
        • disputes
          • POST /v1/disputes
          • GET /v1/disputes/{case_id}
          • POST /v1/disputes/{case_id}/attachments
        • reports
          • GET /v1/reports/eod
          • POST /v1/reports/eod/close
          • GET /v1/reports/txns
          • GET /v1/reports/float
        • content
          • GET /v1/announcements
        • training
          • GET /v1/training/courses
          • POST /v1/training/quizzes/{quiz_id}/submit
        • ussd
          • POST /v1/ussd/session
          • POST /v1/ussd/agent/menu
        • ops
          • GET /v1/health
      • Agent Scope
        • Agent Scope
    • Customer Operations
      • Customer Scope
        • Customer & Merchant Scope
    • Schemas
      • Agent Ops APIs
  • Nova Core Banking Service API
    • core
      • Create account
      • Get account
      • Get balances
      • Create posting (double-entry)
      • Reverse posting
      • Check limits
      • Generate statement
    • Schemas
      • Schemas
        • Amount
        • Account
        • BalanceSet
        • PostingEntry
        • Posting
        • Hold
        • LimitCheckResponse
        • SavingsProduct
        • OverdraftLine
        • StatementRequest
        • Error
Home
Home
  1. Internal

Ensure real-time balance guarantees

1. Column Default#

Use a SQL default expression so Postgres truly defaults to zero even if TypeORM skips it:
  @Column('numeric', {
    name: 'balance',
    precision: 20,
    scale: 6,
    default: () => "'0'",    // ← wrap in () => to emit as SQL literal
    transformer: { … },
  })
  public balance!: Decimal;

2. Eager vs. Lazy Loading#

Right now your join is lazy by default. If you’ll frequently need the balance alongside the account, you could make it eager:
  @OneToOne(() => AccountEntity, (acct) => acct.balance, {
    onDelete: 'CASCADE',
    eager: true,         // load balance automatically with account
    nullable: false,
  })

3. Repository Helper for Concurrency#

Rather than letting multiple transactions slip through, add a service/repo method that uses SELECT … FOR UPDATE:
async adjustBalance(
  accountId: string,
  delta: Decimal
): Promise<Decimal> {
  return await this.manager.transaction(async (mgr) => {
    const bal = await mgr.getRepository(AccountBalanceEntity)
      .createQueryBuilder('b')
      .setLock('pessimistic_write')
      .where('b.accountId = :id', { id: accountId })
      .getOneOrFail();

    bal.balance = bal.balance.plus(delta);
    await mgr.save(bal);
    return bal.balance;
  });
}
That guarantees no two concurrent postings can race.

4. Database Trigger for Safety#

As a last-line guard, you can add a Postgres trigger so that any insert into ledger_entries (our next table) will atomically update account_balances:

5. Audit Trail (Optional)#

If you need to track how balances changed over time, consider a account_balance_history table:
@Entity('account_balance_history')
export class AccountBalanceHistoryEntity extends BaseModel {
  @Column('uuid') accountId!: string;
  @Column('numeric', { precision: 20, scale: 6 }) oldBalance!: Decimal;
  @Column('numeric', { precision: 20, scale: 6 }) newBalance!: Decimal;
  @Column('text') reason!: string;  // e.g. 'posted ledger_entry: …'
}
And have the same trigger insert into it.

Next Up#

With AccountBalanceEntity locked down, the natural “Phase 1” step is to implement the SQL trigger above plus the pessimistic-lock helper in your repository. Once that’s in place, balances will stay accurate in real time. After that, we can move on to wiring up your AccountEntity (the one-to-one link) and then build out TransactionTypeEntity and the rest of the stack.
Modified at 2025-07-14 12:54:01
Previous
Accounts, Transactioins and Ledger Implementatioin
Next
Web App Scaffold
Built with