Skip to Content
TCC SFEC TerminalErreurs & santé

Erreurs & santé de connexion

Chaque requête envoyée au terminal — un objet JSON par trame, écrit <json>END_JSON — passe par un pipeline fixe. Chaque étape peut rejeter la requête avec un error_code distinct. Cette page est la référence de ces codes, des enveloppes d’erreur, de la sonde de liveness pingpong, et de la détection d’un port mort avec reconnexion.

Pipeline de traitement (ordre des contrôles)

Le terminal évalue une requête dans cet ordre exact. La première étape qui échoue produit la réponse ; les étapes suivantes ne s’exécutent pas.

#Étapeerror_code en cas d’échec
1Garde de taille (message.length <= 1048576)MESSAGE_TOO_LARGE
2Parsing JSON dans le contrat, dont décodage strict des enumsINVALID_PAYLOAD
3Validation champ & métier (toutes les règles)VALIDATION_ERROR
4Contrôle de doublon en persistance (invoiceId déjà certifié)DUPLICATE_INVOICE
5Certification / persistance (interne)PROCESSING_ERROR
Ensemble des étapes 2–5 encadré par un timeout de 30 sPROCESSING_TIMEOUT

La validation (étape 3) collecte et renvoie toutes les erreurs d’un coup ; une seule réponse VALIDATION_ERROR peut contenir plusieurs entrées errors[]. Les échecs de décodage d’enum (étape 2) sont eux signalés un à un en INVALID_PAYLOAD, avant toute validation.

Enveloppes d’erreur

Chaque réponse arrive sur le fil comme un objet JSON terminé par END_JSON. Il existe deux formes d’erreur distinctes. Branchez votre logique sur error_code, jamais sur le texte lisible (certains messages sont en français).

Erreur générique (tous les codes sauf VALIDATION_ERROR) :

{"status":"error","invoice_id":"INV-2026-000123","error_code":"DUPLICATE_INVOICE","error_message":"Invoice already exists with this ID: INV-2026-000123","timestamp":1751362800000,"protocol":"TCP"}END_JSON

Erreur de validation (VALIDATION_ERROR uniquement — porte un tableau errors[] et n’a pas de error_message) :

{"status":"error","error_code":"VALIDATION_ERROR","invoice_id":"INV-2026-000123","errors":[{"field":"recipientPhone","message":"Phone number must be in valid Congo format (+242XXXXXXXXX or 0XXXXXXXXX)"},{"field":"currency","message":"Currency must be XAF, USD, or EUR"}],"timestamp":1751362800000,"protocol":"TCP"}END_JSON
⚠️

invoice_id peut être absent. Le terminal ne sérialise pas les nulls : quand l’id n’a pas pu être déterminé (requête trop grande, JSON malformé, enum inconnu, timeout), la clé invoice_id est entièrement omise. Traitez une absence comme « inconnu ». De même, le champ protocol est interne — ignorez-le. Ne faites pas de correspondance sur le texte error_message (mélange de français/anglais) ; branchez sur error_code et, pour la validation, sur errors[].field.

Catalogue des codes d’erreur

error_codeDéclencheurerror_message (exact / modèle)Action client
MESSAGE_TOO_LARGETrame > 1048576 caractères (1 Mo).Message trop volumineuxCorriger la charge utile. Ne pas retenter inchangé. Cause fréquente : un logoImg Base64 volumineux — le retirer/réduire.
INVALID_PAYLOADJSON malformé, vide, ou valeur d’enum inconnue.Invalid JSON: <detail>, Invalid payload: <detail>, ou Empty payloadCorriger la charge utile. Non retentable tel quel. Vérifier la syntaxe JSON et l’orthographe des enums.
VALIDATION_ERRORUne ou plusieurs règles champ/métier/montant ont échoué.(pas de error_message ; voir errors[])Corriger les champs listés, puis renvoyer la requête corrigée. Non retentable tel quel.
DUPLICATE_INVOICEUne facture avec le même invoiceId est déjà certifiée.Invoice already exists with this ID: <invoiceId>Traiter comme un succès idempotent — le document est déjà certifié. Ne pas renvoyer sous le même id, ne pas allouer un nouveau numéro fiscal.
PROCESSING_ERRORÉchec interne pendant certification/persistance, ou toute exception non capturée en étapes 2–5.Erreur de traitement: <detail> ou le texte d’échec du dépôt.Retentable avec backoff. Si transitoire, réussira au retry ; si la facture a persisté, le retry renvoie DUPLICATE_INVOICE (ce qui confirme le succès).
PROCESSING_TIMEOUTTraitement d’une facture > 30000 ms (30 s).Timeout de traitement de la factureRetentable avec backoff. La facture peut avoir abouti ou non — un retry renvoie success ou DUPLICATE_INVOICE ; les deux signifient qu’elle est certifiée.

