- Constantes WELCOME_SUBJECT et WELCOME_TEMPLATE (literals dupliques 3x)
- Suppression dispatchPostAction (10 params) : match inline dans show()
- handleContactForm : elseif au lieu de 2 if independants (CC reduite)
- 21 -> 19 methodes (sous la limite de 20)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remplace les 2 sections (changer mot de passe + générer temporaire) par
un seul bouton "Envoyer un lien de reinitialisation" qui :
1. Génère un mot de passe temporaire (bin2hex 16 chars)
2. Hash et stocke dans User (password + tempPassword)
3. Envoie l'email bienvenue avec le lien set_password au client
Plus de champ mot de passe à saisir manuellement — tout est automatique.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Onglet Securite (tab=securite) :
Statut du compte :
- Email, statut mot de passe (Temporaire jaune / Defini vert)
- 2FA Email (Active/Desactive), 2FA Google (Active/Desactive)
Changer le mot de passe :
- Formulaire avec nouveau mot de passe (min 8 chars)
- Hash via UserPasswordHasherInterface, clearTempPassword
Generer mot de passe temporaire :
- Genere 16 chars aleatoires, hash + setTempPassword
- Affiche le mot de passe en flash (pour renvoi email bienvenue)
- Modal confirmation avant action
Desactiver 2FA :
- Desactive 2FA Email + Google Authenticator + supprime secret + backup codes
- Bouton rouge avec modal confirmation
- Section visible uniquement si au moins 1 methode 2FA active
handleSecurityForm() :
- Actions : reset_password, disable_2fa, generate_temp_password
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Onglet Sites Internet (tab=sites) :
- Table : nom, UUID, type (Vitrine bleu / E-Commerce violet),
statut (Cree / Installation / En ligne / Suspendu / Ferme), date
- Badges colorés par statut
- Message "Aucun site internet" si vide
buildCustomersInfo :
- sites compte maintenant les Website liés au customer (plus hardcodé à 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EsyMailService - 2 nouvelles méthodes de vérification DNS :
checkDnsEsyMail(domain) — config réception (Dovecot) :
- MX → doit pointer vers ESYMAIL_HOSTNAME (mail.esy-web.dev)
- SPF → doit contenir le hostname mail ou include:_spf
- DKIM → sélecteur dkim._domainkey.domain (TXT ou CNAME)
- DMARC → _dmarc.domain doit contenir v=DMARC1
- Retourne ok=true si les 4 checks passent
checkDnsEsyMailer(domain) — config envoi (AWS SES) :
- SES domaine vérifié (isDomainVerified = Success)
- SES DKIM activé et vérifié (getDkimStatus enabled+verified)
- SPF → doit contenir include:amazonses.com
- MAIL FROM → configuré et vérifié (getMailFromStatus = Success)
- Retourne ok=true si les 4 checks passent
Intégration :
- Les checks DNS sont exécutés seulement sur l'onglet NDD (pas sur les
autres onglets pour éviter les appels réseau inutiles)
- Les résultats alimentent configDnsEsyMail et configDnsEsyMailer
dans la sous-ligne de chaque domaine (OK vert / KO rouge)
Configuration :
- .env : ESYMAIL_HOSTNAME (vide par défaut)
- .env.local : ESYMAIL_HOSTNAME=mail.esy-web.dev
- ansible/vault.yml : esymail_hostname pour la prod
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 configurations DNS distinctes par domaine :
- Config Esy-Mail : MX, SPF, DKIM, DMARC pour la réception (Dovecot/Mailcow)
- Config Esy-Mailer : SPF, DKIM SES, MAIL FROM pour l'envoi (AWS SES)
Les 2 sont à false/KO par défaut — seront branchés sur les checks
DNS réels quand les services seront activés sur le domaine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sous chaque NDD dans l'onglet Noms de domaine, une ligne affiche :
- Esy-Mail : check vert si au moins 1 DomainEmail lié + nombre de boîtes
- Esy-Mailer : check vert/rouge (placeholder, false pour le moment)
- Config DNS : OK (vert) si zone Cloudflare configurée, KO (rouge) sinon
buildDomainsInfo() :
- Compte les DomainEmail par domaine
- esyMail = emailCount > 0
- esyMailerConfig = zoneIdCloudflare != null (DNS géré)
- esyMailer = false (sera branché sur l'entité service)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DnsCheckService :
- Ajout getExpirationDate(domain) : requête RDAP pour récupérer
la date d'expiration d'un domaine (events[].eventAction=expiration)
autoDetectDomain :
- Fallback RDAP en fin de détection : si expiredAt est encore null
après check OVH/Cloudflare, interroge RDAP pour l'expiration
- Fonctionne pour tous les TLD (.fr, .com, .dev, etc.)
Flux : OVH serviceInfos → Cloudflare (pas d'expiration) → RDAP fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OvhService (ovh/ovh SDK) :
- listDomains() : liste tous les NDD du compte OVH
- isDomainManaged(fqdn) : vérifie si un domaine est chez OVH
- getDomainInfo(fqdn) : infos domaine (nameServerType, offer, etc.)
- getDomainServiceInfo(fqdn) : expiration, création, status, contacts
- getZoneInfo(fqdn) : zone DNS (OVH ou externe, DNSSEC)
Ajout NDD (onglet Noms de domaine, fiche client) :
- Formulaire : nom de domaine + registrar (auto-détection ou manuel)
- autoDetectDomain() au submit :
1. Check OVH : si trouvé → registrar=OVH, isGestion=true, isBilling=true,
expiredAt depuis serviceInfos, check zone DNS OVH
2. Check Cloudflare : si zone trouvée → zoneCloudflare=active,
zoneIdCloudflare=zoneId. Si registrar=Cloudflare → gestion+billing actifs
3. Si ni OVH ni CF : registrar manuel (Gandi/Autre), isGestion=false
- Suppression NDD avec data-confirm modal
- Colonne Actions ajoutée dans la table
Configuration :
- .env : OVH_KEY, OVH_SECRET, OVH_CUSTOMER
- .env.local : credentials OVH
- ansible/vault.yml : credentials OVH pour prod
- composer.json : ovh/ovh ^3.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UnsubscribeController :
- Route GET /unsubscribe/{email}/{token} (app_unsubscribe)
- Vérifie le token HMAC via UnsubscribeManager::isValidToken
- Si valide : désabonne l'email + page succès
- Si invalide : page erreur avec contact unsubscribe@siteconseil.fr
Templates :
- unsubscribe/success.html.twig : confirmation glassmorphism
- unsubscribe/invalid.html.twig : erreur glassmorphism
ClientsController :
- sendWelcomeEmail : suppression try/catch silencieux pour laisser
remonter les erreurs (sinon mail jamais envoyé sans diagnostic)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Email bienvenue (templates/emails/client_created.html.twig) :
- Envoyé automatiquement à la création du client
- Contenu : plateforme client.siteconseil.fr, identifiant email,
bouton "Choisir mon mot de passe" avec lien app_set_password
- Compatible tous clients mail (table-based, CSS longhand)
ClientsController :
- sendWelcomeEmail() : génère le lien set_password et envoie via MailerService
- Appelé dans create() après ensureDefaultContact
- Route POST /{id}/resend-welcome : renvoie l'email si tempPassword disponible
Fiche client (show.html.twig, onglet Info) :
- Si tempPassword existe : bandeau indigo "Espace client non activé"
+ lien direct vers la page activation + bouton "Renvoyer email bienvenue"
- Si tempPassword null : bandeau vert "Espace client activé"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ensureDefaultContact() :
- Vérifie si le client a au moins 1 contact dans CustomerContact
- Si aucun contact : crée automatiquement un contact avec
firstName/lastName du client, email, phone, role='Directeur',
isBillingEmail=true
- Appelé à la création du client (après flush)
- Appelé à l'ouverture de la fiche client (show) pour les clients
existants qui n'ont pas encore de contact
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sous chaque ligne client, une ligne compacte affiche :
- Raison sociale, SIRET, type entreprise (si disponibles)
- Sites : nombre (placeholder, 0 pour l'instant)
- NDD : nombre de domaines liés au client
- Emails : nombre de DomainEmail liés aux domaines du client
- Sign : check vert/rouge (Esy-Signature activé)
- News : check vert/rouge (Esy-Mailer/Newsletter activé)
- Mail : check vert/rouge (au moins 1 email Esy-Mail)
- Statut paiement : OK (vert) ou IMPAYEE (rouge avec nombre)
ClientsController :
- index() reçoit EntityManagerInterface pour requêter Domain/DomainEmail
- buildCustomersInfo() : construit les compteurs par client
(domains, emails, esyMail depuis DomainEmail count > 0)
- Les flags esySign/esyNewsletter/unpaid/sites seront branchés
quand les entités correspondantes existeront
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Customer entity :
- Ajout STATE_PENDING_DELETE = 'pending_delete'
- Ajout isPendingDelete() pour vérification rapide
ClientsController::delete :
- Route POST /admin/clients/{id}/delete
- Met le state à pending_delete (pas de suppression immédiate)
- Flash message expliquant la suppression automatique cette nuit
- Bloqué si déjà en pending_delete
Template admin/clients/index.html.twig :
- Badge "Suppression" rouge avec animation pulse pour pending_delete
- Bouton "Supprimer" avec data-confirm (modal native navigateur)
- Si pending_delete : boutons Suspendre/Activer masqués,
texte "En attente" affiché
CleanPendingDeleteCommand (app:clean:pending-delete) :
- Cherche tous les clients state = pending_delete
- Pour chaque client :
1. Supprime le customer Stripe (API delete)
2. Supprime de Meilisearch (removeCustomer)
3. Supprime le Customer + User en cascade (Doctrine)
- Log chaque suppression + compteur final
- A planifier en cron nocturne : 0 2 * * * (2h du matin)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Customer entity :
- Ajout geoLat et geoLong (DECIMAL 10,7 nullable)
- Migration : ALTER TABLE customer ADD geo_lat, geo_long
Géocodage automatique :
- API recherche entreprise : récupère siege.latitude/longitude directement
- Fallback API IGN (data.geopf.fr/geocodage/search) si coords absentes
mais adresse remplie — appelé côté PHP dans geocodeIfNeeded()
- Champs hidden geoLat/geoLong dans le formulaire
Code comptable 411_ :
- Préfixe "411_" affiché en dur (glass-dark, non modifiable)
- L'utilisateur saisit uniquement la partie après (ex: 0001_DUPON)
- Si vide : génération automatique via generateUniqueCodeComptable()
- Concaténation '411_' + saisie dans le contrôleur
Tests mis à jour : testGeoCoordinates, HttpClientInterface ajouté dans
tous les appels create(), Customer 100% (48/48, 82/82)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
initStripeCustomer amélioré :
- name : raison sociale (priorité) ou nom complet du contact
- phone : téléphone du client
- address : adresse complète (line1, line2, postal_code, city, country FR)
- metadata : crm_user_id, siret, code_comptable
- tax_id_data : numéro TVA intracommunautaire (eu_vat) si disponible
finalizeStripeCustomer amélioré :
- metadata enrichi avec code_comptable en plus du crm_user_id
Le stripeCustomerId est stocké dans Customer::stripeCustomerId après
la création et persiste en BDD via le flush.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Customer entity :
- Ajout champ ape (VARCHAR 10, nullable) avec getter/setter
- Migration : ALTER TABLE customer ADD ape
Recherche entreprise (entreprise-search.js) :
- RCS construit depuis SIREN + ville du siège (ex: RCS Saint-Quentin 418664058)
- TVA intracommunautaire calculée depuis SIREN (clé modulo 97)
- Code APE/NAF récupéré depuis activite_principale de l'API
- APE affiché dans les résultats de recherche à côté du SIREN/SIRET
- Auto-remplissage des champs : raisonSociale, siret, rcs, numTva, ape,
address, zipCode, city, firstName, lastName
Template create.html.twig :
- Ajout champ "Code APE / NAF" dans la section Entreprise
ClientsController :
- populateCustomerData : ajout setApe depuis le formulaire
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend (ClientsController::entrepriseSearch) :
- Route GET /admin/clients/entreprise-search?q=...
- Proxy PHP vers https://recherche-entreprises.api.gouv.fr/search
(pas d'appel API direct depuis le JS)
- Retourne JSON avec results[], total_results
- Gestion erreur avec 502 si API indisponible
Frontend (assets/modules/entreprise-search.js) :
- Module JS séparé, pas de script inline (CSP compatible)
- Modal glassmorphism avec champ recherche et liste résultats
- Chaque résultat affiche : nom, SIREN, SIRET, adresse, dirigeant, statut
- Au clic sur un résultat, auto-remplissage du formulaire :
raisonSociale, siret, numTva (calcul clé TVA), address, zipCode, city
+ firstName/lastName du dirigeant si les champs sont vides
- Fermeture modal via overlay, bouton X, ou Escape
Template :
- Bouton "Rechercher SIRET / SIREN" à côté du bouton Retour
- Modal HTML avec header glass-dark, champ recherche, zone résultats scrollable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajout champ codeComptable dans le formulaire de création client
(section Entreprise, placeholder avec format EC_XXXX_XXXXX)
- Si rempli : utilise le code saisi, sinon génère automatiquement
via CustomerRepository::generateUniqueCodeComptable()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EsyMailService :
- createMailbox(email, password, quotaMb) : INSERT dans la table mailbox
de la base esymail avec hash bcrypt (BLF-CRYPT compatible Dovecot)
- mailboxExists(email) : vérifie si l'adresse existe déjà
- isAvailable() : vérifie si ESYMAIL_DATABASE_URL est configuré
- Connexion DBAL directe vers la base esymail (séparée de l'EntityManager)
ClientsController :
- Ajout paramètre EsyMailService dans create()
- Ajout méthode createMailboxIfRequested() : vérifie checkbox, valide
email/password, vérifie existence, crée la boîte avec quota choisi
- Flash success/error selon le résultat
Template admin/clients/create.html.twig :
- Section "Messagerie Esy-Mail" avec checkbox toggle
- Champs : adresse email, mot de passe (min 8 chars), quota (1/2/5/10 Go)
- Masqué par défaut, affiché au clic sur la checkbox
Configuration :
- .env : ajout ESYMAIL_DATABASE_URL (vide par défaut)
- .env.local : connexion vers database:5432/esymail
Tests mis à jour avec EsyMailService stubé dans tous les appels create()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- src/Controller/Admin/ClientsController.php: reduce cognitive complexity by extracting private methods and adding error logging
- src/Service/MeilisearchService.php: fill empty catch blocks with error logging for better traceability
- src/Service/UserManagementService.php: use dedicated UserAlreadyExistsException instead of generic Exception
- src/Service/KeycloakAdminService.php: define and use PATH_USERS and AUTH_BEARER constants to reduce duplication
- src/Service/DocuSealService.php: reduce method return points to comply with project standards
- Created UserManagementService to centralize common user creation logic.
- Refactored ClientsController and RevendeursController to use UserManagementService.
- Reduced code duplication in StatsController and RgpdService.
- Fixed type mismatch in StatusController for ServiceMessage author.
- Improved data consistency in MailerService and EmailTrackingController for attachments.
- Added missing MessengerLogRepository.
- Fixed getFlashBag() call in KeycloakAuthenticator using FlashBagAwareSessionInterface.
- Added missing PHPDoc type specifications in WebhookDocuSealController and RgpdService.
- Removed unused MailerService injection in RgpdService.