@accounter/server
Version:
Accounter GraphQL server
408 lines (376 loc) • 15.2 kB
text/typescript
import { GraphQLError } from 'graphql';
import type { Resolvers } from '../../../__generated__/types.js';
import { EMPTY_UUID } from '../../../shared/constants.js';
import { formatCurrency } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ScopeProvider } from '../../auth/providers/scope.provider.js';
import { deleteCharges } from '../../charges/helpers/delete-charges.helper.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import { effectiveDateSupplement } from '../helpers/effective-date.helper.js';
import { TransactionsProvider } from '../providers/transactions.provider.js';
import type {
IGetTransactionsByIdsResult,
IUpdateTransactionsParams,
TransactionsModule,
} from '../types.js';
import { commonChargeFields, commonTransactionFields } from './common.js';
export const transactionsResolvers: TransactionsModule.Resolvers &
Pick<Resolvers, 'UpdateTransactionResult' | 'UpdateTransactionsResult'> = {
Query: {
transactionsByIDs: async (_, { transactionIDs }, { injector }) => {
if (transactionIDs.length === 0) {
return [];
}
const transactions = await injector
.get(TransactionsProvider)
.transactionByIdLoader.loadMany(transactionIDs);
if (!transactions) {
if (transactionIDs.length === 1) {
throw new GraphQLError(`Transaction ID="${transactionIDs[0]}" not found`);
} else {
throw new GraphQLError(`Couldn't find any transactions`);
}
}
transactionIDs.map(id => {
const transaction = transactions.find(
transaction => !(transaction instanceof Error) && transaction.id === id,
);
if (!transaction) {
throw new GraphQLError(`Transaction ID="${id}" not found`);
}
});
return transactionIDs;
},
transactionsByFinancialEntity: async (_, { financialEntityID }, { injector }) => {
const ownerIDs = await injector.get(ScopeProvider).getReadScope();
const transactions = await injector
.get(TransactionsProvider)
.getTransactionsByFilters({
businessIDs: [financialEntityID],
ownerIDs,
})
.then(res => res.map(t => t.id));
return transactions;
},
},
Mutation: {
updateTransaction: async (_, { transactionId, fields }, { injector }) => {
let postUpdateActions = async (): Promise<void> => void 0;
try {
// let charge: ChargesTypes.IGetChargesByIdsResult | undefined;
const existingChargePromise = async () => {
const charge = await injector
.get(ChargesProvider)
.getChargeByIdLoader.load(fields.chargeId ?? '');
if (!charge) {
throw new GraphQLError(`Charge ID="${chargeId}" not valid`);
}
};
const emptyChargePromise = async () => {
// case unlinked from charge
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
if (!transaction) {
throw new GraphQLError(`Transaction ID="${transactionId}" not valid`);
}
if (transaction.charge_id) {
const [charge, transactions, documents] = await Promise.all([
injector.get(ChargesProvider).getChargeByIdLoader.load(transaction.charge_id),
injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(transaction.charge_id),
injector
.get(DocumentsProvider)
.getDocumentsByChargeIdLoader.load(transaction.charge_id),
]);
if (!charge) {
throw new GraphQLError(`Former transaction's charge ID ("${chargeId}") not valid`);
}
// generate new charge
const newCharge = await injector.get(ChargesProvider).generateCharge({
ownerId: charge.owner_id,
userDescription: 'Transaction unlinked from charge',
});
if (!newCharge) {
throw new GraphQLError(
`Failed to generate new charge for transaction ID="${transactionId}"`,
);
}
chargeId = newCharge.id;
if (documents.length === 0 && transactions.length === 1) {
postUpdateActions = async () =>
deleteCharges([charge.id], injector)
.catch(e => {
if (e instanceof GraphQLError) {
throw e;
}
console.error(e);
throw new GraphQLError(
`Failed to delete the empty former charge ID="${charge.id}"`,
);
})
.then(postUpdateActions);
}
}
};
let chargeId = fields.chargeId;
const chargePromise =
chargeId && chargeId !== EMPTY_UUID
? existingChargePromise()
: chargeId === EMPTY_UUID
? emptyChargePromise()
: Promise.resolve();
await chargePromise;
const adjustedFields: IUpdateTransactionsParams = {
transactionIds: [transactionId],
businessId: fields.counterpartyId,
chargeId: chargeId ?? null,
debitDate: fields.effectiveDate ?? null,
isFee: fields.isFee,
};
const res = await injector
.get(TransactionsProvider)
.updateTransactions({ ...adjustedFields });
if (!res[0]?.id) {
throw new GraphQLError('Transaction update failed');
}
await postUpdateActions();
return res[0].id;
} catch (e) {
if (e instanceof GraphQLError) {
throw e;
}
console.error(e);
return {
__typename: 'CommonError',
message: (e as Error)?.message ?? 'Unknown error',
};
}
},
updateTransactions: async (_, { transactionIds, fields }, { injector }) => {
let postUpdateActions = async (): Promise<void> => void 0;
try {
const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
const existingChargePromise = async () => {
// verify charge exists
const charge = await injector
.get(ChargesProvider)
.getChargeByIdLoader.load(fields.chargeId ?? '');
if (!charge) {
throw new GraphQLError(`Charge ID="${chargeId}" not valid`);
}
};
const emptyChargePromise = async () => {
// case unlinked from charge
const transactions = await injector
.get(TransactionsProvider)
.transactionByIdLoader.loadMany(transactionIds)
.then(
res =>
res.filter((t, i) => {
if (t instanceof Error) {
throw new GraphQLError(`Transaction ID="${transactionIds[i]}" not valid`);
}
return true;
}) as IGetTransactionsByIdsResult[],
);
const transactionsWithChargeId = transactions.filter(t => t.charge_id);
if (transactionsWithChargeId.length > 0) {
const chargesToDelete = new Set<string>();
await Promise.all(
transactionsWithChargeId.map(async transaction => {
if (!transaction.charge_id) {
return;
}
const [charge, transactions, documents] = await Promise.all([
injector.get(ChargesProvider).getChargeByIdLoader.load(transaction.charge_id),
injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(transaction.charge_id),
injector
.get(DocumentsProvider)
.getDocumentsByChargeIdLoader.load(transaction.charge_id),
]);
if (!charge) {
throw new GraphQLError(
`Former transaction's charge ID ("${transaction.charge_id}") not valid`,
);
}
if (documents.length === 0 && transactions.length === 1) {
chargesToDelete.add(charge.id);
}
}),
);
if (chargesToDelete.size > 0) {
const oldPostUpdateActions = postUpdateActions;
postUpdateActions = async () => {
await oldPostUpdateActions();
await deleteCharges(Array.from(chargesToDelete), injector).catch(e => {
if (e instanceof GraphQLError) {
throw e;
}
console.error(e);
throw new GraphQLError(
`Failed to delete empty former charges: ${Array.from(chargesToDelete).join(', ')}`,
);
});
};
}
// generate new charge
const newCharge = await injector.get(ChargesProvider).generateCharge({
ownerId,
userDescription: 'Transactions unlinked from charge',
});
if (!newCharge) {
throw new GraphQLError(`Failed to generate new charge for transactions update`);
}
chargeId = newCharge.id;
}
};
let chargeId = fields.chargeId;
const chargePromise =
chargeId && chargeId !== EMPTY_UUID
? existingChargePromise()
: chargeId === EMPTY_UUID
? emptyChargePromise()
: Promise.resolve();
await chargePromise;
const adjustedFields: IUpdateTransactionsParams = {
transactionIds,
businessId: fields.counterpartyId,
chargeId: chargeId ?? null,
debitDate: fields.effectiveDate ?? null,
isFee: fields.isFee,
};
const res = await injector.get(TransactionsProvider).updateTransactions(adjustedFields);
if (!res[0]?.id) {
throw new GraphQLError('Transactions update failed');
}
await postUpdateActions();
return { transactions: res.map(transaction => transaction.id) };
} catch (e) {
if (e instanceof GraphQLError) {
throw e;
}
console.error(e);
return {
__typename: 'CommonError',
message: (e as Error)?.message ?? 'Unknown error',
};
}
},
},
UpdateTransactionResult: {
__resolveType: async (raw, { injector }, _info) => {
if (typeof raw === 'string') {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(raw);
const charge = await injector
.get(ChargesProvider)
.getChargeByIdLoader.load(transaction.charge_id);
if (!charge) {
throw new GraphQLError(`Charge with ID "${transaction.charge_id}" not found`);
}
return charge.type === 'CONVERSION' ? 'ConversionTransaction' : 'CommonTransaction';
}
if (typeof raw === 'object' && '__typename' in raw && raw.__typename === 'CommonError') {
return 'CommonError';
}
throw new GraphQLError('Unexpected type for UpdateTransactionResult');
},
},
UpdateTransactionsResult: {
__resolveType: (obj, _context, _info) => {
if (('__typename' in obj && obj.__typename === 'CommonError') || 'message' in obj)
return 'CommonError';
return 'UpdatedTransactionsSuccessfulResult';
},
},
BankDepositCharge: commonChargeFields,
BusinessTripCharge: commonChargeFields,
CommonCharge: commonChargeFields,
ConversionCharge: commonChargeFields,
CreditcardBankCharge: commonChargeFields,
DividendCharge: commonChargeFields,
FinancialCharge: commonChargeFields,
ForeignSecuritiesCharge: commonChargeFields,
InternalTransferCharge: commonChargeFields,
MonthlyVatCharge: commonChargeFields,
SalaryCharge: commonChargeFields,
ConversionTransaction: {
__isTypeOf: async (transactionId, { injector }) => {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
const charge = await injector
.get(ChargesProvider)
.getChargeByIdLoader.load(transaction.charge_id);
if (!charge) {
throw new GraphQLError(`Charge ID="${transaction.charge_id}" not found`);
}
return charge.type === 'CONVERSION';
},
...commonTransactionFields,
effectiveDate: async (transactionId, __dirname, { injector }) => {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
const date = effectiveDateSupplement(transaction);
if (!date) {
console.error(`Conversion transaction ID="${transactionId}" has no effective date`);
throw new GraphQLError('Conversion transaction must have effective date');
}
return date;
},
type: async (transactionId, __dirname, { injector }) => {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
return Number(transaction.amount) > 0 ? 'QUOTE' : 'BASE';
},
bankRate: async (transactionId, __dirname, { injector }) => {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
return transaction.currency_rate;
},
officialRateToLocal: async (transactionId, _, { injector }) => {
const { defaultLocalCurrency } = await injector
.get(AdminContextProvider)
.getVerifiedAdminContext();
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
return injector
.get(ExchangeProvider)
.getExchangeRates(
defaultLocalCurrency,
formatCurrency(transaction.currency),
transaction.event_date,
);
},
},
CommonTransaction: {
__isTypeOf: async (transactionId, { injector }) => {
const transaction = await injector
.get(TransactionsProvider)
.transactionByIdLoader.load(transactionId);
const charge = await injector
.get(ChargesProvider)
.getChargeByIdLoader.load(transaction.charge_id);
if (!charge) {
throw new GraphQLError(`Charge ID="${transaction.charge_id}" not found`);
}
if (charge instanceof GraphQLError) {
console.error(charge);
throw new GraphQLError(`Charge ID="${transaction.charge_id}" not found`);
}
return charge.type !== 'CONVERSION';
},
...commonTransactionFields,
},
};