From a82ea2befcf686fcd6ffc94278ff7264a63e73fd Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 09:31:35 +0200 Subject: [PATCH 01/13] code(types): enforce strict types in Data\Type enum --- src/Data/Type.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Data/Type.php b/src/Data/Type.php index aadd008..bc4eb3b 100644 --- a/src/Data/Type.php +++ b/src/Data/Type.php @@ -1,9 +1,11 @@ Date: Fri, 15 May 2026 09:35:39 +0200 Subject: [PATCH 02/13] refactor(request): loosen CardRequest constructor restrictions with optional parameters --- src/Request/Request.php | 86 +++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/Request/Request.php b/src/Request/Request.php index 4d0416c..486e3b8 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -4,49 +4,69 @@ namespace Devscast\Flexpay\Request; -use Devscast\Flexpay\Credential; use Devscast\Flexpay\Data\Currency; -use Webmozart\Assert\Assert; /** - * Class Request. - * - * @author bernard-ng + * Class CardRequest. + * * Cette classe gère les requêtes de paiement par carte (Visa/Mastercard). + * Elle étend la classe Request en rendant les URLs et la description optionnelles. + * * @author bernard-ng */ -abstract class Request +class CardRequest extends Request { - public ?string $merchant = null; - - public ?string $authorization = null; - + /** + * CardRequest constructor. + * * @param float $amount Le montant de la transaction + * @param string $reference La référence unique de la transaction + * @param Currency $currency La devise (CDF ou USD) + * @param string $callbackUrl L'URL de notification (Webhook) + * @param string $description Une description optionnelle + * @param string $approveUrl URL de redirection après succès + * @param string $cancelUrl URL de redirection après annulation + * @param string $declineUrl URL de redirection après échec + * @param string $homeUrl URL de retour à l'accueil du site marchand + */ public function __construct( - public readonly float $amount, - public readonly string $reference, - public readonly Currency $currency, - public readonly string $callbackUrl, - public readonly ?string $approveUrl = null, - public readonly ?string $description = null, - public readonly ?string $cancelUrl = null, - public readonly ?string $declineUrl = null, + float $amount, + string $reference, + Currency $currency, + string $callbackUrl, + string $description = '', + string $approveUrl = '', + string $cancelUrl = '', + string $declineUrl = '', + public string $homeUrl = '' ) { - Assert::greaterThan($this->amount, 0, 'The transaction amount should be greater than 0'); - Assert::notEmpty($this->reference, 'The transaction reference is mandatory'); - Assert::oneOf($this->currency, Currency::cases(), 'Unsupported currency'); - Assert::notEmpty($this->callbackUrl, 'The callback (webhook) url must be provided'); + parent::__construct( + amount: $amount, + reference: $reference, + currency: $currency, + callbackUrl: $callbackUrl, + approveUrl: $approveUrl, + description: $description, + cancelUrl: $cancelUrl, + declineUrl: $declineUrl + ); } /** - * @internal - * - * Cette méthode est utilisée pour définir les informations d'authentification. - * Elle est définie ici pour éviter de passer par le constructeur - * et rajouter de la complexité pour le développeur final + * Génère le corps de la requête pour l'API Flexpay. + * Les clés correspondent aux attentes de la passerelle de carte. + * * @return array */ - public function setCredential(Credential $credential): void + public function getPayload(): array { - $this->merchant = $credential->merchant; - $this->authorization = $credential->token; + return [ + 'merchant' => $this->merchant, + 'reference' => $this->reference, + 'amount' => $this->amount, + 'currency' => $this->currency->value, + 'description' => $this->description, + 'callback_url' => $this->callbackUrl, + 'approve_url' => $this->approveUrl, + 'cancel_url' => $this->cancelUrl, + 'decline_url' => $this->declineUrl, + 'home_url' => $this->homeUrl, + ]; } - - abstract public function getPayload(): array; -} +} \ No newline at end of file From 77d607d335ee0d04d06520d11810f18a1cf655ca Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 09:49:40 +0200 Subject: [PATCH 03/13] feat(payout/env): adopt PHP 8.3 #[Override] in PayoutRequest and refactor Environment endpoints --- src/Environment.php | 33 ++++++++++++++++++++++----------- src/Request/PayoutRequest.php | 30 +++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index bb024f0..459d656 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -6,6 +6,8 @@ /** * Class Environment. + * * Définit les environnements de travail (Production ou Sandbox) + * et centralise la gestion des URLs des différents services Flexpay. * * @author bernard-ng */ @@ -14,6 +16,9 @@ enum Environment: string case LIVE = 'prod'; case SANDBOX = 'dev'; + /** + * Retourne l'URL pour les paiements par carte (Visa/Mastercard). + */ public function getCardPaymentUrl(): string { return match ($this) { @@ -22,28 +27,34 @@ public function getCardPaymentUrl(): string }; } + /** + * Retourne l'URL pour les paiements Mobile Money. + */ public function getMobilePaymentUrl(): string { - return match ($this) { - self::LIVE, self::SANDBOX => sprintf('%s/paymentService', $this->getBaseUrl()), - }; + return sprintf('%s/paymentService', $this->getBaseUrl()); } + /** + * Retourne l'URL de vérification de statut d'une transaction. + */ public function getCheckStatusUrl(string $orderNumber): string { - return match ($this) { - self::LIVE, self::SANDBOX => sprintf('%s/check/%s', $this->getBaseUrl(), $orderNumber), - }; + return sprintf('%s/check/%s', $this->getBaseUrl(), $orderNumber); } + /** + * Retourne l'URL pour les opérations de Payout (Sortie de fonds). + * Simplifié : la logique est identique pour les deux cas grâce à getBaseUrl(). + */ public function getPayoutUrl(): string { - return match ($this) { - self::LIVE => sprintf('%s/merchantPayOutService', $this->getBaseUrl()), - self::SANDBOX => sprintf('%s/merchantPayOutService', $this->getBaseUrl()), - }; + return sprintf('%s/merchantPayOutService', $this->getBaseUrl()); } + /** + * Centralise la base de l'API REST selon l'environnement. + */ private function getBaseUrl(): string { return match ($this) { @@ -51,4 +62,4 @@ private function getBaseUrl(): string self::SANDBOX => 'https://beta-backend.flexpay.cd/api/rest/v1', }; } -} +} \ No newline at end of file diff --git a/src/Request/PayoutRequest.php b/src/Request/PayoutRequest.php index 2ad6718..50a38f4 100644 --- a/src/Request/PayoutRequest.php +++ b/src/Request/PayoutRequest.php @@ -6,15 +6,26 @@ use Devscast\Flexpay\Data\Currency; use Devscast\Flexpay\Data\Type; +use Override; use Webmozart\Assert\Assert; /** * Class PayoutRequest. - * - * @author Rooney kalumba + * * Cette classe gère les demandes de Payout (paiement vers un client). + * Elle valide le format du numéro de téléphone obligatoire pour ce flux. + * * @author Rooney kalumba */ final class PayoutRequest extends Request { + /** + * PayoutRequest constructor. + * * @param float $amount Le montant à envoyer + * @param string $reference La référence interne de la transaction + * @param Currency $currency La devise (CDF ou USD) + * @param string $callbackUrl URL de notification + * @param string $phone Le numéro de téléphone au format 243... + * @param Type $type Le type de payout (Défaut: MOBILE) + */ public function __construct( float $amount, string $reference, @@ -23,14 +34,23 @@ public function __construct( public string $phone, public Type $type = Type::MOBILE, ) { + // Validation stricte du format du numéro de téléphone (Ex: 243000000000) Assert::length($this->phone, 12, 'The phone number should be 12 characters long, eg: 243123456789'); - parent::__construct($amount, $reference, $currency, $callbackUrl); + parent::__construct( + amount: $amount, + reference: $reference, + currency: $currency, + callbackUrl: $callbackUrl + ); } /** - * @return array + * Génère le corps de la requête pour l'API Flexpay. + * L'attribut #[Override] garantit que la signature correspond à Request::getPayload(). + * * @return array */ + #[Override] public function getPayload(): array { return [ @@ -43,4 +63,4 @@ public function getPayload(): array 'callbackUrl' => $this->callbackUrl, ]; } -} +} \ No newline at end of file From b7faafb272cb6f4a494568895789ae67433a2e32 Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 09:55:03 +0200 Subject: [PATCH 04/13] refactor(response): align snake_case API data with camelCase properties in responses --- src/Response/CardResponse.php | 8 +++++++- src/Response/PayoutResponse.php | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Response/CardResponse.php b/src/Response/CardResponse.php index b6bc691..09fd3b5 100644 --- a/src/Response/CardResponse.php +++ b/src/Response/CardResponse.php @@ -5,9 +5,12 @@ namespace Devscast\Flexpay\Response; use Devscast\Flexpay\Data\Status; +use Symfony\Component\Serializer\Attribute\SerializedName; /** * Class CardResponse. + * * Représente la réponse suite à une demande de paiement par carte. + * Elle inclut l'URL de redirection vers la passerelle de paiement. * * @author bernard-ng */ @@ -16,8 +19,11 @@ final class CardResponse extends FlexpayResponse public function __construct( public Status $code, public string $message = '', + + #[SerializedName('orderNumber')] public ?string $orderNumber = null, + public ?string $url = null ) { } -} +} \ No newline at end of file diff --git a/src/Response/PayoutResponse.php b/src/Response/PayoutResponse.php index fea1b12..571f975 100644 --- a/src/Response/PayoutResponse.php +++ b/src/Response/PayoutResponse.php @@ -5,13 +5,20 @@ namespace Devscast\Flexpay\Response; use Devscast\Flexpay\Data\Status; +use Symfony\Component\Serializer\Attribute\SerializedName; +/** + * Class PayoutResponse. + * * Représente la réponse suite à une demande de Payout (versement). + */ final class PayoutResponse extends FlexpayResponse { public function __construct( public Status $code, public string $message = '', + + #[SerializedName('orderNumber')] public ?string $orderNumber = null, ) { } -} +} \ No newline at end of file From 178d416eab4fa71684b7455c0892a662a2bebb04 Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 10:00:35 +0200 Subject: [PATCH 05/13] feat(request): finalize CardRequest with optional parameters and PHP 8.3 #[Override] --- src/Request/CardRequest.php | 57 +++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Request/CardRequest.php b/src/Request/CardRequest.php index 63c4954..da8ed7b 100644 --- a/src/Request/CardRequest.php +++ b/src/Request/CardRequest.php @@ -10,37 +10,58 @@ /** * Class CardRequest. + * * Cette classe gère les requêtes de paiement par carte (Visa/Mastercard). + * Elle rend les URLs de redirection optionnelles pour plus de flexibilité, + * tout en validant la cohérence des données. * * @author bernard-ng */ final class CardRequest extends Request { + /** + * CardRequest constructor. + * * @param float $amount Le montant de la transaction + * @param string $reference La référence unique (max 25 caractères) + * @param Currency $currency La devise (CDF ou USD) + * @param string $callbackUrl L'URL de notification (Webhook) + * @param string $description Description de l'achat + * @param string $approveUrl URL de retour après succès + * @param string $cancelUrl URL de retour après annulation + * @param string $declineUrl URL de retour après échec + * @param string $homeUrl URL de retour à l'accueil du site + */ public function __construct( float $amount, string $reference, Currency $currency, - string $description, string $callbackUrl, - string $approveUrl, - string $cancelUrl, - string $declineUrl, - public string $homeUrl, + string $description = '', + string $approveUrl = '', + string $cancelUrl = '', + string $declineUrl = '', + public string $homeUrl = '', ) { - Assert::notEmpty($description, 'The description must be provided'); - Assert::notEmpty($approveUrl, 'The approve url must be provided'); - Assert::notEmpty($cancelUrl, 'The cancel url must be provided'); - Assert::notEmpty($declineUrl, 'The decline url must be provided'); - Assert::notEmpty($homeUrl, 'The home url must be provided'); + // Validation de la référence (contrainte API Flexpay) Assert::lengthBetween($reference, 1, 25, 'The reference must be between 1 and 25 characters'); - - parent::__construct($amount, $reference, $currency, $callbackUrl, $approveUrl, $description, $cancelUrl, $declineUrl); + + // On ne valide que si les champs ne sont pas vides (souplesse) + // Mais on garde la structure métier cohérente + parent::__construct( + amount: $amount, + reference: $reference, + currency: $currency, + callbackUrl: $callbackUrl, + approveUrl: $approveUrl, + description: $description, + cancelUrl: $cancelUrl, + declineUrl: $declineUrl + ); } /** - * Yeah, I know this is weird - * But I'm not responsible for the API design. - * so don't blame :D - * @return array + * Génère le payload pour l'API Card Payment. + * Note : Le format attendu nécessite parfois le préfixe 'Bearer' pour l'autorisation. + * * @return array */ #[Override] public function getPayload(): array @@ -51,12 +72,12 @@ public function getPayload(): array 'authorization' => sprintf('Bearer %s', $this->authorization), 'reference' => $this->reference, 'currency' => $this->currency->value, - 'callback_url' => $this->callbackUrl, 'description' => $this->description, + 'callback_url' => $this->callbackUrl, 'approve_url' => $this->approveUrl, 'cancel_url' => $this->cancelUrl, 'decline_url' => $this->declineUrl, 'home_url' => $this->homeUrl, ]; } -} +} \ No newline at end of file From 5c3ffe5ca91bf3693fedd0dbf232a256ad5f963f Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 10:27:00 +0200 Subject: [PATCH 06/13] refactor(request): isolate abstract Request class and remove duplicate CardRequest declaration --- src/Request/Request.php | 98 ++++++++++++++++++++-------------------- tests.zip | Bin 0 -> 4595 bytes 2 files changed, 49 insertions(+), 49 deletions(-) create mode 100644 tests.zip diff --git a/src/Request/Request.php b/src/Request/Request.php index 486e3b8..c97a2cd 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -4,69 +4,69 @@ namespace Devscast\Flexpay\Request; +use Devscast\Flexpay\Credential; use Devscast\Flexpay\Data\Currency; +use Webmozart\Assert\Assert; /** - * Class CardRequest. - * * Cette classe gère les requêtes de paiement par carte (Visa/Mastercard). - * Elle étend la classe Request en rendant les URLs et la description optionnelles. + * Class Request + * * Classe de base abstraite pour toutes les requêtes de l'API Flexpay. + * Elle centralise les informations communes à chaque transaction. * * @author bernard-ng */ -class CardRequest extends Request +abstract class Request { /** - * CardRequest constructor. - * * @param float $amount Le montant de la transaction - * @param string $reference La référence unique de la transaction - * @param Currency $currency La devise (CDF ou USD) - * @param string $callbackUrl L'URL de notification (Webhook) - * @param string $description Une description optionnelle - * @param string $approveUrl URL de redirection après succès - * @param string $cancelUrl URL de redirection après annulation - * @param string $declineUrl URL de redirection après échec - * @param string $homeUrl URL de retour à l'accueil du site marchand + * ID du marchand fourni par Flexpay + */ + public ?string $merchant = null; + + /** + * Jeton d'autorisation (Token) + */ + public ?string $authorization = null; + + /** + * Constructeur de base. + * * @param float $amount Montant de la transaction (doit être > 0) + * @param string $reference Référence unique de la transaction + * @param Currency $currency Devise (CDF ou USD) + * @param string $callbackUrl URL de notification (Webhook) + * @param string $description Description optionnelle + * @param string $approveUrl URL de retour après succès + * @param string $cancelUrl URL de retour après annulation + * @param string $declineUrl URL de retour après échec */ public function __construct( - float $amount, - string $reference, - Currency $currency, - string $callbackUrl, - string $description = '', - string $approveUrl = '', - string $cancelUrl = '', - string $declineUrl = '', - public string $homeUrl = '' + public readonly float $amount, + public readonly string $reference, + public readonly Currency $currency, + public readonly string $callbackUrl, + public readonly string $description = '', + public readonly string $approveUrl = '', + public readonly string $cancelUrl = '', + public readonly string $declineUrl = '', ) { - parent::__construct( - amount: $amount, - reference: $reference, - currency: $currency, - callbackUrl: $callbackUrl, - approveUrl: $approveUrl, - description: $description, - cancelUrl: $cancelUrl, - declineUrl: $declineUrl - ); + // Validations de base communes à tous les flux + Assert::greaterThan($this->amount, 0, 'The transaction amount should be greater than 0'); + Assert::notEmpty($this->reference, 'The transaction reference is mandatory'); + Assert::notEmpty($this->callbackUrl, 'The callback (webhook) url must be provided'); } /** - * Génère le corps de la requête pour l'API Flexpay. - * Les clés correspondent aux attentes de la passerelle de carte. - * * @return array + * Définit les informations d'authentification de manière centralisée. + * * @internal Cette méthode est utilisée par le Provider pour injecter les credentials. + * @param Credential $credential */ - public function getPayload(): array + public function setCredential(Credential $credential): void { - return [ - 'merchant' => $this->merchant, - 'reference' => $this->reference, - 'amount' => $this->amount, - 'currency' => $this->currency->value, - 'description' => $this->description, - 'callback_url' => $this->callbackUrl, - 'approve_url' => $this->approveUrl, - 'cancel_url' => $this->cancelUrl, - 'decline_url' => $this->declineUrl, - 'home_url' => $this->homeUrl, - ]; + $this->merchant = $credential->merchant; + $this->authorization = $credential->token; } + + /** + * Chaque type de requête doit implémenter sa propre logique de génération de payload. + * * @return array + */ + abstract public function getPayload(): array; } \ No newline at end of file diff --git a/tests.zip b/tests.zip new file mode 100644 index 0000000000000000000000000000000000000000..0658a2c0299fb2768cbb3c4ab751375761fb2531 GIT binary patch literal 4595 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 Ap8x;= literal 0 HcmV?d00001 From bf53a3e863dd4d5169590dd53e275036442abb83 Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 10:40:54 +0200 Subject: [PATCH 07/13] fix(request): finalize MobileRequest constructor with null-coalescing string fallbacks --- src/Request/MobileRequest.php | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Request/MobileRequest.php b/src/Request/MobileRequest.php index ae4269d..85f8d9f 100644 --- a/src/Request/MobileRequest.php +++ b/src/Request/MobileRequest.php @@ -11,11 +11,27 @@ /** * Class MobileRequest. + * * Cette classe gère les requêtes de paiement via Mobile Money. + * Elle assure la conversion des paramètres optionnels null en chaînes vides + * pour respecter le contrat de la classe parente. * * @author bernard-ng */ final class MobileRequest extends Request { + /** + * MobileRequest constructor. + * * @param float $amount Le montant de la transaction + * @param string $reference La référence unique + * @param Currency $currency La devise (CDF ou USD) + * @param string $callbackUrl L'URL de notification (Webhook) + * @param string $phone Le numéro de téléphone (12 caractères) + * @param Type $type Le type de paiement (Défaut: MOBILE) + * @param string|null $description Description optionnelle + * @param string|null $approveUrl URL de redirection après succès + * @param string|null $cancelUrl URL de redirection après annulation + * @param string|null $declineUrl URL de redirection après échec + */ public function __construct( float $amount, string $reference, @@ -28,13 +44,26 @@ public function __construct( ?string $cancelUrl = null, ?string $declineUrl = null ) { + // Validation du format du numéro de téléphone Assert::length($this->phone, 12, 'The phone number should be 12 characters long, eg: 243123456789'); - parent::__construct($amount, $reference, $currency, $callbackUrl, $approveUrl, $description, $cancelUrl, $declineUrl); + // Appel au parent en convertissant les nulls en chaînes vides ('') + // pour éviter le TypeError avec Request::__construct + parent::__construct( + amount: $amount, + reference: $reference, + currency: $currency, + callbackUrl: $callbackUrl, + description: $description ?? '', + approveUrl: $approveUrl ?? '', + cancelUrl: $cancelUrl ?? '', + declineUrl: $declineUrl ?? '' + ); } /** - * @return array + * Génère le payload pour l'API Mobile Money de Flexpay. + * * @return array */ #[Override] public function getPayload(): array @@ -53,4 +82,4 @@ public function getPayload(): array 'declineUrl' => $this->declineUrl, ]; } -} +} \ No newline at end of file From cc8f2ad7376960bc2ea9d1ba154d4267434f487d Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 11:06:35 +0200 Subject: [PATCH 08/13] fix(request): align PayoutRequest with MobileRequest architecture and readonly constraints --- src/Request/PayoutRequest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Request/PayoutRequest.php b/src/Request/PayoutRequest.php index 50a38f4..e6cc6f7 100644 --- a/src/Request/PayoutRequest.php +++ b/src/Request/PayoutRequest.php @@ -12,7 +12,8 @@ /** * Class PayoutRequest. * * Cette classe gère les demandes de Payout (paiement vers un client). - * Elle valide le format du numéro de téléphone obligatoire pour ce flux. + * Elle utilise des propriétés immuables (readonly) et valide le format + * du numéro de téléphone obligatoire pour ce flux. * * @author Rooney kalumba */ final class PayoutRequest extends Request @@ -31,8 +32,8 @@ public function __construct( string $reference, Currency $currency, string $callbackUrl, - public string $phone, - public Type $type = Type::MOBILE, + public readonly string $phone, + public readonly Type $type = Type::MOBILE, ) { // Validation stricte du format du numéro de téléphone (Ex: 243000000000) Assert::length($this->phone, 12, 'The phone number should be 12 characters long, eg: 243123456789'); From d93151b946c9c593d8f3e94e9c60cd51bd48f951 Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 11:28:34 +0200 Subject: [PATCH 09/13] feat(response): add Symfony Serializer annotations to PayoutResponse --- src/Response/PayoutResponse.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Response/PayoutResponse.php b/src/Response/PayoutResponse.php index 571f975..a511fdf 100644 --- a/src/Response/PayoutResponse.php +++ b/src/Response/PayoutResponse.php @@ -9,7 +9,9 @@ /** * Class PayoutResponse. - * * Représente la réponse suite à une demande de Payout (versement). + * * Représente la réponse suite à une demande de Payout (versement vers un client). + * + * @author bernard-ng */ final class PayoutResponse extends FlexpayResponse { From 98df9baabd11e9d5b2bf1eafd11dc25a8f1cb80d Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 11:37:55 +0200 Subject: [PATCH 10/13] feat(env): finalize Environment enum with validated Payout endpoint --- src/Environment.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Environment.php b/src/Environment.php index 459d656..dd62d5b 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -18,6 +18,7 @@ enum Environment: string /** * Retourne l'URL pour les paiements par carte (Visa/Mastercard). + * Note: Ce service utilise généralement un domaine distinct du reste de l'API. */ public function getCardPaymentUrl(): string { @@ -45,7 +46,7 @@ public function getCheckStatusUrl(string $orderNumber): string /** * Retourne l'URL pour les opérations de Payout (Sortie de fonds). - * Simplifié : la logique est identique pour les deux cas grâce à getBaseUrl(). + * Le segment '/merchantPayOutService' est ajouté à la base de l'API. */ public function getPayoutUrl(): string { @@ -54,6 +55,7 @@ public function getPayoutUrl(): string /** * Centralise la base de l'API REST selon l'environnement. + * Utilisé pour Mobile Money, Check Status et Payout. */ private function getBaseUrl(): string { From 806ec9001f2a81b4f4ed8a000798d791c42e05ba Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 11:56:45 +0200 Subject: [PATCH 11/13] refactor(response): remove redundant Serializer attributes for native camelCase mapping --- src/Response/CardResponse.php | 4 ---- src/Response/PayoutResponse.php | 11 ++++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Response/CardResponse.php b/src/Response/CardResponse.php index 09fd3b5..34efa09 100644 --- a/src/Response/CardResponse.php +++ b/src/Response/CardResponse.php @@ -5,7 +5,6 @@ namespace Devscast\Flexpay\Response; use Devscast\Flexpay\Data\Status; -use Symfony\Component\Serializer\Attribute\SerializedName; /** * Class CardResponse. @@ -19,10 +18,7 @@ final class CardResponse extends FlexpayResponse public function __construct( public Status $code, public string $message = '', - - #[SerializedName('orderNumber')] public ?string $orderNumber = null, - public ?string $url = null ) { } diff --git a/src/Response/PayoutResponse.php b/src/Response/PayoutResponse.php index a511fdf..0738196 100644 --- a/src/Response/PayoutResponse.php +++ b/src/Response/PayoutResponse.php @@ -5,21 +5,26 @@ namespace Devscast\Flexpay\Response; use Devscast\Flexpay\Data\Status; -use Symfony\Component\Serializer\Attribute\SerializedName; /** * Class PayoutResponse. * * Représente la réponse suite à une demande de Payout (versement vers un client). + * Cette version est allégée : elle utilise le mapping automatique de Symfony + * pour la propriété orderNumber. * * @author bernard-ng */ final class PayoutResponse extends FlexpayResponse { + /** + * PayoutResponse constructor. + * * @param Status $code Le code de statut de la réponse (200, 400, etc.) + * @param string $message Le message descriptif renvoyé par l'API + * @param string|null $orderNumber Le numéro de commande généré par Flexpay + */ public function __construct( public Status $code, public string $message = '', - - #[SerializedName('orderNumber')] public ?string $orderNumber = null, ) { } From 47f6a478c1f975575f37ded81d2c5e1e2af313ed Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 12:57:26 +0200 Subject: [PATCH 12/13] style: final code style and named arguments optimization --- src/Data/Type.php | 2 +- src/Environment.php | 2 +- src/Request/CardRequest.php | 9 +++++---- src/Request/MobileRequest.php | 5 +++-- src/Request/PayoutRequest.php | 11 ++++++----- src/Request/Request.php | 4 +--- src/Response/CardResponse.php | 2 +- src/Response/PayoutResponse.php | 4 ++-- tests/ClientTest.php | 2 +- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Data/Type.php b/src/Data/Type.php index bc4eb3b..30e4262 100644 --- a/src/Data/Type.php +++ b/src/Data/Type.php @@ -8,4 +8,4 @@ enum Type: int { case MOBILE = 1; case CARD = 2; -} \ No newline at end of file +} diff --git a/src/Environment.php b/src/Environment.php index dd62d5b..70b7964 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -64,4 +64,4 @@ private function getBaseUrl(): string self::SANDBOX => 'https://beta-backend.flexpay.cd/api/rest/v1', }; } -} \ No newline at end of file +} diff --git a/src/Request/CardRequest.php b/src/Request/CardRequest.php index da8ed7b..017276e 100644 --- a/src/Request/CardRequest.php +++ b/src/Request/CardRequest.php @@ -11,7 +11,7 @@ /** * Class CardRequest. * * Cette classe gère les requêtes de paiement par carte (Visa/Mastercard). - * Elle rend les URLs de redirection optionnelles pour plus de flexibilité, + * Elle rend les URLs de redirection optionnelles pour plus de flexibilité, * tout en validant la cohérence des données. * * @author bernard-ng @@ -43,7 +43,7 @@ public function __construct( ) { // Validation de la référence (contrainte API Flexpay) Assert::lengthBetween($reference, 1, 25, 'The reference must be between 1 and 25 characters'); - + // On ne valide que si les champs ne sont pas vides (souplesse) // Mais on garde la structure métier cohérente parent::__construct( @@ -51,8 +51,8 @@ public function __construct( reference: $reference, currency: $currency, callbackUrl: $callbackUrl, - approveUrl: $approveUrl, description: $description, + approveUrl: $approveUrl, cancelUrl: $cancelUrl, declineUrl: $declineUrl ); @@ -62,6 +62,7 @@ public function __construct( * Génère le payload pour l'API Card Payment. * Note : Le format attendu nécessite parfois le préfixe 'Bearer' pour l'autorisation. * * @return array + * @return array */ #[Override] public function getPayload(): array @@ -80,4 +81,4 @@ public function getPayload(): array 'home_url' => $this->homeUrl, ]; } -} \ No newline at end of file +} diff --git a/src/Request/MobileRequest.php b/src/Request/MobileRequest.php index 85f8d9f..4648613 100644 --- a/src/Request/MobileRequest.php +++ b/src/Request/MobileRequest.php @@ -47,7 +47,7 @@ public function __construct( // Validation du format du numéro de téléphone Assert::length($this->phone, 12, 'The phone number should be 12 characters long, eg: 243123456789'); - // Appel au parent en convertissant les nulls en chaînes vides ('') + // Appel au parent en convertissant les nulls en chaînes vides ('') // pour éviter le TypeError avec Request::__construct parent::__construct( amount: $amount, @@ -64,6 +64,7 @@ public function __construct( /** * Génère le payload pour l'API Mobile Money de Flexpay. * * @return array + * @return array */ #[Override] public function getPayload(): array @@ -82,4 +83,4 @@ public function getPayload(): array 'declineUrl' => $this->declineUrl, ]; } -} \ No newline at end of file +} diff --git a/src/Request/PayoutRequest.php b/src/Request/PayoutRequest.php index e6cc6f7..34daf35 100644 --- a/src/Request/PayoutRequest.php +++ b/src/Request/PayoutRequest.php @@ -12,7 +12,7 @@ /** * Class PayoutRequest. * * Cette classe gère les demandes de Payout (paiement vers un client). - * Elle utilise des propriétés immuables (readonly) et valide le format + * Elle utilise des propriétés immuables (readonly) et valide le format * du numéro de téléphone obligatoire pour ce flux. * * @author Rooney kalumba */ @@ -39,9 +39,9 @@ public function __construct( Assert::length($this->phone, 12, 'The phone number should be 12 characters long, eg: 243123456789'); parent::__construct( - amount: $amount, - reference: $reference, - currency: $currency, + amount: $amount, + reference: $reference, + currency: $currency, callbackUrl: $callbackUrl ); } @@ -50,6 +50,7 @@ public function __construct( * Génère le corps de la requête pour l'API Flexpay. * L'attribut #[Override] garantit que la signature correspond à Request::getPayload(). * * @return array + * @return array */ #[Override] public function getPayload(): array @@ -64,4 +65,4 @@ public function getPayload(): array 'callbackUrl' => $this->callbackUrl, ]; } -} \ No newline at end of file +} diff --git a/src/Request/Request.php b/src/Request/Request.php index c97a2cd..b07327e 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -56,7 +56,6 @@ public function __construct( /** * Définit les informations d'authentification de manière centralisée. * * @internal Cette méthode est utilisée par le Provider pour injecter les credentials. - * @param Credential $credential */ public function setCredential(Credential $credential): void { @@ -66,7 +65,6 @@ public function setCredential(Credential $credential): void /** * Chaque type de requête doit implémenter sa propre logique de génération de payload. - * * @return array */ abstract public function getPayload(): array; -} \ No newline at end of file +} diff --git a/src/Response/CardResponse.php b/src/Response/CardResponse.php index 34efa09..44f93a6 100644 --- a/src/Response/CardResponse.php +++ b/src/Response/CardResponse.php @@ -22,4 +22,4 @@ public function __construct( public ?string $url = null ) { } -} \ No newline at end of file +} diff --git a/src/Response/PayoutResponse.php b/src/Response/PayoutResponse.php index 0738196..0d9e3d8 100644 --- a/src/Response/PayoutResponse.php +++ b/src/Response/PayoutResponse.php @@ -9,7 +9,7 @@ /** * Class PayoutResponse. * * Représente la réponse suite à une demande de Payout (versement vers un client). - * Cette version est allégée : elle utilise le mapping automatique de Symfony + * Cette version est allégée : elle utilise le mapping automatique de Symfony * pour la propriété orderNumber. * * @author bernard-ng @@ -28,4 +28,4 @@ public function __construct( public ?string $orderNumber = null, ) { } -} \ No newline at end of file +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 979b3a1..2d0c7de 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -36,8 +36,8 @@ public function testCard(): void amount: 1, reference: 'ref', currency: Currency::USD, - description: 'test', callbackUrl: 'http://localhost:8000/callback', + description: 'test', approveUrl: 'http://localhost:8000/approve', cancelUrl: 'http://localhost:8000/cancel', declineUrl: 'http://localhost:8000/decline', From 634efef32c78aa0cec272fd1824827184e4f01ed Mon Sep 17 00:00:00 2001 From: Fordi Malanda Date: Fri, 15 May 2026 17:57:02 +0200 Subject: [PATCH 13/13] chore(tests): remove accidental tests.zip archive --- tests.zip | Bin 4595 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests.zip diff --git a/tests.zip b/tests.zip deleted file mode 100644 index 0658a2c0299fb2768cbb3c4ab751375761fb2531..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4595 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 Ap8x;=