Skip to content

Commit 3df9d47

Browse files
committed
fix: replace fuzzy matching with reference-based 1:1 matching for Scrypt pending balance
Replace amount+timing greedy matching in getUnmatchedSenders with exact reference matching (bankTx.remittanceInfo === exchangeTx.txId). This eliminates wrong pairings that inflated the pending balance. Additional fixes: - Remove txId filter on EUR receivers (excluded legitimate deposits) - Change status === 'ok' to status !== 'failed' on all Scrypt exchange_tx filters (pending deposits/withdrawals now count as matched) - Reduce sender window from 21 to 7 days (max transfer duration)
1 parent a849c42 commit 3df9d47

2 files changed

Lines changed: 108 additions & 27 deletions

File tree

src/subdomains/supporting/log/__tests__/log-job.service.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,93 @@ describe('LogJobService', () => {
465465
receiver: receiverTx.slice(7),
466466
});
467467
});
468+
469+
// --- getUnmatchedSenders (reference-based matching) ---
470+
471+
it('should match sender and receiver by reference', () => {
472+
const senderTx = [
473+
createCustomBankTx({ id: 1, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-100' }),
474+
];
475+
const receiverTx = [
476+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: 'DEPOSIT-100' }),
477+
];
478+
479+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual([]);
480+
});
481+
482+
it('should return sender when references do not match', () => {
483+
const senderTx = [
484+
createCustomBankTx({ id: 1, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-100' }),
485+
];
486+
const receiverTx = [
487+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: 'DEPOSIT-200' }),
488+
];
489+
490+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual(senderTx);
491+
});
492+
493+
it('should return sender when sender has no reference', () => {
494+
const senderTx = [
495+
createCustomBankTx({ id: 1, created: Util.hoursBefore(24), remittanceInfo: undefined }),
496+
];
497+
const receiverTx = [
498+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: 'DEPOSIT-100' }),
499+
];
500+
501+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual(senderTx);
502+
});
503+
504+
it('should return sender when receiver has no reference', () => {
505+
const senderTx = [
506+
createCustomBankTx({ id: 1, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-100' }),
507+
];
508+
const receiverTx = [
509+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: undefined }),
510+
];
511+
512+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual(senderTx);
513+
});
514+
515+
it('should filter out senders older than 7 days', () => {
516+
const senderTx = [
517+
createCustomBankTx({ id: 1, created: Util.hoursBefore(200), remittanceInfo: 'DEPOSIT-100' }),
518+
];
519+
const receiverTx = [
520+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: 'DEPOSIT-200' }),
521+
];
522+
523+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual([]);
524+
});
525+
526+
it('should return all senders when receiver list is empty', () => {
527+
const senderTx = [
528+
createCustomBankTx({ id: 1, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-100' }),
529+
createCustomBankTx({ id: 2, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-200' }),
530+
];
531+
532+
expect(service.getUnmatchedSenders(senderTx, [])).toEqual(senderTx);
533+
});
534+
535+
it('should match multiple senders partially', () => {
536+
const senderTx = [
537+
createCustomBankTx({ id: 1, created: Util.hoursBefore(48), remittanceInfo: 'DEPOSIT-100' }),
538+
createCustomBankTx({ id: 2, created: Util.hoursBefore(24), remittanceInfo: 'DEPOSIT-200' }),
539+
];
540+
const receiverTx = [
541+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(20), txId: 'DEPOSIT-100' }),
542+
];
543+
544+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual([senderTx[1]]);
545+
});
546+
547+
it('should match ExchangeTx senders by txId against BankTx receivers by remittanceInfo', () => {
548+
const senderTx = [
549+
createCustomExchangeTx({ id: 1, created: Util.hoursBefore(24), txId: 'WITHDRAWAL-50' }),
550+
];
551+
const receiverTx = [
552+
createCustomBankTx({ id: 1, created: Util.hoursBefore(20), remittanceInfo: 'WITHDRAWAL-50' }),
553+
];
554+
555+
expect(service.getUnmatchedSenders(senderTx, receiverTx)).toEqual([]);
556+
});
468557
});

src/subdomains/supporting/log/log-job.service.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export class LogJobService {
371371
(b) => b.accountIban === yapealChfBank.iban && b.creditDebitIndicator === BankTxIndicator.DEBIT,
372372
);
373373
const chfReceiverScryptExchangeTx = recentScryptExchangeTx.filter(
374-
(k) => k.type === ExchangeTxType.DEPOSIT && k.status === 'ok' && k.currency === 'CHF',
374+
(k) => k.type === ExchangeTxType.DEPOSIT && k.status !== 'failed' && k.currency === 'CHF',
375375
);
376376

