Skip to content

PRE-3552: Add UHF refund - #318

Merged
adumont-payplug merged 1 commit into
developfrom
feature/PRE-3552_add_uhf_refund
Sep 3, 2026
Merged

PRE-3552: Add UHF refund#318
adumont-payplug merged 1 commit into
developfrom
feature/PRE-3552_add_uhf_refund

Conversation

@adumont-payplug

Copy link
Copy Markdown
Collaborator

Description

Cette PR ajoute le remboursement pour les paiements Hosted Fields (UHF) côté back-office Sylius, avec confirmation asynchrone via webhook, et inclut l'adaptation nécessaire suite à la fusion de payplug/unified-plugin-core's PRE-3589 sur develop (qui embarque aussi le refactoring PRE-3590 "HF aliasing").

Remboursement (synchrone + partiel/total) :

  • RefundPaymentProcessor route les paiements Hosted Fields vers UPC's createRefund() via un nouvel adaptateur Upc/UnifiedApiRefundCreator (implémentant RefundCreatorInterface), au lieu du SDK legacy
  • L'orderId du remboursement (numéro de commande, ou l'id du paiement à défaut) et le submerchantExternalId/accountId (compte/sous-marchand) sont résolus depuis la config du payment method spécifique au paiement remboursé (Upc/PaymentReferenceHelper::resolveGatewayCredentials()), avec un garde-fou local (LogicException) si l'un des deux n'est pas configuré — évite un aller-retour réseau pour un 400 dont le message ne remonte pas, et évite qu'un remboursement soit routé vers le mauvais compte si le marchand a plusieurs payment methods Hosted Fields configurés
  • Un remboursement total (processHostedFields()) enregistre désormais aussi l'id d'opération du remboursement dans Payment::details['refunds'] — nécessaire pour que le webhook de confirmation puisse retrouver le paiement
  • Remboursement total après un remboursement partiel : le montant enregistré est le solde réellement restant (montant d'origine - somme des remboursements déjà confirmés), pas le montant total d'origine — UnifiedApiPaymentService::createRefund() rembourse le solde restant quand $amount est omis, pas le total
  • Remboursements total et partiel protégés par un même verrou (ILock, clé = id du paiement) : les deux méthodes se bloquent mutuellement en cas de déclenchement concurrent, faute d'idempotency key côté API UPC pour ce flux

Notification webhook du remboursement :

  • HostedFieldsWebhookNotificationHandler distingue maintenant une confirmation de remboursement (via les ids qu'on a nous-mêmes enregistrés dans Payment::details['refunds']) d'une notification de paiement classique, et applique PaymentOutcome::REFUNDED au lieu de PAID — sans ce fix, une confirmation de remboursement aurait été mal interprétée
  • Une confirmation de remboursement dont l'execCode indique un échec n'est jamais forcée en REFUNDED (l'argent n'a pas bougé) ni transmise telle quelle à la state machine du paiement (FAILED y signifie "ce paiement a échoué", pas "ce remboursement a échoué") — seuls le log et le suivi d'idempotence sont appliqués ; l'entrée correspondante dans Payment::details['refunds'] est marquée failed => true pour ne plus compter dans un calcul de solde restant ultérieur
  • Le fallback qui associe une confirmation à un remboursement dont l'id n'a pas pu être capturé à la création exclut désormais explicitement l'id d'opération du paiement d'origine, pour ne pas mal classer une redélivrance tardive de la notification de paiement comme un remboursement
  • Vérifié en conditions réelles (staging) : remboursement + notification bout en bout confirmés fonctionnels

Adaptation à PRE-3590 (UPC) :

  • payplug/unified-plugin-core pointe sur ^1.1.0 (version taguée incluant PRE-3589, publiée depuis) au lieu de dev-develop
  • UnifiedApiHostedPaymentService a été supprimée par PRE-3590 (fusionnée dans UnifiedApiPaymentService::createPayment()) et HostedPaymentOutput renommée en PaymentOutputUpc/UnifiedApiHostedPaymentCreator, Upc/HostedPaymentCreatorInterface, et CaptureHostedPaymentRequestHandler adaptés en conséquence
  • HostedFieldDto a un nouveau paramètre (recurringMode, pour l'aliasing PRE-3590) inséré avant browser/customer dans son constructeur — la construction dans CaptureHostedPaymentRequestHandler est passée en arguments nommés pour éviter ce type de désalignement silencieux à l'avenir

Nettoyage : extraction d'un helper partagé Upc/PaymentReferenceHelper (resolveOrderId(), idToString(), resolveGatewayCredentials()), qui remplace les copies précédemment dupliquées dans RefundPaymentProcessor, HostedFieldsWebhookNotificationHandler, CaptureHostedPaymentRequestHandler et UnifiedApiRefundCreator.

Related Issue

Ticket: PRE-3552

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 💥 Breaking change

✅ Quality Checklist

Local Environment & Hooks

  • Local Git hooks (CaptainHook) are installed and executed cleanly (make install).
  • Commit messages strictly follow the (PRE|SMP)-XXXX: description pattern.
  • Branch name follows (feature|fix|hotfix|refactor)/(PRE|SMP)-XXXX... or (release|patch)/x.y.z.

Testing & Code Quality

  • Coding style rules have been applied locally (ECS — 0 errors).
  • Static analysis passes with no new regressions (PHPStan level max — 0 errors).
  • I have added/updated PHPUnit tests if applicable (364/364 passing locally, 67/67 re-verified against the real embedded app vendor in plugin-dockerized-sylius).
  • No PHP syntax newer than 7.1 introduced in src/ or tests/ — n/a pour ce repo (cible PHP ^8.2), et payplug/unified-plugin-core est maintenant consommé via une version taguée stable plutôt qu'une branche de dev.

CI/CD Deployment Context

  • The CI pipeline passes fully on GitHub, including the compatibility matrix (PHP 7.1 / 7.4 / 8.0 / 8.1 / 8.2) and the quality job.

Notes for Reviewer

  • Cette PR a été revue (review automatisée + review humaine de @hdelaforce-payplug) et tous les points valides ont été corrigés : override incorrect de l'outcome sur échec de remboursement, montant erroné sur un remboursement total après un partiel, faux positif dans le matching webhook, verrous à espaces de noms disjoints, et duplication de code (resolveGatewayCredentials/resolveOrderId/idToString) désormais centralisée dans Upc/PaymentReferenceHelper. Le seul point non retenu (dispatch isHostedFields() répété) a été jugé peu pertinent : le trait DelegatesToHostedFieldsCommandProviderTrait existant est conçu pour un autre usage (délégation entre command providers) et ne s'applique pas tel quel ici.
  • payplug/unified-plugin-core pointe maintenant sur ^1.1.0 (version stable taguée) — plus besoin de suivre dev-develop.
  • Vérification DI faite en conditions réelles dans plugin-dockerized-sylius : debug:autowiring/debug:container/cache:clear confirment que le nouveau paramètre ILock de RefundPaymentProcessor se résout sans ambiguïté.
  • Voir CaptureHostedPaymentRequestHandler::buildHostedFieldDto() pour le changement d'arguments nommés — comportement identique, juste plus sûr face à un futur changement de signature de HostedFieldDto.

@adumont-payplug
adumont-payplug merged commit 6a30301 into develop Sep 3, 2026
13 of 14 checks passed
@adumont-payplug
adumont-payplug deleted the feature/PRE-3552_add_uhf_refund branch September 3, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants