@accounter/server
Version:
Accounter GraphQL server
397 lines (372 loc) • 14.5 kB
text/typescript
import { GraphQLError } from 'graphql';
import { type Injector } from 'graphql-modules';
import { ChargeTypeEnum } from '../../../shared/enums.js';
import { errorSimplifier } from '../../../shared/errors.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ScopeProvider } from '../../auth/providers/scope.provider.js';
import { getMinDate } from '../../ledger/helpers/ledger-lock.js';
import { generateLedgerRecordsForFinancialCharge } from '../../ledger/resolvers/ledger-generation/financial-ledger-generation.resolver.js';
import { MiscExpensesProvider } from '../../misc-expenses/providers/misc-expenses.provider.js';
import { getChargeType } from '../helpers/charge-type.js';
import { getChargeLedgerMeta } from '../helpers/common.helper.js';
import { generateAndTagCharge } from '../helpers/financial-charge.helper.js';
import { ChargesProvider } from '../providers/charges.provider.js';
import type { ChargesModule } from '../types.js';
import { commonChargeFields } from './common.js';
// Validate an explicit write-target business against the caller's memberships,
// then load that business's admin context (not the request's primary), so
// charge generation uses the correct per-business tax configuration.
async function getWriteTargetAdminContext(injector: Injector, ownerId: string) {
const target = await injector.get(ScopeProvider).resolveWriteTarget(ownerId);
const targetAdminContext = await injector
.get(AdminContextProvider)
.adminContextByOwnerIdLoader.load(target);
if (!targetAdminContext) {
throw new GraphQLError('Admin context not found for the target business', {
extensions: { code: 'NOT_FOUND' },
});
}
return targetAdminContext;
}
export const financialChargesResolvers: ChargesModule.Resolvers = {
Query: {
annualFinancialCharges: async (_, { ownerId, year }, { injector }) => {
// This report is shaped around a single business's tax categories. When a
// business is requested explicitly, validate it is within the read scope
// and use its admin context; otherwise default to the request's primary.
let targetAdminContext;
if (ownerId) {
const [scopedId] = await injector.get(ScopeProvider).getReadScope([ownerId]);
targetAdminContext = await injector
.get(AdminContextProvider)
.adminContextByOwnerIdLoader.load(scopedId);
if (!targetAdminContext) {
throw new GraphQLError('Admin context not found for the requested business', {
extensions: { code: 'NOT_FOUND' },
});
}
} else {
targetAdminContext = await injector.get(AdminContextProvider).getVerifiedAdminContext();
}
const charges = await injector.get(ChargesProvider).getChargesByFilters({
ownerIds: [targetAdminContext.ownerId],
type: 'FINANCIAL',
fromAnyDate: year,
toAnyDate: year,
});
return {
id: `${year}-financial-charges`,
revaluationCharge: charges.find(c =>
c.business_array?.includes(
targetAdminContext.general.taxCategories.exchangeRevaluationTaxCategoryId,
),
),
taxExpensesCharge: charges.find(c =>
c.business_array?.includes(targetAdminContext.authorities.taxExpensesTaxCategoryId),
),
depreciationCharge: targetAdminContext.depreciation.accumulatedDepreciationTaxCategoryId
? charges.find(c =>
c.business_array?.includes(
targetAdminContext.depreciation.accumulatedDepreciationTaxCategoryId!,
),
)
: null,
recoveryReserveCharge: targetAdminContext.salaries.recoveryReserveTaxCategoryId
? charges.find(c =>
c.business_array?.includes(targetAdminContext.salaries.recoveryReserveTaxCategoryId!),
)
: null,
vacationReserveCharge: targetAdminContext.salaries.vacationReserveTaxCategoryId
? charges.find(c =>
c.business_array?.includes(targetAdminContext.salaries.vacationReserveTaxCategoryId!),
)
: null,
bankDepositsRevaluationCharge: targetAdminContext.bankDeposits
.bankDepositInterestIncomeTaxCategoryId
? charges.find(c =>
c.business_array?.includes(targetAdminContext.bankDeposits.bankDepositBusinessId!),
)
: null,
};
},
},
Mutation: {
generateRevaluationCharge: async (_, { date, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
general: {
taxCategories: { exchangeRevaluationTaxCategoryId },
},
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && date <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
try {
const charge = await generateAndTagCharge(
injector,
ownerId,
exchangeRevaluationTaxCategoryId,
`Revaluation charge for ${date}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating revaluation charge', e);
}
},
generateBankDepositsRevaluationCharge: async (_, { date, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
bankDeposits: { bankDepositInterestIncomeTaxCategoryId },
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && date <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
try {
if (!bankDepositInterestIncomeTaxCategoryId) {
throw new GraphQLError('Bank deposit interest income tax category missing');
}
const charge = await generateAndTagCharge(
injector,
ownerId,
bankDepositInterestIncomeTaxCategoryId,
`Bank deposits revaluation charge for ${date}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating bank deposits revaluation charge', e);
}
},
generateTaxExpensesCharge: async (_, { year, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
authorities: { taxExpensesTaxCategoryId },
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && `${year}-12-31` <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
try {
const charge = await generateAndTagCharge(
injector,
ownerId,
taxExpensesTaxCategoryId,
`Tax expenses charge for ${year.substring(0, 4)}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating tax expenses charge', e);
}
},
generateDepreciationCharge: async (_, { year, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
depreciation: { accumulatedDepreciationTaxCategoryId },
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && `${year}-12-31` <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
if (!accumulatedDepreciationTaxCategoryId) {
throw new GraphQLError('Accumulated depreciation tax category missing');
}
try {
const charge = await generateAndTagCharge(
injector,
ownerId,
accumulatedDepreciationTaxCategoryId,
`Depreciation charge for ${year.substring(0, 4)}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating depreciation charge', e);
}
},
generateRecoveryReserveCharge: async (_, { year, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
salaries: { recoveryReserveTaxCategoryId },
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && `${year}-12-31` <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
if (!recoveryReserveTaxCategoryId) {
throw new GraphQLError('Recovery reserve tax category missing');
}
try {
const charge = await generateAndTagCharge(
injector,
ownerId,
recoveryReserveTaxCategoryId,
`Recovery reserve charge for ${year.substring(0, 4)}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating recovery reserve charge', e);
}
},
generateVacationReserveCharge: async (_, { year, ownerId }, context, info) => {
const { injector } = context;
const {
ledgerLock,
salaries: { vacationReserveTaxCategoryId },
} = await getWriteTargetAdminContext(injector, ownerId);
if (ledgerLock && `${year}-12-31` <= ledgerLock) {
throw new GraphQLError('Cannot generate revaluation charge for locked period');
}
if (!vacationReserveTaxCategoryId) {
throw new GraphQLError('Vacation reserve tax category missing');
}
try {
const charge = await generateAndTagCharge(
injector,
ownerId,
vacationReserveTaxCategoryId,
`Vacation reserves charge for ${year.substring(0, 4)}`,
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating vacation reserves charge', e);
}
},
generateBalanceCharge: async (_, { description, balanceRecords }, context, info) => {
const { injector } = context;
if (!balanceRecords.length) {
throw new GraphQLError('Balance charge must include balance records');
}
const { defaultTaxCategoryId, ownerId, ledgerLock } = await injector
.get(AdminContextProvider)
.getVerifiedAdminContext();
if (!defaultTaxCategoryId) {
throw new GraphQLError('Default tax category missing');
}
try {
if (ledgerLock) {
const minDate = getMinDate(
balanceRecords.map(record => [new Date(record.invoiceDate), record.valueDate]).flat(),
);
if (minDate && dateToTimelessDateString(minDate) <= ledgerLock) {
throw new GraphQLError('Cannot generate balance charge for locked period');
}
}
const charge = await generateAndTagCharge(
injector,
ownerId,
defaultTaxCategoryId,
description,
);
await Promise.all(
balanceRecords.map(async record => {
if (!record.debtorId && !record.creditorId) {
throw new GraphQLError(
'Balance record must include at least one counterparty (debtor or creditor)',
);
}
await injector
.get(MiscExpensesProvider)
.insertExpense({ ...record, chargeId: charge.id })
.catch(() => {
throw new GraphQLError('Error adding balance records');
});
}),
);
await generateLedgerRecordsForFinancialCharge(
charge,
{ insertLedgerRecordsIfNotExists: true },
context,
info,
);
return charge;
} catch (e) {
throw errorSimplifier('Error generating balance charge', e);
}
},
},
FinancialCharge: {
__isTypeOf: async (DbCharge, { injector }) =>
(await getChargeType(DbCharge, injector).catch(error => {
throw errorSimplifier('Failed to determine charge type', error);
})) === ChargeTypeEnum.Financial,
...commonChargeFields,
type: () => 'FINANCIAL',
vat: () => null,
totalAmount: () => null,
property: () => false,
isInvoicePaymentDifferentCurrency: () => false,
minEventDate: async (DbCharge, _, { injector }) => {
try {
const { ledgerMinInvoiceDate } = await getChargeLedgerMeta(DbCharge.id, injector);
return ledgerMinInvoiceDate;
} catch (error) {
throw errorSimplifier('Failed to fetch min event date', error);
}
},
minDebitDate: async (DbCharge, _, { injector }) => {
try {
const { ledgerMinValueDate } = await getChargeLedgerMeta(DbCharge.id, injector);
return ledgerMinValueDate;
} catch (error) {
throw errorSimplifier('Failed to fetch min debit date', error);
}
},
maxEventDate: async (DbCharge, _, { injector }) => {
try {
const { ledgerMaxInvoiceDate } = await getChargeLedgerMeta(DbCharge.id, injector);
return ledgerMaxInvoiceDate;
} catch (error) {
throw errorSimplifier('Failed to fetch max event date', error);
}
},
maxDebitDate: async (DbCharge, _, { injector }) => {
try {
const { ledgerMaxValueDate } = await getChargeLedgerMeta(DbCharge.id, injector);
return ledgerMaxValueDate;
} catch (error) {
throw errorSimplifier('Failed to fetch max debit date', error);
}
},
// minDocumentsDate:
// validationData:
// metadata:
yearsOfRelevance: () => null,
},
};