@accounter/server
Version:
Accounter GraphQL server
223 lines • 9.84 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { dateToTimelessDateString, reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { identifyInterestTransactionIds } from '../../ledger/helpers/bank-deposit-ledger-generation.helper.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
const getTransactionsByBankDeposits = sql `
SELECT
cbd.deposit_id,
cbd.account_id as deposit_account_id,
t.*
FROM accounter_schema.charges_bank_deposits cbd
INNER JOIN accounter_schema.transactions t
ON cbd.id = t.charge_id
WHERE deposit_id IN $$depositIds;`;
const getDepositTransactionsByChargeId = sql `
SELECT
cbd.deposit_id,
cbd.account_id as deposit_account_id,
t.*
FROM accounter_schema.charges_bank_deposits cbd
LEFT JOIN accounter_schema.transactions t
ON cbd.id = t.charge_id
WHERE deposit_id IN (
SELECT cbd2.deposit_id
FROM accounter_schema.charges_bank_deposits cbd2
WHERE cbd2.id = $chargeId
)
AND ($includeCharge OR t.charge_id <> $chargeId);`;
const insertOrUpdateBankDepositCharge = sql `
INSERT INTO accounter_schema.charges_bank_deposits (id, deposit_id, account_id, owner_id)
VALUES ($chargeId, $depositId, $accountId, $ownerId)
ON CONFLICT (id) DO UPDATE SET deposit_id = EXCLUDED.deposit_id, account_id = EXCLUDED.account_id, owner_id = EXCLUDED.owner_id;
`;
const deleteBankDepositChargesByChargeIds = sql `
DELETE FROM accounter_schema.charges_bank_deposits
WHERE id IN $$chargeIds;
`;
const getAllDepositsWithTransactions = sql `
SELECT
cbd.deposit_id,
t.id,
t.currency,
t.debit_date,
t.event_date,
t.amount,
t.current_balance,
t.charge_id
FROM accounter_schema.charges_bank_deposits cbd
LEFT JOIN accounter_schema.transactions t
ON cbd.id = t.charge_id
WHERE cbd.deposit_id IS NOT NULL
ORDER BY cbd.deposit_id, COALESCE(t.debit_date, t.event_date);`;
let BankDepositChargesProvider = class BankDepositChargesProvider {
db;
adminContextProvider;
transactionsProvider;
constructor(db, adminContextProvider, transactionsProvider) {
this.db = db;
this.adminContextProvider = adminContextProvider;
this.transactionsProvider = transactionsProvider;
}
async batchTransactionsByBankDeposits(depositIds) {
const transactions = await getTransactionsByBankDeposits.run({
depositIds,
}, this.db);
return depositIds.map(id => transactions.filter(t => t.deposit_id === id));
}
getTransactionsByBankDepositLoader = new DataLoader((keys) => this.batchTransactionsByBankDeposits(keys));
async getDepositTransactionsByChargeId(chargeId, includeCharge = false) {
return getDepositTransactionsByChargeId.run({ chargeId, includeCharge }, this.db);
}
async insertOrUpdateBankDepositCharge(params) {
const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
return insertOrUpdateBankDepositCharge.run(reassureOwnerIdExists(params, ownerId), this.db);
}
async getAllDepositsWithMetadata() {
const rows = await getAllDepositsWithTransactions.run(undefined, this.db);
// Group transactions by deposit_id
const depositMap = new Map();
// Identify interest transactions using shared helper
const interestTransactionIds = identifyInterestTransactionIds(rows, {
getId: r => r.id,
getChargeId: r => r.charge_id,
getAmount: r => Number(r.amount ?? 0),
});
for (const row of rows) {
if (!row.deposit_id)
continue;
if (!depositMap.has(row.deposit_id)) {
depositMap.set(row.deposit_id, {
id: row.deposit_id,
currency: row.currency,
openDate: row.debit_date ?? row.event_date,
closeDate: null,
currentBalance: 0,
totalInterest: 0,
totalDeposit: 0,
currencyError: [],
transactionIds: [],
});
}
const deposit = depositMap.get(row.deposit_id);
deposit.transactionIds.push(row.id);
// Validate single currency
if (deposit.currency && row.currency && deposit.currency !== row.currency) {
const alreadyInErrors = deposit.currencyError.includes(row.id);
if (!alreadyInErrors) {
deposit.currencyError.push(row.id);
}
}
// Update openDate (earliest transaction)
const txDate = row.debit_date ?? row.event_date;
if (!deposit.openDate || (txDate && txDate < deposit.openDate)) {
deposit.openDate = txDate;
}
// Track balance and interest separately
if (row.amount) {
const amount = Number(row.amount);
if (interestTransactionIds.has(row.id)) {
deposit.totalInterest += amount;
}
else {
deposit.currentBalance += amount;
if (amount > 0) {
deposit.totalDeposit += amount;
}
}
}
// If balance reaches zero, update closeDate (interest doesn't affect closure)
if (Math.abs(deposit.currentBalance) < 0.005 && txDate) {
deposit.closeDate = txDate;
}
}
// Ensure closeDate reflects final state: if not closed, closeDate must be null
for (const d of depositMap.values()) {
if (Math.abs(d.currentBalance) >= 0.005) {
d.closeDate = null;
}
}
return Array.from(depositMap.values()).map(deposit => ({
id: deposit.id,
currency: deposit.currency,
openDate: deposit.openDate ? dateToTimelessDateString(deposit.openDate) : null,
closeDate: deposit.closeDate ? dateToTimelessDateString(deposit.closeDate) : null,
currentBalance: deposit.currentBalance,
totalInterest: deposit.totalInterest,
totalDeposit: deposit.totalDeposit,
currencyError: deposit.currencyError,
transactionIds: deposit.transactionIds,
}));
}
async createDeposit(currency) {
// Generate unique deposit ID using timestamp and random suffix
const depositId = `${currency}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
return {
id: depositId,
currency,
openDate: null,
closeDate: null,
currentBalance: 0,
totalInterest: 0,
currencyError: [],
transactionIds: [],
};
}
async deleteChargeDepositsByChargeIds(chargeIds) {
return deleteBankDepositChargesByChargeIds.run({ chargeIds }, this.db);
}
async assignChargeToDeposit(chargeId, depositId) {
const [depositTransactions, transactions] = await Promise.all([
// Get target deposit transactions to validate currency
this.getTransactionsByBankDepositLoader.load(depositId),
this.transactionsProvider.transactionsByChargeIDLoader.load(chargeId),
]);
let accountId = null;
// Check currency conflict
if (depositTransactions.length > 0) {
const depositCurrency = depositTransactions[0].currency;
const depositAccountId = depositTransactions[0].deposit_account_id;
for (const transaction of transactions) {
const transactionCurrency = transaction.currency;
if (depositCurrency && transactionCurrency !== depositCurrency) {
throw new Error(`Currency conflict: Transaction currency (${transactionCurrency}) does not match deposit currency (${depositCurrency})`);
}
if (depositAccountId !== transaction.account_id) {
throw new Error(`Account conflict: Transaction account (${transaction.account_id}) does not match deposit account (${depositAccountId})`);
}
}
accountId = depositAccountId;
}
else {
for (const transaction of transactions) {
accountId ??= transaction.account_id;
if (accountId !== transaction.account_id) {
throw new Error(`Account conflict: Transactions accounts are inconsistent`);
}
}
}
await this.insertOrUpdateBankDepositCharge({ chargeId, depositId, accountId });
// Return updated deposit metadata
const allDeposits = await this.getAllDepositsWithMetadata();
const updatedDeposit = allDeposits.find(d => d.id === depositId);
if (!updatedDeposit) {
throw new Error('Deposit not found after assignment');
}
return updatedDeposit;
}
};
BankDepositChargesProvider = __decorate([
Injectable({
scope: Scope.Operation,
global: true,
}),
__metadata("design:paramtypes", [TenantAwareDBClient,
AdminContextProvider,
TransactionsProvider])
], BankDepositChargesProvider);
export { BankDepositChargesProvider };
//# sourceMappingURL=bank-deposit-charges.provider.js.map