Files
crm_ecosplay/src/Controller/Admin/SyncController.php

399 lines
16 KiB
PHP
Raw Normal View History

2026-04-01 15:42:52 +02:00
<?php
namespace App\Controller\Admin;
feat: gestion complete Devis + Avis de paiement + DocuSeal signature + mails Devis : - Entity DevisLine (pos, title, description, priceHt) liee a Devis (OneToMany cascade/orphanRemoval) - Champs ajoutes sur Devis : customer (ManyToOne), submissionId, state machine (created/send/accepted/refused/cancel), raisonMessage, totaux HT/TVA/TTC, updatedAt, setUpdatedAt public - Relation Devis <-> Advert changee de ManyToOne a OneToOne nullable - Vich Attribute (migration Annotation -> Attribute) pour unsignedPdf/signedPdf/auditPdf - DevisController CRUD complet : create (form repeater lignes + boutons rapides TarificationService), edit, cancel (libere OrderNumber), generate-pdf, send, resend, create-advert, events - DevisPdf (FPDF/FPDI) : header legacy (logo, num, date, client), body lignes, summary totaux, footer SITECONSEIL + pagination, champ signature DocuSeal sur page devis + derniere page CGV - OrderNumberService : preview() et generate() reutilisent les OrderNumber non utilises (isUsed=false) en priorite - OrderNumber::markAsUnused() ajoute DocuSeal integration devis : - DocuSealService : sendDevisForSignature (avec completed_redirect_url), resendDevisSignature (archive ancienne submission), getSubmitterSlug, downloadSignedDevis (sauvegarde via Vich UploadedFile test=true) - WebhookDocuSealController : dispatch par doc_type devis/attestation, handleDevisEvent (form.completed -> STATE_ACCEPTED + download PDF signe/audit, form.declined -> STATE_REFUSED + raison) - DocusealEvent entity pour tracer form.viewed/started/completed/declined en temps reel - Page evenements admin /admin/devis/{id}/events avec badges et payload JSON Signature client : - DevisProcessController : page publique /devis/process/{id}/{hmac} securisee par HMAC, boutons Signer (redirect DocuSeal) / Refuser (motif optionnel) - Pages confirmation : signed.html.twig (merci + recap) et refused.html.twig (confirmation refus + motif) - Nelmio whitelist : signature.esy-web.dev + signature.siteconseil.fr Avis de paiement : - Entity AdvertLine (pos, title, description, priceHt) liee a Advert - Advert refactorise : customer, state, totaux, raisonMessage, submissionId, advertFile (Vich mapping advert_pdf), lines collection, updatedAt - AdvertController : generate-pdf, send (mail + PJ + lien paiement), resend (rappel), cancel (delie devis, libere OrderNumber), search Meilisearch - AdvertPdf (FPDF/FPDI) : QR code Endroid pointant vers /order/{numOrder}, texte "Scannez pour payer" - OrderPaymentController : page publique /order/{numOrder} avec detail prestations, totaux, options paiement (placeholder) - Creation auto depuis devis signe : copie client, totaux, lignes, meme OrderNumber Meilisearch : - Index customer_devis et customer_advert avec searchable (numOrder, customerName, customerEmail, state) et filterable (customerId, state) - CRUD indexation sur chaque action (create, edit, send, cancel, create-advert) - Recherche AJAX dans tabs Devis et Avis avec debounce + dropdown glassmorphism - Sync admin : boutons syncDevis / syncAdverts + compteurs dans /admin/sync Emails : - MailerService : VCF auto (fiche contact SARL SITECONSEIL) en PJ sur tous les mails, bloc HTML pieces jointes injecte automatiquement (exclut .asc/.p7z/smime) avec icone trombone + taille fichier - Templates : devis_to_sign, devis_signed_client/admin (PJ signed+audit), devis_refused_client/admin, advert_send (PJ + bouton paiement), ndd_expiration - TestMailCommand : option --force-dsn pour envoyer via un DSN SMTP specifique (test prod depuis dev) Commande NDD : - app:ndd:check : verifie expiration domaines <= 30j, envoie mail groupe a monitor@siteconseil.fr - Cron quotidien 8h (docker + ansible) Divers : - Titles templates : CRM SITECONSEIL -> SARL SITECONSEIL (52 fichiers) - VAULT_URL dev = https://kms.esy-web.dev (comme prod) - app.js : initDevisLines (repeater + drag & drop), initTabSearch, toggle refus devis - app.scss : styles drag & drop - setasign/fpdi-fpdf installe pour fusion PDF - 5 migrations Doctrine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:44:35 +02:00
use App\Entity\Advert;
use App\Entity\CustomerContact;
feat: gestion complete Devis + Avis de paiement + DocuSeal signature + mails Devis : - Entity DevisLine (pos, title, description, priceHt) liee a Devis (OneToMany cascade/orphanRemoval) - Champs ajoutes sur Devis : customer (ManyToOne), submissionId, state machine (created/send/accepted/refused/cancel), raisonMessage, totaux HT/TVA/TTC, updatedAt, setUpdatedAt public - Relation Devis <-> Advert changee de ManyToOne a OneToOne nullable - Vich Attribute (migration Annotation -> Attribute) pour unsignedPdf/signedPdf/auditPdf - DevisController CRUD complet : create (form repeater lignes + boutons rapides TarificationService), edit, cancel (libere OrderNumber), generate-pdf, send, resend, create-advert, events - DevisPdf (FPDF/FPDI) : header legacy (logo, num, date, client), body lignes, summary totaux, footer SITECONSEIL + pagination, champ signature DocuSeal sur page devis + derniere page CGV - OrderNumberService : preview() et generate() reutilisent les OrderNumber non utilises (isUsed=false) en priorite - OrderNumber::markAsUnused() ajoute DocuSeal integration devis : - DocuSealService : sendDevisForSignature (avec completed_redirect_url), resendDevisSignature (archive ancienne submission), getSubmitterSlug, downloadSignedDevis (sauvegarde via Vich UploadedFile test=true) - WebhookDocuSealController : dispatch par doc_type devis/attestation, handleDevisEvent (form.completed -> STATE_ACCEPTED + download PDF signe/audit, form.declined -> STATE_REFUSED + raison) - DocusealEvent entity pour tracer form.viewed/started/completed/declined en temps reel - Page evenements admin /admin/devis/{id}/events avec badges et payload JSON Signature client : - DevisProcessController : page publique /devis/process/{id}/{hmac} securisee par HMAC, boutons Signer (redirect DocuSeal) / Refuser (motif optionnel) - Pages confirmation : signed.html.twig (merci + recap) et refused.html.twig (confirmation refus + motif) - Nelmio whitelist : signature.esy-web.dev + signature.siteconseil.fr Avis de paiement : - Entity AdvertLine (pos, title, description, priceHt) liee a Advert - Advert refactorise : customer, state, totaux, raisonMessage, submissionId, advertFile (Vich mapping advert_pdf), lines collection, updatedAt - AdvertController : generate-pdf, send (mail + PJ + lien paiement), resend (rappel), cancel (delie devis, libere OrderNumber), search Meilisearch - AdvertPdf (FPDF/FPDI) : QR code Endroid pointant vers /order/{numOrder}, texte "Scannez pour payer" - OrderPaymentController : page publique /order/{numOrder} avec detail prestations, totaux, options paiement (placeholder) - Creation auto depuis devis signe : copie client, totaux, lignes, meme OrderNumber Meilisearch : - Index customer_devis et customer_advert avec searchable (numOrder, customerName, customerEmail, state) et filterable (customerId, state) - CRUD indexation sur chaque action (create, edit, send, cancel, create-advert) - Recherche AJAX dans tabs Devis et Avis avec debounce + dropdown glassmorphism - Sync admin : boutons syncDevis / syncAdverts + compteurs dans /admin/sync Emails : - MailerService : VCF auto (fiche contact SARL SITECONSEIL) en PJ sur tous les mails, bloc HTML pieces jointes injecte automatiquement (exclut .asc/.p7z/smime) avec icone trombone + taille fichier - Templates : devis_to_sign, devis_signed_client/admin (PJ signed+audit), devis_refused_client/admin, advert_send (PJ + bouton paiement), ndd_expiration - TestMailCommand : option --force-dsn pour envoyer via un DSN SMTP specifique (test prod depuis dev) Commande NDD : - app:ndd:check : verifie expiration domaines <= 30j, envoie mail groupe a monitor@siteconseil.fr - Cron quotidien 8h (docker + ansible) Divers : - Titles templates : CRM SITECONSEIL -> SARL SITECONSEIL (52 fichiers) - VAULT_URL dev = https://kms.esy-web.dev (comme prod) - app.js : initDevisLines (repeater + drag & drop), initTabSearch, toggle refus devis - app.scss : styles drag & drop - setasign/fpdi-fpdf installe pour fusion PDF - 5 migrations Doctrine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:44:35 +02:00
use App\Entity\Devis;
use App\Entity\Domain;
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
use App\Entity\Facture;
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
use App\Entity\StripeWebhookSecret;
use App\Entity\Website;
2026-04-01 15:42:52 +02:00
use App\Repository\CustomerRepository;
feat: index Meilisearch price_auto + bouton sync tarifs + statuts Stripe src/Service/MeilisearchService.php: - indexPrice(): indexe un PriceAutomatic dans l'index price_auto - removePrice(): supprime un tarif de l'index - searchPrices(): recherche dans les tarifs - setupIndexes(): ajout de l'index price_auto avec searchableAttributes (type, title, description) et filterableAttributes (type, period) - serializePrice(): serialise id, type, title, description, priceHt, monthPrice, period, stripeId, stripeAbonnementId src/Service/TarificationService.php: - Injection optionnelle de MeilisearchService dans le constructeur - ensureDefaultPrices(): apres flush des nouveaux tarifs, les indexe automatiquement dans Meilisearch src/Controller/Admin/SyncController.php: - Injection de PriceAutomaticRepository dans index() pour compter les tarifs - Nouvelle route POST /admin/sync/prices: synchronise tous les tarifs dans l'index price_auto de Meilisearch - syncAll(): inclut maintenant les tarifs dans la synchronisation complete templates/admin/sync/index.html.twig: - Nouveau bloc "Tarifs" avec index price_auto, compteur en base, bouton "Synchroniser" vert templates/admin/tarification/index.html.twig: - Header de chaque tarif: badges de statut Stripe - "Stripe OK" (vert) si stripeId renseigne, "Non sync" (rouge) sinon - "Abo OK" (vert) si stripeAbonnementId renseigne, "Abo non sync" (rouge) sinon (affiche uniquement si monthPrice != 0.00) - Champ Stripe Price ID abonnement masque si monthPrice == 0.00 (pas d'abonnement pour les paiements uniques) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:45:52 +02:00
use App\Repository\PriceAutomaticRepository;
2026-04-01 15:42:52 +02:00
use App\Repository\RevendeurRepository;
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
use App\Repository\StripeWebhookSecretRepository;
use Doctrine\ORM\EntityManagerInterface;
2026-04-01 15:42:52 +02:00
use App\Service\MeilisearchService;
feat: sync automatique Stripe pour les tarifs + boutons sync admin src/Service/StripePriceService.php (nouveau): - Utilise Stripe SDK v20 (StripeClient) avec STRIPE_SK - syncPrice(): pour chaque PriceAutomatic, cree ou retrouve le produit Stripe via metadata price_auto_type, puis cree le Stripe Price (unique et/ou recurrent selon monthPrice) - ensureProduct(): cherche un produit existant par metadata, le cree sinon, met a jour nom/description si modifies - createStripePrice(): cree un prix Stripe en centimes, avec tax_behavior=exclusive, recurring si monthPrice > 0 avec interval=month (ou year si period >= 12) - updateStripePriceIfNeeded(): si le montant a change, archive l'ancien prix Stripe et en cree un nouveau (Stripe ne permet pas de modifier le montant d'un prix existant) - syncAll(): synchronise tous les tarifs, retourne synced + errors src/Service/TarificationService.php: - Injection optionnelle de StripePriceService - ensureDefaultPrices(): apres creation des tarifs, sync automatique avec Stripe (cree produits + prix) en plus de Meilisearch src/Controller/Admin/TarificationController.php: - edit(): apres mise a jour d'un tarif, sync automatique avec Stripe (cree/archive/recree les prix si montant change) et Meilisearch - Flash d'erreur si Stripe echoue, les modifs locales sont sauvegardees src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/prices: synchronise tous les tarifs avec Stripe via StripePriceService::syncAll() templates/admin/sync/index.html.twig: - Section "Stripe" avec bouton "Synchroniser Stripe" (violet) pour les tarifs, avec confirmation avant execution - Section Meilisearch tarifs renommee "Tarifs - Meilisearch" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:50:27 +02:00
use App\Service\StripePriceService;
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
use App\Service\StripeWebhookService;
2026-04-01 15:42:52 +02:00
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
use Symfony\Component\DependencyInjection\Attribute\Autowire;
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
use Symfony\Component\HttpFoundation\Request;
2026-04-01 15:42:52 +02:00
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/admin/sync', name: 'app_admin_sync_')]
#[IsGranted('ROLE_ROOT')]
class SyncController extends AbstractController
{
#[Route('', name: 'index')]
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
public function index(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository, PriceAutomaticRepository $priceRepository, StripeWebhookSecretRepository $secretRepository, EntityManagerInterface $em, MeilisearchService $meilisearch): Response
2026-04-01 15:42:52 +02:00
{
$prices = $priceRepository->findAll();
$stripeSynced = 0;
$stripeNotSynced = 0;
foreach ($prices as $price) {
if (null !== $price->getStripeId() && '' !== $price->getStripeId()) {
++$stripeSynced;
} else {
++$stripeNotSynced;
}
}
$webhookSecrets = $secretRepository->findAll();
$customers = $customerRepository->findAll();
$customersSynced = 0;
$customersNotSynced = 0;
foreach ($customers as $c) {
if (null !== $c->getStripeCustomerId() && '' !== $c->getStripeCustomerId()) {
++$customersSynced;
} else {
++$customersNotSynced;
}
}
2026-04-01 15:42:52 +02:00
return $this->render('admin/sync/index.html.twig', [
'totalCustomers' => \count($customers),
2026-04-01 15:42:52 +02:00
'totalRevendeurs' => $revendeurRepository->count([]),
'totalPrices' => \count($prices),
'stripeSynced' => $stripeSynced,
'stripeNotSynced' => $stripeNotSynced,
'customersSynced' => $customersSynced,
'customersNotSynced' => $customersNotSynced,
'totalContacts' => $em->getRepository(CustomerContact::class)->count([]),
'totalDomains' => $em->getRepository(Domain::class)->count([]),
'totalWebsites' => $em->getRepository(Website::class)->count([]),
feat: gestion complete Devis + Avis de paiement + DocuSeal signature + mails Devis : - Entity DevisLine (pos, title, description, priceHt) liee a Devis (OneToMany cascade/orphanRemoval) - Champs ajoutes sur Devis : customer (ManyToOne), submissionId, state machine (created/send/accepted/refused/cancel), raisonMessage, totaux HT/TVA/TTC, updatedAt, setUpdatedAt public - Relation Devis <-> Advert changee de ManyToOne a OneToOne nullable - Vich Attribute (migration Annotation -> Attribute) pour unsignedPdf/signedPdf/auditPdf - DevisController CRUD complet : create (form repeater lignes + boutons rapides TarificationService), edit, cancel (libere OrderNumber), generate-pdf, send, resend, create-advert, events - DevisPdf (FPDF/FPDI) : header legacy (logo, num, date, client), body lignes, summary totaux, footer SITECONSEIL + pagination, champ signature DocuSeal sur page devis + derniere page CGV - OrderNumberService : preview() et generate() reutilisent les OrderNumber non utilises (isUsed=false) en priorite - OrderNumber::markAsUnused() ajoute DocuSeal integration devis : - DocuSealService : sendDevisForSignature (avec completed_redirect_url), resendDevisSignature (archive ancienne submission), getSubmitterSlug, downloadSignedDevis (sauvegarde via Vich UploadedFile test=true) - WebhookDocuSealController : dispatch par doc_type devis/attestation, handleDevisEvent (form.completed -> STATE_ACCEPTED + download PDF signe/audit, form.declined -> STATE_REFUSED + raison) - DocusealEvent entity pour tracer form.viewed/started/completed/declined en temps reel - Page evenements admin /admin/devis/{id}/events avec badges et payload JSON Signature client : - DevisProcessController : page publique /devis/process/{id}/{hmac} securisee par HMAC, boutons Signer (redirect DocuSeal) / Refuser (motif optionnel) - Pages confirmation : signed.html.twig (merci + recap) et refused.html.twig (confirmation refus + motif) - Nelmio whitelist : signature.esy-web.dev + signature.siteconseil.fr Avis de paiement : - Entity AdvertLine (pos, title, description, priceHt) liee a Advert - Advert refactorise : customer, state, totaux, raisonMessage, submissionId, advertFile (Vich mapping advert_pdf), lines collection, updatedAt - AdvertController : generate-pdf, send (mail + PJ + lien paiement), resend (rappel), cancel (delie devis, libere OrderNumber), search Meilisearch - AdvertPdf (FPDF/FPDI) : QR code Endroid pointant vers /order/{numOrder}, texte "Scannez pour payer" - OrderPaymentController : page publique /order/{numOrder} avec detail prestations, totaux, options paiement (placeholder) - Creation auto depuis devis signe : copie client, totaux, lignes, meme OrderNumber Meilisearch : - Index customer_devis et customer_advert avec searchable (numOrder, customerName, customerEmail, state) et filterable (customerId, state) - CRUD indexation sur chaque action (create, edit, send, cancel, create-advert) - Recherche AJAX dans tabs Devis et Avis avec debounce + dropdown glassmorphism - Sync admin : boutons syncDevis / syncAdverts + compteurs dans /admin/sync Emails : - MailerService : VCF auto (fiche contact SARL SITECONSEIL) en PJ sur tous les mails, bloc HTML pieces jointes injecte automatiquement (exclut .asc/.p7z/smime) avec icone trombone + taille fichier - Templates : devis_to_sign, devis_signed_client/admin (PJ signed+audit), devis_refused_client/admin, advert_send (PJ + bouton paiement), ndd_expiration - TestMailCommand : option --force-dsn pour envoyer via un DSN SMTP specifique (test prod depuis dev) Commande NDD : - app:ndd:check : verifie expiration domaines <= 30j, envoie mail groupe a monitor@siteconseil.fr - Cron quotidien 8h (docker + ansible) Divers : - Titles templates : CRM SITECONSEIL -> SARL SITECONSEIL (52 fichiers) - VAULT_URL dev = https://kms.esy-web.dev (comme prod) - app.js : initDevisLines (repeater + drag & drop), initTabSearch, toggle refus devis - app.scss : styles drag & drop - setasign/fpdi-fpdf installe pour fusion PDF - 5 migrations Doctrine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:44:35 +02:00
'totalDevis' => $em->getRepository(Devis::class)->count([]),
'totalAdverts' => $em->getRepository(Advert::class)->count([]),
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
'totalFactures' => $em->getRepository(Facture::class)->count([]),
'webhookSecrets' => $webhookSecrets,
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
'msCustomers' => $meilisearch->getIndexCount('customer'),
'msResellers' => $meilisearch->getIndexCount('reseller'),
'msPrices' => $meilisearch->getIndexCount('price_auto'),
'msContacts' => $meilisearch->getIndexCount('customer_contact'),
'msDomains' => $meilisearch->getIndexCount('customer_ndd'),
'msWebsites' => $meilisearch->getIndexCount('customer_website'),
'msDevis' => $meilisearch->getIndexCount('customer_devis'),
'msAdverts' => $meilisearch->getIndexCount('customer_advert'),
'msFactures' => $meilisearch->getIndexCount('customer_facture'),
2026-04-01 15:42:52 +02:00
]);
}
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
#[Route('/purge-indexes', name: 'purge_indexes', methods: ['POST'])]
public function purgeIndexes(MeilisearchService $meilisearch): Response
{
$meilisearch->purgeAllIndexes();
$this->addFlash('success', 'Tous les index Meilisearch ont ete purges.');
return $this->redirectToRoute('app_admin_sync_index');
}
2026-04-01 15:42:52 +02:00
#[Route('/customers', name: 'customers', methods: ['POST'])]
public function syncCustomers(CustomerRepository $customerRepository, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$customers = $customerRepository->findAll();
foreach ($customers as $customer) {
$meilisearch->indexCustomer($customer);
}
$this->addFlash('success', \count($customers).' client(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync clients : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/contacts', name: 'contacts', methods: ['POST'])]
public function syncContacts(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$contacts = $em->getRepository(CustomerContact::class)->findAll();
foreach ($contacts as $contact) {
$meilisearch->indexContact($contact);
}
$this->addFlash('success', \count($contacts).' contact(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync contacts : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/domains', name: 'domains', methods: ['POST'])]
public function syncDomains(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$domains = $em->getRepository(Domain::class)->findAll();
foreach ($domains as $domain) {
$meilisearch->indexDomain($domain);
}
$this->addFlash('success', \count($domains).' domaine(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync domaines : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/websites', name: 'websites', methods: ['POST'])]
public function syncWebsites(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$websites = $em->getRepository(Website::class)->findAll();
foreach ($websites as $website) {
$meilisearch->indexWebsite($website);
}
$this->addFlash('success', \count($websites).' site(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync sites : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
feat: gestion complete Devis + Avis de paiement + DocuSeal signature + mails Devis : - Entity DevisLine (pos, title, description, priceHt) liee a Devis (OneToMany cascade/orphanRemoval) - Champs ajoutes sur Devis : customer (ManyToOne), submissionId, state machine (created/send/accepted/refused/cancel), raisonMessage, totaux HT/TVA/TTC, updatedAt, setUpdatedAt public - Relation Devis <-> Advert changee de ManyToOne a OneToOne nullable - Vich Attribute (migration Annotation -> Attribute) pour unsignedPdf/signedPdf/auditPdf - DevisController CRUD complet : create (form repeater lignes + boutons rapides TarificationService), edit, cancel (libere OrderNumber), generate-pdf, send, resend, create-advert, events - DevisPdf (FPDF/FPDI) : header legacy (logo, num, date, client), body lignes, summary totaux, footer SITECONSEIL + pagination, champ signature DocuSeal sur page devis + derniere page CGV - OrderNumberService : preview() et generate() reutilisent les OrderNumber non utilises (isUsed=false) en priorite - OrderNumber::markAsUnused() ajoute DocuSeal integration devis : - DocuSealService : sendDevisForSignature (avec completed_redirect_url), resendDevisSignature (archive ancienne submission), getSubmitterSlug, downloadSignedDevis (sauvegarde via Vich UploadedFile test=true) - WebhookDocuSealController : dispatch par doc_type devis/attestation, handleDevisEvent (form.completed -> STATE_ACCEPTED + download PDF signe/audit, form.declined -> STATE_REFUSED + raison) - DocusealEvent entity pour tracer form.viewed/started/completed/declined en temps reel - Page evenements admin /admin/devis/{id}/events avec badges et payload JSON Signature client : - DevisProcessController : page publique /devis/process/{id}/{hmac} securisee par HMAC, boutons Signer (redirect DocuSeal) / Refuser (motif optionnel) - Pages confirmation : signed.html.twig (merci + recap) et refused.html.twig (confirmation refus + motif) - Nelmio whitelist : signature.esy-web.dev + signature.siteconseil.fr Avis de paiement : - Entity AdvertLine (pos, title, description, priceHt) liee a Advert - Advert refactorise : customer, state, totaux, raisonMessage, submissionId, advertFile (Vich mapping advert_pdf), lines collection, updatedAt - AdvertController : generate-pdf, send (mail + PJ + lien paiement), resend (rappel), cancel (delie devis, libere OrderNumber), search Meilisearch - AdvertPdf (FPDF/FPDI) : QR code Endroid pointant vers /order/{numOrder}, texte "Scannez pour payer" - OrderPaymentController : page publique /order/{numOrder} avec detail prestations, totaux, options paiement (placeholder) - Creation auto depuis devis signe : copie client, totaux, lignes, meme OrderNumber Meilisearch : - Index customer_devis et customer_advert avec searchable (numOrder, customerName, customerEmail, state) et filterable (customerId, state) - CRUD indexation sur chaque action (create, edit, send, cancel, create-advert) - Recherche AJAX dans tabs Devis et Avis avec debounce + dropdown glassmorphism - Sync admin : boutons syncDevis / syncAdverts + compteurs dans /admin/sync Emails : - MailerService : VCF auto (fiche contact SARL SITECONSEIL) en PJ sur tous les mails, bloc HTML pieces jointes injecte automatiquement (exclut .asc/.p7z/smime) avec icone trombone + taille fichier - Templates : devis_to_sign, devis_signed_client/admin (PJ signed+audit), devis_refused_client/admin, advert_send (PJ + bouton paiement), ndd_expiration - TestMailCommand : option --force-dsn pour envoyer via un DSN SMTP specifique (test prod depuis dev) Commande NDD : - app:ndd:check : verifie expiration domaines <= 30j, envoie mail groupe a monitor@siteconseil.fr - Cron quotidien 8h (docker + ansible) Divers : - Titles templates : CRM SITECONSEIL -> SARL SITECONSEIL (52 fichiers) - VAULT_URL dev = https://kms.esy-web.dev (comme prod) - app.js : initDevisLines (repeater + drag & drop), initTabSearch, toggle refus devis - app.scss : styles drag & drop - setasign/fpdi-fpdf installe pour fusion PDF - 5 migrations Doctrine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:44:35 +02:00
#[Route('/devis', name: 'devis', methods: ['POST'])]
public function syncDevis(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$items = $em->getRepository(Devis::class)->findAll();
foreach ($items as $item) {
$meilisearch->indexDevis($item);
}
$this->addFlash('success', \count($items).' devis synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync devis : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/adverts', name: 'adverts', methods: ['POST'])]
public function syncAdverts(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$items = $em->getRepository(Advert::class)->findAll();
foreach ($items as $item) {
$meilisearch->indexAdvert($item);
}
$this->addFlash('success', \count($items).' avis de paiement synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync avis : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
feat: comptabilite + prestataires + rapport financier + stats dynamiques Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
#[Route('/factures', name: 'factures', methods: ['POST'])]
public function syncFactures(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$items = $em->getRepository(Facture::class)->findAll();
foreach ($items as $item) {
$meilisearch->indexFacture($item);
}
$this->addFlash('success', \count($items).' facture(s) synchronisee(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync factures : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
2026-04-01 15:42:52 +02:00
#[Route('/revendeurs', name: 'revendeurs', methods: ['POST'])]
public function syncRevendeurs(RevendeurRepository $revendeurRepository, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$revendeurs = $revendeurRepository->findAll();
foreach ($revendeurs as $revendeur) {
$meilisearch->indexRevendeur($revendeur);
}
$this->addFlash('success', \count($revendeurs).' revendeur(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync revendeurs : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
feat: index Meilisearch price_auto + bouton sync tarifs + statuts Stripe src/Service/MeilisearchService.php: - indexPrice(): indexe un PriceAutomatic dans l'index price_auto - removePrice(): supprime un tarif de l'index - searchPrices(): recherche dans les tarifs - setupIndexes(): ajout de l'index price_auto avec searchableAttributes (type, title, description) et filterableAttributes (type, period) - serializePrice(): serialise id, type, title, description, priceHt, monthPrice, period, stripeId, stripeAbonnementId src/Service/TarificationService.php: - Injection optionnelle de MeilisearchService dans le constructeur - ensureDefaultPrices(): apres flush des nouveaux tarifs, les indexe automatiquement dans Meilisearch src/Controller/Admin/SyncController.php: - Injection de PriceAutomaticRepository dans index() pour compter les tarifs - Nouvelle route POST /admin/sync/prices: synchronise tous les tarifs dans l'index price_auto de Meilisearch - syncAll(): inclut maintenant les tarifs dans la synchronisation complete templates/admin/sync/index.html.twig: - Nouveau bloc "Tarifs" avec index price_auto, compteur en base, bouton "Synchroniser" vert templates/admin/tarification/index.html.twig: - Header de chaque tarif: badges de statut Stripe - "Stripe OK" (vert) si stripeId renseigne, "Non sync" (rouge) sinon - "Abo OK" (vert) si stripeAbonnementId renseigne, "Abo non sync" (rouge) sinon (affiche uniquement si monthPrice != 0.00) - Champ Stripe Price ID abonnement masque si monthPrice == 0.00 (pas d'abonnement pour les paiements uniques) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:45:52 +02:00
#[Route('/prices', name: 'prices', methods: ['POST'])]
public function syncPrices(PriceAutomaticRepository $priceRepository, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$prices = $priceRepository->findAll();
foreach ($prices as $price) {
$meilisearch->indexPrice($price);
}
$this->addFlash('success', \count($prices).' tarif(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync tarifs : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
#[Route('/stripe/webhooks', name: 'stripe_webhooks', methods: ['POST'])]
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
public function syncStripeWebhooks(
StripeWebhookService $webhookService,
StripeWebhookSecretRepository $secretRepository,
EntityManagerInterface $em,
#[Autowire(env: 'WEBHOOK_BASE_URL')] string $webhookBaseUrl,
): Response {
if ('' === $webhookBaseUrl) {
$this->addFlash('error', 'WEBHOOK_BASE_URL non configuree dans .env.local');
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
return $this->redirectToRoute('app_admin_sync_index');
}
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
$result = $webhookService->createAllWebhooks(rtrim($webhookBaseUrl, '/'));
$typeMap = [
'Main Light' => StripeWebhookSecret::TYPE_MAIN_LIGHT,
'Main Instant' => StripeWebhookSecret::TYPE_MAIN_INSTANT,
'Connect Light' => StripeWebhookSecret::TYPE_CONNECT_LIGHT,
'Connect Instant' => StripeWebhookSecret::TYPE_CONNECT_INSTANT,
];
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
foreach ($result['created'] as $wh) {
fix: corrections SonarQube - qualité code, accessibilité, complexité cognitive Propriétés inutilisées supprimées : - CheckDnsCommand : suppression de $urlGenerator (jamais lu, seulement injecté) - PurgeEmailTrackingCommand : suppression de $repository (jamais lu, requêtes via $em->createQueryBuilder directement), suppression import EmailTrackingRepository Corrections PHPStan / types : - SyncController : suppression $wh['status'] ?? 'created' redondant, accès direct à $wh['status'] car le type retour inclut désormais status: string - StripeWebhookService : PHPDoc createAllWebhooks corrigé de list<array{type, url, id}> vers list<array{type, url, id, status, secret?}> pour refléter les clés status et secret effectivement présentes - DnsReportController : suppression ?? '' sur EXPECTED_MX[$domain] (clé toujours existante) - CloudflareService : ajout @param array<string, mixed> $query sur request() - CheckDnsCommand : suppression ?? '' sur EXPECTED_MX[$domain], ajout PHPDoc @param list<array<string, mixed>> $cfRecords sur checkMailcow Méthode manquante : - DnsCheckService : ajout getDkimTxtRecord() qui parcourt les TXT records et retourne le premier commençant par 'v=DKIM1', appelé dans checkDkim() Code mort supprimé : - MailcowService : suppression is_array($data) toujours vrai sur retour de $response->toArray(false), retour direct - DnsInfraHelper : suppression getFirstTxtValueRaw() identique à getFirstTxtValue(), simplification de getActualDnsValue() qui n'appelle plus le fallback Constantes pour littéraux dupliqués : - DnsInfraHelper : ajout LABEL_AWS_SES, LABEL_MAILCOW, LABEL_MAILCOW_DNS, NOT_FOUND, NOT_CONFIGURED — remplace les chaînes 'AWS SES' (10×), 'Non trouve' (4×), 'Non configure' (3×), 'Mailcow' et 'Mailcow DNS' - Utilisation dans CheckDnsCommand (checkAwsSes, checkSesDomain, checkSesDkim, checkSesMailFrom, checkSesBounce, checkMailcow) Réduction complexité cognitive checkAwsSes (61 → ~10 par méthode) : - Extraction checkSesDomain() : vérifie isDomainVerified, ajoute check + erreur/succès - Extraction checkSesDkim() : vérifie getDkimStatus (enabled+verified), parcourt les tokens DKIM CNAME avec enrichLastCheck - Extraction checkSesMailFrom() : vérifie getMailFromStatus, MAIL FROM MX (checkMxExists + getMxValues), MAIL FROM TXT (checkTxtContains + getTxtSpfValue) - Extraction checkSesBounce() : vérifie getNotificationStatus (forwarding ou bounce_topic) Accessibilité WCAG AA : - app.scss : contraste sidebar-nav-item augmenté de rgba(255,255,255,0.6) à rgba(255,255,255,0.75) pour ratio de contraste suffisant sur fond sombre - tarification/index.html.twig : ajout for/id sur les 5 paires label/input (title-{id}, priceHt-{id}, monthPrice-{id}, period-{id}, description-{id}) - membres.html.twig : ajout for/id sur les 15 checkboxes de groupes (group-member, group-admin, group-esy-web, ..., group-esy-ndd), remplacement du label titre par <span> (n'est pas associé à un contrôle) - 2fa_google.html.twig : ajout for="trusted-device" et id="trusted-device" sur le checkbox de confiance appareil - tarif.html.twig : ajout <thead class="sr-only"> avec <th>Option</th><th>Tarif</th> sur la table options Esy-Mail (table sans en-têtes) Ansible : - vault.yml : ajout discord_webhook pour le déploiement prod Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:41:17 +02:00
if ('exists' === $wh['status']) {
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
$this->addFlash('success', $wh['type'].' : deja configure ('.$wh['id'].')');
} else {
$this->addFlash('success', $wh['type'].' : cree ('.$wh['id'].')');
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
if (isset($wh['secret'], $typeMap[$wh['type']])) {
$dbType = $typeMap[$wh['type']];
$existing = $secretRepository->findByType($dbType);
if (null !== $existing) {
$existing->setSecret($wh['secret']);
$existing->setEndpointId($wh['id']);
} else {
$entity = new StripeWebhookSecret($dbType, $wh['secret'], $wh['id']);
$em->persist($entity);
}
}
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
}
}
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
$em->flush();
feat: creation automatique des webhooks Stripe + controllers de reception src/Service/StripeWebhookService.php (nouveau): - createAllWebhooks(baseUrl): cree 4 webhook endpoints dans Stripe: - /webhooks/stripe/main/light: customer.created/updated/deleted, product.created/updated, price.created/updated, invoice.created/ finalized/payment_succeeded/payment_failed, subscription.created/ updated/deleted - /webhooks/stripe/main/instant: checkout.session.completed/expired, payment_intent.succeeded/payment_failed, charge.succeeded/failed/ refunded/dispute.created, invoice.paid/payment_failed, customer.subscription.trial_will_end/deleted - /webhooks/stripe/connect/light: account.updated/application. authorized/deauthorized, transfer.created/updated, payout.created/ paid/failed - /webhooks/stripe/connect/instant: payment_intent.succeeded/ payment_failed, charge.succeeded/failed/refunded, checkout.session.completed - Verification si le webhook existe deja par URL avant creation (pas de doublon) - listWebhooks(): liste tous les webhooks du compte - deleteWebhook(): supprime un webhook par ID src/Controller/WebhookStripeController.php (nouveau): - 4 routes POST sans authentification (firewall webhooks): /webhooks/stripe/main/light, /webhooks/stripe/main/instant, /webhooks/stripe/connect/light, /webhooks/stripe/connect/instant - Verification de signature Stripe via Stripe\Webhook::constructEvent() - Log de chaque evenement avec channel, type et event_id - TODO pour dispatcher vers les handlers specifiques src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/webhooks: prend une URL de base en parametre, appelle createAllWebhooks(), affiche les resultats (cree/existe deja) en flash messages templates/admin/sync/index.html.twig: - Nouveau bloc "Webhooks Stripe" (bleu) avec champ URL de base (pre-rempli avec l'URL courante), bouton "Creer les webhooks", et liste des 4 endpoints avec leurs evenements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:53:29 +02:00
foreach ($result['errors'] as $error) {
$this->addFlash('error', 'Stripe Webhook : '.$error);
}
return $this->redirectToRoute('app_admin_sync_index');
}
feat: sync automatique Stripe pour les tarifs + boutons sync admin src/Service/StripePriceService.php (nouveau): - Utilise Stripe SDK v20 (StripeClient) avec STRIPE_SK - syncPrice(): pour chaque PriceAutomatic, cree ou retrouve le produit Stripe via metadata price_auto_type, puis cree le Stripe Price (unique et/ou recurrent selon monthPrice) - ensureProduct(): cherche un produit existant par metadata, le cree sinon, met a jour nom/description si modifies - createStripePrice(): cree un prix Stripe en centimes, avec tax_behavior=exclusive, recurring si monthPrice > 0 avec interval=month (ou year si period >= 12) - updateStripePriceIfNeeded(): si le montant a change, archive l'ancien prix Stripe et en cree un nouveau (Stripe ne permet pas de modifier le montant d'un prix existant) - syncAll(): synchronise tous les tarifs, retourne synced + errors src/Service/TarificationService.php: - Injection optionnelle de StripePriceService - ensureDefaultPrices(): apres creation des tarifs, sync automatique avec Stripe (cree produits + prix) en plus de Meilisearch src/Controller/Admin/TarificationController.php: - edit(): apres mise a jour d'un tarif, sync automatique avec Stripe (cree/archive/recree les prix si montant change) et Meilisearch - Flash d'erreur si Stripe echoue, les modifs locales sont sauvegardees src/Controller/Admin/SyncController.php: - Nouvelle route POST /admin/sync/stripe/prices: synchronise tous les tarifs avec Stripe via StripePriceService::syncAll() templates/admin/sync/index.html.twig: - Section "Stripe" avec bouton "Synchroniser Stripe" (violet) pour les tarifs, avec confirmation avant execution - Section Meilisearch tarifs renommee "Tarifs - Meilisearch" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:50:27 +02:00
#[Route('/stripe/prices', name: 'stripe_prices', methods: ['POST'])]
public function syncStripePrices(StripePriceService $stripePriceService): Response
{
$result = $stripePriceService->syncAll();
if ([] === $result['errors']) {
$this->addFlash('success', $result['synced'].' tarif(s) synchronise(s) avec Stripe.');
} else {
$this->addFlash('success', $result['synced'].' tarif(s) synchronise(s) avec Stripe.');
foreach ($result['errors'] as $error) {
$this->addFlash('error', 'Stripe : '.$error);
}
}
return $this->redirectToRoute('app_admin_sync_index');
}
/** @codeCoverageIgnore */
#[Route('/stripe/customers', name: 'stripe_customers', methods: ['POST'])]
public function syncStripeCustomers(
CustomerRepository $customerRepository,
EntityManagerInterface $em,
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
): Response {
if ('' === $stripeSecretKey || 'sk_test_***' === $stripeSecretKey) {
$this->addFlash('error', 'STRIPE_SK non configuree.');
return $this->redirectToRoute('app_admin_sync_index');
}
\Stripe\Stripe::setApiKey($stripeSecretKey);
$customers = $customerRepository->findAll();
$created = 0;
$updated = 0;
$errors = 0;
foreach ($customers as $customer) {
try {
$params = [
'email' => $customer->getEmail(),
'name' => $customer->getRaisonSociale() ?? $customer->getFullName(),
'phone' => $customer->getPhone(),
'metadata' => [
'crm_user_id' => (string) $customer->getUser()->getId(),
'siret' => $customer->getSiret() ?? '',
'code_comptable' => $customer->getCodeComptable() ?? '',
],
];
if (null !== $customer->getStripeCustomerId() && '' !== $customer->getStripeCustomerId()) {
\Stripe\Customer::update($customer->getStripeCustomerId(), $params);
++$updated;
} else {
$stripeCustomer = \Stripe\Customer::create($params);
$customer->setStripeCustomerId($stripeCustomer->id);
++$created;
}
} catch (\Throwable $e) {
$this->addFlash('error', 'Stripe client '.$customer->getEmail().' : '.$e->getMessage());
++$errors;
}
}
$em->flush();
$this->addFlash('success', $created.' client(s) cree(s), '.$updated.' mis a jour dans Stripe'.(0 < $errors ? ', '.$errors.' erreur(s)' : '').'.');
return $this->redirectToRoute('app_admin_sync_index');
}
2026-04-01 15:42:52 +02:00
#[Route('/all', name: 'all', methods: ['POST'])]
feat: index Meilisearch price_auto + bouton sync tarifs + statuts Stripe src/Service/MeilisearchService.php: - indexPrice(): indexe un PriceAutomatic dans l'index price_auto - removePrice(): supprime un tarif de l'index - searchPrices(): recherche dans les tarifs - setupIndexes(): ajout de l'index price_auto avec searchableAttributes (type, title, description) et filterableAttributes (type, period) - serializePrice(): serialise id, type, title, description, priceHt, monthPrice, period, stripeId, stripeAbonnementId src/Service/TarificationService.php: - Injection optionnelle de MeilisearchService dans le constructeur - ensureDefaultPrices(): apres flush des nouveaux tarifs, les indexe automatiquement dans Meilisearch src/Controller/Admin/SyncController.php: - Injection de PriceAutomaticRepository dans index() pour compter les tarifs - Nouvelle route POST /admin/sync/prices: synchronise tous les tarifs dans l'index price_auto de Meilisearch - syncAll(): inclut maintenant les tarifs dans la synchronisation complete templates/admin/sync/index.html.twig: - Nouveau bloc "Tarifs" avec index price_auto, compteur en base, bouton "Synchroniser" vert templates/admin/tarification/index.html.twig: - Header de chaque tarif: badges de statut Stripe - "Stripe OK" (vert) si stripeId renseigne, "Non sync" (rouge) sinon - "Abo OK" (vert) si stripeAbonnementId renseigne, "Abo non sync" (rouge) sinon (affiche uniquement si monthPrice != 0.00) - Champ Stripe Price ID abonnement masque si monthPrice == 0.00 (pas d'abonnement pour les paiements uniques) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:45:52 +02:00
public function syncAll(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository, PriceAutomaticRepository $priceRepository, MeilisearchService $meilisearch): Response
2026-04-01 15:42:52 +02:00
{
try {
$meilisearch->setupIndexes();
$customers = $customerRepository->findAll();
foreach ($customers as $customer) {
$meilisearch->indexCustomer($customer);
}
$revendeurs = $revendeurRepository->findAll();
foreach ($revendeurs as $revendeur) {
$meilisearch->indexRevendeur($revendeur);
}
feat: index Meilisearch price_auto + bouton sync tarifs + statuts Stripe src/Service/MeilisearchService.php: - indexPrice(): indexe un PriceAutomatic dans l'index price_auto - removePrice(): supprime un tarif de l'index - searchPrices(): recherche dans les tarifs - setupIndexes(): ajout de l'index price_auto avec searchableAttributes (type, title, description) et filterableAttributes (type, period) - serializePrice(): serialise id, type, title, description, priceHt, monthPrice, period, stripeId, stripeAbonnementId src/Service/TarificationService.php: - Injection optionnelle de MeilisearchService dans le constructeur - ensureDefaultPrices(): apres flush des nouveaux tarifs, les indexe automatiquement dans Meilisearch src/Controller/Admin/SyncController.php: - Injection de PriceAutomaticRepository dans index() pour compter les tarifs - Nouvelle route POST /admin/sync/prices: synchronise tous les tarifs dans l'index price_auto de Meilisearch - syncAll(): inclut maintenant les tarifs dans la synchronisation complete templates/admin/sync/index.html.twig: - Nouveau bloc "Tarifs" avec index price_auto, compteur en base, bouton "Synchroniser" vert templates/admin/tarification/index.html.twig: - Header de chaque tarif: badges de statut Stripe - "Stripe OK" (vert) si stripeId renseigne, "Non sync" (rouge) sinon - "Abo OK" (vert) si stripeAbonnementId renseigne, "Abo non sync" (rouge) sinon (affiche uniquement si monthPrice != 0.00) - Champ Stripe Price ID abonnement masque si monthPrice == 0.00 (pas d'abonnement pour les paiements uniques) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:45:52 +02:00
$prices = $priceRepository->findAll();
foreach ($prices as $price) {
$meilisearch->indexPrice($price);
}
$this->addFlash('success', \count($customers).' client(s), '.\count($revendeurs).' revendeur(s) et '.\count($prices).' tarif(s) synchronise(s).');
2026-04-01 15:42:52 +02:00
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
refactor: stocker les secrets webhook Stripe en BDD au lieu de .env.local src/Entity/StripeWebhookSecret.php (nouveau): - Constantes TYPE_MAIN_LIGHT, TYPE_MAIN_INSTANT, TYPE_CONNECT_LIGHT, TYPE_CONNECT_INSTANT pour les 4 types de webhook - type: string(30) unique, identifie le webhook (main_light, etc.) - secret: string(255), le signing secret retourne par Stripe (whsec_xxx) - endpointId: string nullable, l'ID de l'endpoint Stripe (we_xxx) - createdAt: DateTimeImmutable src/Repository/StripeWebhookSecretRepository.php (nouveau): - findByType(): trouve un secret par type - getSecret(): retourne directement la valeur du secret ou null src/Controller/WebhookStripeController.php (reecrit): - Les 4 routes lisent le secret depuis la BDD via StripeWebhookSecretRepository::getSecret() au lieu de variables d'env - Retourne HTTP 503 si le secret n'est pas encore configure - Plus besoin des variables STRIPE_WH_*_SECRET dans .env src/Controller/Admin/SyncController.php: - syncStripeWebhooks(): sauvegarde les secrets en BDD (cree ou met a jour StripeWebhookSecret par type) - Suppression de saveSecretsToEnvLocal() (plus de modification .env.local) - URL de base lue depuis WEBHOOK_BASE_URL (env) .env: - Suppression des 4 variables STRIPE_WH_*_SECRET (stockees en BDD) - Ajout WEBHOOK_BASE_URL (vide par defaut) docker/ngrok/sync.sh: - Ecrit aussi WEBHOOK_BASE_URL en plus de OUTSIDE_URL ansible/env.local.j2: - WEBHOOK_BASE_URL=https://stripe.siteconseil.fr pour la prod migrations/Version20260402205935.php: - Table stripe_webhook_secret avec type unique, secret, endpoint_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:51 +02:00
2026-04-01 15:42:52 +02:00
}