Skip to content

Commit

Permalink
Implement parsing of the Amount from the Transaction ID (#537)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
yyosifov and igoychev authored Aug 30, 2023
1 parent 0118cba commit dda9c2e
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 4 deletions.
238 changes: 238 additions & 0 deletions apps/api/src/tasks/bank-import/import-transactions.task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
35 changes: 31 additions & 4 deletions apps/api/src/tasks/bank-import/import-transactions.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}(?<amount>[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[],
Expand All @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit dda9c2e

Please sign in to comment.