UNPKG

@accounter/server

Version:
223 lines • 9.84 kB
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