Décodage strict des enums → INVALID_PAYLOAD

Cinq champs sont des enums décodés strictement. Toute valeur hors de l’ensemble autorisé interrompt le parsing et donne INVALID_PAYLOAD (avec un préfixe Invalid payload:).

ChampValeurs JSON autorisées (exactes, sensibles à la casse)
invoiceTypesalesInvoice, creditNote
recipientTypeindividual, business, government, foreign
paymentMethodcash, bank_transfer, mobile_money, cheque, card
items[].typeproduct, service
items[].discountTypefixed, percentage

Exemple — envoyer "recipientType":"corporation" produit :

{"status":"error","error_code":"INVALID_PAYLOAD","error_message":"Invalid payload: Invalid value 'corporation' for RecipientType. Allowed: [individual, business, government, foreign]","timestamp":1751362800000,"protocol":"TCP"}END_JSON

Règles de validation (VALIDATION_ERROR)

Toutes les règles sont évaluées ensemble ; chaque violation ajoute une entrée errors[] avec le chemin field et le message exacts.

Identification & lignes

Chemin de champRèglemessage exact
invoiceIdRequis, non vide.Invoice ID is required
invoiceIdLongueur max 23.Invoice ID must not exceed 23 characters
items[i].designationRequis, non vide.Designation is required
items[i].unitPrice>= 0.01.Unit price must be greater than 0
items[i].quantity>= 0.001.Quantity must be greater than 0
items[i].taxRateRequis, chaîne non vide.Tax rate is required
additionalTaxes[i].taxCodeRequis, non vide (si additionalTaxes présent).Tax code is required
referenceInvoiceIdRequis, non vide quand invoiceType == creditNote.Reference invoice ID is required for credit notes
currencyExactement l’une de XAF, USD, EUR.Currency must be XAF, USD, or EUR

Règles destinataire par recipientType

Formats : NIU = exactement 16 ou 17 caractères · Téléphone Congo ^(\+242\|242)?0[456]\d{7}$ · Téléphone international (FOREIGN) ^\+?[1-9]\d{1,14}$ · Email ^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$.

recipientTypeChamps requisRègle NIURègle téléphone
individualaucun (tous optionnels, mais validés si présents)si présent : 16 ou 17 caractèressi présent : format Congo
businessname, niu, phone, email, addressrequis, 16 ou 17 caractèresformat Congo
governmentname, niu, phone, email, addressrequis, 16 ou 17 caractèresformat Congo
foreignname, phone, email, address (niu optionnel)si présent : 16 ou 17 caractèresformat international

Messages notables : Recipient name is required for <label> recipients, NIU is required for <label> recipients, NIU must be exactly 16 or 17 characters, Phone number is required for <label> recipients, Phone number must be in valid Congo format (+242XXXXXXXXX or 0XXXXXXXXX), Phone number must be in valid international format, Email is required for <label> recipients, Invalid email format, Address is required for <label> recipients. (<label> vaut business, government ou foreign.)

Cohérence des montants (anti-fraude, tolérance ±0.01)

field renvoyéFormule (à ±0.01 près)Modèle de message
subtotalsubtotal == Σ items[i].netAmountSubtotal is incoherent: declared <d> but sum of item net amounts is <c>
totalTaxAmounttotalTaxTAmount + totalTaxRAmount == Σ items[i].taxAmountTotal taxes are incoherent: declared <d> but sum of item taxes is <c>
totalAmounttotalAmount == subtotal + totalTaxTAmount + totalTaxRAmount − discountAmount + electronicStampDuty + additionalCentTaxTotal amount is incoherent: declared <d> but calculated <c>
items[i].subtotalsubtotal == unitPrice * quantityItem subtotal is incoherent: <up> * <qty> should be <exp>, not <sub>
items[i].netAmountnetAmount == subtotal − discountAmountItem net amount is incoherent: <sub> - <disc> should be <exp>, not <net>
items[i].totalAmounttotalAmount == netAmount + taxAmountItem total amount is incoherent: <net> + <tax> should be <exp>, not <total>
⚠️

Le chemin renvoyé pour le contrôle de taxe est totalTaxAmount, même si les valeurs comparées sont totalTaxTAmount + totalTaxRAmount. Ces contrôles sont ignorés si items est vide.

DUPLICATE_INVOICE et idempotence

Le contrôle de doublon est fondé uniquement sur invoiceId : si l’id existe déjà, la requête échoue en DUPLICATE_INVOICE et aucun nouveau numéro fiscal n’est frappé.

  • Traitez DUPLICATE_INVOICE comme un signal de succès idempotent, pas un échec dur. Conservez le numéro fiscal / QR reçu sur la réponse de succès d’origine ; ne réémettez pas et ne renumérotez pas.
  • C’est ce qui rend le retry-après-échec sûr. Après un PROCESSING_TIMEOUT ou un lien coupé, renvoyez la requête identique avec le même invoiceId. Vous obtiendrez soit status:"success" (non persistée), soit DUPLICATE_INVOICE (persistée) — les deux confirment la certification. N’allouez jamais un nouvel invoiceId pour un retry.
