Aller au contenu

Plugin PrestaShop

Le module titreoepay est le pont entre une boutique PrestaShop 8 et l’orchestrateur Titreo E-Pay. Il ne contient aucune logique de paiement : il délègue tout au backend (création de la session, choix des instruments, débit cartes cadeaux + CB, remboursements). C’est un thin connector qui se contente de trois responsabilités :

  1. Créer une session de paiement côté API quand le client choisit Titreo E-Pay au checkout, puis le rediriger vers la page de paiement hébergée.
  2. Recevoir le webhook signé renvoyé par le backend et mettre à jour le statut de la commande PrestaShop en conséquence.
  3. Déclencher les remboursements (depuis le back-office ou depuis un changement d’état de commande).

Tout ce qui est apparence (logo, couleurs, polices), gestion d’équipe et suivi des transactions a migré dans la Console marchand : le plugin ne sert plus qu’à connecter la boutique à l’API. Pour situer cette brique dans l’ensemble, voir la vue d’ensemble de l’écosystème.

Le code vit dans plugins/prestashop/titreoepay/. Le module se déclare en version 0.2.0 (constructeur de titreoepay.php) ; le config.xml affiche encore 0.1.0 — un écart cosmétique à corriger.

plugins/prestashop/titreoepay/
├── titreoepay.php classe module (install, hooks, callApi)
├── config.xml manifest module
├── controllers/front/redirect.php crée la session + redirige vers le checkout
├── controllers/front/webhook.php reçoit les events signés, maj commande
├── controllers/front/returnpayment.php page de retour client (+ polling)
├── controllers/admin/AdminTitreoEpayRefundController.php refund depuis le BO
├── cron/cleanup-pending.php purge des tables techniques
├── views/js/return-poll.js polling JS de la page de retour
├── views/templates/ panel BO, formulaire refund, page retour
└── translations/error-codes.php libellés des codes d'erreur

TitreoEpay (dans titreoepay.php) étend PaymentModule. Elle déclare trois contrôleurs front (redirect, returnpayment, webhook) et active le mode multi-devises par cases à cocher.

La méthode install() crée deux tables techniques, enregistre les hooks, crée un état de commande personnalisé et initialise les clés de configuration.

Table Rôle
*_titreoepay_pending Mapping id_cart → titreo_session_id → id_order. Pivot central : le webhook s’en sert pour retrouver le panier puis la commande. Clé primaire id_cart, contrainte unique sur titreo_session_id.
*_titreoepay_event_seen Déduplication des événements webhook : un event_id déjà vu n’est pas rejoué.

Hooks enregistrés :

Hook Usage
paymentOptions Affiche le bouton « Payer avec Titreo E-Pay » au checkout.
displayPaymentReturn Retour de paiement côté thème.
actionOrderStatusPostUpdate Si la commande passe à l’état « Remboursé » dans le BO, déclenche un refund total via l’API.
displayAdminOrderMainBottom / displayAdminOrder Affiche le panneau Titreo dans la page commande du BO (compatibilité 1.7.7+ et versions antérieures).

install() crée aussi un état de commande dédié « litige ouvert » (installDisputedState(), clé TITREOEPAY_OS_DISPUTED) utilisé quand un événement de litige arrive, et initialise quatre clés de configuration (voir Configuration).

La méthode publique callApi($method, $path, $body, $extraHeaders) centralise tous les appels HTTP sortants (cURL) vers le backend : elle ajoute l’en-tête Authorization: Bearer <API key> et renvoie ['ok', 'status', 'body']. Les contrôleurs redirect.php et returnpayment.php ont leur propre cURL inline (pour la création de session et le client-token) ; les autres passent par callApi.

sequenceDiagram
    participant C as "Client (navigateur)"
    participant PS as "PrestaShop (module)"
    participant API as "Backend Titreo E-Pay"
    participant CO as "Checkout hébergé"
    C->>PS: "Choisit Titreo E-Pay (hookPaymentOptions)"
    C->>PS: "controller=redirect"
    PS->>API: "POST /v1/sessions (Idempotency-Key)"
    API-->>PS: "{ id: sessionId }"
    PS->>PS: "INSERT titreoepay_pending (cart→session)"
    PS->>API: "POST /v1/sessions/:id/client-token"
    API-->>PS: "{ token }"
    PS-->>C: "Redirect vers CHECKOUT_URL/:locale/c/:id?t=...&success=...&cancel=..."
    C->>CO: "Paie (cartes cadeaux + CB)"
    API->>PS: "POST controller=webhook (X-Titreo-Signature)"
    PS->>PS: "validateOrder + UPDATE id_order"
    CO-->>C: "Redirect vers returnpayment"
    C->>PS: "controller=returnpayment"
    PS-->>C: "order-confirmation (ou polling si webhook en retard)"

hookPaymentOptions() propose un bouton dont l’action pointe vers le contrôleur redirect. TitreoEpayRedirectModuleFrontController::postProcess() :

  • vérifie que le panier est complet (client, adresses) ;
  • calcule le montant total en centimes ;
  • appelle POST /v1/sessions avec reference, totalAmount, currency et un en-tête Idempotency-Key aléatoire ;
  • enregistre le mapping cart → session dans titreoepay_pending (en supprimant d’abord toute entrée existante pour le même panier ou la même session, afin qu’une nouvelle tentative écrase la précédente) ;
  • demande un client-token court (POST /v1/sessions/:id/client-token) destiné au navigateur ;
  • redirige vers la page de checkout hébergée :
{CHECKOUT_URL}/{locale}/c/{sessionId}?t={clientToken}&success={returnUrl}&cancel={returnUrl}

