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`
This commit is contained in:
Guillaume Taquet Gasperini
2025-07-19 10:23:10 +02:00
committed by GitHub
parent 5db7026435
commit 76de8bf67f
3 changed files with 69 additions and 41 deletions

View File

@@ -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);

View File

@@ -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',