377377
// sender and receiver data
@@ -401,20 +401,20 @@ export class LogJobService {
401401
b.instructedCurrency,
402402
);
403403
const eurReceiverScryptExchangeTx = recentScryptExchangeTx.filter(
404-
(k) => k.type === ExchangeTxType.DEPOSIT && k.status === 'ok' && k.currency === 'EUR' && k.txId,
404+
(k) => k.type === ExchangeTxType.DEPOSIT && k.status !== 'failed' && k.currency === 'EUR',
405405
);
406406

407407
// CHF: Scrypt -> Yapeal
408408
const chfSenderScryptExchangeTx = recentScryptExchangeTx.filter(
409-
(k) => k.type === ExchangeTxType.WITHDRAWAL && k.status === 'ok' && k.currency === 'CHF',
409+
(k) => k.type === ExchangeTxType.WITHDRAWAL && k.status !== 'failed' && k.currency === 'CHF',
410410
);
411411
const chfReceiverScryptBankTx = recentScryptBankTx.filter(
412412
(b) => b.accountIban === yapealChfBank.iban && b.creditDebitIndicator === BankTxIndicator.CREDIT,
413413
);
414414

415415
// EUR: Scrypt -> Bank
416416
const eurSenderScryptExchangeTx = recentScryptExchangeTx.filter(
417-
(k) => k.type === ExchangeTxType.WITHDRAWAL && k.status === 'ok' && k.currency === 'EUR',
417+
(k) => k.type === ExchangeTxType.WITHDRAWAL && k.status !== 'failed' && k.currency === 'EUR',
418418
);
419419
const eurReceiverScryptBankTx = recentScryptBankTx.filter(
420420
(b) => eurBankIbans.includes(b.accountIban) && b.creditDebitIndicator === BankTxIndicator.CREDIT,
@@ -1061,34 +1061,26 @@ export class LogJobService {
10611061
senderTx: (BankTx | ExchangeTx)[],
10621062
receiverTx: (BankTx | ExchangeTx)[],
10631063
): (BankTx | ExchangeTx)[] {
1064-
const before21Days = Util.daysBefore(21);
1065-
const recentSenders = senderTx.filter((s) => s.created > before21Days);
1064+
const before7Days = Util.daysBefore(7);
1065+
const recentSenders = senderTx.filter((s) => s.created > before7Days);
10661066

10671067
if (!recentSenders.length || !receiverTx.length) return [...recentSenders];
10681068

1069-
const sortedSenders = [...recentSenders].sort((a, b) => a.id - b.id);
1070-
const sortedReceivers = [...receiverTx].sort((a, b) => a.id - b.id);
1071-
const matchedSenderIds = new Set<number>();
1072-
1073-
for (const receiver of sortedReceivers) {
1074-
const receiverAmount = receiver instanceof BankTx ? receiver.instructedAmount : receiver.amount;
1075-
1076-
const match = sortedSenders.find((s) => {
1077-
if (matchedSenderIds.has(s.id)) return false;
1078-
1079-
const senderAmount = s instanceof BankTx ? s.instructedAmount : s.amount;
1080-
const senderDate = s instanceof BankTx ? s.valueDate : s.created;
1081-
const daysDiff = Math.abs(Util.daysDiff(senderDate, receiver.created));
1082-
1083-
return s instanceof BankTx
1084-
? senderAmount === receiverAmount && daysDiff <= 5 && receiver.created > s.created
1085-
: senderAmount === receiverAmount && receiver.created > s.created;
1086-
});
1087-
1088-
if (match) matchedSenderIds.add(match.id);
1069+
const receiverRefs = new Set<string>();
1070+
for (const r of receiverTx) {
1071+
const ref = this.getTxReference(r);
1072+
if (ref) receiverRefs.add(ref);
10891073
}
10901074

1091-
return sortedSenders.filter((s) => !matchedSenderIds.has(s.id));
1075+
return recentSenders.filter((s) => {
1076+
const ref = this.getTxReference(s);
1077+
return !ref || !receiverRefs.has(ref);
1078+
});
1079+
}
1080+
1081+
private getTxReference(tx: BankTx | ExchangeTx): string | undefined {
1082+
if (tx instanceof BankTx) return tx.remittanceInfo?.trim() || undefined;
1083+
return tx.txId?.trim() || undefined;
10921084
}
10931085

10941086
public filterSenderPendingList(

0 commit comments

Comments
 (0)