From dda9c2e36e31e3e332541850359efbd2f055c30b Mon Sep 17 00:00:00 2001 From: yyosifov Date: Wed, 30 Aug 2023 09:15:08 +0300 Subject: [PATCH] Implement parsing of the Amount from the Transaction ID (#537) * Implement parsing of the Amount from the Transaction ID for EUR transactions * Mark unrecognized if we cannot parse and try parsing for all currencies different than BGN --------- Co-authored-by: igoychev --- .../import-transactions.task.spec.ts | 238 ++++++++++++++++++ .../bank-import/import-transactions.task.ts | 35 ++- 2 files changed, 269 insertions(+), 4 deletions(-) diff --git a/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts b/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts index c2a8e3c5f..cce78f1af 100644 --- a/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts +++ b/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts @@ -503,6 +503,244 @@ describe('ImportTransactionsTask', () => { expect(prepareBankTrxSpy).not.toHaveBeenCalled() }) + + it('should handle EUR currency and parse the BGN equivalent from the transactionId', () => { + const eurTransaction: IrisTransactionInfo = { + transactionId: + 'Booked_6516347588_70001524349032963FTRO23184809601C202307034024.69_20230703', + bookingDate: '2023-07-03', + creditorAccount: { + iban: 'BG66UNCR70001524349032', + }, + creditorName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + debtorAccount: { + iban: 'BG21UNCR111111111111', + }, + debtorName: 'Name not relevant for the example', + remittanceInformationUnstructured: '98XF-SZ50-RC8H', + transactionAmount: { + amount: 2069.25, + currency: 'EUR', + }, + exchangeRate: null, + valueDate: '2023-07-03', + creditDebitIndicator: 'CREDIT', + } + + // eslint-disable-next-line + // @ts-ignore + const preparedTransactions = irisTasks.prepareBankTransactionRecords( + [eurTransaction], + irisIBANAccountMock, + ) + + expect(preparedTransactions.length).toEqual(1) + const actual = preparedTransactions[0] + + // We expect to have converted the Amount from EUR to BGN by parsing the transaction ID + const expected = { + id: 'Booked_6516347588_70001524349032963FTRO23184809601C202307034024.69_20230703', + ibanNumber: 'BG66UNCR70009994349032', + bankName: 'UniCredit', + transactionDate: new Date('2023-07-03T00:00:00.000Z'), + senderName: 'Name not relevant for the example', + recipientName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + senderIban: 'BG21UNCR111111111111', + recipientIban: 'BG66UNCR70001524349032', + type: 'credit', + amount: 402469, + currency: 'BGN', + description: '98XF-SZ50-RC8H', + matchedRef: '98XF-SZ50-RC8H', + } + + expect(actual).toEqual(expected) + }) + + it('should handle USD currency and parse the BGN equivalent from the transactionId', () => { + const eurTransaction: IrisTransactionInfo = { + transactionId: + 'Booked_6516347588_70001524349032963FTRO23184809601C2023010361.12_20230103', + bookingDate: '2023-01-03', + creditorAccount: { + iban: 'BG66UNCR70001524349032', + }, + creditorName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + debtorAccount: { + iban: 'BG21UNCR111111111111', + }, + debtorName: 'Name not relevant for the example', + remittanceInformationUnstructured: '98XF-SZ50-RC8H', + transactionAmount: { + amount: 30.56, + currency: 'USD', + }, + exchangeRate: null, + valueDate: '2023-01-03', + creditDebitIndicator: 'CREDIT', + } + + // eslint-disable-next-line + // @ts-ignore + const preparedTransactions = irisTasks.prepareBankTransactionRecords( + [eurTransaction], + irisIBANAccountMock, + ) + + expect(preparedTransactions.length).toEqual(1) + const actual = preparedTransactions[0] + + // We expect to have converted the Amount from EUR to BGN by parsing the transaction ID + const expected = { + id: 'Booked_6516347588_70001524349032963FTRO23184809601C2023010361.12_20230103', + ibanNumber: 'BG66UNCR70009994349032', + bankName: 'UniCredit', + transactionDate: new Date('2023-01-03T00:00:00.000Z'), + senderName: 'Name not relevant for the example', + recipientName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + senderIban: 'BG21UNCR111111111111', + recipientIban: 'BG66UNCR70001524349032', + type: 'credit', + amount: 6112, + currency: 'BGN', + description: '98XF-SZ50-RC8H', + matchedRef: '98XF-SZ50-RC8H', + } + + expect(actual).toEqual(expected) + }) + + it('should set matchedRef to null when the EUR currency amount cannot be parsed from the transaction id', () => { + const eurTransaction: IrisTransactionInfo = { + transactionId: + 'Booked_6516347588_70001524349032963FTRO23184809601C20230703notanumber_20230703', + bookingDate: '2023-07-03', + creditorAccount: { + iban: 'BG66UNCR70001524349032', + }, + creditorName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + debtorAccount: { + iban: 'BG21UNCR111111111111', + }, + debtorName: 'Name not relevant for the example', + remittanceInformationUnstructured: '98XF-SZ50-RC8H', + transactionAmount: { + amount: 2069.25, + currency: 'EUR', + }, + exchangeRate: null, + valueDate: '2023-07-03', + creditDebitIndicator: 'CREDIT', + } + + // eslint-disable-next-line + // @ts-ignore + const preparedTransactions = irisTasks.prepareBankTransactionRecords( + [eurTransaction], + irisIBANAccountMock, + ) + + expect(preparedTransactions.length).toEqual(1) + const actual = preparedTransactions[0] + + // We expect to have converted the Amount from EUR to BGN by parsing the transaction ID + const expected = { + id: 'Booked_6516347588_70001524349032963FTRO23184809601C20230703notanumber_20230703', + ibanNumber: 'BG66UNCR70009994349032', + bankName: 'UniCredit', + transactionDate: new Date('2023-07-03T00:00:00.000Z'), + senderName: 'Name not relevant for the example', + recipientName: 'СДРУЖЕНИЕ ПОДКРЕПИ БГ', + senderIban: 'BG21UNCR111111111111', + recipientIban: 'BG66UNCR70001524349032', + type: 'credit', + amount: 206925, + currency: 'EUR', + description: '98XF-SZ50-RC8H', + matchedRef: null, + } + + expect(actual).toEqual(expected) + }) + + describe('extractAmountFromTransactionId', () => { + it('can parse a whole number', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C202307032018_20230703', + '2023-07-03', + ) + + expect(amount).toBe(2018) + }) + + it('can parse a floating number', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C202307031300.500_20230703', + '2023-07-03', + ) + + expect(amount).toBe(1300.5) + }) + + it('can parse a zero', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C202307030_20230703', + '2023-07-03', + ) + + expect(amount).toBe(0) + }) + + it('will not parse a negative number', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C20230703-2018_20230703', + '2023-07-03', + ) + + expect(amount).toBe(NaN) + }) + + it('will not parse empty number', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C20230703_20230703', + '2023-07-03', + ) + + expect(amount).toBe(NaN) + }) + + it('will not parse invalid floating number', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C20230703130.10.500_20230703', + '2023-07-03', + ) + + expect(amount).toBe(NaN) + }) + + it('will not parse string', () => { + // eslint-disable-next-line + // @ts-ignore + const amount = irisTasks.extractAmountFromTransactionId( + 'Booked_6516347588_70001524349032963FTRO23184809601C20230703test_20230703', + '2023-07-03', + ) + + expect(amount).toBe(NaN) + }) + }) }) describe('notifyForExpiringIrisConsentTASK', () => { diff --git a/apps/api/src/tasks/bank-import/import-transactions.task.ts b/apps/api/src/tasks/bank-import/import-transactions.task.ts index 78db8eb5b..ec7fc1543 100644 --- a/apps/api/src/tasks/bank-import/import-transactions.task.ts +++ b/apps/api/src/tasks/bank-import/import-transactions.task.ts @@ -277,6 +277,15 @@ export class IrisTasks { return transactions.length === count } + private extractAmountFromTransactionId(transactionId, transactionValueDate): number { + const formattedDate = DateTime.fromISO(transactionValueDate).toFormat('yyyyMMdd') + const matchAmountRegex = new RegExp(`${formattedDate}(?[0-9.]+)_${formattedDate}`) + + const amount = Number(transactionId.match(matchAmountRegex)?.groups?.amount) + + return amount + } + // Only prepares the data, without inserting it in the DB private prepareBankTransactionRecords( transactions: IrisTransactionInfo[], @@ -296,13 +305,31 @@ export class IrisTasks { } // Try to recognize campaign payment reference - const matchedRef = trx.remittanceInformationUnstructured + let matchedRef = trx.remittanceInformationUnstructured ?.trim() .replace(/[ _]+/g, '-') .match(this.regexPaymentRef) + const transactionAmount = { + amount: trx.transactionAmount?.amount, + currency: trx.transactionAmount?.currency, + } + const id = trx.transactionId?.trim() || '' + + // If we receive a transaction with Currency different than BGN - try parsing from the transaction id the amount in BGN + if (trx.transactionAmount?.currency !== Currency.BGN && trx.transactionAmount?.amount > 0) { + const amount = this.extractAmountFromTransactionId(id, trx.valueDate) + if (amount) { + transactionAmount.amount = amount + transactionAmount.currency = Currency.BGN + } else { + // mark as unrecognized + matchedRef = null; + } + } + filteredTransactions.push({ - id: trx.transactionId?.trim() || ``, + id: id, ibanNumber: ibanAccount.iban, bankName: ibanAccount.bankName, bankIdCode: this.bankBIC, @@ -312,8 +339,8 @@ export class IrisTasks { senderIban: trx.debtorAccount?.iban?.trim(), recipientIban: trx.creditorAccount?.iban?.trim(), type: trx.creditDebitIndicator === 'CREDIT' ? 'credit' : 'debit', - amount: toMoney(trx.transactionAmount?.amount), - currency: trx.transactionAmount?.currency, + amount: toMoney(transactionAmount.amount), + currency: transactionAmount.currency, description: trx.remittanceInformationUnstructured?.trim(), // Not saved in the DB, it's added only for convinience and efficiency matchedRef: matchedRef ? matchedRef[0] : null,