@accounter/server
Version:
Accounter GraphQL server
891 lines (821 loc) • 30.8 kB
text/typescript
import { describe, expect, it } from 'vitest';
import type { IGetChargesByIdsResult } from '../../../charges/types.js';
import type { IGetReferenceMergeCandidatesResult } from '../../types.js';
import { buildMergeChargesByTransactionReferencePlan } from '../merge-charges-by-reference.helper.js';
function buildCharge(
id: string,
overrides: Partial<IGetChargesByIdsResult> = {},
): IGetChargesByIdsResult {
return {
id,
accountant_status: 'PENDING',
created_at: new Date('2026-01-01T00:00:00.000Z'),
documents_optional_flag: false,
invoice_payment_currency_diff: null,
is_property: false,
optional_vat: false,
owner_id: 'owner-1',
tax_category_id: null,
type: 'COMMON',
updated_at: new Date('2026-01-01T00:00:00.000Z'),
user_description: null,
...overrides,
};
}
function buildCandidate(
id: string,
chargeId: string,
reference: string,
eventDate: string,
overrides: Partial<IGetReferenceMergeCandidatesResult> = {},
): IGetReferenceMergeCandidatesResult {
return {
id,
account_id: 'account-1',
amount: '10.00',
business_id: null,
charge_id: chargeId,
counter_account: null,
created_at: new Date('2026-01-01T00:00:00.000Z'),
currency: 'ILS',
currency_rate: '1',
current_balance: '0',
debit_date: null,
debit_date_override: null,
debit_timestamp: null,
event_date: new Date(eventDate),
is_fee: false,
origin_key: 'origin-key',
origin_user_description: null,
owner_id: 'owner-1',
source_description: null,
source_id: `source-${id}`,
source_origin: 'POALIM_ILS',
source_reference: reference,
updated_at: new Date('2026-01-01T00:00:00.000Z'),
...overrides,
};
}
function buildChargeById(charges: IGetChargesByIdsResult[]) {
return new Map(charges.map(charge => [charge.id, charge]));
}
describe('buildMergeChargesByTransactionReferencePlan', () => {
it('builds separate plans for disconnected candidate clusters', () => {
const charges = [
buildCharge('charge-a', { created_at: new Date('2026-01-01T00:00:00.000Z') }),
buildCharge('charge-b', { created_at: new Date('2026-01-02T00:00:00.000Z') }),
buildCharge('charge-c', { created_at: new Date('2026-01-03T00:00:00.000Z') }),
buildCharge('charge-d', { created_at: new Date('2026-01-04T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-a', 'charge-a', 'REF-1', '2026-01-01T10:00:00.000Z'),
buildCandidate('tx-b', 'charge-b', 'REF-1', '2026-01-01T14:00:00.000Z'),
buildCandidate('tx-c', 'charge-c', 'REF-1', '2026-03-15T10:00:00.000Z'),
buildCandidate('tx-d', 'charge-d', 'REF-1', '2026-03-15T14:00:00.000Z'),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-1',
baseChargeId: 'charge-a',
chargeIdsToMerge: ['charge-b'],
},
{
reference: 'REF-1',
baseChargeId: 'charge-c',
chargeIdsToMerge: ['charge-d'],
},
]);
});
it('matches candidates by wide date window when descriptions contain each other', () => {
const charges = [
buildCharge('charge-a', { created_at: new Date('2026-01-01T00:00:00.000Z') }),
buildCharge('charge-b', { created_at: new Date('2026-01-02T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-a', 'charge-a', 'REF-2', '2026-01-01T10:00:00.000Z', {
source_description: 'payment for vendor abc',
}),
buildCandidate('tx-b', 'charge-b', 'REF-2', '2026-01-20T10:00:00.000Z', {
source_description: 'vendor abc',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-2',
baseChargeId: 'charge-a',
chargeIdsToMerge: ['charge-b'],
},
]);
});
it('prefers a non-fee charge as the merge base', () => {
const charges = [
buildCharge('charge-fee', {
created_at: new Date('2026-01-01T00:00:00.000Z'),
}),
buildCharge('charge-main', {
created_at: new Date('2026-01-02T00:00:00.000Z'),
}),
];
const candidates = [
buildCandidate('tx-fee', 'charge-fee', 'REF-3', '2026-01-01T10:00:00.000Z', {
is_fee: true,
}),
buildCandidate('tx-main', 'charge-main', 'REF-3', '2026-01-01T12:00:00.000Z', {
is_fee: false,
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-3',
baseChargeId: 'charge-main',
chargeIdsToMerge: ['charge-fee'],
},
]);
});
it('keeps the first assignment when a target charge appears in multiple plans', () => {
const charges = [
buildCharge('charge-main-1', { created_at: new Date('2026-01-01T00:00:00.000Z') }),
buildCharge('charge-main-2', { created_at: new Date('2026-01-03T00:00:00.000Z') }),
buildCharge('charge-target', {
created_at: new Date('2026-01-02T00:00:00.000Z'),
user_description: 'unlinked from charge',
}),
];
const candidates = [
buildCandidate('tx-1', 'charge-main-1', 'REF-4', '2026-01-01T10:00:00.000Z'),
buildCandidate('tx-2', 'charge-target', 'REF-4', '2026-01-01T11:00:00.000Z', {
is_fee: true,
}),
buildCandidate('tx-3', 'charge-main-2', 'REF-5', '2026-01-02T10:00:00.000Z'),
buildCandidate('tx-4', 'charge-target', 'REF-5', '2026-01-02T11:00:00.000Z', {
is_fee: true,
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.plans).toEqual([
{
reference: 'REF-4',
baseChargeId: 'charge-main-1',
chargeIdsToMerge: ['charge-target'],
},
]);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('charge-target');
});
it('consolidates transitive plans into a single merge base', () => {
const charges = [
buildCharge('charge-a', { created_at: new Date('2026-01-01T00:00:00.000Z') }),
buildCharge('charge-b', { created_at: new Date('2026-01-02T00:00:00.000Z') }),
buildCharge('charge-c', { created_at: new Date('2026-01-03T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-a-ref-a', 'charge-a', 'REF-A', '2026-01-01T10:00:00.000Z', {
amount: '500.00',
}),
buildCandidate('tx-b-ref-a', 'charge-b', 'REF-A', '2026-01-01T11:00:00.000Z', {
amount: '-10.00',
is_fee: true,
}),
buildCandidate('tx-b-ref-b', 'charge-b', 'REF-B', '2026-01-02T10:00:00.000Z', {
amount: '650.00',
}),
buildCandidate('tx-c-ref-b', 'charge-c', 'REF-B', '2026-01-02T11:00:00.000Z', {
amount: '-12.00',
is_fee: true,
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-A',
baseChargeId: 'charge-a',
chargeIdsToMerge: ['charge-b', 'charge-c'],
},
]);
});
it('does not merge recurring monthly payments with identical details', () => {
const charges = [
buildCharge('charge-june', { created_at: new Date('2026-06-01T00:00:00.000Z') }),
buildCharge('charge-july', { created_at: new Date('2026-07-01T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-june', 'charge-june', 'REF-6', '2026-06-01T10:00:00.000Z', {
amount: '-2500.00',
source_description: 'standing order recurring service payment',
}),
buildCandidate('tx-july', 'charge-july', 'REF-6', '2026-07-01T10:00:00.000Z', {
amount: '-2500.00',
source_description: 'standing order recurring service payment',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge distinct same-reference income payments with different amounts', () => {
const charges = [
buildCharge('charge-income-1', { created_at: new Date('2026-04-19T00:00:00.000Z') }),
buildCharge('charge-income-2', { created_at: new Date('2026-04-25T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-income-1', 'charge-income-1', 'REF-7', '2026-04-19T10:00:00.000Z', {
currency: 'EUR',
amount: '1000.00',
source_description: 'incoming transfer from customer alpha',
}),
buildCandidate('tx-income-2', 'charge-income-2', 'REF-7', '2026-04-25T10:00:00.000Z', {
currency: 'EUR',
amount: '1100.00',
source_description: 'incoming transfer from customer alpha',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('merges same-day principal and interest records under one reference', () => {
const charges = [
buildCharge('charge-interest', { created_at: new Date('2026-07-06T00:00:00.000Z') }),
buildCharge('charge-principal', { created_at: new Date('2026-07-06T00:00:01.000Z') }),
];
const candidates = [
buildCandidate('tx-interest', 'charge-interest', 'REF-8', '2026-07-06T10:00:00.000Z', {
amount: '0.02',
source_description: 'deposit interest credit',
}),
buildCandidate('tx-principal', 'charge-principal', 'REF-8', '2026-07-06T10:00:00.000Z', {
amount: '395.04',
source_description: 'deposit withdrawal principal',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-8',
baseChargeId: 'charge-interest',
chargeIdsToMerge: ['charge-principal'],
},
]);
});
it('does not merge same-reference same-day income transfers from different clients', () => {
const charges = [
buildCharge('charge-client-a', { created_at: new Date('2026-08-15T00:00:00.000Z') }),
buildCharge('charge-client-b', { created_at: new Date('2026-08-16T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-client-a', 'charge-client-a', 'REF-10', '2026-08-15T10:00:00.000Z', {
currency: 'USD',
amount: '1492.50',
source_description: 'incoming transfer client alpha',
}),
buildCandidate('tx-client-b', 'charge-client-b', 'REF-10', '2026-08-16T10:00:00.000Z', {
currency: 'USD',
amount: '3292.50',
source_description: 'incoming transfer client beta',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge three monthly same-reference payments from the same client', () => {
const charges = [
buildCharge('charge-month-1', { created_at: new Date('2026-01-06T00:00:00.000Z') }),
buildCharge('charge-month-2', { created_at: new Date('2026-02-04T00:00:00.000Z') }),
buildCharge('charge-month-3', { created_at: new Date('2026-03-04T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-month-1', 'charge-month-1', 'REF-11', '2026-01-06T10:00:00.000Z', {
currency: 'ILS',
amount: '8985.50',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
buildCandidate('tx-month-2', 'charge-month-2', 'REF-11', '2026-02-04T10:00:00.000Z', {
currency: 'ILS',
amount: '8795.75',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
buildCandidate('tx-month-3', 'charge-month-3', 'REF-11', '2026-03-04T10:00:00.000Z', {
currency: 'ILS',
amount: '8859.00',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge repeated same-reference payments in shorter cadence', () => {
const charges = [
buildCharge('charge-repeat-1', { created_at: new Date('2026-10-02T00:00:00.000Z') }),
buildCharge('charge-repeat-2', { created_at: new Date('2026-10-28T00:00:00.000Z') }),
buildCharge('charge-repeat-3', { created_at: new Date('2026-11-12T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-repeat-1', 'charge-repeat-1', 'REF-12', '2026-10-02T10:00:00.000Z', {
currency: 'ILS',
amount: '9147.75',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
buildCandidate('tx-repeat-2', 'charge-repeat-2', 'REF-12', '2026-10-28T10:00:00.000Z', {
currency: 'ILS',
amount: '9233.50',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
buildCandidate('tx-repeat-3', 'charge-repeat-3', 'REF-12', '2026-11-12T10:00:00.000Z', {
currency: 'ILS',
amount: '9146.50',
source_description: 'rtgs incoming payment customer omega',
counter_account: '912-799-705101',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge repeated client transfer with distinct swift fee references', () => {
const charges = [
buildCharge('charge-swift-1', { created_at: new Date('2026-02-17T00:00:00.000Z') }),
buildCharge('charge-swift-2', { created_at: new Date('2026-02-09T00:00:00.000Z') }),
];
const candidates = [
buildCandidate('tx-swift-fee-1', 'charge-swift-1', 'SWIFT-REF-1', '2026-02-17T10:00:00.000Z', {
currency: 'USD',
amount: '-25.00',
is_fee: true,
source_description: 'swift fee vendor invoice 40430',
counter_account: 'SWIFT',
}),
buildCandidate('tx-transfer-1', 'charge-swift-1', 'REF-13', '2026-02-17T10:00:00.000Z', {
currency: 'USD',
amount: '2575.00',
source_description: 'incoming transfer vendor alpha',
}),
buildCandidate('tx-transfer-2', 'charge-swift-2', 'REF-13', '2026-02-09T10:00:00.000Z', {
currency: 'USD',
amount: '2575.00',
source_description: 'incoming transfer vendor alpha',
}),
buildCandidate('tx-swift-fee-2', 'charge-swift-2', 'SWIFT-REF-2', '2026-02-09T10:00:00.000Z', {
currency: 'USD',
amount: '-25.00',
is_fee: true,
source_description: 'swift fee vendor invoice 40443',
counter_account: 'SWIFT',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge repeated conversion charges with the same bank reference', () => {
const charges = [
buildCharge('charge-conversion-1', {
created_at: new Date('2026-07-17T00:00:00.000Z'),
type: 'CONVERSION',
user_description: 'USD to EUR conversion',
}),
buildCharge('charge-conversion-2', {
created_at: new Date('2026-06-29T00:00:00.000Z'),
type: 'CONVERSION',
user_description: 'USD to EUR conversion',
}),
];
const candidates = [
buildCandidate('tx-conversion-1-usd', 'charge-conversion-1', 'CONV-10086', '2026-07-15T10:00:00.000Z', {
currency: 'USD',
amount: '-115945.91',
source_description: 'conversion',
}),
buildCandidate('tx-conversion-1-eur', 'charge-conversion-1', 'CONV-10086', '2026-07-15T10:00:00.000Z', {
currency: 'EUR',
amount: '99000.00',
source_description: 'conversion',
}),
buildCandidate('tx-conversion-2-eur', 'charge-conversion-2', 'CONV-10086', '2026-06-26T10:00:00.000Z', {
currency: 'EUR',
amount: '101000.00',
source_description: 'conversion',
}),
buildCandidate('tx-conversion-2-usd', 'charge-conversion-2', 'CONV-10086', '2026-06-26T10:00:00.000Z', {
currency: 'USD',
amount: '-118935.56',
source_description: 'conversion',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge foreign-securities charges across cycles by shared fee reference', () => {
const charges = [
buildCharge('charge-securities-a', {
created_at: new Date('2026-03-04T00:00:00.000Z'),
type: 'FOREIGN_SECURITIES',
}),
buildCharge('charge-securities-b', {
created_at: new Date('2026-02-05T00:00:00.000Z'),
type: 'FOREIGN_SECURITIES',
}),
];
const candidates = [
buildCandidate('tx-securities-payment-a', 'charge-securities-a', 'SEC-REF-A', '2026-03-04T10:00:00.000Z', {
currency: 'USD',
amount: '2178.66',
source_description: 'securities settlement payment batch',
}),
buildCandidate('tx-securities-fee-a', 'charge-securities-a', 'SEC-FEE-REF', '2026-03-04T10:00:00.000Z', {
currency: 'ILS',
amount: '-33.48',
is_fee: true,
source_description: 'securities settlement payment fee',
}),
buildCandidate('tx-securities-payment-b', 'charge-securities-b', 'SEC-REF-B', '2026-02-05T10:00:00.000Z', {
currency: 'USD',
amount: '2640.09',
source_description: 'securities settlement payment batch',
}),
buildCandidate('tx-securities-fee-b', 'charge-securities-b', 'SEC-FEE-REF', '2026-02-05T10:00:00.000Z', {
currency: 'ILS',
amount: '-40.74',
is_fee: true,
source_description: 'securities settlement payment fee',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('merges foreign-securities fee charge when main description contains padded fee reference', () => {
const charges = [
buildCharge('charge-securities-main', {
created_at: new Date('2026-07-08T00:00:00.000Z'),
type: 'FOREIGN_SECURITIES',
}),
buildCharge('charge-securities-fee', {
created_at: new Date('2026-07-08T00:00:01.000Z'),
type: 'FOREIGN_SECURITIES',
}),
];
const candidates = [
buildCandidate('tx-securities-fee', 'charge-securities-fee', '79915674', '2026-07-07T10:00:00.000Z', {
currency: 'ILS',
amount: '-88.80',
is_fee: true,
source_origin: 'POALIM',
source_description: 'fsec payment fee',
}),
buildCandidate('tx-securities-main', 'charge-securities-main', '3901767', '2026-07-07T10:00:00.000Z', {
currency: 'USD',
amount: '4521.61',
source_origin: 'POALIM',
source_description: 'securities settlement 0079915674',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: '79915674',
baseChargeId: 'charge-securities-main',
chargeIdsToMerge: ['charge-securities-fee'],
},
]);
});
it('merges foreign-securities fee charge when main description contains padded fee reference and charges types are not defined', () => {
const charges = [
buildCharge('charge-securities-main', {
created_at: new Date('2026-07-08T00:00:00.000Z'),
}),
buildCharge('charge-securities-fee', {
created_at: new Date('2026-07-08T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-securities-fee', 'charge-securities-fee', '79915674', '2026-07-07T10:00:00.000Z', {
currency: 'ILS',
amount: '-88.80',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ני"עז עמ קניה FSEC BUY FEE',
}),
buildCandidate('tx-securities-main', 'charge-securities-main', '3901767', '2026-07-07T10:00:00.000Z', {
currency: 'USD',
amount: '4521.61',
source_origin: 'POALIM',
source_description: 'ניע"ז תשלומים 0079915674',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: '79915674',
baseChargeId: 'charge-securities-main',
chargeIdsToMerge: ['charge-securities-fee'],
},
]);
});
it('merges foreign-securities fee with padded reference alias from main transaction description', () => {
const charges = [
buildCharge('charge-foreign-main', {
created_at: new Date('2026-08-10T00:00:00.000Z'),
}),
buildCharge('charge-foreign-fee', {
created_at: new Date('2026-08-10T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-foreign-fee', 'charge-foreign-fee', '12345678', '2026-08-09T10:00:00.000Z', {
currency: 'ILS',
amount: '-41.20',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ניעז עמ תשלום fsec pymnt fee',
}),
buildCandidate('tx-foreign-main', 'charge-foreign-main', '7654321', '2026-08-09T10:00:00.000Z', {
currency: 'USD',
amount: '3120.55',
source_origin: 'POALIM',
source_description: 'ניע"ז תשלומים 0012345678',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: '12345678',
baseChargeId: 'charge-foreign-main',
chargeIdsToMerge: ['charge-foreign-fee'],
},
]);
});
it('merges foreign-securities fee when main transaction contains zero-padded fee reference', () => {
const charges = [
buildCharge('charge-fsec-main', {
created_at: new Date('2026-09-03T00:00:00.000Z'),
}),
buildCharge('charge-fsec-fee', {
created_at: new Date('2026-09-03T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-fsec-fee', 'charge-fsec-fee', '88997766', '2026-09-02T10:00:00.000Z', {
currency: 'ILS',
amount: '-37.45',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ניעז עמ תשלום fsec pymnt fee',
}),
buildCandidate('tx-fsec-main', 'charge-fsec-main', '2233445', '2026-09-02T10:00:00.000Z', {
currency: 'USD',
amount: '4270.00',
source_origin: 'POALIM',
source_description: 'ניע"ז תשלומים 0088997766',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: '88997766',
baseChargeId: 'charge-fsec-main',
chargeIdsToMerge: ['charge-fsec-fee'],
},
]);
});
it('does not merge foreign-securities alias pair when source origin is not POALIM', () => {
const charges = [
buildCharge('charge-origin-main', {
created_at: new Date('2026-10-03T00:00:00.000Z'),
}),
buildCharge('charge-origin-fee', {
created_at: new Date('2026-10-03T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-origin-fee', 'charge-origin-fee', '90112233', '2026-10-02T10:00:00.000Z', {
currency: 'ILS',
amount: '-18.00',
is_fee: true,
source_origin: 'POALIM_ILS',
source_description: 'ניעז עמ תשלום fsec pymnt fee',
}),
buildCandidate('tx-origin-main', 'charge-origin-main', '3322110', '2026-10-02T10:00:00.000Z', {
currency: 'USD',
amount: '1810.00',
source_origin: 'POALIM_ILS',
source_description: 'ניע"ז תשלומים 0090112233',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge foreign-securities alias pair across different dates', () => {
const charges = [
buildCharge('charge-date-main', {
created_at: new Date('2026-11-03T00:00:00.000Z'),
}),
buildCharge('charge-date-fee', {
created_at: new Date('2026-11-03T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-date-fee', 'charge-date-fee', '77123456', '2026-11-01T10:00:00.000Z', {
currency: 'ILS',
amount: '-21.00',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ניעז עמ תשלום fsec pymnt fee',
}),
buildCandidate('tx-date-main', 'charge-date-main', '4411002', '2026-11-03T10:00:00.000Z', {
currency: 'USD',
amount: '2205.50',
source_origin: 'POALIM',
source_description: 'ניע"ז תשלומים 0077123456',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('does not merge foreign-securities alias pair when non-fee references differ', () => {
const charges = [
buildCharge('charge-action-a', {
created_at: new Date('2026-12-03T00:00:00.000Z'),
}),
buildCharge('charge-action-b', {
created_at: new Date('2026-12-03T00:00:01.000Z'),
}),
];
const candidates = [
buildCandidate('tx-action-a-main', 'charge-action-a', '3900138', '2026-12-02T10:00:00.000Z', {
currency: 'USD',
amount: '78.55',
source_origin: 'POALIM',
source_description: 'ניע"ז תשלומים 0077448736',
}),
buildCandidate('tx-action-a-fee', 'charge-action-a', '77448736', '2026-12-02T10:00:00.000Z', {
currency: 'ILS',
amount: '-9.10',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ניעז עמ תשלום fsec pymnt fee',
}),
buildCandidate('tx-action-b-fee', 'charge-action-b', '1501588', '2026-12-02T10:00:00.000Z', {
currency: 'USD',
amount: '-75.38',
is_fee: true,
source_origin: 'POALIM',
source_description: 'ניע"ז עמ קניה 0077448736',
}),
buildCandidate('tx-action-b-main', 'charge-action-b', '1501588', '2026-12-02T10:00:00.000Z', {
currency: 'USD',
amount: '-75379.79',
source_origin: 'POALIM',
source_description: 'ניע"ז קניה 0077448736',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([]);
});
it('keeps conversion pair separate and merges transfer with related fees', () => {
const charges = [
buildCharge('charge-conversion', {
created_at: new Date('2026-03-02T00:00:00.000Z'),
type: 'CONVERSION',
}),
buildCharge('charge-transfer', {
created_at: new Date('2026-03-02T00:00:01.000Z'),
type: null,
}),
buildCharge('charge-fee-a', {
created_at: new Date('2026-03-02T00:00:02.000Z'),
type: null,
}),
buildCharge('charge-fee-b', {
created_at: new Date('2026-03-05T00:00:02.000Z'),
type: null,
}),
];
const candidates = [
buildCandidate('tx-conversion-base', 'charge-conversion', 'REF-9', '2026-03-02T10:00:00.000Z', {
currency: 'EUR',
amount: '1000.00',
source_description: 'conversion batch 26',
}),
buildCandidate('tx-conversion-quote', 'charge-conversion', 'REF-9', '2026-03-02T10:00:00.000Z', {
currency: 'USD',
amount: '-1180.00',
source_description: 'conversion batch 26',
}),
buildCandidate('tx-transfer', 'charge-transfer', 'REF-9', '2026-03-02T10:00:00.000Z', {
currency: 'EUR',
amount: '-1000.00',
source_description: 'fx transfer batch 26',
}),
buildCandidate('tx-fee-a', 'charge-fee-a', 'REF-9', '2026-03-02T10:00:00.000Z', {
currency: 'ILS',
amount: '-2.00',
is_fee: true,
source_description: 'fx transfer commission batch 26',
}),
buildCandidate('tx-fee-b', 'charge-fee-b', 'REF-9', '2026-03-05T10:00:00.000Z', {
currency: 'EUR',
amount: '-10.00',
is_fee: true,
source_description: 'bank fee for transfer batch 26',
}),
];
const result = buildMergeChargesByTransactionReferencePlan({
candidates,
chargeById: buildChargeById(charges),
});
expect(result.errors).toEqual([]);
expect(result.plans).toEqual([
{
reference: 'REF-9',
baseChargeId: 'charge-transfer',
chargeIdsToMerge: ['charge-fee-a', 'charge-fee-b'],
},
]);
});
});