@accounter/server
Version:
255 lines (225 loc) • 8.27 kB
text/typescript
import type { IGetFinancialEntitiesByIdsResult } from '@modules/financial-entities/types.js';
import type { IGetLedgerRecordsByDatesResult } from '@modules/ledger/types.js';
import type { CommentaryProto } from '../types.js';
export type LedgerRecordDecorations = Partial<{
credit_entity_sort_code1: number;
credit_entity_sort_code2: number;
debit_entity_sort_code1: number;
debit_entity_sort_code2: number;
}>;
export type DecoratedLedgerRecord = IGetLedgerRecordsByDatesResult & LedgerRecordDecorations;
export function decorateLedgerRecords(
ledgerRecords: IGetLedgerRecordsByDatesResult[],
financialEntitiesDict: Map<string, IGetFinancialEntitiesByIdsResult>,
): DecoratedLedgerRecord[] {
return ledgerRecords.map(record => {
const sortCodes: LedgerRecordDecorations = {};
if (record.credit_entity1) {
const sortCode = financialEntitiesDict.get(record.credit_entity1)?.sort_code;
if (!sortCode) {
console.error(`No sort code for financial entity ${record.credit_entity1}`);
}
sortCodes.credit_entity_sort_code1 = sortCode ?? undefined;
}
if (record.credit_entity2) {
const sortCode = financialEntitiesDict.get(record.credit_entity2)?.sort_code;
if (!sortCode) {
console.error(`No sort code for financial entity ${record.credit_entity2}`);
}
sortCodes.credit_entity_sort_code2 = sortCode ?? undefined;
}
if (record.debit_entity1) {
const sortCode = financialEntitiesDict.get(record.debit_entity1)?.sort_code;
if (!sortCode) {
console.error(`No sort code for financial entity ${record.debit_entity1}`);
}
sortCodes.debit_entity_sort_code1 = sortCode ?? undefined;
}
if (record.debit_entity2) {
const sortCode = financialEntitiesDict.get(record.debit_entity2)?.sort_code;
if (!sortCode) {
console.error(`No sort code for financial entity ${record.debit_entity2}`);
}
sortCodes.debit_entity_sort_code2 = sortCode ?? undefined;
}
return {
...record,
...sortCodes,
};
});
}
export function updateRecords(
amountsByEntity: Map<number, { amount: number; records: Map<string, number> }>,
amount: number,
sortCode: number,
financialEntityId: string,
) {
const currentRecord = amountsByEntity.get(sortCode);
if (currentRecord) {
currentRecord.amount += amount;
const record = currentRecord.records.get(financialEntityId);
if (record) {
currentRecord.records.set(financialEntityId, record + amount);
} else {
currentRecord.records.set(financialEntityId, amount);
}
} else {
amountsByEntity.set(sortCode, { amount, records: new Map([[financialEntityId, amount]]) });
}
}
export function amountBySortCodeValidation(
unfilteredRecords: DecoratedLedgerRecord[],
validation: (sortCode: number) => boolean,
negate: boolean = false,
): CommentaryProto {
let amount = 0;
const amountsByEntity = new Map<
number,
{
amount: number;
records: Map<string, number>;
}
>();
const sign = negate ? -1 : 1;
unfilteredRecords.map(record => {
if (record.credit_entity1 && validation(record.credit_entity_sort_code1!)) {
const recordAmount = Number(record.credit_local_amount1) * sign;
updateRecords(
amountsByEntity,
recordAmount,
record.credit_entity_sort_code1!,
record.credit_entity1,
);
amount += recordAmount;
}
if (record.credit_entity2 && validation(record.credit_entity_sort_code2!)) {
const recordAmount = Number(record.credit_local_amount2) * sign;
updateRecords(
amountsByEntity,
recordAmount,
record.credit_entity_sort_code2!,
record.credit_entity2,
);
amount += recordAmount;
}
if (record.debit_entity1 && validation(record.debit_entity_sort_code1!)) {
const recordAmount = Number(record.debit_local_amount1) * sign * -1;
updateRecords(
amountsByEntity,
recordAmount,
record.debit_entity_sort_code1!,
record.debit_entity1,
);
amount += recordAmount;
}
if (record.debit_entity2 && validation(record.debit_entity_sort_code2!)) {
const recordAmount = Number(record.debit_local_amount2) * sign * -1;
updateRecords(
amountsByEntity,
recordAmount,
record.debit_entity_sort_code2!,
record.debit_entity2,
);
amount += recordAmount;
}
});
const records = Array.from(amountsByEntity.entries()).map(([sortCode, data]) => ({
sortCode,
amount: data.amount,
records: Array.from(data.records.entries()).map(([financialEntityId, amount]) => ({
financialEntityId,
amount,
})),
}));
return { amount, records };
}
export function amountByFinancialEntityIdValidation(
unfilteredRecords: DecoratedLedgerRecord[],
validation: (financialEntityId: string) => boolean,
negate: boolean = false,
): number {
let amount = 0;
const sign = negate ? -1 : 1;
unfilteredRecords.map(record => {
if (record.credit_entity1 && validation(record.credit_entity1)) {
amount += Number(record.credit_local_amount1) * sign;
}
if (record.credit_entity2 && validation(record.credit_entity2)) {
amount += Number(record.credit_local_amount2) * sign;
}
if (record.debit_entity1 && validation(record.debit_entity1)) {
amount += Number(record.debit_local_amount1) * sign * -1;
}
if (record.debit_entity2 && validation(record.debit_entity2)) {
amount += Number(record.debit_local_amount2) * sign * -1;
}
});
return amount;
}
export function getProfitLossReportAmounts(decoratedLedgerRecords: DecoratedLedgerRecord[]) {
const { amount: revenueAmount, records: revenueRecords } = amountBySortCodeValidation(
decoratedLedgerRecords,
sortCode => Math.floor(sortCode / 100) === 8,
);
const { amount: costOfSalesAmount, records: costOfSalesRecords } = amountBySortCodeValidation(
decoratedLedgerRecords,
sortCode => [910, 911, 912].includes(sortCode),
);
const grossProfitAmount = revenueAmount + costOfSalesAmount;
const {
amount: researchAndDevelopmentExpensesAmount,
records: researchAndDevelopmentExpensesRecords,
} = amountBySortCodeValidation(decoratedLedgerRecords, sortCode =>
[920, 921, 922, 923, 924, 925, 930, 931].includes(sortCode),
);
const { amount: marketingExpensesAmount, records: marketingExpensesRecords } =
amountBySortCodeValidation(decoratedLedgerRecords, sortCode => [935, 936].includes(sortCode));
const {
amount: managementAndGeneralExpensesAmount,
records: managementAndGeneralExpensesRecords,
} = amountBySortCodeValidation(
decoratedLedgerRecords,
sortCode => sortCode >= 940 && sortCode <= 948, // 940, 941, 942, 943, 944, 945, 947, 948
);
const operatingProfitAmount =
grossProfitAmount +
researchAndDevelopmentExpensesAmount +
marketingExpensesAmount +
managementAndGeneralExpensesAmount;
const { amount: financialExpensesAmount, records: financialExpensesRecords } =
amountBySortCodeValidation(decoratedLedgerRecords, sortCode => sortCode === 990);
const { amount: otherIncomeAmount, records: otherIncomeRecords } = amountBySortCodeValidation(
decoratedLedgerRecords,
sortCode => sortCode === 995,
);
const profitBeforeTaxAmount = operatingProfitAmount + financialExpensesAmount + otherIncomeAmount;
const profitBeforeTaxRecords = [
...revenueRecords,
...costOfSalesRecords,
...researchAndDevelopmentExpensesRecords,
...marketingExpensesRecords,
...managementAndGeneralExpensesRecords,
...financialExpensesRecords,
...otherIncomeRecords,
];
return {
revenueAmount,
revenueRecords,
costOfSalesAmount,
costOfSalesRecords,
grossProfitAmount,
researchAndDevelopmentExpensesAmount,
researchAndDevelopmentExpensesRecords,
marketingExpensesAmount,
marketingExpensesRecords,
managementAndGeneralExpensesAmount,
managementAndGeneralExpensesRecords,
operatingProfitAmount,
financialExpensesAmount,
financialExpensesRecords,
otherIncomeAmount,
otherIncomeRecords,
profitBeforeTaxAmount,
profitBeforeTaxRecords,
};
}