SubscriptionApiController
extends AbstractController
in package
Contrôleur d'API des abonnements et de leur souscription payante.
Il expose le catalogue des plans actifs, l'abonnement courant de l'utilisateur, puis
le tunnel de passage au plan Pro : création d'un abonnement et d'un paiement en
attente dans une transaction, génération d'un PaymentIntent Stripe destiné au client
mobile, et confirmation ultérieure du paiement qui active l'abonnement et déclenche
l'email de confirmation. La devise utilisée est le franc guinéen (gnf) et le
paiement Orange Money est déclaré indisponible.
Préfixe de route : /api/subscription. Toutes les actions exigent
IS_AUTHENTICATED_FULLY (jeton JWT).
Attributes
- #[IsGranted]
- 'IS_AUTHENTICATED_FULLY'
- #[Route]
- '/api/subscription'
- #[Tag]
- $name: 'Subscription API'
Table of Contents
Properties
- $entityManager : EntityManagerInterface
- $featuresService : PredefinedFeaturesService
- $mailerService : MailerService
- $stripeService : StripeService
- $subscriptionService : SubscriptionService
Methods
- __construct() : mixed
- confirmPayment() : JsonResponse
- Confirme le paiement et active l'abonnement
- getCurrentSubscription() : JsonResponse
- Récupère l'abonnement actuel de l'utilisateur
- getPlans() : JsonResponse
- Récupère tous les plans disponibles
- upgradeToProProcess() : JsonResponse
- Initie le processus d'upgrade vers un plan Pro
- processCreditCardPaymentApi() : JsonResponse
- Traite le paiement par carte bancaire via Stripe pour l'API mobile
Properties
$entityManager read-only
private
EntityManagerInterface
$entityManager
$featuresService
private
PredefinedFeaturesService
$featuresService
$mailerService read-only
private
MailerService
$mailerService
$stripeService read-only
private
StripeService
$stripeService
$subscriptionService read-only
private
SubscriptionService
$subscriptionService
Methods
__construct()
public
__construct(EntityManagerInterface $entityManager, SubscriptionService $subscriptionService, StripeService $stripeService, MailerService $mailerService, PredefinedFeaturesService $featuresService) : mixed
Parameters
- $entityManager : EntityManagerInterface
-
Gestionnaire d'entités Doctrine, utilisé pour les dépôts, la persistance et la transaction d'upgrade.
- $subscriptionService : SubscriptionService
-
Service métier des abonnements (éligibilité à l'upgrade, quota de cartes, activation).
- $stripeService : StripeService
-
Passerelle Stripe (création et récupération des PaymentIntent).
- $mailerService : MailerService
-
Service d'envoi de l'email de confirmation d'abonnement.
- $featuresService : PredefinedFeaturesService
-
Service de résolution des fonctionnalités d'un plan en libellés et icônes.
confirmPayment()
Confirme le paiement et active l'abonnement
public
confirmPayment(Request $request, SessionInterface $session) : JsonResponse
L'identifiant d'abonnement est résolu dans l'ordre suivant : clé subscription_id
du corps, valeur pending_subscription_id en session, puis métadonnée
subscription_id du PaymentIntent. Le PaymentIntent est relu côté Stripe et doit
être au statut succeeded. L'abonnement doit exister, appartenir à l'utilisateur
authentifié et être encore au statut pending. Il est alors activé par
SubscriptionService::activateSubscription(), le paiement correspondant
(retrouvé par stripePaymentIntentId) passe au statut completed s'il existe, la
clé de session est purgée, puis un email de confirmation est envoyé — un échec
d'envoi est journalisé via error_log() sans compromettre la réponse de succès.
Route : /api/subscription/confirm-payment (POST), nom api_subscription_confirm_payment.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Corps attendu : {"payment_intent_id": "string — obligatoire, identifiant du PaymentIntent Stripe", "subscription_id": "int — optionnel, sinon repris de la session ou des métadonnées Stripe"}
Réponses : 200 abonnement activé ({success: true, message, subscription}),
400 payment_intent_id manquant, 400 paiement non abouti côté Stripe
(payment_status renvoyé), 400 aucun abonnement identifiable, 400 abonnement déjà
traité (current_status renvoyé), 403 abonnement appartenant à un autre utilisateur,
404 abonnement introuvable, 500 erreur de l'API Stripe ou exception interne,
401 requête non authentifiée.
Parameters
- $request : Request
-
Requête HTTP dont le corps JSON porte les identifiants de paiement.
- $session : SessionInterface
-
Session contenant éventuellement l'abonnement en attente, purgée en cas de succès.
Attributes
- #[Post]
- $path: '/api/subscription/confirm-payment'
- $summary: 'Confirme le paiement et active l\'abonnement'
- $tags: ['Subscription API']
- #[RequestBody]
- $required: true
- $content: new OA\JsonContent(required: ['payment_intent_id'], properties: [new OA\Property(property: 'payment_intent_id', type: 'string', example: 'pi_1234567890'), new OA\Property(property: 'subscription_id', type: 'integer', example: 123)], type: 'object')
- #[Response]
- $response: 200
- $description: 'Abonnement activé avec succès'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'message', type: 'string', example: 'Abonnement activé avec succès'), new OA\Property(property: 'subscription', properties: [new OA\Property(property: 'id', type: 'integer', example: 123), new OA\Property(property: 'status', type: 'string', example: 'active'), new OA\Property(property: 'plan_name', type: 'string', example: 'Pro')], type: 'object')], type: 'object')
- #[Route]
- '/confirm-payment'
- $name: 'api_subscription_confirm_payment'
- $methods: ['POST']
Return values
JsonResponse —Objet JSON de succès décrivant l'abonnement activé, ou objet d'erreur.
getCurrentSubscription()
Récupère l'abonnement actuel de l'utilisateur
public
getCurrentSubscription() : JsonResponse
Lit l'abonnement actif porté par l'utilisateur. En l'absence d'abonnement, la
réponse reste un succès avec data à null et un message explicite plutôt qu'une
404. Sinon la charge utile agrège les données de l'abonnement (dates formatées au
format ISO 8601, null si non renseignées) enrichies de deux valeurs calculées :
l'éligibilité à un upgrade via SubscriptionService::canUpgrade() et le
nombre de cartes restantes via getRemainingCardsForUser().
Route : /api/subscription/current (GET), nom api_subscription_current.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Réponses : 200 abonnement renvoyé, 200 aucun abonnement actif (data à null),
500 erreur interne ({success: false, message, error}), 401 requête non authentifiée.
Attributes
- #[Get]
- $path: '/api/subscription/current'
- $summary: 'Récupère l\'abonnement actuel de l\'utilisateur'
- $tags: ['Subscription API']
- #[Response]
- $response: 200
- $description: 'Abonnement actuel de l\'utilisateur'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'data', properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'plan_name', type: 'string', example: 'Free'), new OA\Property(property: 'status', type: 'string', example: 'active'), new OA\Property(property: 'billing_cycle', type: 'string', example: 'monthly'), new OA\Property(property: 'amount', type: 'string', example: '0.00'), new OA\Property(property: 'start_date', type: 'string', example: '2024-01-01T00:00:00+00:00'), new OA\Property(property: 'end_date', type: 'string', example: null), new OA\Property(property: 'next_billing_date', type: 'string', example: null), new OA\Property(property: 'can_upgrade', type: 'boolean', example: true), new OA\Property(property: 'cards_remaining', type: 'integer', example: 1)], type: 'object')], type: 'object')
- #[Route]
- '/current'
- $name: 'api_subscription_current'
- $methods: ['GET']
Return values
JsonResponse —Objet JSON {success: bool, data: array<string, mixed>|null}.
getPlans()
Récupère tous les plans disponibles
public
getPlans() : JsonResponse
Charge uniquement les plans marqués isActive = true et les met en forme
manuellement. Les fonctionnalités brutes du plan sont enrichies en libellés et
icônes par PredefinedFeaturesService::getSelectedFeaturesWithIcons(), avec
repli sur un tableau vide. Les champs is_popular et badge sont renvoyés bien
qu'ils ne figurent pas dans le schéma OpenApi déclaré. Toute exception est
interceptée et convertie en réponse 500 incluant le message technique.
Route : /api/subscription/plans (GET), nom api_subscription_plans.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Réponses : 200 catalogue renvoyé ({success: true, data: [...]}),
500 erreur lors du chargement ({success: false, message, error}),
401 requête non authentifiée.
Attributes
- #[Get]
- $path: '/api/subscription/plans'
- $summary: 'Récupère tous les plans disponibles'
- $tags: ['Subscription API']
- #[Response]
- $response: 200
- $description: 'Liste des plans disponibles'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'name', type: 'string', example: 'Pro'), new OA\Property(property: 'description', type: 'string', example: 'Plan professionnel'), new OA\Property(property: 'monthly_price', type: 'string', example: '50000'), new OA\Property(property: 'yearly_price', type: 'string', example: '500000'), new OA\Property(property: 'cards_included', type: 'integer', example: 10), new OA\Property(property: 'features', type: 'array', items: new OA\Items(type: 'string')), new OA\Property(property: 'is_active', type: 'boolean', example: true)], type: 'object'))], type: 'object')
- #[Route]
- '/plans'
- $name: 'api_subscription_plans'
- $methods: ['GET']
Return values
JsonResponse —Objet JSON {success: bool, data: list<array<string, mixed>>}
où chaque plan porte id, name, description, monthly_price,
yearly_price, cards_included, features, is_active, is_popular, badge.
upgradeToProProcess()
Initie le processus d'upgrade vers un plan Pro
public
upgradeToProProcess(Request $request, SessionInterface $session) : JsonResponse
Valide successivement la méthode de paiement (credit_card ou orange_money),
le cycle de facturation (monthly par défaut, ou yearly), puis l'éligibilité de
l'utilisateur via SubscriptionService::canUpgrade(). Le plan est retrouvé par
son nom littéral « Pro » et le prix est choisi selon le cycle. Un Subscription
au statut pending et un Payment au statut pending sont créés puis persistés
dans une transaction explicite ; l'identifiant de l'abonnement est mémorisé en session
sous pending_subscription_id pour la confirmation ultérieure.
Pour Orange Money, la méthode de paiement est enregistrée, la transaction est validée,
mais l'opération est refusée avec un code 503 — l'abonnement en attente reste créé.
Pour la carte bancaire, le traitement est délégué à
SubscriptionApiController::processCreditCardPaymentApi() avant validation de
la transaction. Toute exception provoque un rollback() et une réponse 500.
Route : /api/subscription/upgrade-to-pro (POST), nom api_subscription_upgrade_to_pro.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Corps attendu : {"payment_method": "string — obligatoire, credit_card|orange_money", "billing_cycle": "string — optionnel, monthly|yearly, défaut monthly"}
Réponses : 200 PaymentIntent créé ({success: true, client_secret, payment_intent_id, subscription_id, amount, currency, message}), 400 méthode de paiement invalide,
400 cycle de facturation invalide, 400 abonnement actif déjà en place,
404 plan « Pro » absent en base, 503 paiement Orange Money indisponible,
500 échec Stripe ou exception interne, 401 requête non authentifiée.
Parameters
- $request : Request
-
Requête HTTP dont le corps JSON porte le mode de paiement et le cycle.
- $session : SessionInterface
-
Session utilisée pour mémoriser l'abonnement en attente.
Attributes
- #[Post]
- $path: '/api/subscription/upgrade-to-pro'
- $summary: 'Initie le processus d\'upgrade vers le plan Pro'
- $tags: ['Subscription API']
- #[RequestBody]
- $required: true
- $content: new OA\JsonContent(required: ['billing_cycle', 'payment_method'], properties: [new OA\Property(property: 'billing_cycle', type: 'string', enum: ['monthly', 'yearly'], example: 'monthly'), new OA\Property(property: 'payment_method', type: 'string', enum: ['credit_card', 'orange_money'], example: 'credit_card')], type: 'object')
- #[Response]
- $response: 200
- $description: 'Processus d\'upgrade initié avec succès'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'client_secret', type: 'string', example: 'pi_1234567890_secret_abcdef'), new OA\Property(property: 'payment_intent_id', type: 'string', example: 'pi_1234567890'), new OA\Property(property: 'subscription_id', type: 'integer', example: 123), new OA\Property(property: 'amount', type: 'string', example: '50000'), new OA\Property(property: 'currency', type: 'string', example: 'gnf'), new OA\Property(property: 'message', type: 'string', example: 'PaymentIntent créé avec succès')], type: 'object')
- #[Response]
- $response: 400
- $description: 'Erreur de validation'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: false), new OA\Property(property: 'message', type: 'string', example: 'Données invalides')], type: 'object')
- #[Route]
- '/upgrade-to-pro'
- $name: 'api_subscription_upgrade_to_pro'
- $methods: ['POST']
Return values
JsonResponse —Réponse JSON de succès ou d'erreur (le type de retour n'est pas déclaré sur la signature).
processCreditCardPaymentApi()
Traite le paiement par carte bancaire via Stripe pour l'API mobile
private
processCreditCardPaymentApi(Subscription $subscription, Payment $payment) : JsonResponse
Méthode interne appelée par SubscriptionApiController::upgradeToProProcess().
Elle construit les métadonnées transmises à Stripe (identifiant d'abonnement,
identifiant utilisateur, nom du plan suffixé « Annuel » ou « Mensuel », cycle et
description), marque le paiement comme réglé par carte, crée le PaymentIntent en
gnf avec le montant converti en entier, puis enregistre l'identifiant du
PaymentIntent sur le paiement. Le client_secret renvoyé est destiné à être
consommé par le SDK flutter_stripe côté application mobile.
Aucune route : méthode privée, non exposée en HTTP.
Réponses : 200 PaymentIntent créé, 500 erreur remontée par l'API Stripe (ApiErrorException interceptée).
Parameters
- $subscription : Subscription
-
Abonnement en attente à financer.
- $payment : Payment
-
Paiement en attente associé, mis à jour avec la méthode et l'identifiant Stripe.
Return values
JsonResponse —Objet JSON {success, client_secret, payment_intent_id, subscription_id, amount, currency, message}, ou objet d'erreur en cas d'échec Stripe.