MANUEL TECHNIQUE : Ajouter une Nouvelle Banque
Version: 1.0
Date: 2025-11-12
Difficulté: ⭐⭐ Intermédiaire
Durée: 30-45 minutes
Vue d'Ensemble
Ce guide vous permet d'ajouter une nouvelle banque au système de traitement des emails bancaires en 4 étapes simples.
L'architecture extensible permet d'ajouter des parsers personnalisés sans modifier le code existant.
Pré-requis
- Accès au code source
- Échantillons d'emails de la nouvelle banque (minimum 3)
- Connaissance Python (base)
- Accès base de données
Architecture
flowchart TD
EP["EmailProcessor<br/>(orchestrateur, inchangé)"] --> GP["_get_parser<br/>(sélection automatique du parser)"]
GP --> Q{"bank.parser_class<br/>existe ?"}
Q -->|OUI| SPE["Parser spécifique<br/>(AFGBankParser, SGBankParser, XYZBankParser…)"]
Q -->|NON| GEN["Parser générique<br/>(HTMLParser ou PDFParser)"]
SPE --> EX["Extraction des données<br/>(méthodes surchargées dans le parser spécifique)"]
GEN --> EX
1.1 Récupérer Échantillons
Obtenez minimum 3 emails de transaction de la nouvelle banque couvrant:
- Opération CREDIT
- Opération DEBIT
- Formats différents si applicable
1.2 Identifier Type Email
HTML ou PDF ?
- HTML: Email contient données dans le corps (inspecter source HTML)
- PDF: Email contient pièce jointe PDF
1.3 Mapper Champs
Identifiez où se trouvent les données dans l'email:
| Donnée Requise |
Où dans l'email ? |
Exemple |
| proforma_reference |
Champ "Motif", "Référence", etc. |
"ATT-JOE-123" |
| description |
Champ "Libellé", "Description" |
"VIREMENT RECU" |
| account_number |
Numéro compte |
"00012345678" |
| amount |
Montant |
"1,500,000.00" |
| currency |
Devise |
"XAF" |
| transaction_date |
Date transaction |
"12-Nov-2025" |
| operation_type |
Type (Débit/Crédit) |
"Débité" |
⚠️ IMPORTANT: Notez le mapping spécifique de la banque. Parfois le champ "Description" contient la référence et vice-versa!
1.4 Exemple : AFG Bank
| Email AFG Bank:
La description → "DEPOT D ESPECES - KAMGA BOGNE..."
Numéro de réference → "012CHDP251811504"
Mapping AFG:
proforma_reference ← "La description" (texte long)
description ← "Numéro de réference" (code court)
|
ÉTAPE 2 : Créer Parser Spécifique
2.1 Créer Fichier Parser
Chemin: backend/app/services/parsers/xyz_bank_parser.py
Remplacez xyz par nom banque (ex: ecobank_parser.py, bicec_parser.py)
2.2 Template Parser HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 | """
XYZ Bank Parser - Parser spécifique [Nom Banque]
Mapping spécifique XYZ Bank:
- proforma_reference = [Décrire mapping]
- description = [Décrire mapping]
"""
import re
from typing import Optional, Dict
from email.message import Message
from bs4 import BeautifulSoup
import logging
from app.services.parsers.html_parser import HTMLParser
logger = logging.getLogger(__name__)
class XYZBankParser(HTMLParser):
"""
Parser spécifique [Nom Banque] (HTML)
Hérite de HTMLParser mais override les méthodes d'extraction
pour respecter le mapping spécifique de la banque.
"""
def __init__(self):
super().__init__()
logger.debug("XYZBankParser initialized")
# OPTIONNEL: Override parse() seulement si logique différente
# Sinon, hériter de HTMLParser.parse() et juste override méthodes extract
def _extract_proforma_reference(self, text: str) -> Optional[str]:
"""
Extrait proforma_reference selon format XYZ Bank
Adaptez le pattern regex selon votre email
"""
# Exemple: Chercher "Motif : XXXXX"
match = re.search(
r'Motif\s*:?\s*(.+?)(?:\n|$)',
text,
re.IGNORECASE
)
if match:
proforma = match.group(1).strip()
proforma = re.sub(r'\s+', ' ', proforma) # Nettoyer espaces
if len(proforma) > 5: # Validation minimum
logger.debug(f"XYZ proforma extracted: {proforma[:50]}...")
return proforma
logger.warning("No XYZ proforma found")
return None
def _extract_description(self, text: str) -> Optional[str]:
"""
Extrait description selon format XYZ Bank
"""
# Exemple: Chercher "Libellé : XXXXX"
match = re.search(
r'Libell[ée]\s*:?\s*(.+?)(?:\n|$)',
text,
re.IGNORECASE
)
if match:
description = match.group(1).strip()
description = re.sub(r'\s+', ' ', description)
if description:
logger.debug(f"XYZ description extracted: {description[:50]}...")
return description
logger.warning("No XYZ description found")
return None
# OPTIONNEL: Override autres méthodes si format spécial
# def _extract_amount(self, text: str) -> Optional[float]:
# # Logique spécifique extraction montant
# pass
|
2.3 Template Parser PDF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 | """
XYZ Bank Parser - Parser spécifique [Nom Banque] (PDF)
"""
import re
from typing import Optional, Dict
from email.message import Message
import logging
from app.services.parsers.pdf_parser import PDFParser
logger = logging.getLogger(__name__)
class XYZBankParser(PDFParser):
"""
Parser spécifique [Nom Banque] (PDF)
"""
def __init__(self):
super().__init__()
logger.debug("XYZBankParser initialized")
def _extract_proforma_reference(self, text: str) -> Optional[str]:
"""
Extrait proforma depuis PDF XYZ Bank
text contient le texte extrait du PDF avec pdfplumber
"""
# Adaptez selon structure PDF
match = re.search(
r'R[ée]f[ée]rence\s*:?\s*([A-Z0-9-]+)',
text,
re.IGNORECASE
)
if match:
ref = match.group(1).strip()
logger.debug(f"XYZ proforma: {ref}")
return ref
return None
def _extract_description(self, text: str) -> Optional[str]:
"""
Extrait description depuis PDF
"""
match = re.search(
r'Description\s*:?\s*(.+?)(?:\n|$)',
text,
re.IGNORECASE
)
if match:
desc = match.group(1).strip()
return desc
return None
|
2.4 Conseils Patterns Regex
Patterns courants:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | # Montant avec séparateurs
r'Montant\s*:?\s*([\d\s,\.]+)'
# Date formats variés
r'(\d{1,2}[-/]\w{3}[-/]\d{4})' # 12-Nov-2025
r'(\d{2}/\d{2}/\d{4})' # 12/11/2025
# Opération type
r'(D[ée]bit|Cr[ée]dit)'
# Compte
r'Compte\s*:?\s*([\d\-]+)'
# Texte jusqu'au prochain champ
r'Motif\s*(.+?)(?=\n\s*Type|$)' # Capturer jusqu'à "Type" ou fin
|
Regex tips:
- \s* : 0+ espaces/newlines
- \s+ : 1+ espaces/newlines
- .+? : Capturer minimum (non-greedy)
- (?:\n|$) : Jusqu'à newline ou fin
- re.IGNORECASE : Insensible casse
- re.DOTALL : . matche aussi newlines
ÉTAPE 3 : Enregistrer Parser
3.1 Exporter dans __init__.py
Fichier: backend/app/services/parsers/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12 | # Ajouter import
from app.services.parsers.xyz_bank_parser import XYZBankParser
# Ajouter à __all__
__all__ = [
'BaseParser',
'HTMLParser',
'PDFParser',
'AFGBankParser',
'SGBankParser',
'XYZBankParser' # ← AJOUTER ICI
]
|
3.2 Ajouter Import Dynamique
Fichier: backend/app/services/email_processor.py
Méthode: _get_parser()
| def _get_parser(self, bank):
if bank.parser_class:
# ... code existant ...
# AJOUTER VOTRE BANQUE:
elif bank.parser_class == "XYZBankParser":
from app.services.parsers.xyz_bank_parser import XYZBankParser
parser = XYZBankParser()
# ... reste du code ...
|
Position exacte: Après les blocs AFGBankParser et SGBankParser, avant else:.
ÉTAPE 4 : Configuration Base de Données
4.1 Ajouter Banque
SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | INSERT INTO banks (
name,
email_pattern,
payment_source,
payment_method,
reference_bank,
processing_method,
parser_class,
is_active
) VALUES (
'XYZ Bank Cameroun', -- Nom banque
'notifications@xyzbank.cm', -- Pattern email (ou '*@xyzbank.cm')
'XYZ-BANK-CM-237', -- Label source paiement
'BANK_DEPOSIT', -- Méthode paiement
'XYZ_BANK_REF', -- Référence banque (optionnel)
'EMAIL_BODY', -- ou 'PDF_ATTACHMENT'
'XYZBankParser', -- ← NOM CLASSE PARSER
TRUE -- Actif
);
|
Via Python:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | from app.database import SessionLocal
from app.models.bank import Bank, ProcessingMethodEnum
db = SessionLocal()
new_bank = Bank(
name='XYZ Bank Cameroun',
email_pattern='notifications@xyzbank.cm',
payment_source='XYZ-BANK-CM-237',
payment_method='BANK_DEPOSIT',
reference_bank='XYZ_BANK_REF',
processing_method=ProcessingMethodEnum.EMAIL_BODY, # ou PDF_ATTACHMENT
parser_class='XYZBankParser', # ← IMPORTANT
is_active=True
)
db.add(new_bank)
db.commit()
print(f"Banque ajoutée: ID={new_bank.id}")
db.close()
|
4.2 Vérifier Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13 | from app.database import SessionLocal
from app.models.bank import Bank
db = SessionLocal()
bank = db.query(Bank).filter(Bank.name == 'XYZ Bank Cameroun').first()
print(f"Nom: {bank.name}")
print(f"Email Pattern: {bank.email_pattern}")
print(f"Processing Method: {bank.processing_method.value}")
print(f"Parser Class: {bank.parser_class}")
db.close()
|
ÉTAPE 5 : Tests
5.1 Test Parser Isolé
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | from app.services.email_client import EmailClient
from app.services.parsers.xyz_bank_parser import XYZBankParser
from email import message_from_bytes
client = EmailClient()
client.connect()
# Récupérer email test
status, messages = client.connection.search(None, '(FROM "notifications@xyzbank.cm")')
email_ids = messages[0].split()
if len(email_ids) > 0:
email_id = email_ids[0]
status, msg_data = client.connection.fetch(email_id, '(RFC822)')
for response_part in msg_data:
if isinstance(response_part, tuple):
email_msg = message_from_bytes(response_part[1])
# Tester parser
parser = XYZBankParser()
data = parser.parse(email_msg)
if data:
print("✓ Parse OK")
print(f"Proforma: {data.get('proforma_reference')}")
print(f"Description: {data.get('description')}")
print(f"Amount: {data.get('amount')}")
print(f"Currency: {data.get('currency')}")
else:
print("✗ Parse FAILED")
client.disconnect()
|
5.2 Test Workflow Complet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | from app.services.email_processor import EmailProcessor
from app.database import SessionLocal
db = SessionLocal()
processor = EmailProcessor(db)
# Tester avec email XYZ
email_msg = ... # Message email
sender_email = "notifications@xyzbank.cm"
transaction_id = processor.process_email(email_msg, sender_email)
if transaction_id:
from app.models.transaction import Transaction
tx = db.query(Transaction).filter(Transaction.id == transaction_id).first()
print(f"✓ Transaction créée: ID={tx.id}")
print(f" Proforma: {tx.proforma_reference}")
print(f" Description: {tx.description}")
print(f" Status: {tx.status.value}")
else:
print("✗ Échec création transaction")
db.close()
|
5.3 Checklist Validation
Exemples Réels
Exemple 1 : AFG Bank (HTML, mapping inversé)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | class AFGBankParser(HTMLParser):
def _extract_afg_proforma(self, text: str) -> Optional[str]:
# AFG: proforma = "La description" (texte long)
match = re.search(
r'La\s+description\s*(.+?)\s*Type\s+de\s+transaction',
text,
re.IGNORECASE | re.DOTALL
)
if match:
description = match.group(1).strip()
description = re.sub(r'\s+', ' ', description)
if len(description) > 15:
return description
return None
def _extract_afg_description(self, text: str) -> Optional[str]:
# AFG: description = "Numéro de réference" (code court)
match = re.search(
r'Num[ée]ro\s+de\s+r[ée]f[ée]rence\s*:?\s*([A-Z0-9-]+)',
text,
re.IGNORECASE
)
if match:
return match.group(1).strip()
return None
|
Exemple 2 : SG Bank (PDF, standard)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | class SGBankParser(PDFParser):
def _extract_proforma_reference(self, text: str) -> Optional[str]:
# SG: proforma = "Motif" (code court)
match = re.search(r'Motif\s*:?\s*([A-Z0-9-]+)', text, re.IGNORECASE)
if match:
motif = match.group(1).strip()
if re.match(r'^[A-Z0-9-]{3,}$', motif):
return motif
return None
def _extract_description(self, text: str) -> Optional[str]:
# SG: description = "Libellé" (texte descriptif)
match = re.search(
r'Libell[ée]\s*:?\s*(.+?)(?:\n|$)',
text,
re.IGNORECASE
)
if match:
libelle = match.group(1).strip()
return re.sub(r'\s+', ' ', libelle)
return None
|
Dépannage
Parser ne trouve pas les données
Problème: No XYZ proforma found
Solutions:
1. Vérifier pattern regex avec email réel
2. Inspecter texte extrait: print(text) dans méthode
3. Utiliser regex online tester (regex101.com)
4. Vérifier encoding (UTF-8, ISO-8859-1)
Problème: Toutes transactions ont ce status
Solutions:
1. Vérifier _extract_proforma_reference() retourne valeur
2. Vérifier longueur minimum respectée
3. Logger proforma extraite: logger.debug(f"Proforma: {proforma}")
Parser non chargé
Problème: Parser générique utilisé au lieu du spécifique
Solutions:
1. Vérifier bank.parser_class en DB (exact spelling)
2. Vérifier import dynamique dans _get_parser()
3. Redémarrer backend: docker compose restart backend
4. Vérifier logs: docker compose logs backend | grep "Loaded bank-specific"
Erreur "value too long"
Problème: value too long for type character varying(50)
Solutions:
1. Vérifier taille colonne DB (proforma_reference doit être VARCHAR(255))
2. Créer migration si nécessaire pour agrandir colonne
Récapitulatif 4 Étapes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | ┌─────────────────────────────────────────────────────┐
│ ÉTAPE 1: Analyser Format Email │
│ ✓ Récupérer 3+ échantillons │
│ ✓ Identifier HTML ou PDF │
│ ✓ Mapper champs (proforma, description, etc.) │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ÉTAPE 2: Créer Parser Spécifique │
│ ✓ Créer xyz_bank_parser.py │
│ ✓ Hériter HTMLParser ou PDFParser │
│ ✓ Override _extract_proforma_reference() │
│ ✓ Override _extract_description() │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ÉTAPE 3: Enregistrer Parser │
│ ✓ Exporter dans parsers/__init__.py │
│ ✓ Ajouter import dynamique dans EmailProcessor │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ÉTAPE 4: Configuration DB │
│ ✓ INSERT INTO banks (..., parser_class='XYZBank') │
│ ✓ Vérifier config │
│ ✓ Tester avec emails réels │
└─────────────────────────────────────────────────────┘
|
Temps total: 30-45 minutes
Support
Questions fréquentes:
Q: Puis-je utiliser parser générique sans créer parser spécifique?
R: Oui! Laissez parser_class=NULL en DB. Le système utilisera HTMLParser ou PDFParser générique.
Q: Que faire si banque a plusieurs formats d'email?
R: Créez logique dans parser pour détecter format et adapter extraction.
Q: Comment débugger patterns regex?
R: Utilisez logger.debug() pour afficher texte extrait et résultats intermédiaires.
Q: Parser peut hériter d'un autre parser spécifique?
R: Oui, mais généralement mieux hériter de HTMLParser/PDFParser directement.
Version: 1.0
Dernière mise à jour: 2025-11-12
Testé avec: AFG Bank, Société Générale Cameroun