From 4eb989d12dbdbcdcd85dc2ed90af0e17ae0124c1 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 23 Jun 2026 13:53:37 +0100 Subject: [PATCH 1/2] feat: add multi-currency ledger support with currency metadata and conversion --- src/services/ledgerService.ts | 113 +++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/src/services/ledgerService.ts b/src/services/ledgerService.ts index 1093f5bf..ab9e6169 100644 --- a/src/services/ledgerService.ts +++ b/src/services/ledgerService.ts @@ -1,5 +1,6 @@ import { Pool, PoolClient } from 'pg'; import { pool } from '../config/database'; +import { SupportedCurrency, currencyService, BASE_CURRENCY } from './currency'; /** * Double-Entry Ledger Service @@ -64,10 +65,12 @@ export class LedgerService { description: string, entries: LedgerEntry[], transactionId?: string, - postedBy?: string + postedBy?: string, + currency?: SupportedCurrency, + conversionRate?: number ): Promise { const client = await this.pool.connect(); - + try { await client.query('BEGIN'); @@ -86,6 +89,18 @@ export class LedgerService { ); } + // Attach currency metadata if provided + const enrichedEntries = (currency && conversionRate) + ? entries.map(e => ({ + ...e, + metadata: { + ...(e.metadata || {}), + currency, + conversionRate, + }, + })) + : entries; + // Call the database function to post atomically const result = await client.query( `SELECT * FROM post_transaction($1, $2, $3, $4, $5)`, @@ -94,7 +109,7 @@ export class LedgerService { description, transactionId || null, postedBy || null, - JSON.stringify(entries) + JSON.stringify(enrichedEntries) ] ); @@ -113,47 +128,113 @@ export class LedgerService { client.release(); } } + const client = await this.pool.connect(); + + try { + await client.query('BEGIN'); + + // Validate entries + if (!entries || entries.length < 2) { + throw new Error('At least 2 entries required for double-entry'); + } + + // Calculate totals for client-side validation + const totalDebits = entries.reduce((sum, e) => sum + (e.debit_amount || 0), 0); + const totalCredits = entries.reduce((sum, e) => sum + (e.credit_amount || 0), 0); + + if (Math.abs(totalDebits - totalCredits) > 0.0000001) { + throw new Error( + `Transaction not balanced: debits=${totalDebits} credits=${totalCredits}` + ); + } + + // Call the database function to post atomically + const result = await client.query( + `SELECT * FROM post_transaction($1, $2, $3, $4, $5)`, + [ + referenceNumber, + description, + transactionId || null, + postedBy || null, + JSON.stringify(entries) + ] + ); + + await client.query('COMMIT'); + + return result.rows.map(row => ({ + entry_id: row.entry_id, + account_code: row.account_code, + debit: parseFloat(row.debit), + credit: parseFloat(row.credit) + })); + } catch (error) { + await client.query('ROLLBACK'); + throw error; /** - * Post a deposit transaction - * Debit: Mobile Money Float (asset increases) - * Credit: Customer Balances (liability increases) + * Post a deposit transaction with currency conversion. + * `amount` and `fee` are in the original `currency`. + * The amounts are converted to base currency (USD) for ledger accounting. + * Metadata records original currency and conversion rate. */ - async postDeposit( + async postDepositWithCurrency( amount: number, fee: number, + currency: SupportedCurrency, referenceNumber: string, transactionId: string, userId: string ): Promise { + // Convert amounts to base currency using CurrencyService + const amountConversion = currencyService.convert(amount, currency, BASE_CURRENCY); + const feeConversion = currencyService.convert(fee, currency, BASE_CURRENCY); + const entries: LedgerEntry[] = [ { account_code: '1100', // Mobile Money Float - debit_amount: amount, - description: 'Customer deposit received' + debit_amount: amountConversion.convertedAmount, + description: 'Customer deposit received', + metadata: { + originalAmount: amount, + originalCurrency: currency, + conversionRate: amountConversion.rate, + }, }, { account_code: '2000', // Customer Balances - credit_amount: amount - fee, - description: 'Customer balance credited' - } + credit_amount: amountConversion.convertedAmount - feeConversion.convertedAmount, + description: 'Customer balance credited', + metadata: { + originalAmount: amount - fee, + originalCurrency: currency, + conversionRate: amountConversion.rate, + }, + }, ]; // Add fee revenue if applicable if (fee > 0) { entries.push({ account_code: '4100', // Deposit Fee Revenue - credit_amount: fee, - description: 'Deposit fee earned' + credit_amount: feeConversion.convertedAmount, + description: 'Deposit fee earned', + metadata: { + originalAmount: fee, + originalCurrency: currency, + conversionRate: feeConversion.rate, + }, }); } return this.postTransaction( referenceNumber, - `Deposit: ${amount} (fee: ${fee})`, + `Deposit: ${amount} ${currency} (fee: ${fee} ${currency})`, entries, transactionId, - userId + userId, + currency, + amountConversion.rate ); } From ef88571c54ffd77b925859f176ae7847fbe4a95a Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 23 Jun 2026 14:23:37 +0100 Subject: [PATCH 2/2] feat: add multi-currency ledger support for GHS and NGN with conversion handling --- src/services/ledgerService.ts | 44 +---------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/src/services/ledgerService.ts b/src/services/ledgerService.ts index ab9e6169..b54444f3 100644 --- a/src/services/ledgerService.ts +++ b/src/services/ledgerService.ts @@ -128,49 +128,7 @@ export class LedgerService { client.release(); } } - const client = await this.pool.connect(); - - try { - await client.query('BEGIN'); - - // Validate entries - if (!entries || entries.length < 2) { - throw new Error('At least 2 entries required for double-entry'); - } - - // Calculate totals for client-side validation - const totalDebits = entries.reduce((sum, e) => sum + (e.debit_amount || 0), 0); - const totalCredits = entries.reduce((sum, e) => sum + (e.credit_amount || 0), 0); - - if (Math.abs(totalDebits - totalCredits) > 0.0000001) { - throw new Error( - `Transaction not balanced: debits=${totalDebits} credits=${totalCredits}` - ); - } - - // Call the database function to post atomically - const result = await client.query( - `SELECT * FROM post_transaction($1, $2, $3, $4, $5)`, - [ - referenceNumber, - description, - transactionId || null, - postedBy || null, - JSON.stringify(entries) - ] - ); - - await client.query('COMMIT'); - - return result.rows.map(row => ({ - entry_id: row.entry_id, - account_code: row.account_code, - debit: parseFloat(row.debit), - credit: parseFloat(row.credit) - })); - } catch (error) { - await client.query('ROLLBACK'); - throw error; +/* Duplicate block removed */ /** * Post a deposit transaction with currency conversion.