PhysicalCardApiController
extends AbstractController
in package
Points d'entrée REST du parcours de commande des cartes de visite physiques.
Le contrôleur expose le catalogue (produits Card non masqués, modèles CardModel actifs),
la vérification de l'éligibilité à la carte offerte pour les abonnés Pro, la création d'une
commande à partir d'une carte virtuelle existante, la consultation des cartes physiques de
l'utilisateur connecté, la mise à jour administrative du statut, ainsi que le paiement Djomy
(démarrage de la page de paiement hébergée puis vérification du statut).
Toutes les routes sont préfixées par /api/physical-cards, nommées api_physical_cards_*,
réservées aux utilisateurs pleinement authentifiés (IS_AUTHENTICATED_FULLY, JWT) et
regroupées sous le tag OpenAPI « Physical Cards ». Sauf mention contraire, les lectures et
écritures sont systématiquement filtrées sur l'utilisateur courant.
Attributes
- #[IsGranted]
- 'IS_AUTHENTICATED_FULLY'
- #[Route]
- '/api/physical-cards'
- $name: 'api_physical_cards_'
- #[Tag]
- $name: 'Physical Cards'
- $description: 'Gestion des cartes physiques'
Table of Contents
Properties
- $businessCardRepository : BusinessCardAizeRepository
- $cardModelRepository : CardModelRepository
- $cardRepository : CardRepository
- $defaultProCardService : DefaultProCardService
- $djomyCheckoutService : DjomyCheckoutService
- $entityManager : EntityManagerInterface
- $logger : LoggerInterface
- $mailerService : MailerService
- $mobilePaymentService : MobilePaymentService
- $physicalCardRepository : PhysicalCardRepository
- $subscriptionService : SubscriptionService
- $uploaderService : UploaderService
Methods
- __construct() : mixed
- checkFreeCardEligibility() : JsonResponse
- Indique si l'utilisateur connecté peut encore bénéficier de la carte physique offerte.
- confirmPayment() : JsonResponse
- Vérifie le statut du paiement d'une carte physique (route conservée pour la compatibilité)
- createOrder() : JsonResponse
- Crée une carte physique et la commande associée à partir d'une carte virtuelle de l'utilisateur.
- getModels() : JsonResponse
- Renvoie les modèles graphiques de carte actuellement activés.
- getProducts() : JsonResponse
- Renvoie le catalogue des produits de carte physique commercialisables.
- getUserOrders() : JsonResponse
- Liste les cartes physiques commandées par l'utilisateur connecté.
- pay() : JsonResponse
- Démarre le paiement Djomy d'une carte physique
- show() : JsonResponse
- Renvoie le détail d'une carte physique appartenant à l'utilisateur connecté.
- updateStatus() : JsonResponse
- Modifie le statut de suivi d'une carte physique, réservé aux administrateurs.
- createOrderForPhysicalCard() : Order
- Crée une commande pour une carte physique
- isValidHexColor() : bool
- Valide qu'une couleur est au format hexadécimal valide
- parseMoney() : float
- Convertit un montant textuel en nombre à virgule flottante exploitable pour un calcul.
- validateImageFile() : void
- Valide un fichier image uploadé
- validateLogoFile() : void
- Valide un fichier logo uploadé
Properties
$businessCardRepository read-only
private
BusinessCardAizeRepository
$businessCardRepository
$cardModelRepository read-only
private
CardModelRepository
$cardModelRepository
$cardRepository read-only
private
CardRepository
$cardRepository
$defaultProCardService read-only
private
DefaultProCardService
$defaultProCardService
$djomyCheckoutService read-only
private
DjomyCheckoutService
$djomyCheckoutService
$entityManager read-only
private
EntityManagerInterface
$entityManager
$logger read-only
private
LoggerInterface
$logger
$mailerService read-only
private
MailerService
$mailerService
$mobilePaymentService read-only
private
MobilePaymentService
$mobilePaymentService
$physicalCardRepository read-only
private
PhysicalCardRepository
$physicalCardRepository
$subscriptionService read-only
private
SubscriptionService
$subscriptionService
$uploaderService read-only
private
UploaderService
$uploaderService
Methods
__construct()
public
__construct(EntityManagerInterface $entityManager, PhysicalCardRepository $physicalCardRepository, CardRepository $cardRepository, CardModelRepository $cardModelRepository, BusinessCardAizeRepository $businessCardRepository, SubscriptionService $subscriptionService, DefaultProCardService $defaultProCardService, UploaderService $uploaderService, DjomyCheckoutService $djomyCheckoutService, MobilePaymentService $mobilePaymentService, LoggerInterface $logger, MailerService $mailerService) : mixed
Parameters
- $entityManager : EntityManagerInterface
-
Gestionnaire Doctrine : accès direct aux dépôts
Card,CardModel,CardColorOptionetCardPrintOption, persistance et flush. - $physicalCardRepository : PhysicalCardRepository
-
Dépôt des cartes physiques : comptage par utilisateur, listage et recherche par identifiant.
- $cardRepository : CardRepository
-
Dépôt des produits de carte, utilisé pour résoudre le
cardProductIddemandé. - $cardModelRepository : CardModelRepository
-
Dépôt des modèles de carte, utilisé pour résoudre le
cardModelIdoptionnel. - $businessCardRepository : BusinessCardAizeRepository
-
Dépôt des cartes virtuelles, utilisé pour vérifier la propriété de la carte source.
- $subscriptionService : SubscriptionService
-
Détermine l'accès aux fonctionnalités premium (abonnement Pro).
- $defaultProCardService : DefaultProCardService
-
Fournit la carte Pro offerte configurée et l'état « première commande ».
- $uploaderService : UploaderService
-
Enregistre sur disque les images recto/verso et le logo transmis.
- $djomyCheckoutService : DjomyCheckoutService
-
Ouvre la transaction Djomy d'un paiement de carte.
- $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.
- $mailerService : MailerService
-
Envoie l'e-mail de confirmation de commande de carte physique.
checkFreeCardEligibility()
Indique si l'utilisateur connecté peut encore bénéficier de la carte physique offerte.
public
checkFreeCardEligibility() : JsonResponse
Trois informations sont combinées : l'accès aux fonctionnalités premium
(SubscriptionService::hasAccessToPremiumFeatures()), le fait qu'aucune carte physique ne
soit déjà rattachée à l'utilisateur (DefaultProCardService::isFirstPhysicalCardOrder())
et l'existence d'une carte Pro par défaut configurée globalement. Le champ eligible vaut
true uniquement si l'abonnement premium est actif ET s'il s'agit de la première commande ;
il ne tient donc pas compte de la présence effective de la carte offerte, contrairement à
shouldUseDefaultProduct qui exige en plus qu'une carte par défaut soit paramétrée.
Le champ reason est un libellé explicatif hiérarchisé : « Abonnement Pro requis » si
l'abonnement manque, sinon « Première commande déjà utilisée » si une carte physique
existe déjà, sinon « Première commande Pro ». defaultProduct décrit la carte offerte
(identifiant, nom, description, prix en chaîne, isActive = négation de isHide()) ou vaut
null si aucune n'est configurée. Méthode de lecture seule, sans effet de bord.
Route : /api/physical-cards/eligibility/free-card (GET), nom
api_physical_cards_eligibility_free_card. Authentification pleine requise.
Réponses : 200 dans tous les cas, y compris lorsque l'utilisateur n'est pas éligible ; 401 si le jeton est absent ou invalide.
Attributes
- #[Get]
- $path: '/api/physical-cards/eligibility/free-card'
- $description: 'Vérifie si l\'utilisateur est éligible à une carte physique gratuite'
- $summary: 'Vérifier l\'éligibilité à une carte gratuite'
- $tags: ['Physical Cards']
- $responses: [new OA\Response(response: 200, description: 'Statut d\'éligibilité', content: new OA\JsonContent(properties: [new OA\Property(property: 'eligible', type: 'boolean', example: true), new OA\Property(property: 'reason', type: 'string', example: 'Première commande Pro'), new OA\Property(property: 'hasProSubscription', type: 'boolean', example: true), new OA\Property(property: 'shouldUseDefaultProduct', type: 'boolean', example: true), new OA\Property(property: 'defaultProduct', properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'name', type: 'string', example: 'Carte Pro Offerte'), new OA\Property(property: 'description', type: 'string', example: 'Produit offert par défaut'), new OA\Property(property: 'price', type: 'string', example: '29999'), new OA\Property(property: 'isActive', type: 'boolean', example: true)], type: 'object', nullable: true)])), new OA\Response(response: 401, description: 'Non authentifié')]
- #[Route]
- '/eligibility/free-card'
- $name: 'eligibility_free_card'
- $methods: ['GET']
Return values
JsonResponse —Objet JSON {eligible, reason, hasProSubscription, shouldUseDefaultProduct, defaultProduct}.
confirmPayment()
Vérifie le statut du paiement d'une carte physique (route conservée pour la compatibilité)
public
confirmPayment(int $id) : JsonResponse
Retrouve le paiement de la commande de la carte (le paiement COMPLETED s'il existe, sinon le plus
récent) puis relit son statut chez Djomy via MobilePaymentService::checkStatus(), comme
GET /api/payments/{id}/status. Si Djomy confirme le succès, la carte et la commande sont confirmées par
PaymentFulfillmentService. Le corps de la requête n'est pas lu.
Route : /api/physical-cards/{id}/confirm-payment (POST), nom api_physical_cards_confirm_payment.
Réponses : 200 {success, data: {payment_id, status, subscription_status, order_status, physical_card_id, card_status}}, 401 non authentifié, 404 carte introuvable ou sans paiement.
Parameters
- $id : int
-
Identifiant de la carte physique.
Attributes
- #[Post]
- $path: '/api/physical-cards/{id}/confirm-payment'
- $description: 'Relit le statut du paiement de la carte chez Djomy (équivalent de GET /api/payments/{id}/status)'
- $summary: 'Vérifier le paiement d\'une carte physique'
- $tags: ['Physical Cards']
- $parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))]
- $responses: [new OA\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: null), new OA\Property(property: 'order_status', type: 'string', nullable: true, example: 'En traitement'), new OA\Property(property: 'physical_card_id', type: 'integer', example: 7), new OA\Property(property: 'card_status', type: 'string', example: 'CONFIRMED')], type: 'object')])), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 404, description: 'Carte physique ou paiement introuvable')]
- #[Route]
- '/{id}/confirm-payment'
- $name: 'confirm_payment'
- $requirements: ['id' => '\d+']
- $methods: ['POST']
Return values
JsonResponse —Statut du paiement, ou objet d'erreur.
createOrder()
Crée une carte physique et la commande associée à partir d'une carte virtuelle de l'utilisateur.
public
createOrder(Request $request) : JsonResponse
Le corps est lu en multipart/form-data (champs texte via $request->request, fichiers via
$request->files), et non en JSON. Le déroulement est le suivant :
détermination de l'éligibilité à la gratuité ($eligibleForFreeBase = aucune carte physique
déjà comptée pour l'utilisateur ET accès premium ET carte Pro par défaut configurée) ;
validation des champs obligatoires et des longueurs (adresse ≤ 500, frontText et
backText ≤ 500, designerInstructions ≤ 1000) ; vérification que la carte virtuelle
appartient bien à l'utilisateur ; résolution du produit — la carte Pro par défaut est
imposée par le serveur lorsque cardProductId est omis et que l'utilisateur est éligible,
ce que signale le drapeau serverEnforcedProduct de la réponse. Les options couleur et
impression fournies doivent être actives et rattachées au produit choisi ; à défaut de
colorOptionId, la première option couleur active est retenue, et à défaut de
printOptionId, la première option d'impression active marquée par défaut, sinon la
première option d'impression active. Les fichiers envoyés sont validés puis stockés via
UploaderService ; toute erreur d'upload est accumulée et interrompt la création.
Le prix est la somme du prix du produit (remplacé par 0 si la base est offerte, c'est-à-dire
si l'utilisateur est éligible et que le produit retenu est bien la carte Pro par défaut) et
des suppléments couleur et impression, chaque valeur passant par parseMoney(). Si ce total
est nul et que la base est offerte, la carte passe en STATUS_CONFIRMED avec un Payment
de 0,00 en méthode PRO_SUBSCRIPTION et une commande en Order::EN_TRAITEMENT, puis un
e-mail de confirmation est tenté (les échecs d'envoi sont seulement journalisés). Sinon la
carte passe en STATUS_PENDING_PAYMENT avec une commande en Order::DRAFT, en attente du
paiement Djomy. Dans les deux cas la carte est initialement créée en STATUS_PENDING.
Route : /api/physical-cards/create-order (POST), nom api_physical_cards_create_order.
Authentification requise (IS_AUTHENTICATED en plus de la contrainte de classe).
Corps attendu (multipart) : {"virtualCardId": "int — carte virtuelle source, obligatoire", "shippingAddress": "string — adresse de livraison, obligatoire, 500 caractères maximum", "cardProductId": "int — produit commandé, obligatoire sauf si la carte offerte s'applique", "cardModelId": "int — modèle graphique actif, optionnel", "designerInstructions": "string — consignes au graphiste, 1000 caractères maximum", "frontText": "string — texte recto, 500 caractères maximum", "backText": "string — texte verso, 500 caractères maximum", "colorOptionId": "int — option couleur active du produit, optionnel", "printOptionId": "int — option d'impression active du produit, optionnel", "fontFamily": "string — police retenue, optionnel", "customFrontImage": "fichier — visuel recto personnalisé, optionnel", "customBackImage": "fichier — visuel verso personnalisé, optionnel", "logoFile": "fichier — logo, optionnel"}
Réponses : 201 lorsque la carte et la commande sont créées, que ce soit en carte offerte ou
en attente de paiement ; 400 si virtualCardId ou shippingAddress manque, si
cardProductId manque hors cas de gratuité, si une longueur maximale est dépassée, si
l'option couleur ou d'impression est inconnue, inactive ou étrangère au produit, ou si un
upload échoue ; 404 si la carte virtuelle est introuvable ou n'appartient pas à
l'utilisateur, si le produit est introuvable ou masqué, ou si le modèle est introuvable ou
inactif ; 500 si une exception survient pendant le traitement ; 401 si le jeton est absent
ou invalide.
Parameters
- $request : Request
-
Requête HTTP portant les champs multipart et les fichiers téléversés.
Attributes
- #[IsGranted]
- 'IS_AUTHENTICATED'
- #[Post]
- $path: '/api/physical-cards/create-order'
- $description: 'Créer une nouvelle commande de carte physique'
- $summary: 'Créer une commande de carte physique'
- $security: [['bearerAuth' => []]]
- $requestBody: new OA\RequestBody(description: 'Données de la commande', required: true, content: new OA\MediaType(mediaType: 'multipart/form-data', schema: new OA\Schema(required: ['virtualCardId', 'shippingAddress'], properties: ['virtualCardId' => new OA\Property(property: 'virtualCardId', description: 'ID de la carte virtuelle', type: 'integer'), 'cardProductId' => new OA\Property(property: 'cardProductId', description: 'ID du produit de carte', type: 'integer'), 'cardModelId' => new OA\Property(property: 'cardModelId', description: 'ID du modèle de carte (optionnel)', type: 'integer'), 'shippingAddress' => new OA\Property(property: 'shippingAddress', description: 'Adresse de livraison', type: 'string'), 'designerInstructions' => new OA\Property(property: 'designerInstructions', description: 'Instructions pour le designer', type: 'string'), 'frontText' => new OA\Property(property: 'frontText', description: 'Texte pour la face avant', type: 'string'), 'backText' => new OA\Property(property: 'backText', description: 'Texte pour la face arrière', type: 'string'), 'colorOptionId' => new OA\Property(property: 'colorOptionId', description: 'ID de l\'option couleur', type: 'integer'), 'printOptionId' => new OA\Property(property: 'printOptionId', description: 'ID de l\'option d\'impression', type: 'integer'), 'fontFamily' => new OA\Property(property: 'fontFamily', description: 'Police de caractères', type: 'string'), 'customFrontImage' => new OA\Property(property: 'customFrontImage', description: 'Image personnalisée recto', type: 'string', format: 'binary'), 'customBackImage' => new OA\Property(property: 'customBackImage', description: 'Image personnalisée verso', type: 'string', format: 'binary'), 'logoFile' => new OA\Property(property: 'logoFile', description: 'Fichier logo', type: 'string', format: 'binary')])))
- $tags: ['Physical Cards']
- $responses: [new OA\Response(response: 201, description: 'Commande créée avec succès', content: new OA\JsonContent(properties: ['serverEnforcedProduct' => new OA\Property(property: 'serverEnforcedProduct', type: 'boolean'), 'message' => new OA\Property(property: 'message', type: 'string'), 'physicalCard' => new OA\Property(property: 'physicalCard', properties: ['id' => new OA\Property(property: 'id', type: 'integer'), 'status' => new OA\Property(property: 'status', type: 'string'), 'price' => new OA\Property(property: 'price', type: 'string'), 'isFree' => new OA\Property(property: 'isFree', type: 'boolean'), 'orderId' => new OA\Property(property: 'orderId', type: 'integer'), 'usedProduct' => new OA\Property(property: 'usedProduct', properties: ['id' => new OA\Property(property: 'id', type: 'integer'), 'name' => new OA\Property(property: 'name', type: 'string'), 'description' => new OA\Property(property: 'description', type: 'string'), 'price' => new OA\Property(property: 'price', type: 'string'), 'isDefault' => new OA\Property(property: 'isDefault', type: 'boolean')], type: 'object')], type: 'object')])), new OA\Response(response: 400, description: 'Données invalides'), new OA\Response(response: 403, description: 'Accès refusé ou offre PRO requise'), new OA\Response(response: 404, description: 'Ressource non trouvée')]
- #[Route]
- '/create-order'
- $name: 'create_order'
- $methods: ['POST']
Return values
JsonResponse —Objet JSON {serverEnforcedProduct, message, physicalCard} où
physicalCard contient l'identifiant, le statut, le prix, isFree,
l'identifiant de commande et le produit effectivement utilisé ; ou un
objet {error, details?} en cas d'échec.
getModels()
Renvoie les modèles graphiques de carte actuellement activés.
public
getModels() : JsonResponse
Charge les entités CardModel filtrées sur isActive = true et les sérialise avec leur
identifiant, leur nom, leur type et les chemins des visuels recto (frontImage) et verso
(backImage). Comme pour le catalogue produit, la vérification d'abonnement Pro est
commentée : l'utilisateur courant est lu mais aucun filtrage ni refus n'en découle.
Route : /api/physical-cards/models (GET), nom api_physical_cards_get_models.
Authentification pleine requise.
Réponses : 200 toujours, avec la liste (éventuellement vide) ; 401 si le jeton est absent ou invalide.
Attributes
- #[Get]
- $path: '/api/physical-cards/models'
- $description: 'Récupère la liste des modèles de cartes physiques disponibles'
- $summary: 'Lister les modèles de cartes disponibles'
- $tags: ['Physical Cards']
- $responses: [new OA\Response(response: 200, description: 'Liste des modèles de cartes', content: new OA\JsonContent(type: 'array', items: new OA\Items(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'name', type: 'string', example: 'Modèle Élégant'), new OA\Property(property: 'type', type: 'string', example: 'Business'), new OA\Property(property: 'frontImage', type: 'string', example: '/uploads/models/elegant-front.jpg'), new OA\Property(property: 'backImage', type: 'string', example: '/uploads/models/elegant-back.jpg'), new OA\Property(property: 'isActive', type: 'boolean', example: true)]))), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 403, description: 'Accès refusé - Abonnement Pro requis')]
- #[Route]
- '/models'
- $name: 'get_models'
- $methods: ['GET']
Return values
JsonResponse —Tableau JSON de array<string, mixed> décrivant chaque modèle actif.
getProducts()
Renvoie le catalogue des produits de carte physique commercialisables.
public
getProducts() : JsonResponse
Charge toutes les entités Card dont le drapeau isHide vaut false et les projette en
un tableau JSON plat (identifiant, nom, description, prix converti en chaîne, isActive
calculé comme la négation de isHide()). L'utilisateur courant est récupéré mais n'est
plus utilisé : le contrôle d'abonnement Pro est présent en commentaire et donc inactif,
de sorte que le statut 403 documenté dans l'attribut OpenAPI n'est jamais renvoyé.
Route : /api/physical-cards/products (GET), nom api_physical_cards_get_products.
Authentification pleine requise (IS_AUTHENTICATED_FULLY au niveau de la classe).
Réponses : 200 toujours, avec la liste (éventuellement vide) ; 401 si le jeton est absent ou invalide.
Attributes
- #[Get]
- $path: '/api/physical-cards/products'
- $description: 'Récupère la liste des produits de cartes physiques disponibles'
- $summary: 'Lister les produits de cartes disponibles'
- $tags: ['Physical Cards']
- $responses: [new OA\Response(response: 200, description: 'Liste des produits de cartes', content: new OA\JsonContent(type: 'array', items: new OA\Items(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'name', type: 'string', example: 'Carte Standard'), new OA\Property(property: 'description', type: 'string', example: 'Carte de visite standard en papier de qualité'), new OA\Property(property: 'price', type: 'string', example: '29.99'), new OA\Property(property: 'isActive', type: 'boolean', example: true)]))), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 403, description: 'Accès refusé - Abonnement Pro requis')]
- #[Route]
- '/products'
- $name: 'get_products'
- $methods: ['GET']
Return values
JsonResponse —Tableau JSON de array<string, mixed> décrivant chaque produit visible.
getUserOrders()
Liste les cartes physiques commandées par l'utilisateur connecté.
public
getUserOrders() : JsonResponse
Interroge PhysicalCardRepository sur le seul utilisateur courant, tri décroissant sur
createdAt, et projette chaque PhysicalCard (et non l'entité Order) en un objet
contenant l'identifiant, le statut, le prix exposé sous la clé totalPrice avec la valeur
de repli '0.00', la date de création au format ISO 8601 (format('c')) et la date de mise
à jour, null tant que la carte n'a jamais été modifiée.
Route : /api/physical-cards/orders (GET), nom api_physical_cards_user_orders.
Authentification pleine requise.
Réponses : 200 toujours, avec un tableau éventuellement vide ; 401 si le jeton est absent ou invalide.
Attributes
- #[Get]
- $path: '/api/physical-cards/orders'
- $description: 'Récupère toutes les commandes de cartes physiques de l\'utilisateur'
- $summary: 'Récupérer les commandes utilisateur'
- $tags: ['Physical Cards']
- $responses: [new OA\Response(response: 200, description: 'Liste des commandes', content: new OA\JsonContent(type: 'array', items: new OA\Items(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'status', type: 'string', example: 'pending'), new OA\Property(property: 'totalPrice', type: 'string', example: '29.99'), new OA\Property(property: 'createdAt', type: 'string', format: 'date-time'), new OA\Property(property: 'updatedAt', type: 'string', format: 'date-time')]))), new OA\Response(response: 401, description: 'Non authentifié')]
- #[Route]
- '/orders'
- $name: 'user_orders'
- $methods: ['GET']
Return values
JsonResponse —Tableau JSON reflétant une list<PhysicalCard> sérialisée.
pay()
Démarre le paiement Djomy d'une carte physique
public
pay(int $id, Request $request) : JsonResponse
Remplace l'ancienne création de PaymentIntent Stripe. Contrôles : la carte appartient à l'utilisateur
(404 sinon), elle n'est pas déjà payée ou en traitement (403), son prix est strictement positif (400),
le téléphone du payeur (payer_phone) est valide (400). La commande de la carte est créée si elle manque
et doit encore attendre un paiement (DRAFT, EN_ATTENTE ou EN_ATTENTE_PAIEMENT, sinon 409). Le
paiement PENDING de la commande est réutilisé, sinon un Payment est créé pour le prix de la
carte, comme dans PhysicalCardController::startDjomyPayment() côté web. La devise est toujours GNF
côté serveur : le corps de la requête n'est jamais lu pour elle. La transaction est ouverte par
DjomyCheckoutService::start() ; la carte et la commande ne sont confirmées que par
PaymentFulfillmentService (webhook ou vérification de statut).
Route : /api/physical-cards/{id}/pay (POST), nom api_physical_cards_pay.
Corps attendu : {"payer_phone": "string, obligatoire"}
Réponses : 201 {success, data: {payment_id, physical_card_id, order_id, redirect_url, status, amount, currency}}, 400 carte gratuite, téléphone invalide ou paiement non payable, 401 non authentifié,
403 carte déjà payée, 404 carte introuvable, 409 paiement déjà en cours ou déjà confirmé, commande qui
ne peut plus être payée, 503 Djomy non configuré ou indisponible.
Parameters
- $id : int
-
Identifiant de la carte physique.
- $request : Request
-
Requête HTTP dont le corps JSON porte
payer_phone.
Attributes
- #[Post]
- $path: '/api/physical-cards/{id}/pay'
- $description: 'Démarre le paiement Djomy d\'une carte physique et renvoie l\'URL de la page de paiement'
- $summary: 'Payer une carte physique avec Djomy'
- $requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(required: ['payer_phone'], properties: [new OA\Property(property: 'payer_phone', description: 'Téléphone du payeur', type: 'string', example: '+224 622 00 00 00')]))
- $tags: ['Physical Cards']
- $parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))]
- $responses: [new OA\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: 'physical_card_id', type: 'integer', example: 7), new OA\Property(property: 'order_id', type: 'integer', example: 15), 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: 150000), new OA\Property(property: 'currency', type: 'string', example: 'GNF')], type: 'object')])), new OA\Response(response: 400, description: 'Carte gratuite, téléphone invalide ou paiement non payable'), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 403, description: 'Carte déjà payée ou en traitement'), new OA\Response(response: 404, description: 'Carte physique non trouvée'), new OA\Response(response: 409, description: 'Paiement déjà en cours ou déjà confirmé'), new OA\Response(response: 503, description: 'Paiement en ligne indisponible')]
- #[Route]
- '/{id}/pay'
- $name: 'pay'
- $requirements: ['id' => '\d+']
- $methods: ['POST']
Return values
JsonResponse —Réponse JSON de succès ou d'erreur.
show()
Renvoie le détail d'une carte physique appartenant à l'utilisateur connecté.
public
show(int $id) : JsonResponse
La recherche combine l'identifiant et l'utilisateur courant dans un unique findOneBy() :
une carte existante mais appartenant à un autre utilisateur est donc traitée comme
introuvable et provoque un 404, jamais un 403. La charge utile reprend l'identifiant, le
statut, le prix sous la clé totalPrice avec la valeur de repli '0.00', la date de
création au format ISO 8601 et la date de mise à jour, null si elle n'a jamais été
renseignée.
Route : /api/physical-cards/{id} (GET), nom api_physical_cards_show.
Authentification pleine requise.
Réponses : 200 si la carte appartient à l'utilisateur ; 404 si elle est inexistante ou détenue par un autre utilisateur ; 401 si le jeton est absent ou invalide. Le 403 documenté dans l'attribut OpenAPI n'est pas produit par le code.
Parameters
- $id : int
-
Identifiant de la carte physique demandée, extrait de l'URL.
Attributes
- #[Get]
- $path: '/api/physical-cards/{id}'
- $description: 'Récupère les détails d\'une carte physique spécifique'
- $summary: 'Récupérer une carte physique'
- $tags: ['Physical Cards']
- $parameters: [new OA\Parameter(name: 'id', description: 'ID de la carte physique', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))]
- $responses: [new OA\Response(response: 200, description: 'Détails de la carte physique', content: new OA\JsonContent(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'status', type: 'string', example: 'pending'), new OA\Property(property: 'totalPrice', type: 'string', example: '29.99'), new OA\Property(property: 'createdAt', type: 'string', format: 'date-time'), new OA\Property(property: 'updatedAt', type: 'string', format: 'date-time')])), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 403, description: 'Accès refusé'), new OA\Response(response: 404, description: 'Carte non trouvée')]
- #[Route]
- '/{id}'
- $name: 'show'
- $methods: ['GET']
Return values
JsonResponse —Objet JSON {id, status, totalPrice, createdAt, updatedAt} ou
{error} si la carte est introuvable.
updateStatus()
Modifie le statut de suivi d'une carte physique, réservé aux administrateurs.
public
updateStatus(int $id, Request $request) : JsonResponse
La carte est recherchée par son seul identifiant, sans filtre sur le propriétaire : un
administrateur peut donc agir sur la carte de n'importe quel utilisateur. Le statut soumis
doit appartenir à la liste blanche STATUS_PENDING, STATUS_PROCESSING, STATUS_SHIPPED
et STATUS_DELIVERED ; les statuts STATUS_PENDING_PAYMENT et STATUS_CONFIRMED, pilotés
par le parcours de paiement (createPaymentIntent(), confirmPayment(), ou la création
d'une carte offerte), sont volontairement refusés ici. Aucune transition n'est contrôlée :
n'importe quel statut autorisé peut succéder à n'importe quel autre, y compris en retour
arrière. En cas de succès, updatedAt est repositionné à l'instant courant et la
modification est écrite via un flush(). Le message d'erreur en cas de statut invalide
énumère les valeurs acceptées.
Route : /api/physical-cards/{id}/status (PATCH), nom api_physical_cards_update_status.
Rôle ROLE_ADMIN requis.
Corps attendu : {"status": "string — nouveau statut, parmi pending, processing, shipped, delivered"}
Réponses : 200 si le statut est appliqué ; 400 si la clé status est absente du corps JSON
ou si sa valeur n'est pas dans la liste blanche ; 404 si aucune carte physique ne porte cet
identifiant ; 403 si l'appelant n'est pas administrateur ; 401 si le jeton est absent ou
invalide.
Parameters
- $id : int
-
Identifiant de la carte physique à mettre à jour, extrait de l'URL.
- $request : Request
-
Requête HTTP dont le corps JSON porte le nouveau statut.
Attributes
- #[IsGranted]
- 'ROLE_ADMIN'
- #[Patch]
- $path: '/api/physical-cards/{id}/status'
- $description: 'Met à jour le statut d\'une carte physique (admin uniquement)'
- $summary: 'Mettre à jour le statut'
- $requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(properties: [new OA\Property(property: 'status', type: 'string', enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'], example: 'processing')]))
- $tags: ['Physical Cards']
- $parameters: [new OA\Parameter(name: 'id', description: 'ID de la carte physique', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))]
- $responses: [new OA\Response(response: 200, description: 'Statut mis à jour avec succès', content: new OA\JsonContent(properties: [new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'status', type: 'string', example: 'processing'), new OA\Property(property: 'updatedAt', type: 'string', format: 'date-time')])), new OA\Response(response: 400, description: 'Statut invalide'), new OA\Response(response: 401, description: 'Non authentifié'), new OA\Response(response: 403, description: 'Accès refusé - Admin requis'), new OA\Response(response: 404, description: 'Carte non trouvée')]
- #[Route]
- '/{id}/status'
- $name: 'update_status'
- $methods: ['PATCH']
Return values
JsonResponse —Objet JSON {id, status, updatedAt} après mise à jour, ou {error}.
createOrderForPhysicalCard()
Crée une commande pour une carte physique
private
createOrderForPhysicalCard(PhysicalCard $physicalCard) : Order
Parameters
- $physicalCard : PhysicalCard
Return values
OrderisValidHexColor()
Valide qu'une couleur est au format hexadécimal valide
private
isValidHexColor(string $color) : bool
Parameters
- $color : string
Return values
boolparseMoney()
Convertit un montant textuel en nombre à virgule flottante exploitable pour un calcul.
private
parseMoney(string|null $value) : float
Une valeur null — cas d'une option couleur ou d'impression absente ou sans supplément —
donne 0.0, ce qui permet de sommer directement les prix optionnels. Sinon, les espaces
sont supprimés et les virgules remplacées par des points afin d'accepter les écritures
localisées du type 1 234,50, puis la chaîne normalisée est convertie par transtypage
(float). Ce transtypage n'échoue jamais : une chaîne vide ou non numérique produit 0.0
et une chaîne partiellement numérique n'est lue que jusqu'au premier caractère invalide.
Attention, un séparateur de milliers écrit avec un point (1.234,50) est mal interprété,
les deux points se retrouvant dans la chaîne normalisée.
Parameters
- $value : string|null
-
Montant brut issu d'une entité (
Card::getPrice(), prix d'option).
Return values
float —Montant numérique, 0.0 en l'absence de valeur exploitable.
validateImageFile()
Valide un fichier image uploadé
private
validateImageFile(UploadedFile $file) : void
Parameters
- $file : UploadedFile
validateLogoFile()
Valide un fichier logo uploadé
private
validateLogoFile(UploadedFile $file) : void
Parameters
- $file : UploadedFile