@accounter/server
Version:
Accounter GraphQL server
177 lines (158 loc) • 6.51 kB
text/typescript
import { GraphQLError } from 'graphql';
import { Currency } from '../../../shared/enums.js';
import { errorSimplifier } from '../../../shared/errors.js';
import { formatFinancialAmount } from '../../../shared/helpers/index.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import { BankDepositChargesProvider } from '../providers/bank-deposit-charges.provider.js';
import { BankDepositsProvider } from '../providers/bank-deposits.provider.js';
import type { BankDepositsModule } from '../types.js';
export const bankDepositChargesResolvers: BankDepositsModule.Resolvers = {
Query: {
depositByCharge: async (_, { chargeId }, { injector }) => {
try {
return await injector
.get(BankDepositChargesProvider)
.getBankDepositByChargeIdLoader.load(chargeId);
} catch (e) {
throw errorSimplifier('Error fetching deposit by charge', e);
}
},
relevantDepositsForCharge: async (_, { chargeId }, { injector }) => {
try {
const transactions = await injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(chargeId);
if (transactions.length === 0) {
return { id: chargeId, deposits: [], error: null };
}
const currencies = new Set(transactions.map(t => t.currency).filter(Boolean));
const accountIds = new Set(transactions.map(t => t.account_id).filter(Boolean));
if (currencies.size > 1) {
return {
id: chargeId,
deposits: [],
error: `Charge has transactions in multiple currencies (${[...currencies].join(', ')})`,
};
}
if (accountIds.size > 1) {
return {
id: chargeId,
deposits: [],
error: 'Charge has transactions from multiple accounts',
};
}
const chargeCurrency = currencies.size === 1 ? ([...currencies][0] as Currency) : null;
const chargeAccountId = accountIds.size === 1 ? [...accountIds][0] : null;
const allOpen = await injector.get(BankDepositsProvider).getAllOpenBankDeposits();
const relevant = allOpen.filter(deposit => {
const currencyMatch =
deposit.currency == null ||
chargeCurrency == null ||
deposit.currency === chargeCurrency;
const accountMatch =
deposit.account_id == null ||
chargeAccountId == null ||
deposit.account_id === chargeAccountId;
return currencyMatch && accountMatch;
});
return { id: chargeId, deposits: relevant, error: null };
} catch (e) {
throw errorSimplifier('Error fetching relevant deposits for charge', e);
}
},
},
Mutation: {
assignChargeToDeposit: async (_, { chargeId, depositId }, { injector }) => {
try {
await injector.get(BankDepositChargesProvider).assignChargeToDeposit(chargeId, depositId);
return injector
.get(BankDepositChargesProvider)
.getBankDepositByChargeIdLoader.load(chargeId)
.then(deposit => {
if (!deposit) {
throw new GraphQLError('Deposit not found for charge after assignment');
}
return deposit;
});
} catch (e) {
throw errorSimplifier('Error assigning transaction to deposit', e);
}
},
createDepositFromCharge: async (_, { chargeId, name }, { injector }) => {
try {
const transactions = await injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(chargeId);
if (transactions.length === 0) {
throw new GraphQLError(
'Deposit creation from charge requires charges that have at least one transaction',
);
}
const currencies = new Set<Currency>();
const accountIds = new Set<string>();
let openDate: Date | null = null;
transactions.map(t => {
currencies.add(t.currency as Currency);
if (t.account_id) {
accountIds.add(t.account_id);
}
// Derive open_date from the earliest debit_date, falling back to event_date
const transactionDate = t.debit_date ?? t.event_date;
if (transactionDate && (!openDate || transactionDate < openDate)) {
openDate = transactionDate;
}
});
// Validate single currency and single account across all transactions
if (currencies.size > 1) {
throw new GraphQLError(
`Cannot create deposit: charge has transactions in multiple currencies (${[...currencies].join(', ')})`,
);
}
if (accountIds.size > 1) {
throw new GraphQLError(
`Cannot create deposit: charge has transactions from multiple accounts`,
);
}
const currency = [...currencies][0];
const accountId = [...accountIds][0] ?? null;
const deposit = await injector.get(BankDepositsProvider).insertBankDeposit({
name,
currency,
accountId,
openDate,
closeDate: null,
});
if (!deposit) {
throw new GraphQLError('Failed to create bank deposit');
}
await injector
.get(BankDepositChargesProvider)
.upsertBankDepositCharge({ chargeId, depositId: deposit.id });
injector.get(BankDepositChargesProvider).getBankDepositByChargeIdLoader.clear(chargeId);
return deposit;
} catch (e) {
throw errorSimplifier('Error creating deposit from charge', e);
}
},
},
BankDeposit: {
metadata: async (deposit, _, { injector }) => {
try {
return await injector
.get(BankDepositChargesProvider)
.getBankDepositMetadataLoader.load(deposit.id);
} catch (e) {
throw errorSimplifier(`Error fetching metadata for deposit ${deposit.name}`, e);
}
},
},
BankDepositMetadata: {
id: metadata => metadata.id,
currentBalance: metadata => formatFinancialAmount(metadata.currentBalance),
totalInterest: metadata => formatFinancialAmount(metadata.totalInterest),
totalDeposit: metadata => formatFinancialAmount(metadata.totalDeposit),
potentialCloseDate: metadata =>
Math.abs(metadata.currentBalance) >= 0.005 ? metadata.potentialCloseDate : null,
transactions: metadata => metadata.transactionIds,
},
};