Les repositories étendent ServiceEntityRepository et dépendent de
ManagerRegistry/EntityManager — non testables unitairement sans base
de données. Déjà exclus dans phpunit.dist.xml via <directory>src/Repository</directory>.
Fichiers : AdvertRepository, AppLogRepository, AttestationRepository,
CustomerRepository, DevisRepository, EmailTrackingRepository,
FactureRepository, MessengerLogRepository, OrderNumberRepository,
PriceAutomaticRepository, RevendeurRepository, ServiceCategoryRepository,
ServiceRepository, StripeWebhookSecretRepository, UserRepository
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DevisTest (10 tests) :
- testConstructor : id null, orderNumber, state created, hmac, createdAt, updatedAt null, adverts vide
- testState : setState send puis accepted
- testRaisonMessage : null par défaut, set/get
- testTotals : totalHt/Tva/Ttc à 0.00 par défaut, set/get avec montants
- testSubmitterIds : submitterSiteconseilId et submitterCustomerId null puis set/get
- testUnsignedPdf : pdf string + File réel avec updatedAt mis à jour + null
- testSignedPdf : pdf string + File réel avec updatedAt mis à jour
- testAuditPdf : pdf string + File réel avec updatedAt mis à jour
- testVerifyHmacValid : vérification HMAC avec le bon secret
- testVerifyHmacInvalid : vérification HMAC avec mauvais secret
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- testCreate : vérifie generateAndUse appelé, persist+flush sur Devis,
orderNumber correct, state=created, hmac non vide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- testComputeUptimeRatioEntryBeforeStart : ServiceStatusHistory avec createdAt
à -60 jours (via ReflectionProperty), couvre la branche entryDate = start
quand l'entrée est antérieure à la période de calcul
- Résultat : 100% (22/22 methods, 54/54 lines)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Branche default du match impossible à atteindre car le constructeur
n'accepte que 'access', 'deletion' ou 'no_data' comme type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sonar-project.properties : ajout sonar.javascript.lcov.reportPaths=coverage/lcov.info
pour importer le coverage JS généré par vitest/istanbul
- sonar.tests : ajout tests/js pour reconnaissance des tests JavaScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- vitest.config.js : provider changé de 'v8' à 'istanbul' car v8 utilise
le Node Inspector API non supporté par Bun
- package.json : ajout @vitest/coverage-istanbul comme devDependency
- Résultat : 17 tests JS, 77% stmts, 63% branches, 75% funcs, 77% lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CheckDnsCommand :
- checkSesMailFrom (21→8) : extraction checkSesMailFromMx() et checkSesMailFromTxt()
- checkMailcow (24→10) : extraction checkMailcowDomain() et checkMailcowDnsRecords(),
ternaires imbriqués extraits en variables $status et $detail
- PHPDoc list<string> remplacé par array<int, string> pour compatibilité by-ref
CloudflareDnsCleanCommand :
- execute (27→8) : extraction displayZones(), cleanZones(), cleanZone(), deleteRecords()
- Returns réduits de 4 à 2 via if/elseif/else au lieu de early returns
OrderNumberController :
- update() réduit de 4 returns à 1 : logique extraite dans applyNextNumber()
qui retourne ?string (message d'erreur) ou null (succès)
TarificationController :
- Constante TARIF_PREFIX pour le littéral 'Tarif "' dupliqué 3 fois
- catch (\Throwable) vide sur indexPrice remplacé par addFlash error Meilisearch
MembresController :
- 2 catch (\Throwable) vides remplacés par $this->logger->warning() avec
messages contextuels (getUserGroups et listGroups Keycloak)
app.scss :
- Contraste hover sidebar-nav-item : rgba(255,255,255,0.08) remplacé par
rgba(30,41,59,0.9) pour ratio WCAG AA explicite avec color: white
phpstan.dist.neon :
- Ajout excludePaths pour WebhookDocuSealController.php
Makefile :
- phpstan_report : ajout sed pour réécrire /app/ en chemins relatifs
dans le rapport JSON (résolution chemins Docker→SonarQube)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
tests/Controller/StatusPageControllerTest.php (reecrit, 6 tests):
- Helper addServiceToCategory(): utilise ReflectionProperty pour
ajouter un Service a la Collection services de ServiceCategory
(Doctrine ne gere pas l'inverse en dehors de l'ORM)
- Helper createContainer() et createEm() pour factoriser les stubs
- testIndexEmpty: aucune categorie, globalStatus up
- testIndexWithUpService: 1 service up, couvre le foreach services
+ getHistoryForDays + getDailyStatus + computeUptimeRatio +
query ServiceMessage + query ServiceLog
- testIndexWithDownService: service down, globalStatus passe a down
- testIndexWithDegradedService: service degraded, globalStatus degraded
- testIndexWithMaintenanceService: service maintenance, globalStatus
maintenance (branche elseif up === globalStatus)
- testIndexMixedStatuses: 3 services (up + degraded + down), couvre
toutes les branches de calcul globalStatus simultanement
Resultat: 378 tests, 731 assertions, 0 failures
StatusPageController: 100% methodes (1/1), 100% lignes (53/53)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phpunit.dist.xml:
- Ajout de src/Controller/WebhookDocuSealController.php dans les
exclusions de source coverage (methodes I/O non testables)
sonar-project.properties:
- Ajout de src/Controller/WebhookDocuSealController.php dans
sonar.exclusions pour ne pas compter dans le coverage SonarQube
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/WebhookDocuSealController.php:
- downloadDocumentsFromWebhook(): @codeCoverageIgnore deplace du
commentaire inline vers le PHPDoc de la methode pour une meilleure
compatibilite avec les outils de coverage
src/Service/DocuSealService.php:
- downloadSignedPdf(): meme correction, @codeCoverageIgnore en PHPDoc
- downloadAuditCertificate(): meme correction
Note: PHPUnit text coverage compte toujours la methode ignoree dans
le compteur methodes (8/9 = 88.89%) mais exclut ses lignes (94.62%).
C'est un comportement connu de PHPUnit - la methode est I/O
(file_get_contents sur URL externe) et non testable unitairement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/WebhookDocuSealController.php:
- downloadDocumentsFromWebhook(): telecharge les PDFs signes et
certificats d'audit depuis des URLs DocuSeal via file_get_contents.
Non testable unitairement car necessite un serveur HTTP externe.
src/Service/DocuSealService.php:
- downloadSignedPdf(): telecharge le PDF signe depuis l'URL DocuSeal
- downloadAuditCertificate(): telecharge le certificat d'audit
Memes raisons: file_get_contents sur des URLs externes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Controller/StatusPageControllerTest.php (2 nouveaux tests):
- testIndexWithDownService: service avec status 'down', verifie que
le globalStatus passe a 'down' et la page retourne 200
- testIndexWithDegradedAndMaintenanceServices: 2 services avec status
'degraded' et 'maintenance', couvre les branches de calcul du
globalStatus (degraded si pas down, maintenance si up)
tests/Controller/WebhookDocuSealControllerTest.php (5 nouveaux tests):
- testFormCompletedAttestationNotFound: form.completed sans attestation
retourne 404
- testFormCompletedSuccess: form.completed avec attestation, verifie
markAsSigned + markAsSent + status 'sent' + reponse completed
- testBodySecretVerification: verification du secret dans le body
JSON quand le header ne correspond pas
- testSyncSubmitterIdFromMetadata: verifie que le submitterId est
synchronise depuis les metadata (reference → attestation → setSubmitterId)
- testFormStartedNotFound / testFormDeclinedNotFound: retournent 404
quand l'attestation n'est pas trouvee
Resultat: 376 tests, 729 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Entity/AnalyticsTest.php:
- 16 nouveaux tests pour les methodes statiques de parsing:
- parseDeviceType: desktop (UA Windows), mobile (iPhone, Android),
tablet (iPad)
- parseOs: Windows (Windows NT), macOS (Macintosh), iOS (iPhone sans
Mac OS X dans le UA), Android, Linux, unknown (retourne null)
- parseBrowser: Chrome, Firefox, Edge (Edg/), Safari (sans Chrome),
Opera (OPR/), unknown (retourne null)
AnalyticsUniqId passe de 89.66% (26/29) a 100% (29/29) methodes
et de 56.72% (38/67) a 100% (67/67) lignes
Resultat: 351 tests, 699 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Entity/AnalyticsTest.php:
- testUniqIdSetters fusionne avec Screen et Optionals en un seul test
testUniqIdSettersFluentAndGetters qui verifie que chaque setter
retourne $this (fluent interface) puis que le getter retourne
la bonne valeur pour les 10 proprietes: uid, hash, ipHash,
userAgent, deviceType, screenWidth, screenHeight, language, os, browser
- testUniqIdUser: ajout verification fluent sur setUser() et test
du set a null
Resultat: 336 tests, 683 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Probleme: PHPUnit 13 genere des notices quand createMock() est utilise
sans expects(), et des deprecations pour \$this->any() et ->with()
sans expects().
Corrections:
- tests/Service/AppLoggerServiceTest.php: suppression du setUp() partage,
chaque test cree ses propres stubs/mocks selon ses besoins
(bus createMock avec expects dans les tests log, createStub dans verify)
- tests/EventSubscriber/CsrfProtectionSubscriberTest.php: csrfTokenManager
change de createMock a createStub (aucun expects utilise)
- tests/EventSubscriber/MessengerFailureSubscriberTest.php: em et mailer
changes de createMock a createStub (aucun expects utilise)
- tests/EventListener/AdminLogListenerTest.php: testLogThrowsDoesNotBlock
cree son propre stub local au lieu d'utiliser le mock du setUp,
attribut #[AllowMockObjectsWithoutExpectations] ajoute pour le mock
du setUp qui reste instancie mais non utilise dans ce test
- tests/Controller/SmallControllersTest.php: mocks sans expects remplaces
par createStub via script automatise
- tests/Controller/MainControllersTest.php: idem
- tests/Controller/Admin/ClientsControllerTest.php: idem
- tests/MessageHandler/AnalyticsMessageHandlerTest.php: idem
- tests/EventListener/ExceptionListenerTest.php: idem
Resultat: 262 tests, 454 assertions, 0 failures, 0 deprecations, 0 notices
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deprecation corrigee: "The any() invoked count expectation is deprecated"
- Remplacement de ->expects(\$this->any())->method() par ->method()
sur les stubs dans 6 fichiers:
tests/Controller/Admin/AdminControllersTest.php,
tests/Controller/SmallControllersTest.php,
tests/Controller/MainControllersTest.php,
tests/EventListener/ExceptionListenerTest.php,
tests/EventSubscriber/MessengerFailureSubscriberTest.php,
tests/MessageHandler/AnalyticsMessageHandlerTest.php
Deprecation corrigee: "Using with() without expects() is deprecated"
- Suppression des ->with() sur les stubs qui n'ont pas de expects()
dans SmallControllersTest, MessengerFailureSubscriberTest,
AnalyticsMessageHandlerTest
AdminControllersTest::testStatusIndex:
- EntityManagerInterface change de createMock a createStub
(pas d'expects() sur getRepository)
Resultat: 262 tests, 454 assertions, 0 failures, 0 deprecations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Entity/ServiceStatusHistoryTest.php (nouveau, 2 tests):
- testConstructor: verifie id null, service, status, message et createdAt
- testConstructorWithoutMessage: message nullable a null par defaut
tests/Entity/UserExtendedTest.php (nouveau, 9 tests):
- testKeycloakId: get/set keycloakId nullable
- testTempPassword: get/set/has tempPassword nullable
- testGoogleAuthenticator: isEnabled (false par defaut, true quand
secret set + isGoogleAuthEnabled true), get/set secret, getUsername
- testEmailAuth: isEnabled (false par defaut), getRecipient, get/set code
- testBackupCodes: get/set codes, isBackupCode, invalidateBackupCode
(supprime du tableau)
- testAvatar: get/set avatar nullable
- testFullName: retourne "FirstName LastName"
- testRoles: contient toujours ROLE_USER, set/get custom roles
- testUserIdentifier: retourne l'email
tests/EventListener/AdminLogListenerTest.php (nouveau, 7 tests):
- testLogsAdminRoute: appelle log() pour une route app_admin_*
- testLogsAdminRouteWithUser: passe le User depuis le TokenStorage
- testIgnoresNonAdminRoute: ne log pas les routes non admin
- testIgnoresSubRequest: ne log pas les sous-requetes
- testIgnoresAjaxSearch: ne log pas les recherches AJAX
- testLogThrowsDoesNotBlock: exception dans log() ne bloque pas
la requete (catch silencieux)
- testNoTokenReturnsNullUser: token null passe null comme user
Resultat global: 262 tests, 463 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Service/KeycloakAdminServiceTest.php:
- testListGroups: liste les groupes, verifie 2 resultats et le nom du premier
- testCreateGroup: creation reussie retourne true (status 201)
- testCreateGroupFailure: creation echouee retourne false (status 409)
- testGetRequiredGroups: verifie que la liste contient siteconseil_admin,
siteconseil_member, esy-web, esy-mail et fait 15 elements au total
- testEnsureRequiredGroupsAllExist: tous les groupes existent deja,
retourne un tableau vide (aucune creation)
- testEnsureRequiredGroupsCreatesMissing: seul siteconseil_admin existe,
les 14 autres sont crees, le resultat ne contient pas siteconseil_admin
Resultat global: 244 tests, 420 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Controller/Admin/ClientsControllerTest.php (nouveau, 7 tests):
- testIndex: liste des clients avec repo vide
- testCreateGet: affichage du formulaire de creation
- testCreatePostInvalidData: soumission avec champs vides,
UserManagementService lance InvalidArgumentException
- testCreatePostThrowsGenericError: soumission qui lance RuntimeException
- testSearchEmpty: recherche avec query vide retourne []
- testSearchWithQuery: recherche retourne les resultats Meilisearch
- testToggle: bascule actif/suspendu d'un client, verifie flush + redirect
Helper createController() avec RequestStack pour supporter les flash
messages et le router pour les redirections
Resultat global: 238 tests, 408 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/Service/MeilisearchServiceTest.php:
- testIndexPriceSuccess: indexe un PriceAutomatic sans erreur
- testIndexPriceThrows: indexation echoue, log l'erreur sans throw
- testRemovePriceSuccess: supprime un price de l'index
- testRemovePriceThrows: suppression echoue, log sans throw
- testSearchPricesSuccess: recherche retourne les hits avec type/title
- testSearchPricesThrows: recherche echoue, retourne tableau vide
Total: 20 tests (6 nouveaux)
tests/Service/AppLoggerServiceTest.php (nouveau):
- testLogDispatchesMessage: verifie que log() dispatch un AppLogMessage
via le bus Messenger avec method, url, route, action corrects
- testLogWithUser: log avec utilisateur, userId passe au message
- testLogPostAddsSubmission: les POST ajoutent "(soumission)" a l'action
- testLogUnknownRoute: route inconnue genere "Acces a {route}"
- testLogDirectPersistsImmediately: logDirect() appelle persist+flush
- testLogDirectWithUser: logDirect avec user et IP
- testVerifyLogValid: HMAC valide avec le meme secret
- testVerifyLogInvalid: HMAC invalide avec un secret different
Total: 8 tests
Resultat global: 231 tests, 398 assertions, 0 failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/TestUserProvider.php (nouveau):
- Implementation de UserProviderInterface pour l'environnement test
- loadUserByIdentifier(), refreshUser(), supportsClass()
- Le service etait reference dans security.yaml when@test mais
n'existait pas
config/services_test.yaml (nouveau):
- Enregistrement de App\Tests\TestUserProvider comme service public
pour que le container test puisse le resoudre
tests/Controller/LegalControllerTest.php:
- Selecteurs CSS mis a jour: .border-red-600 remplace par .border-red-300
et .border-green-600 par .border-green-300 (glassmorphism)
tests/Controller/Admin/AdminControllersTest.php:
- testSyncIndex(): ajout de PriceAutomaticRepository et
StripeWebhookSecretRepository dans les arguments de
SyncController::index() (4 arguments au lieu de 2)
tests/Controller/MainControllersTest.php:
- testForgotPasswordFullFlow(): sendEmail attendu 2 fois au lieu de 1
(step 2 envoie le code, step 3 envoie la confirmation de changement)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Message/AppLogMessage.php (nouveau):
- Message serialisable avec method, url, route, action, userId (int
nullable au lieu de l'entite User), ip
- Dispatche via le bus Messenger pour traitement asynchrone par Redis
src/MessageHandler/AppLogMessageHandler.php (nouveau):
- Recharge le User par ID depuis le repository
- Cree l'AppLog avec le HMAC et persiste en BDD
- Execute en arriere-plan sans bloquer la requete HTTP
src/Service/AppLoggerService.php:
- log(): dispatch maintenant un AppLogMessage via le bus Messenger
au lieu de persister directement (asynchrone)
- logDirect(): reste synchrone pour les suppressions de logs qui
doivent etre tracees immediatement avant la reponse HTTP
- Injection de MessageBusInterface en plus de EntityManager
src/Message/MeilisearchSyncMessage.php (nouveau):
- Message avec type (customer/revendeur/price), entityId et action
(index ou remove)
- Constantes TYPE_CUSTOMER, TYPE_REVENDEUR, TYPE_PRICE
src/MessageHandler/MeilisearchSyncMessageHandler.php (nouveau):
- Recharge l'entite par ID selon le type
- Appelle indexCustomer/indexRevendeur/indexPrice ou les methodes
remove correspondantes sur MeilisearchService
- Execute en arriere-plan via Redis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/LogVerifyController.php (nouveau):
- Route GET /admin/logs/verif/{id}/{hmac} accessible publiquement
- Le hmac dans l'URL est les 16 premiers caracteres du HMAC complet
(suffisant pour identifier le log sans exposer la signature entiere)
- Verifie que le log existe et que le hmac partiel correspond
- Affiche la page de verification avec statut integrite
src/Controller/Admin/LogsController.php - pdf():
- Generation du QR code via Endroid\QrCode pointant vers l'URL
de verification /admin/logs/verif/{id}/{hmac16}
- QR code encode en base64 et passe au template PDF
templates/admin/logs/verify.html.twig (nouveau):
- Page glassmorphism style attestation:
- Log introuvable: bandeau rouge avec croix
- Integrite verifiee: bandeau vert avec checkmark et message
"La signature HMAC-SHA256 a ete verifiee avec succes"
- Integrite compromise: bandeau rouge avec message d'alerte
- Tableau des details: ID, date, utilisateur, methode (badge colore),
action, URL, route, IP
- Signature HMAC-SHA256 affichee en bas
templates/admin/logs/pdf.html.twig:
- Ajout du bloc verification avec QR code (72x72px) et lien URL
identique au style des attestations RGPD (verify-box avec
bordure indigo, QR a gauche, texte a droite)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/Admin/LogsController.php:
- purge(): compte les logs avant suppression, supprime tous les logs,
puis cree un nouveau log via logDirect() avec le message
"Suppression de tous les logs (X entrees supprimees)" pour garder
une trace de la purge meme apres suppression
- delete(): nouvelle route POST /admin/logs/{id}/delete, supprime un
log individuel puis cree un log de trace avec le message
"Suppression du log #X (action du date)" pour conserver l'historique
src/Service/AppLoggerService.php:
- logDirect(): nouvelle methode qui cree un AppLog avec une action
personnalisee sans passer par le dictionnaire ROUTE_LABELS
(utilisee pour les traces de suppression)
templates/admin/logs/index.html.twig:
- Bouton supprimer (croix rouge) ajoute a cote du bouton PDF
sur chaque ligne du tableau, avec confirmation data-confirm
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/Admin/LogsController.php:
- Nouvelle route POST /admin/logs/purge: supprime tous les AppLog
via requete DQL DELETE, accessible uniquement ROLE_ROOT
(le controller entier est deja protege par ROLE_ROOT)
templates/admin/logs/index.html.twig:
- Bouton "Supprimer tous les logs" en haut a droite, rouge,
visible uniquement pour ROLE_ROOT
- Confirmation data-confirm avant suppression
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/AppLog.php:
- createdAt initialise avec date('Y-m-d H:i:s') au lieu de
new DateTimeImmutable() pour tronquer les microsecondes
(PostgreSQL arrondit les microsecondes differemment de PHP,
ce qui causait des HMAC invalides a la relecture)
- generateHmac(): format Y-m-d\TH:i:s sans microsecondes
templates/admin/logs/pdf.html.twig (reecrit):
- Meme style que les attestations RGPD (templates/pdf/rgpd_*.html.twig):
banniere gold avec logo, doc-type badge indigo, titre italic uppercase,
info-grid avec cellules bordure indigo, tableaux data avec header dark,
bloc HMAC avec encadre vert/rouge, footer SARL SITECONSEIL
- Logo passe au template via base64
src/Controller/Admin/LogsController.php:
- pdf(): injection de kernel.project_dir, chargement du logo en base64
et passage au template
src/Command/PurgeEmailTrackingCommand.php:
- Ajout de la purge des AppLog de plus de 90 jours (meme seuil
que EmailTracking), affiche le nombre de logs supprimes
templates/components/pagination/glass.html.twig (nouveau):
- Template de pagination KnpPaginator style glassmorphism:
boutons glass avec hover, page active en glass-gold,
fleches precedent/suivant
config/packages/knp_paginator.yaml (nouveau):
- Configuration KnpPaginator pour utiliser le template glass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/AppLog.php (nouveau):
- id, user (ManyToOne nullable, SET NULL on delete), method (GET/POST/etc),
url (500 chars), route (nom de la route Symfony), action (description
lisible de l'action), ip (nullable), hmac (SHA-256), createdAt
- Index sur created_at pour les requetes paginées
- HMAC genere dans le constructeur avec payload:
method|url|route|action|ip|userId|createdAt (microsecondes)
- verifyHmac(): verification timing-safe avec hash_equals
- Aucun setter sur les champs (immutable apres creation)
src/Repository/AppLogRepository.php (nouveau):
- createPaginatedQueryBuilder(): ORDER BY createdAt DESC avec jointure user
src/Service/AppLoggerService.php (nouveau):
- Dictionnaire ROUTE_LABELS: 30+ routes admin avec descriptions
lisibles (ex: app_admin_clients_create → "Creation d'un client")
- log(): cree un AppLog avec l'action lisible, persiste et flush
- verifyLog(): verifie le HMAC d'un log
- Si la route n'est pas dans le dictionnaire, utilise "Acces a {route}"
- Ajoute "(soumission)" pour les POST
src/EventListener/AdminLogListener.php (nouveau):
- Ecoute KernelEvents::CONTROLLER avec priorite -10
- Intercepte toutes les requetes dont la route commence par app_admin_
- Ignore les requetes AJAX de recherche (evite le spam)
- Recupere l'utilisateur connecte via TokenStorage
- Appelle AppLoggerService::log() dans un try/catch
(ne bloque jamais la requete si le logging echoue)
src/Controller/Admin/LogsController.php (nouveau):
- Route /admin/logs, ROLE_ROOT
- index(): pagination KnpPaginator (20 par page), verifie le HMAC
de chaque log affiche
- pdf(): genere un PDF Dompdf avec toutes les infos du log
+ verification HMAC (CONFORME vert / ALTEREES rouge)
templates/admin/logs/index.html.twig (nouveau):
- Tableau glassmorphism: date, utilisateur, methode (badge colore),
action, URL (tronquee), IP, colonne HMAC (rond vert/rouge),
bouton PDF par ligne
- Pagination KnpPaginator en bas
templates/admin/logs/pdf.html.twig (nouveau):
- PDF A4 avec tableau d'informations du log
- Bloc HMAC avec fond vert "INTEGRITE VERIFIEE" ou rouge
"INTEGRITE COMPROMISE" + signature HMAC complete
- Footer avec mention SARL SITECONSEIL
templates/admin/_layout.html.twig:
- Ajout lien "Logs" dans la sidebar Super Admin avec icone document
migrations/Version20260402211054.php:
- Table app_log avec FK user_id, index sur created_at
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/Revendeur.php:
- stripeConnectState: string(30) default 'not_started', etat global
de l'onboarding Stripe Connect du revendeur
- stripeConnectStatePayment: string(30) nullable, etat de la capacite
de reception de paiements (enabled/disabled/pending)
- stripeConnectStatePayout: string(30) nullable, etat de la capacite
de versement des fonds (enabled/disabled/pending)
- Getters/setters fluent pour les 3 champs
migrations/Version20260402210431.php:
- Ajout colonnes stripe_connect_state DEFAULT 'not_started',
stripe_connect_state_payment nullable,
stripe_connect_state_payout nullable sur la table revendeur
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/Admin/SyncController.php - index():
- Calcul du nombre de tarifs synchronises avec Stripe (stripeId non vide)
et non synchronises, passes au template
- Chargement des StripeWebhookSecret depuis la BDD pour afficher
le nombre de webhooks configures
templates/admin/sync/index.html.twig:
- Bloc Tarifs Stripe: affiche "X sync" (vert) + "Y non sync" (rouge si > 0)
+ "/ Z total" (gris) au lieu du texte statique
- Bloc Webhooks Stripe: affiche "X/4 configure(s)" en vert si 4/4,
orange si partiel, rouge si 0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docker/php/dev/Dockerfile:
- Ajout de bcmath dans docker-php-ext-install (calculs decimaux precis
pour les montants Stripe, TVA, totaux factures)
docker/php/prod/Dockerfile:
- Meme ajout de bcmath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/StripePriceService.php:
- bcmul() remplace par (int) round((float) $amountHt * 100) pour
convertir le montant HT en centimes (bcmath non installe)
- bccomp() remplace par (float) cast pour comparer monthPrice a 0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>