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 (ou reprise) d'un abonnement et d'un paiement
en attente, ouverture d'une transaction Djomy dont l'URL de paiement est renvoyée au client
mobile, puis vérification du statut du paiement. L'abonnement n'est activé que par
PaymentFulfillmentService (webhook ou vérification de statut). La devise
est le franc guinéen (GNF).
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
- $djomyCheckoutService : DjomyCheckoutService
- $entityManager : EntityManagerInterface
- $featuresService : PredefinedFeaturesService
- $logger : LoggerInterface
- $mobilePaymentService : MobilePaymentService
- $subscriptionService : SubscriptionService
Methods
- __construct() : mixed
- confirmPayment() : JsonResponse
- Vérifie le statut du paiement d'abonnement (route conservée pour la compatibilité)
- getCurrentSubscription() : JsonResponse
- Récupère l'abonnement actuel de l'utilisateur
- getPlans() : JsonResponse
- Récupère tous les plans disponibles
- upgradeToProProcess() : JsonResponse
- Initie le passage au plan Pro et démarre le paiement Djomy
- findReusablePendingPayment() : Payment|null
- Retrouve le paiement en attente d'une précédente tentative pour le même plan et le même cycle.
Properties
$djomyCheckoutService read-only
private
DjomyCheckoutService
$djomyCheckoutService
$entityManager read-only
private
EntityManagerInterface
$entityManager
$featuresService
private
PredefinedFeaturesService
$featuresService
$logger read-only
private
LoggerInterface
$logger
$mobilePaymentService read-only
private
MobilePaymentService
$mobilePaymentService
$subscriptionService read-only
private
SubscriptionService
$subscriptionService
Methods
__construct()
public
__construct(EntityManagerInterface $entityManager, SubscriptionService $subscriptionService, DjomyCheckoutService $djomyCheckoutService, MobilePaymentService $mobilePaymentService, LoggerInterface $logger, 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).
- $djomyCheckoutService : DjomyCheckoutService
-
Ouvre la transaction Djomy d'un paiement.
- $mobilePaymentService : MobilePaymentService
-
Réponses de démarrage, d'échec et de statut communes à l'API mobile.
- $logger : LoggerInterface
-
Journalise les échecs de démarrage, sans donnée sensible.
- $featuresService : PredefinedFeaturesService
-
Service de résolution des fonctionnalités d'un plan en libellés et icônes.
confirmPayment()
Vérifie le statut du paiement d'abonnement (route conservée pour la compatibilité)
public
confirmPayment(Request $request) : JsonResponse
Lit payment_id dans le corps, retrouve le paiement d'abonnement de l'utilisateur authentifié (404
sinon, y compris pour le paiement d'un autre utilisateur) puis relit son statut chez Djomy via
MobilePaymentService::checkStatus(), comme GET /api/payments/{id}/status. Si Djomy confirme le
succès, l'abonnement est activé par PaymentFulfillmentService. Plus aucun identifiant
Stripe n'est lu.
Route : /api/subscription/confirm-payment (POST), nom api_subscription_confirm_payment.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Corps attendu : {"payment_id": "int, obligatoire"}
Réponses : 200 {success, data: {payment_id, status, subscription_status, order_status}},
400 payment_id manquant, 401 non authentifié, 404 paiement introuvable.
Parameters
- $request : Request
-
Requête HTTP dont le corps JSON porte
payment_id.
Attributes
- #[Post]
- $path: '/api/subscription/confirm-payment'
- $summary: 'Vérifie le statut du paiement d\'abonnement (équivalent de GET /api/payments/{id}/status)'
- $tags: ['Subscription API']
- #[RequestBody]
- $required: true
- $content: new OA\JsonContent(required: ['payment_id'], properties: [new OA\Property(property: 'payment_id', type: 'integer', example: 42)], type: 'object')
- #[Response]
- $response: 200
- $description: 'Statut du paiement'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'data', properties: [new OA\Property(property: 'payment_id', type: 'integer', example: 42), new OA\Property(property: 'status', type: 'string', enum: ['COMPLETED', 'PENDING', 'FAILED'], example: 'COMPLETED'), new OA\Property(property: 'subscription_status', type: 'string', nullable: true, example: 'ACTIVE'), new OA\Property(property: 'order_status', type: 'string', nullable: true, example: null)], type: 'object')], type: 'object')
- #[Response]
- $response: 400
- $description: 'payment_id manquant'
- #[Response]
- $response: 401
- $description: 'Non authentifié'
- #[Response]
- $response: 404
- $description: 'Paiement introuvable'
- #[Route]
- '/confirm-payment'
- $name: 'api_subscription_confirm_payment'
- $methods: ['POST']
Return values
JsonResponse —Statut du paiement, 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 passage au plan Pro et démarre le paiement Djomy
public
upgradeToProProcess(Request $request) : JsonResponse
Valide le moyen de paiement (seule la valeur djomy est acceptée), le cycle de facturation
(monthly par défaut, ou yearly) et le téléphone du payeur (payer_phone, normalisé par
DjomyService::normalizePhone()), puis l'éligibilité de l'utilisateur via
SubscriptionService::canUpgrade(). Le plan est retrouvé par son nom « Pro ». Comme sur le web,
le Payment PENDING lié à un Subscription PENDING d'une tentative précédente (même plan,
même cycle) est réutilisé ; sinon l'abonnement et le paiement sont créés dans une transaction. La
transaction Djomy est ensuite ouverte par DjomyCheckoutService::start(). L'abonnement n'est activé
que par PaymentFulfillmentService (webhook ou vérification de statut), jamais ici.
Un échec de démarrage annule la création et est traduit par MobilePaymentService::failureResponse().
Route : /api/subscription/upgrade-to-pro (POST), nom api_subscription_upgrade_to_pro.
Requiert IS_AUTHENTICATED_FULLY (hérité de la classe).
Corps attendu : {"billing_cycle": "monthly|yearly (défaut monthly)", "payment_method": "djomy", "payer_phone": "string, obligatoire"}
Réponses : 201 {success, data: {payment_id, subscription_id, redirect_url, status, amount, currency}},
400 moyen de paiement, cycle, téléphone invalide ou abonnement déjà actif, 401 non authentifié,
404 plan « Pro » absent, 409 paiement déjà en cours ou déjà payé, 503 Djomy non configuré ou indisponible.
Parameters
- $request : Request
-
Requête HTTP dont le corps JSON porte le moyen de paiement, le cycle et le téléphone.
Attributes
- #[Post]
- $path: '/api/subscription/upgrade-to-pro'
- $summary: 'Démarre le paiement Djomy de l\'abonnement Pro'
- $tags: ['Subscription API']
- #[RequestBody]
- $required: true
- $content: new OA\JsonContent(required: ['payment_method', 'payer_phone'], properties: [new OA\Property(property: 'billing_cycle', type: 'string', enum: ['monthly', 'yearly'], example: 'monthly'), new OA\Property(property: 'payment_method', type: 'string', enum: ['djomy'], example: 'djomy'), new OA\Property(property: 'payer_phone', type: 'string', example: '+224 622 00 00 00')], type: 'object')
- #[Response]
- $response: 201
- $description: 'Paiement Djomy démarré : ouvrir redirect_url dans un navigateur'
- $content: new OA\JsonContent(properties: [new OA\Property(property: 'success', type: 'boolean', example: true), new OA\Property(property: 'data', properties: [new OA\Property(property: 'payment_id', type: 'integer', example: 42), new OA\Property(property: 'subscription_id', type: 'integer', example: 123), new OA\Property(property: 'redirect_url', type: 'string', example: 'https://pay.djomy.africa/...'), new OA\Property(property: 'status', type: 'string', example: 'PENDING'), new OA\Property(property: 'amount', type: 'integer', example: 50000), new OA\Property(property: 'currency', type: 'string', example: 'GNF')], type: 'object')], type: 'object')
- #[Response]
- $response: 400
- $description: 'Données invalides (moyen de paiement, cycle, téléphone) ou abonnement déjà actif'
- #[Response]
- $response: 401
- $description: 'Non authentifié'
- #[Response]
- $response: 404
- $description: 'Plan Pro indisponible'
- #[Response]
- $response: 409
- $description: 'Un paiement est déjà en cours ou a déjà été confirmé'
- #[Response]
- $response: 503
- $description: 'Paiement en ligne indisponible'
- #[Route]
- '/upgrade-to-pro'
- $name: 'api_subscription_upgrade_to_pro'
- $methods: ['POST']
Return values
JsonResponse —Réponse JSON de succès ou d'erreur.
findReusablePendingPayment()
Retrouve le paiement en attente d'une précédente tentative pour le même plan et le même cycle.
private
findReusablePendingPayment(User $user, Plan $plan, string $cycle) : Payment|null
Seuls les paiements Pro en libre-service (sans commande ni entreprise) dont l'abonnement est encore en attente sont repris, s'ils n'ont pas encore de transaction ou si leur prestataire est Djomy. Même règle que le parcours web.
Parameters
- $user : User
-
Utilisateur authentifié.
- $plan : Plan
-
Plan Pro.
- $cycle : string
-
Subscription::BILLING_MONTHLYouSubscription::BILLING_YEARLY.
Return values
Payment|null —Paiement réutilisable, ou null.