La locale est forcée à fr ou en. Les URLs success/cancel repointent vers le contrôleur returnpayment de la boutique.

TitreoEpayWebhookModuleFrontController est le cœur de la synchronisation. C’est un endpoint serveur-à-serveur : il neutralise la redirection canonique de PrestaShop (canonicalRedirection() surchargée en no-op) pour éviter qu’un 302 ne casse la livraison.

Séquence de postProcess() :

  1. Vérification de signature (verifySignature) : l’en-tête X-Titreo-Signature contient t=<timestamp>,v1=<hmac>. Le HMAC-SHA256 est calculé sur timestamp.payload avec le webhook secret, comparé en temps constant (hash_equals). Les signatures de plus d’une heure sont rejetées (stale_signature).
  2. Déduplication : si l’event_id est déjà dans titreoepay_event_seen, réponse 200 { duplicate: true } sans retraitement.
  3. Résolution du panier : findPending() retrouve la ligne pending par session_id. Si absente (course possible avec redirect.php), une attente de 1,5 s puis un retry. Toujours absente → 202 unknown_session (le PSP re-livrera selon son outbox).
  4. Dispatch par type d’événement (dispatchEvent) :
Événement / statut Action sur la commande
session.completed / COMPLETED validateOrder() (état PS_OS_PAYMENT), puis UPDATE id_order dans pending. Idempotent si la commande existe déjà.
session.failed / session.expired / CANCELED Passe la commande en PS_OS_CANCELED + note.
session.refunded / FULLY_REFUNDED Passe en PS_OS_REFUND + note.
session.partially_refunded / PARTIALLY_REFUNDED Note de remboursement partiel ; bascule en PS_OS_REFUND si le cumulé atteint le total.
session.disputed Passe à l’état custom « litige ouvert » + note.
*.action_required Note demandant une intervention manuelle.
  1. Marquage vu : insertion de l’event_id dans event_seen (INSERT IGNORE), puis réponse 200 { ok: true }.

returnpayment.php gère le retour du navigateur après le checkout. Deux cas :

  • Webhook déjà arrivé : la ligne pending a un id_order → redirection directe vers order-confirmation.
  • Webhook en retard : la page affiche return.tpl et lance return-poll.js, qui interroge GET /v1/sessions/:id toutes les 2 s (max 60 s, backoff sur erreur) avec un client-token fraîchement émis. Dès qu’un état terminal est détecté (COMPLETED, FAILED, etc.), le JS recharge la page (qui redirige alors vers order-confirmation) ou affiche un message d’erreur traduit issu du catalogue (errorMessageCatalog() / translations/error-codes.php).

Deux chemins déclenchent un refund :

  • Panneau commande (AdminTitreoEpayRefundController) : un formulaire propose montant et stratégie (all, gift_first, cb_first) et envoie POST /v1/sessions/:id/refund avec une Idempotency-Key dérivée de la commande.
  • Changement d’état : passer la commande à l’état « Remboursé » déclenche hookActionOrderStatusPostUpdate, qui lance un refund total (strategy: all).

Configurée dans la page module du BO (getContent() / renderForm()). Quatre clés stockées via Configuration :

Clé Rôle Valeur par défaut à l’install
TITREOEPAY_API_BASE_URL Base de l’API backend. http://api:3000
TITREOEPAY_API_KEY Clé API marchand (tep_…), en-tête Authorization. vide (à renseigner)
TITREOEPAY_WEBHOOK_SECRET Secret HMAC de vérification des webhooks. vide
TITREOEPAY_CHECKOUT_URL Base de la page de paiement hébergée. http://localhost:3100

API base URL et API key sont requis. Le webhook ne traite aucun événement si le secret est absent (réponse 500 webhook_secret_missing).

Aspect État
Flux checkout hébergé Fonctionnel (redirect + retour + polling).
Webhook signé + idempotence Fonctionnel (HMAC, double déduplication).
Refund BO + par état de commande Fonctionnel (stratégies all / gift_first / cb_first).
États de commande couverts completed, failed/expired/canceled, refund total/partiel, litige.

Restes connus :

  • Versions désynchronisées : titreoepay.php annonce 0.2.0, config.xml reste à 0.1.0.
  • Aucun test automatisé ; validation manuelle uniquement.
  • Traductions partielles : seul error-codes.php est traduit ; les chaînes du module et des templates ne sont pas internationalisées de façon exhaustive.
  • Valeurs par défaut orientées dev local (api:3000, localhost:3100) : à durcir avant production.
  • Cron non sécurisé en HTTP : prévu pour exécution CLI ; aucune protection par token si exposé via web.
  • Pas de packaging Marketplace (validation PrestaShop, archive zip distribuable).

Pour le détail des contrats (signature webhook, format de session, en-têtes d’idempotence) partagés avec le backend, voir Contrats inter-briques et Sécurité. Le module est l’analogue PrestaShop des autres connecteurs décrits dans la vue d’ensemble de l’écosystème.