@accounter/server
Version:
271 lines (248 loc) • 9.52 kB
text/typescript
import { startOfDay } from 'date-fns';
import { Injector } from 'graphql-modules';
import { IGetChargesByIdsResult } from '@modules/charges/types.js';
import { DEFAULT_FINANCIAL_ENTITY_ID, EMPTY_UUID } from '@shared/constants';
import { formatCurrency } from '@shared/helpers';
import type { LedgerProto } from '@shared/types';
import { LedgerProvider } from '../providers/ledger.provider.js';
import type {
IGetLedgerRecordsByChargesIdsResult,
IInsertLedgerRecordsParams,
IUpdateLedgerRecordParams,
} from '../types.js';
type LedgerRecordInput = IInsertLedgerRecordsParams['ledgerRecords'][number];
const comparisonKeys: Array<keyof IGetLedgerRecordsByChargesIdsResult> = [
'credit_entity1',
'credit_entity2',
'credit_foreign_amount1',
'credit_foreign_amount2',
'credit_local_amount1',
'credit_local_amount2',
'currency',
'debit_entity1',
'debit_entity2',
'debit_foreign_amount1',
'debit_foreign_amount2',
'debit_local_amount1',
'debit_local_amount2',
'description',
'invoice_date',
'reference1',
'value_date',
];
const dateKeys: Array<keyof IGetLedgerRecordsByChargesIdsResult> = ['value_date', 'invoice_date'];
export function ledgerRecordsGenerationFullMatchComparison(
storageRecords: IGetLedgerRecordsByChargesIdsResult[],
newRecords: readonly IGetLedgerRecordsByChargesIdsResult[],
) {
// compare existing records with new records
const unmatchedStorageRecords = [...storageRecords];
const unmatchedNewRecords: IGetLedgerRecordsByChargesIdsResult[] = [];
const fullMatches = new Map<number, string | undefined>();
newRecords.map((newRecord, index) => {
const matchIndex = unmatchedStorageRecords.findIndex(storageRecord => {
return isExactMatch(storageRecord, newRecord);
});
if (matchIndex !== -1) {
fullMatches.set(index, unmatchedStorageRecords[matchIndex].id);
unmatchedStorageRecords.splice(matchIndex, 1);
return;
}
unmatchedNewRecords.push(newRecord);
});
return {
unmatchedStorageRecords,
unmatchedNewRecords,
fullMatches,
isFullyMatched:
storageRecords.length === newRecords.length && newRecords.length === fullMatches.size,
};
}
export function ledgerRecordsGenerationPartialMatchComparison(
storageRecords: IGetLedgerRecordsByChargesIdsResult[],
newRecords: IGetLedgerRecordsByChargesIdsResult[],
) {
const matches = new Map<string, IGetLedgerRecordsByChargesIdsResult>();
const unmatchedNewRecords = [...newRecords];
storageRecords.map(storageRecord => {
let maxScore = 0;
let bestMatch: IGetLedgerRecordsByChargesIdsResult | undefined = undefined;
unmatchedNewRecords.map(newRecord => {
const score = getMatchScore(storageRecord, newRecord);
if (score < 0) {
return;
}
if (score > maxScore) {
maxScore = score;
bestMatch = newRecord;
}
});
if (bestMatch) {
matches.set(storageRecord.id, bestMatch);
unmatchedNewRecords.splice(unmatchedNewRecords.indexOf(bestMatch), 1);
}
});
const diffs = Array.from(matches.entries()).map(([storageId, newRecord]) => {
const storageRecord = storageRecords.find(record => record.id === storageId);
if (!storageRecord) {
throw new Error('Storage record not found');
}
const recordDiffs: IGetLedgerRecordsByChargesIdsResult = {
...newRecord,
id: storageRecord.id,
};
return recordDiffs;
});
return {
toUpdate: [
...diffs,
...unmatchedNewRecords.map(record => ({
...record,
id: EMPTY_UUID,
})),
],
toRemove: storageRecords.filter(record => !diffs.find(diff => diff.id === record.id)),
};
}
function isExactMatch(
storageRecord: IGetLedgerRecordsByChargesIdsResult,
newRecord: IGetLedgerRecordsByChargesIdsResult,
): boolean {
return comparisonKeys.reduce((isMatch, key) => {
if (!isMatch) {
return false;
}
if (dateKeys.includes(key)) {
const newDate = newRecord[key] ? startOfDay(new Date(storageRecord[key] as Date)) : undefined;
return (storageRecord[key] as Date)?.getTime() === newDate?.getTime();
}
if (typeof storageRecord[key] === 'string' && !Number.isNaN(Number(storageRecord[key]))) {
return Number(storageRecord[key]).toFixed(2) === Number(newRecord[key]).toFixed(2);
}
return storageRecord[key] === newRecord[key];
}, true as boolean);
}
export function convertToStorageInputRecord(record: LedgerProto): LedgerRecordInput {
return {
chargeId: record.chargeId,
ownerId: record.ownerId ?? DEFAULT_FINANCIAL_ENTITY_ID,
creditEntity1: record.creditAccountID1,
creditEntity2: record.creditAccountID2,
creditForeignAmount1: record.creditAmount1,
creditForeignAmount2: record.creditAmount2,
creditLocalAmount1: record.localCurrencyCreditAmount1,
creditLocalAmount2: record.localCurrencyCreditAmount2,
currency: record.currency,
debitEntity1: record.debitAccountID1,
debitEntity2: record.debitAccountID2,
debitForeignAmount1: record.debitAmount1,
debitForeignAmount2: record.debitAmount2,
debitLocalAmount1: record.localCurrencyDebitAmount1,
debitLocalAmount2: record.localCurrencyDebitAmount2,
description: record.description,
invoiceDate: record.invoiceDate,
reference1: record.reference1,
valueDate: record.valueDate,
};
}
function getMatchScore(
storageRecord: IGetLedgerRecordsByChargesIdsResult,
newRecord: IGetLedgerRecordsByChargesIdsResult,
): number {
return comparisonKeys.reduce((cumulativeScore, key) => {
const factor = storageRecord[key] ? 1 : 0.5;
let scoreDirection: number;
if (dateKeys.includes(key)) {
scoreDirection =
(storageRecord[key] as Date)?.getTime() === (newRecord[key] as Date)?.getTime() ? 1 : -1;
} else {
scoreDirection = storageRecord[key] === newRecord[key] ? 1 : -1;
}
const addedScore = factor * scoreDirection;
return cumulativeScore + addedScore;
}, 0);
}
export function convertLedgerRecordToProto(
record: IGetLedgerRecordsByChargesIdsResult,
): LedgerProto {
return {
id: record.id,
creditAccountID1: record.credit_entity1 ?? undefined,
creditAccountID2: record.credit_entity2 ?? undefined,
debitAccountID1: record.debit_entity1 ?? undefined,
debitAccountID2: record.debit_entity2 ?? undefined,
creditAmount1: record.credit_foreign_amount1
? Number(record.credit_foreign_amount1)
: undefined,
creditAmount2: record.credit_foreign_amount2
? Number(record.credit_foreign_amount2)
: undefined,
debitAmount1: record.debit_foreign_amount1 ? Number(record.debit_foreign_amount1) : undefined,
debitAmount2: record.debit_foreign_amount2 ? Number(record.debit_foreign_amount2) : undefined,
localCurrencyCreditAmount1: Number(record.credit_local_amount1),
localCurrencyCreditAmount2: record.credit_local_amount2
? Number(record.credit_local_amount2)
: undefined,
localCurrencyDebitAmount1: Number(record.debit_local_amount1),
localCurrencyDebitAmount2: record.debit_local_amount2
? Number(record.debit_local_amount2)
: undefined,
description: record.description ?? undefined,
invoiceDate: record.invoice_date,
reference1: record.reference1 ?? undefined,
valueDate: record.value_date,
currency: formatCurrency(record.currency),
isCreditorCounterparty: false, // redundant value
ownerId: record.owner_id ?? DEFAULT_FINANCIAL_ENTITY_ID,
currencyRate: undefined,
chargeId: record.charge_id,
};
}
export function convertLedgerRecordToInput(
record: IGetLedgerRecordsByChargesIdsResult,
): LedgerRecordInput | IUpdateLedgerRecordParams {
return {
chargeId: record.charge_id ?? undefined,
ownerId: record.owner_id ?? DEFAULT_FINANCIAL_ENTITY_ID,
creditEntity1: record.credit_entity1 ?? undefined,
creditEntity2: record.credit_entity2 ?? undefined,
debitEntity1: record.debit_entity1 ?? undefined,
debitEntity2: record.debit_entity2 ?? undefined,
creditForeignAmount1: record.credit_foreign_amount1
? Number(record.credit_foreign_amount1)
: undefined,
creditForeignAmount2: record.credit_foreign_amount2
? Number(record.credit_foreign_amount2)
: undefined,
debitForeignAmount1: record.debit_foreign_amount1
? Number(record.debit_foreign_amount1)
: undefined,
debitForeignAmount2: record.debit_foreign_amount2
? Number(record.debit_foreign_amount2)
: undefined,
creditLocalAmount1: Number(record.credit_local_amount1),
creditLocalAmount2: record.credit_local_amount2
? Number(record.credit_local_amount2)
: undefined,
debitLocalAmount1: Number(record.debit_local_amount1),
debitLocalAmount2: record.debit_local_amount2 ? Number(record.debit_local_amount2) : undefined,
description: record.description ?? undefined,
invoiceDate: record.invoice_date,
reference1: record.reference1 ?? undefined,
valueDate: record.value_date,
currency: formatCurrency(record.currency),
ledgerId: record.id,
};
}
export async function storeInitialGeneratedRecords(
charge: IGetChargesByIdsResult,
records: LedgerProto[],
injector: Injector,
) {
if (!charge.ledger_count || Number(charge.ledger_count) === 0) {
const ledgerRecords: IInsertLedgerRecordsParams['ledgerRecords'] = records.map(
convertToStorageInputRecord,
);
await injector.get(LedgerProvider).insertLedgerRecords({ ledgerRecords });
}
}