Skip to content

Commit

Permalink
feat(backend): improve PSQL balance calculation (#2881)
Browse files Browse the repository at this point in the history
* chore(backend): add check constraint to ledger transfers

* feat(backend): update balance calculation to use psql query
  • Loading branch information
mkurapov authored Aug 26, 2024
1 parent 90a1415 commit 0248bc2
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('ledgerTransfers', function (table) {
table.check(
`("state" != 'PENDING') OR ("expiresAt" IS NOT NULL)`,
null,
'check_pending_requires_expires_at'
)
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('ledgerTransfers', function (table) {
table.dropChecks(['check_pending_requires_expires_at'])
})
}
21 changes: 21 additions & 0 deletions packages/backend/src/accounting/psql/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ describe('Balances', (): void => {
})
})

test('ignores expired pending transfers', async (): Promise<void> => {
await createLedgerTransfer(
{
ledger: account.ledger,
creditAccountId: account.id,
debitAccountId: peerAccount.id,
state: LedgerTransferState.PENDING,
expiresAt: new Date(Date.now() - 1),
amount: 10n
},
knex
)

await expect(getAccountBalances(serviceDeps, account)).resolves.toEqual({
creditsPosted: 0n,
creditsPending: 0n,
debitsPosted: 0n,
debitsPending: 0n
})
})

describe('calculates balances for single transfers', (): void => {
const amounts = {
credit: {
Expand Down
65 changes: 35 additions & 30 deletions packages/backend/src/accounting/psql/balance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { LedgerAccount } from './ledger-account/model'
import { LedgerTransferState } from '../service'
import { ServiceDependencies } from './service'
import { getAccountTransfers } from './ledger-transfer'
import { TransactionOrKnex } from 'objection'

export interface AccountBalance {
Expand All @@ -16,38 +14,45 @@ export async function getAccountBalances(
account: LedgerAccount,
trx?: TransactionOrKnex
): Promise<AccountBalance> {
const { credits, debits } = await getAccountTransfers(
deps,
account.id,
undefined, // No limit for balances
trx
)
try {
const queryResult = await (trx ?? deps.knex).raw(
`
SELECT
COALESCE(SUM("amount") FILTER(WHERE "creditAccountId" = :accountId AND "state" = 'POSTED'), 0) AS "creditsPosted",
COALESCE(SUM("amount") FILTER(WHERE "creditAccountId" = :accountId AND "state" = 'PENDING'), 0) AS "creditsPending",
COALESCE(SUM("amount") FILTER(WHERE "debitAccountId" = :accountId AND "state" = 'POSTED'), 0) AS "debitsPosted",
COALESCE(SUM("amount") FILTER(WHERE "debitAccountId" = :accountId AND "state" = 'PENDING'), 0) AS "debitsPending"
FROM "ledgerTransfers"
WHERE ("creditAccountId" = :accountId OR "debitAccountId" = :accountId)
AND ("state" = 'POSTED' OR ("state" = 'PENDING' AND "expiresAt" > NOW()));
`,
{ accountId: account.id }
)

let creditsPosted = 0n
let creditsPending = 0n
let debitsPosted = 0n
let debitsPending = 0n

for (const credit of credits) {
if (credit.state === LedgerTransferState.POSTED) {
creditsPosted += credit.amount
} else if (credit.state === LedgerTransferState.PENDING) {
creditsPending += credit.amount
if (queryResult?.rows < 1) {
throw new Error('No results when fetching balance for account')
}
}

for (const debit of debits) {
if (debit.state === LedgerTransferState.POSTED) {
debitsPosted += debit.amount
} else if (debit.state === LedgerTransferState.PENDING) {
debitsPending += debit.amount
const creditsPosted = BigInt(queryResult.rows[0].creditsPosted)
const creditsPending = BigInt(queryResult.rows[0].creditsPending)
const debitsPosted = BigInt(queryResult.rows[0].debitsPosted)
const debitsPending = BigInt(queryResult.rows[0].debitsPending)

return {
creditsPosted,
creditsPending,
debitsPosted,
debitsPending
}
}
} catch (err) {
deps.logger.error(
{
err,
accountId: account.id
},
'Could not fetch balances for account'
)

return {
creditsPosted,
creditsPending,
debitsPosted,
debitsPending
throw err
}
}
6 changes: 5 additions & 1 deletion packages/backend/src/tests/ledgerTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ export const createLedgerTransfer = async (
creditAccountId: creditAccountId,
debitAccountId: debitAccountId,
amount: amount ?? 10n,
expiresAt,
ledger,
state: state ?? LedgerTransferState.POSTED,
expiresAt:
expiresAt ??
(state === LedgerTransferState.PENDING
? new Date(Date.now() + 86_400_000)
: undefined),
type
})
}

0 comments on commit 0248bc2

Please sign in to comment.