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>
DomainSyncListener (postPersist, postRemove sur Domain) :
- postPersist : crée le domaine dans esymail si inexistant
- postRemove : supprime le domaine dans esymail (cascade mailboxes)
DomainEmailSyncListener (postPersist, postUpdate, postRemove sur DomainEmail) :
- postPersist : crée le domaine si besoin + crée la boîte mail dans
esymail avec mot de passe temporaire (bcrypt BLF-CRYPT)
- postUpdate : met à jour displayName, quota, isActive dans esymail
- postRemove : supprime la boîte dans esymail
Flux : Doctrine flush → Listener → EsyMailService → base esymail → Dovecot
Les listeners vérifient isAvailable() avant chaque opération (no-op si
ESYMAIL_DATABASE_URL non configuré)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entity Domain :
- customer (ManyToOne, CASCADE) : client propriétaire du domaine
- fqdn (unique) : nom de domaine complet, lowercase auto
- registrar : bureau d'enregistrement (OVH, Gandi, etc.)
- zoneCloudflare : statut zone Cloudflare (active, pending, etc.)
- zoneIdCloudflare : identifiant zone Cloudflare
- expiredAt : date d'expiration du domaine
- isGestion : domaine géré par SITECONSEIL
- isBilling : domaine facturé par SITECONSEIL
- createdAt / updatedAt : timestamps
- isExpired() : vérifie si le domaine est expiré
- isExpiringSoon(days) : vérifie si expiration dans les N jours
DomainTest (4 tests, 25 assertions) :
- testConstructor : valeurs par défaut, fqdn lowercase/trim
- testSetters : tous les setters/getters
- testIsExpired : null/passé/futur
- testIsExpiringSoon : null/15j (true pour 30j)/60j (false pour 30j)
Migration : CREATE TABLE domain avec FK customer ON DELETE CASCADE
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EsyMailService - connexion DBAL directe vers base esymail :
Gestion domaines :
- listDomains() : liste avec count mailboxes par domaine
- getDomain(name) : détails d'un domaine
- createDomain(name, maxMailboxes, defaultQuotaMb) : création
- updateDomain(name, maxMailboxes, defaultQuotaMb, isActive) : mise à jour
- deleteDomain(name) : suppression cascade (mailboxes + alias)
- domainExists(name) : vérification existence
Gestion boîtes mail :
- listMailboxes(?domain) : liste toutes ou par domaine
- getMailbox(email) : détails d'une boîte
- createMailbox(email, password, ?displayName, quotaMb) : création avec
hash bcrypt BLF-CRYPT, vérification domaine existant
- updateMailbox(email, displayName, quotaMb, isActive) : mise à jour
- changePassword(email, newPassword) : changement mot de passe
- deleteMailbox(email) : suppression
- mailboxExists(email) : vérification existence
- countMailboxes(domain) : nombre de boîtes par domaine
Gestion alias :
- listAliases(?domain) : liste tous ou par domaine
- createAlias(source, destination, domain) : création redirection
- deleteAlias(id) : suppression
Stats :
- getStats() : compteurs domains, mailboxes, aliases, active_mailboxes
Base de données esymail :
- Table domain : name unique, max_mailboxes, default_quota_mb, is_active
- Table mailbox : email unique, password bcrypt, domain FK, display_name,
quota_mb, is_active, timestamps
- Table alias : source/destination unique, domain FK, is_active
- Domaines dev : siteconseil.fr, esy-web.dev
- Compte test : test@siteconseil.fr / test1234
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le socket Unix /var/spool/postfix/private/auth ne fonctionne pas entre
containers Docker. Remplacé par un inet_listener TCP sur port 12345
pour permettre l'auth SASL depuis le container Postfix.
Suppression de la référence à l'user postfix (inexistant dans le
container Dovecot Alpine).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RELAYHOST changé de "" vers "[mailpit]:1025" : tous les emails
sortants passent par Mailpit (visible sur http://localhost:8025)
- smtp_tls_security_level=none : pas de TLS vers Mailpit en dev
- depends_on mailpit ajouté pour garantir le démarrage
En prod : RELAYHOST sera vide (envoi direct) ou vers AWS SES.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'image dovecot/dovecot:latest est minimaliste sans shell ni gestionnaire
de paquets. Remplacement par alpine:3.20 avec dovecot, dovecot-pop3d
et dovecot-pgsql installés via apk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Customer::generateCodeComptable() : préfixe changé de 'EC' vers '411'
(compte client 411 du plan comptable général)
- Format : 411_XXXX_XXXXX (ex: 411_0001_SITEC)
- Template : placeholder mis à jour
- Test : assertStringStartsWith('411_')
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>
SubdomainRedirectListener (priority 256, KernelEvents::REQUEST) :
- webmail.siteconseil.fr → /webmail (app_webmail_login)
- status.siteconseil.fr → /status (app_status)
- Ignore les sub-requests, ne redirige pas si déjà sur la bonne route
- Mapping dans constante SUBDOMAIN_ROUTES (extensible)
Tests (6 tests) :
- testWebmailRedirect : host webmail → redirect /webmail
- testStatusRedirect : host status → redirect /status
- testNoRedirectOnCrmHost : host crm → pas de redirect
- testNoRedirectWhenAlreadyOnTarget : déjà sur /webmail → pas de redirect
- testNoRedirectOnSubPath : /status/api → pas de redirect
- testSubRequestIgnored : sub-request ignorée
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TarificationService :
- Ajout NOSONAR sur DEFAULT_PRICES (données de configuration, pas de logique
dupliquée — les littéraux 500.00, 100.00, 50.00, 30.00 sont des prix distincts)
- Ajout LoggerInterface dans le constructeur
- Remplacement catch (\Throwable) vide par log warning avec message Stripe
- Conversion DEFAULT_PRICES de const vers méthode statique getDefaultPricesData()
pour permettre l'utilisation de constantes PHP dans les valeurs
Nettoyage @codeCoverageIgnore redondants :
- Suppression des PHPDoc @codeCoverageIgnore sur les repositories (déjà exclus
via phpunit.dist.xml directory et phpstan.dist.neon)
- Suppression @codeCoverageIgnore sur WebhookDocuSealController et CheckDnsCommand
(déjà exclus via phpunit.dist.xml file)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supprime les warnings SonarQube "Failed to resolve file path(s)" pour
les 15 fichiers Repository exclus de SonarQube mais encore analysés
par PHPStan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajout constante PATH_GROUPS = '/groups' (remplace 4 littéraux dupliqués)
- Fusion des 2 if imbriqués dans ensureRequiredGroups en une seule condition
avec && (in_array + createGroup)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fusion catch SignatureVerificationException et catch Throwable en un seul
catch Throwable avec instanceof pour différencier le message d'erreur.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- checkAccess() : suppression paramètre $devis (inutilisé)
- Suppression TODO et code commenté
- Remplacement early return redondant par if négatif avec logger warning
- Ajout LoggerInterface pour tracer les accès non-employé
- Suppression import App\Entity\Devis (plus utilisé)
- Tests mis à jour avec LoggerInterface dans le constructeur
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dernière validation (counter < 1) convertie en expression ternaire
pour respecter la limite de 3 returns par méthode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OrderNumberController :
- Extraction validateNumber() avec les 3 validations (format, existence, minimum)
- applyNextNumber réduit à 2 returns (erreur validation ou null succès)
sonar-project.properties :
- Ajout templates/email/** et templates/emails/** dans sonar.exclusions
(templates HTML email non analysables par SonarQube)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Templates PDF :
- _base.html.twig : blocs verify_box et hmac_section avec contenu par défaut
(QR code, verify_url, HMAC-SHA256) au lieu de blocs vides
- rgpd_access.html.twig : suppression blocs verify_box et hmac_section dupliqués
(héritent du parent)
- rgpd_deletion.html.twig : idem
- rgpd_no_data.html.twig : idem
DevisPdfControllerTest (8 tests) :
- testDevisNotFound : devis null lance NotFoundHttpException
- testUnsignedPdfNotSet : unsignedPdf null lance NotFoundHttpException
- testFileNotExists : fichier absent lance NotFoundHttpException
- testUnsignedPdfSuccess : PDF unsigned retourné en BinaryFileResponse
- testSignedPdfSuccess : PDF signed retourné
- testAuditPdfSuccess : PDF audit retourné
- testAccessAsNonEmploye : accès sans ROLE_EMPLOYE (branche checkAccess)
- testDefaultTypeNull : type inconnu lance NotFoundHttpException
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le tableau DEFAULT_PRICES contient 16 entrées de données avec la même
structure (title, description, priceHt, monthPrice, period) — c'est de
la configuration, pas du code dupliqué.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supprime les warnings SonarQube "Failed to resolve file path(s)" en
alignant les exclusions entre sonar.exclusions, phpunit.dist.xml et
phpstan.dist.neon pour les fichiers API live déjà ignorés.
- phpunit.dist.xml : ajout DnsReportController.php et CheckDnsCommand.php
dans source/exclude
- phpstan.dist.neon : ajout DnsReportController.php dans excludePaths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>