From 76de8bf67f3e5688b7aa6e34050f440a30e39676 Mon Sep 17 00:00:00 2001 From: Guillaume Taquet Gasperini <9089317+Aerion@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:23:10 +0200 Subject: [PATCH] Fix Boursorama GoCardless transaction ordering (#5344) I wrongly thought that all the card transactions will always have their first line with the `CARTE` identifier. But as I have seen recently, it's not the case, and we shouldn't rely on the ordering of the array returned by the Boursorama GoCardless integration. Thus, check for transaction patterns in all of the lines of the unstructured array. This addresses a true case (added in test) where the payee name was wrongly extracted as being `110,04 Gbp / 1 Euro = 0,860763454` --- .../banks/boursobank_bousfrppxxx.js | 83 ++++++++++--------- .../tests/boursobank_bousfrppxxx.spec.js | 21 +++++ upcoming-release-notes/5344.md | 6 ++ 3 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 upcoming-release-notes/5344.md diff --git a/packages/sync-server/src/app-gocardless/banks/boursobank_bousfrppxxx.js b/packages/sync-server/src/app-gocardless/banks/boursobank_bousfrppxxx.js index 9d2eb9aa2d..45cef56832 100644 --- a/packages/sync-server/src/app-gocardless/banks/boursobank_bousfrppxxx.js +++ b/packages/sync-server/src/app-gocardless/banks/boursobank_bousfrppxxx.js @@ -30,61 +30,62 @@ export default { .filter(line => line.startsWith('Réf : ') === false); const infoArray = editedTrans.remittanceInformationUnstructuredArray; - const firstLine = infoArray[0]; let match; - /* - * Some transactions always have their identifier in the first line (e.g. card), - * while others have it **randomly** in any of the lines (e.g. transfers). - */ - - // Check the first line for specific patterns - if ((match = firstLine.match(regexCard))) { + // Transactions can have their identifier in any line, as the order of lines is not guaranteed. + // This is why we check all lines for specific patterns. + if ((match = infoArray.find(line => regexCard.test(line)))) { // Card transaction - const payeeName = match.groups.payeeName; - editedTrans.payeeName = title(payeeName); - editedTrans.notes = `Carte ${match.groups.date}`; + const cardMatch = match.match(regexCard); + editedTrans.payeeName = title(cardMatch.groups.payeeName); + editedTrans.notes = `Carte ${cardMatch.groups.date}`; if (infoArray.length > 1) { - editedTrans.notes += ' ' + infoArray.slice(1).join(' '); + editedTrans.notes += ' ' + infoArray.filter(l => l !== match).join(' '); } - } else if ((match = firstLine.match(regexLoan))) { + } else if ((match = infoArray.find(line => regexLoan.test(line)))) { // Loan editedTrans.payeeName = 'Prêt bancaire'; - editedTrans.notes = firstLine; - } else if ((match = firstLine.match(regexAtmWithdrawal))) { + editedTrans.notes = match; + } else if ( + (match = infoArray.find(line => regexAtmWithdrawal.test(line))) + ) { // ATM withdrawal + const atmMatch = match.match(regexAtmWithdrawal); editedTrans.payeeName = 'Retrait DAB'; - editedTrans.notes = `Retrait ${match.groups.date} ${match.groups.locationName}`; + editedTrans.notes = `Retrait ${atmMatch.groups.date} ${atmMatch.groups.locationName}`; if (infoArray.length > 1) { - editedTrans.notes += ' ' + infoArray.slice(1).join(' '); + editedTrans.notes += ' ' + infoArray.filter(l => l !== match).join(' '); } - } else if ((match = firstLine.match(regexCreditNote))) { + } else if ((match = infoArray.find(line => regexCreditNote.test(line)))) { // Credit note (refund) - editedTrans.payeeName = title(match.groups.payeeName); - editedTrans.notes = `Avoir ${match.groups.date}`; - } else { - // For the next patterns, we need to check all lines as the identifier can be anywhere - if ((match = infoArray.find(line => regexInstantTransfer.test(line)))) { - // Instant transfer - editedTrans.payeeName = title(match.replace(regexInstantTransfer, '')); - editedTrans.notes = infoArray.filter(l => l !== match).join(' '); - } else if ((match = infoArray.find(line => regexSepa.test(line)))) { - // SEPA transfer - editedTrans.payeeName = title(match.replace(regexSepa, '')); - editedTrans.notes = infoArray.filter(l => l !== match).join(' '); - } else if ((match = infoArray.find(line => regexTransfer.test(line)))) { - // Other transfer - // Must be evaluated after the other transfers as they're more specific - // (here VIR only) - const infoArrayWithoutLine = infoArray.filter(l => l !== match); - editedTrans.payeeName = title(infoArrayWithoutLine.join(' ')); - editedTrans.notes = match.replace(regexTransfer, ''); - } else { - // Unknown transaction type - editedTrans.payeeName = title(firstLine.replace(/ \d+$/, '')); - editedTrans.notes = infoArray.slice(1).join(' '); + const creditMatch = match.match(regexCreditNote); + editedTrans.payeeName = title(creditMatch.groups.payeeName); + editedTrans.notes = `Avoir ${creditMatch.groups.date}`; + if (infoArray.length > 1) { + editedTrans.notes += ' ' + infoArray.filter(l => l !== match).join(' '); } + } else if ( + (match = infoArray.find(line => regexInstantTransfer.test(line))) + ) { + // Instant transfer + editedTrans.payeeName = title(match.replace(regexInstantTransfer, '')); + editedTrans.notes = infoArray.filter(l => l !== match).join(' '); + } else if ((match = infoArray.find(line => regexSepa.test(line)))) { + // SEPA transfer + editedTrans.payeeName = title(match.replace(regexSepa, '')); + editedTrans.notes = infoArray.filter(l => l !== match).join(' '); + } else if ((match = infoArray.find(line => regexTransfer.test(line)))) { + // Other transfer + // Must be evaluated after the other transfers as they're more specific + // (here VIR only) + const infoArrayWithoutLine = infoArray.filter(l => l !== match); + editedTrans.payeeName = title(infoArrayWithoutLine.join(' ')); + editedTrans.notes = match.replace(regexTransfer, ''); + } else { + // Unknown transaction type + editedTrans.payeeName = title(infoArray[0].replace(/ \d+$/, '')); + editedTrans.notes = infoArray.slice(1).join(' '); } return Fallback.normalizeTransaction(transaction, booked, editedTrans); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js index 88a03d08c3..0340f130b5 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js @@ -19,6 +19,19 @@ describe('BoursoBank', () => { 'Payee Name', 'Carte 03/02/25 2,80 NZD / 1 euro = 1,818181818', ], + [ + [ + '2,80 NZD / 1 euro = 1,818181818', + 'CARTE 03/02/25 PAYEE NAME CB*1234', + ], + 'Payee Name', + 'Carte 03/02/25 2,80 NZD / 1 euro = 1,818181818', + ], + [ + ['110,04 GBP / 1 euro = 0,860763454', 'CARTE 13/07/25 PAYEE NAME'], + 'Payee Name', + 'Carte 13/07/25 110,04 GBP / 1 euro = 0,860763454', + ], [ ['RETRAIT DAB 01/03/25 My location CB*9876'], 'Retrait DAB', @@ -32,6 +45,14 @@ describe('BoursoBank', () => { 'Retrait DAB', 'Retrait 01/03/25 My location 2,80 NZD / 1 euro = 1,818181818', ], + [ + [ + '2,80 NZD / 1 euro = 1,818181818', + 'RETRAIT DAB 01/03/25 My location CB*9876', + ], + 'Retrait DAB', + 'Retrait 01/03/25 My location 2,80 NZD / 1 euro = 1,818181818', + ], [ ['VIR Text put by the sender', 'PAYEE NAME'], 'Payee Name', diff --git a/upcoming-release-notes/5344.md b/upcoming-release-notes/5344.md new file mode 100644 index 0000000000..1cf7725207 --- /dev/null +++ b/upcoming-release-notes/5344.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [Aerion] +--- + +Fix Boursorama GoCardless wrong payee for multiline transactions