⚠️

DUPLICATE_INVOICE prouve l’existence, pas une certification valide. Si le sous-système fiscal n’était pas prêt à la première arrivée, le terminal peut persister la ligne avec sfec_number/qr_code nuls ; chaque renvoi de cet id retourne alors DUPLICATE_INVOICE indéfiniment. Ne traitez un doublon comme « déjà certifié » que si vous avez réellement reçu un success antérieur portant un sfec_number et un qr_code non vides pour cet id — sinon le document n’est pas certifié et exige une réconciliation manuelle.

Logique de gestion (référence)

def handle_response(resp: dict) -> str: status = resp.get("status") if status == "success": return "certified" # stocker sfec_number + qr_code code = resp.get("error_code") if code == "DUPLICATE_INVOICE": return "already_certified" # succès idempotent : réconcilier, ne PAS renvoyer if code in ("PROCESSING_ERROR", "PROCESSING_TIMEOUT"): # transitoire : retenter le MÊME invoiceId avec backoff ; # un DUPLICATE_INVOICE ultérieur confirme le succès. return "retry" if code == "VALIDATION_ERROR": for e in resp.get("errors", []): log(f"fix {e['field']}: {e['message']}") return "fix_and_resend" # non retentable inchangé if code in ("INVALID_PAYLOAD", "MESSAGE_TOO_LARGE"): return "fix_payload" # malformé / surdimensionné / mauvais enum return "unknown_error"

Santé de connexion — sonde pingpong

Sur un lien actif, la liveness est gratuite : chaque facture envoyée revient en trame success ou error, ce qui prouve déjà que le port est vivant. Pour prouver activement qu’un lien inactif (idle) est en vie, écrivez une trame ping ; le pont répond par une trame pong.

{"type":"ping","timestamp":"2026-07-01T10:30:45.123Z","source":"erp"}END_JSON {"type":"pong","timestamp":"2026-07-01T10:30:45.456Z","source":"middleware"}END_JSON

Le timestamp est une chaîne ISO-8601 UTC ; le pont ne la valide pas. source vaut "erp" sur votre ping et "middleware" sur la réponse.

Vous ne recevrez jamais de ping non sollicité et n’avez jamais à en répondre : seule la direction pingpong initiée par votre logiciel est active.

Détection d’un port mort

Depuis le client, il existe exactement trois signaux indépendants. Une intégration robuste gère les trois.

#SignalManifestation
aUne écriture ou lecture lève une erreur d’E/Sun write(...END_JSON) ou un read lève une exception série/IO → le lien est perdu ; marquer down
bLa lecture expire sans trame encadréevous avez envoyé une requête mais aucune trame terminée par END_JSON n’est arrivée dans le délai → traiter comme down
cLe port COM disparaîtle pont a été arrêté ou le terminal débranché, si bien que l’ouverture (ou la prochaine lecture) de COM10 échoue → down
⚠️

Ne faites pas confiance à un indicateur « open » en cache. Un objet SerialPort/Serial peut se déclarer ouvert alors que le pont ou la liaison avec le terminal a été rompu en silence. Seuls une vraie erreur d’E/S (a), une lecture expirée sans trame END_JSON (b) ou une ouverture ratée (c) révèlent un pair mort.

Reconnexion avec backoff

Quand l’un des signaux (a)/(b)/(c) se déclenche, fermez COM10 et rouvrez-le, puis reprenez. Il n’y a rien à attendre après la réouverture : dès que COM10 est rouvert, vous pouvez envoyer.

sur DOWN : fermer COM10 delai = 1s boucle : essayer : ouvrir COM10 ; sortir sinon : dormir(delai) ; delai = min(delai * 2, 30s) # plafond de backoff pour chaque facture non acquittée : # même invoiceId à chaque fois send_frame(ser, inv) ; enregistrer(read_frame(ser))
  • Backoff. Réouverture avec backoff exponentiel plafonné + gigue (1 s → 2 s → 4 s → … → 30 s). Gardez le plafond bas pour rétablir la liaison rapidement.
  • Si la réouverture échoue en boucle, le port est réellement absent (signal c) : le pont a été arrêté ou le terminal débranché. Redémarrez le pont fourni.
  • Ne renvoyez que les factures non acquittées, en réutilisant le même invoiceId. Un DUPLICATE_INVOICE confirme qu’elle était déjà certifiée — arrêtez de retenter (récupérez le numéro fiscal / QR depuis le success d’origine que vous avez persisté, le terminal ne les réémet pas).

Étape suivante → Exemples de code