From 890da18c15be1903458f443c6f04cef299c371b3 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Fri, 16 Jan 2026 13:15:42 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(Stripe):=20Int=C3=A8gre?= =?UTF-8?q?=20Stripe=20pour=20la=20gestion=20des=20paiements=20et=20les=20?= =?UTF-8?q?webhooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute Stripe pour la synchronisation des clients et la configuration des webhooks. Crée une commande pour synchroniser les clients locaux avec Stripe. Ajoute un champ customerId à l'entité Customer. ``` --- .env | 9 +- composer.json | 1 + composer.lock | 61 +++++++- config/packages/stripe.yaml | 7 + migrations/Version20260116114457.php | 32 ++++ migrations/Version20260116121121.php | 32 ++++ migrations/Version20260116121156.php | 32 ++++ src/Command/StripeCommand.php | 67 +++++++++ src/Entity/Customer.php | 15 ++ src/Entity/StripeConfig.php | 65 +++++++++ src/Repository/StripeConfigRepository.php | 43 ++++++ src/Service/Stripe/Client.php | 169 ++++++++++++++++++++++ src/Twig/StripeExtension.php | 26 ++++ symfony.lock | 12 ++ templates/dashboard/base.twig | 17 +++ templates/dashboard/customer.twig | 26 +++- 16 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 config/packages/stripe.yaml create mode 100644 migrations/Version20260116114457.php create mode 100644 migrations/Version20260116121121.php create mode 100644 migrations/Version20260116121156.php create mode 100644 src/Command/StripeCommand.php create mode 100644 src/Entity/StripeConfig.php create mode 100644 src/Repository/StripeConfigRepository.php create mode 100644 src/Service/Stripe/Client.php create mode 100644 src/Twig/StripeExtension.php diff --git a/.env b/.env index 154c066..d07c8a4 100644 --- a/.env +++ b/.env @@ -79,11 +79,12 @@ NOTIFUSE_EMAIL= NOTIFUSE_ACCOUNT= NOTIFUSE_LIST= -STRIPE_PK= -STRIPE_SK= +STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE79Tr8treeHX9KMcZtvcQZ0X8VSm00Q6GQ365V +STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR STRIPE_WEBHOOKS_SECRET= SIGN_URL= +STRIPE_BASEURL=https://e3358705e82c.ngrok-free.app MINIO_S3_URL= MINIO_S3_CLIENT_ID= @@ -91,3 +92,7 @@ MINIO_S3_CLIENT_SECRET= MINIO_S3_CLIENT_BUCKET= ESY_SEARCH_KEY=b09d9a708b427d495c39fe6e8fc5361fe33fee57a0435f3e1bf3ed8155f2a277 + +###> stripe/stripe-php ### +STRIPE_SECRET_KEY=sk_test_*** +###< stripe/stripe-php ### diff --git a/composer.json b/composer.json index 7257fe5..c7de42f 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "spomky-labs/web-push-bundle": "^3.1", "stancer/stancer": ">=2.0.1", "stevenmaguire/oauth2-keycloak": "^5.1", + "stripe/stripe-php": "^19.1", "symfony/amazon-mailer": "7.3.*", "symfony/asset": "7.3.*", "symfony/asset-mapper": "7.3.*", diff --git a/composer.lock b/composer.lock index cfe966e..168ebad 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f476989731711ededed94397298a39d6", + "content-hash": "bae39e4278669ceaf6f28005f1d75605", "packages": [ { "name": "async-aws/core", @@ -9393,6 +9393,65 @@ }, "time": "2023-10-24T06:10:44+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v19.1.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "4e3de7211645699b1f5b5f1f1b45bd9faf369426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/4e3de7211645699b1f5b5f1f1b45bd9faf369426", + "reference": "4e3de7211645699b1f5b5f1f1b45bd9faf369426", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.72.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v19.1.0" + }, + "time": "2025-12-16T19:48:17+00:00" + }, { "name": "symfony/amazon-mailer", "version": "v7.3.0", diff --git a/config/packages/stripe.yaml b/config/packages/stripe.yaml new file mode 100644 index 0000000..1e075c6 --- /dev/null +++ b/config/packages/stripe.yaml @@ -0,0 +1,7 @@ +services: + stripe.client: + class: 'Stripe\StripeClient' + arguments: + - '%env(STRIPE_SECRET_KEY)%' + + Stripe\StripeClient: '@stripe.client' diff --git a/migrations/Version20260116114457.php b/migrations/Version20260116114457.php new file mode 100644 index 0000000..03a0090 --- /dev/null +++ b/migrations/Version20260116114457.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE customer ADD customer_id VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE customer DROP customer_id'); + } +} diff --git a/migrations/Version20260116121121.php b/migrations/Version20260116121121.php new file mode 100644 index 0000000..b6c2bb3 --- /dev/null +++ b/migrations/Version20260116121121.php @@ -0,0 +1,32 @@ +addSql('CREATE TABLE stripe_config (id SERIAL NOT NULL, name VARCHAR(255) DEFAULT NULL, webhook_id INT NOT NULL, secret VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE stripe_config'); + } +} diff --git a/migrations/Version20260116121156.php b/migrations/Version20260116121156.php new file mode 100644 index 0000000..e97fa1b --- /dev/null +++ b/migrations/Version20260116121156.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE stripe_config ALTER webhook_id TYPE VARCHAR(255)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE stripe_config ALTER webhook_id TYPE INT'); + } +} diff --git a/src/Command/StripeCommand.php b/src/Command/StripeCommand.php new file mode 100644 index 0000000..ee586ef --- /dev/null +++ b/src/Command/StripeCommand.php @@ -0,0 +1,67 @@ +title('Synchronisation Stripe : Clients & Webhooks'); + + // 1. Synchronisation des clients manquants + $io->section('Synchronisation des clients'); + $customers = $this->entityManager->getRepository(Customer::class)->findBy(['customerId' => null]); + + if (empty($customers)) { + $io->success('Tous les clients sont déjà synchronisés.'); + } else { + $io->progressStart(count($customers)); + + foreach ($customers as $customer) { + $result = $this->client->createCustomer($customer); + + if ($result['state']) { + $this->entityManager->persist($customer); + } else { + $io->error(sprintf('Échec pour %s : %s', $customer->getEmail(), $result['message'])); + } + + $io->progressAdvance(); + } + + $this->entityManager->flush(); + $io->progressFinish(); + $io->success('Synchronisation des clients terminée.'); + } + + // 2. Configuration des Webhooks + $io->section('Configuration des Webhooks'); + $this->client->webhooks(); + $io->success('La configuration Stripe est à jour.'); + + return Command::SUCCESS; + } +} diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 3e7d02f..6375740 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -34,6 +34,9 @@ class Customer #[ORM\Column(length: 255, nullable: true)] private ?string $siret = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $customerId = null; + public function getId(): ?int { return $this->id; @@ -122,4 +125,16 @@ class Customer return $this; } + + public function getCustomerId(): ?string + { + return $this->customerId; + } + + public function setCustomerId(?string $customerId): static + { + $this->customerId = $customerId; + + return $this; + } } diff --git a/src/Entity/StripeConfig.php b/src/Entity/StripeConfig.php new file mode 100644 index 0000000..3f47d79 --- /dev/null +++ b/src/Entity/StripeConfig.php @@ -0,0 +1,65 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): static + { + $this->name = $name; + + return $this; + } + + public function getWebhookId(): ?int + { + return $this->webhookId; + } + + public function setWebhookId(string $webhookId): static + { + $this->webhookId = $webhookId; + + return $this; + } + + public function getSecret(): ?string + { + return $this->secret; + } + + public function setSecret(string $secret): static + { + $this->secret = $secret; + + return $this; + } +} diff --git a/src/Repository/StripeConfigRepository.php b/src/Repository/StripeConfigRepository.php new file mode 100644 index 0000000..d836754 --- /dev/null +++ b/src/Repository/StripeConfigRepository.php @@ -0,0 +1,43 @@ + + */ +class StripeConfigRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, StripeConfig::class); + } + + // /** + // * @return StripeConfig[] Returns an array of StripeConfig objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('s') + // ->andWhere('s.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('s.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?StripeConfig + // { + // return $this->createQueryBuilder('s') + // ->andWhere('s.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/Stripe/Client.php b/src/Service/Stripe/Client.php new file mode 100644 index 0000000..59b3ed9 --- /dev/null +++ b/src/Service/Stripe/Client.php @@ -0,0 +1,169 @@ +client = new StripeClient($stripeSk); + } + + /** + * Vérifie la connexion avec l'API Stripe + */ + public function check(): array + { + try { + // Appel léger pour valider la clé API + $this->client->accounts->all(['limit' => 1]); + + return [ + 'state' => true, + 'message' => 'Connexion établie avec Stripe' + ]; + } catch (AuthenticationException $e) { + return ['state' => false, 'message' => 'Clé API Stripe invalide ou expirée.']; + } catch (ApiConnectionException $e) { + return ['state' => false, 'message' => 'Problème de connexion réseau avec Stripe.']; + } catch (\Exception $e) { + return ['state' => false, 'message' => 'Erreur : ' . $e->getMessage()]; + } + } + + /** + * Crée un client sur Stripe et met à jour l'entité locale avec l'ID Stripe + */ + public function createCustomer(Customer $customer): array + { + try { + $stripeCustomer = $this->client->customers->create([ + 'name' => sprintf('%s %s', $customer->getSurname(), $customer->getName()), + 'email' => $customer->getEmail(), + 'phone' => $customer->getPhone(), + 'metadata' => [ + 'internal_id' => $customer->getId(), + 'type' => $customer->getType(), + ], + 'description' => 'Client synchronisé depuis Ludikevent Intranet', + ]); + + $customer->setCustomerId($stripeCustomer->id); + // Note: Le flush est à faire dans le contrôleur pour valider la transaction globale + + return [ + 'state' => true, + 'message' => 'Client synchronisé avec succès.', + 'id' => $stripeCustomer->id + ]; + } catch (ApiErrorException $e) { + return ['state' => false, 'message' => 'Erreur Stripe : ' . $e->getMessage()]; + } + } + + /** + * Configure, met à jour et sauvegarde les secrets des Webhooks + */ + public function webhooks(): array + { + $baseUrl = $_ENV['STRIPE_BASEURL'] ?? 'https://votre-domaine.fr'; + + // Configuration des routes attendues + $configs = [ + 'refund' => [ + 'url' => $baseUrl . '/webhooks/refund', + 'events' => ['refund.created', 'refund.failed', 'refund.updated'] + ], + 'payment' => [ + 'url' => $baseUrl . '/webhooks/payment-intent', + 'events' => [ + 'payment_intent.created', + 'payment_intent.canceled', + 'payment_intent.succeeded', + 'payment_intent.amount_capturable_updated' + ] + ] + ]; + + $report = []; + + try { + // Récupération des endpoints existants chez Stripe + $existingEndpoints = $this->client->webhookEndpoints->all(['limit' => 100]); + + foreach ($configs as $name => $config) { + $stripeEndpoint = null; + + // On cherche si l'URL est déjà enregistrée chez Stripe + foreach ($existingEndpoints->data as $endpoint) { + if ($endpoint->url === $config['url']) { + $stripeEndpoint = $endpoint; + break; + } + } + + // Recherche de la config correspondante en BDD + $dbConfig = $this->em->getRepository(StripeConfig::class)->findOneBy(['name' => $name]); + if (!$dbConfig) { + $dbConfig = new StripeConfig(); + $dbConfig->setName($name); + } + + + + if ($stripeEndpoint) { + // MISE À JOUR de l'endpoint chez Stripe + $this->client->webhookEndpoints->update($stripeEndpoint->id, [ + 'enabled_events' => $config['events'] + ]); + + $dbConfig->setWebhookId($stripeEndpoint->id); + $report[$name] = ['status' => 'updated', 'url' => $config['url']]; + } else { + // CRÉATION de l'endpoint chez Stripe + $newEndpoint = $this->client->webhookEndpoints->create([ + 'url' => $config['url'], + 'enabled_events' => $config['events'], + 'description' => 'Ludikevent Webhook - ' . $name + ]); + + $dbConfig->setWebhookId($newEndpoint->id); + $dbConfig->setSecret($newEndpoint->secret); // On sauve le secret whsec_... + + $report[$name] = ['status' => 'created', 'url' => $config['url']]; + } + + $this->em->persist($dbConfig); + } + + $this->em->flush(); + return ['state' => true, 'data' => $report]; + + } catch (ApiErrorException $e) { + return ['state' => false, 'message' => 'Erreur API Stripe : ' . $e->getMessage()]; + } catch (\Exception $e) { + return ['state' => false, 'message' => 'Erreur système : ' . $e->getMessage()]; + } + } + + /** + * Accès direct au client Stripe pour des besoins spécifiques + */ + public function getNativeClient(): StripeClient + { + return $this->client; + } +} diff --git a/src/Twig/StripeExtension.php b/src/Twig/StripeExtension.php new file mode 100644 index 0000000..2e9e51a --- /dev/null +++ b/src/Twig/StripeExtension.php @@ -0,0 +1,26 @@ +client->check(); + } +} diff --git a/symfony.lock b/symfony.lock index 0282f7a..b8fbe78 100644 --- a/symfony.lock +++ b/symfony.lock @@ -194,6 +194,18 @@ "spomky-labs/web-push-bundle": { "version": "3.1.2" }, + "stripe/stripe-php": { + "version": "19.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "19.0", + "ref": "d6829c693e3927a8972c7671d74a1a5c505712b0" + }, + "files": [ + "config/packages/stripe.yaml" + ] + }, "symfony/amazon-mailer": { "version": "7.3", "recipe": { diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index b1c3516..6d3503d 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -128,6 +128,23 @@ + {# MESSAGE D'ERREUR STRIPE #} + {% if syncStripe().state == false %} +
+
+ + + +
+
+

Erreur de synchronisation Stripe

+

+ "{{ syncStripe().message }}" +

+
+
+ {% endif %} +
{% block body %}{% endblock %}
diff --git a/templates/dashboard/customer.twig b/templates/dashboard/customer.twig index e55bee3..e5b7790 100644 --- a/templates/dashboard/customer.twig +++ b/templates/dashboard/customer.twig @@ -46,15 +46,39 @@ {# 1. IDENTITÉ #}
+ {# Avatar avec initiales #}
{{ customer.surname|first|upper }}{{ customer.name|first|upper }}
+
+ {# Nom et Civilité #}
{{ customer.civ }} {{ customer.surname|upper }} {{ customer.name }}
-
Client ID: #{{ customer.id }}
+ + {# ID Interne et État Stripe #} +
+ {# Badge ID Interne #} + + ID: #{{ customer.id }} + + + {% if customer.customerId %} + {# ÉTAT : SYNCHRONISÉ (VERT) #} +
+ + Stripe synchronisé +
+ {% else %} + {# ÉTAT : NON SYNCHRONISÉ (ROUGE) #} +
+ + Stripe non synchronisé +
+ {% endif %} +