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/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/Entity/PriceAutomatic.php:
- type: ajout contrainte unique pour eviter les doublons
- monthPrice: decimal(10,2) default 0.00, prix mensuel recurrent
- period: smallint default 1, duree de la periode en mois
(1=mensuel, 3=trimestriel, 12=annuel)
- stripeId: string nullable, ID du Stripe Price pour le paiement unique
- stripeAbonnementId: string nullable, ID du Stripe Price pour l'abonnement
src/Service/TarificationService.php (nouveau):
- Constante DEFAULT_PRICES avec 16 tarifs par defaut:
esyweb_business (500€ + 100€/mois), esyweb_premium (3200€ + 100€/mois),
ecommerce_business (999€ + 150€/mois), ecommerce_premium (5110€ + 150€/mois),
esymail (50€ + 30€/mois), esymailer (50€ + 30€/mois),
esydefender_pro (50€ + 60€/mois periode 3), esymeet (50€ + 30€/mois),
esytchat (50€ + 15€/mois), esycreator (500€ + 100€/mois periode 3),
ndd_depot (20€), ndd_renouvellement (20€/an), ndd_gestion (30€/an),
ndd_reactivation (50€), formation_pack10h (500€), formation_heure (70€)
- ensureDefaultPrices(): verifie les tarifs existants, cree ceux manquants
- getAll(), getByType(), getDefaultTypes()
src/Controller/Admin/TarificationController.php (nouveau):
- Route /admin/tarification, ROLE_ROOT
- index(): appelle ensureDefaultPrices() pour creer les tarifs manquants
automatiquement a chaque visite, affiche tous les tarifs editables
- edit(): met a jour titre, description, prixHt, monthPrice, period,
stripeId, stripeAbonnementId via formulaire POST
templates/admin/tarification/index.html.twig (nouveau):
- Liste de tous les tarifs sous forme de cards glassmorphism
- Header dark avec titre, type (badge) et prix
- Formulaire d'edition inline: titre, prix unique, prix mensuel,
periode (select 1/2/3/6/12 mois), Stripe Price ID unique,
Stripe Price ID abonnement, description (textarea)
- Bouton enregistrer par tarif
templates/admin/_layout.html.twig:
- Ajout lien "Tarification" dans la sidebar Super Admin avec icone dollar
migrations/Version20260402204223.php:
- Ajout colonnes month_price, period, stripe_id, stripe_abonnement_id
sur price_automatic + index unique sur type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/Devis.php:
- Constantes STATE_CREATED, STATE_SEND, STATE_ACCEPTED, STATE_REFUSED,
STATE_CANCEL pour les 5 etats possibles du devis
- state: string(20) default 'created', cycle de vie du devis
(created → send → accepted/refused/cancel)
- raisonMessage: text nullable, motif de refus ou annulation
migrations/Version20260402203711.php:
- Ajout colonnes state VARCHAR(20) DEFAULT 'created' et
raison_message TEXT nullable sur la table devis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/Devis.php:
- totalHt: decimal(10,2) default 0.00, montant hors taxes du devis
- totalTva: decimal(10,2) default 0.00, montant de la TVA
- totalTtc: decimal(10,2) default 0.00, montant toutes taxes comprises
- Getters/setters pour les 3 champs
migrations/Version20260402203631.php:
- Ajout colonnes total_ht, total_tva, total_ttc sur la table devis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/Devis.php:
- submitterSiteconseilId (int nullable): ID du soumetteur cote SITECONSEIL
dans DocuSeal apres signature
- submitterCustomerId (int nullable): ID du soumetteur cote client
dans DocuSeal apres signature
- unsignedPdf (string nullable) + unsignedPdfFile (Vich): PDF non signe
- signedPdf (string nullable) + signedPdfFile (Vich): PDF signe
- auditPdf (string nullable) + auditPdfFile (Vich): certificat d'audit
- updatedAt (DateTimeImmutable nullable): mis a jour automatiquement
a chaque upload de fichier via les setters *File()
- Annotation #[Vich\Uploadable] sur la classe
- Les 3 champs fichier utilisent le mapping 'devis_pdf'
config/packages/vich_uploader.yaml:
- Nouveau mapping devis_pdf: stockage dans public/uploads/devis
avec SmartUniqueNamer pour eviter les collisions de noms
config/packages/security.yaml:
- Nouvelle regle access_control: /uploads/devis requiert ROLE_USER
(empeche l'acces aux PDF de devis sans etre connecte)
migrations/Version20260402203334.php:
- Ajout colonnes submitter_siteconseil_id, submitter_customer_id,
unsigned_pdf, signed_pdf, audit_pdf, updated_at sur la table devis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/Devis.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "devis|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals (timing-safe)
src/Entity/Advert.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "advert|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals
src/Entity/Facture.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "facture|numOrder|splitIndex|createdAt"
signe avec APP_SECRET (inclut splitIndex pour differencier les splits)
- verifyHmac(): verification par hash_equals
src/Service/DevisService.php, AdvertService.php, FactureService.php:
- Injection de APP_SECRET via #[Autowire('%env(APP_SECRET)%')]
- Passage du hmacSecret aux constructeurs des entites
migrations/Version20260402203207.php:
- Ajout colonne hmac VARCHAR(128) NOT NULL sur devis, advert, facture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changement de modele:
- Le meme OrderNumber est partage entre Devis, Advert et Facture
(ex: 04/2026-00001 pour les 3)
- Les relations OrderNumber passent de OneToOne a ManyToOne pour
permettre le partage du meme numero
src/Entity/Order.php supprime, remplace par:
src/Entity/Facture.php (nouveau):
- orderNumber: ManyToOne vers OrderNumber (meme numero que l'Advert parent)
- advert: ManyToOne vers Advert (nullable)
- splitIndex: smallint, suffixe pour factures multiples sur un meme advert
(0 = pas de suffixe, 1 = -1, 2 = -2, etc.)
- getInvoiceNumber(): retourne le numero complet avec suffixe si splitIndex > 0
(ex: 04/2026-00001 ou 04/2026-00001-2)
src/Entity/Devis.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- adverts: OneToMany vers Advert (inchange)
src/Entity/Advert.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- orders: renomme en factures, OneToMany vers Facture
src/Repository/OrderRepository.php supprime, remplace par:
src/Repository/FactureRepository.php (nouveau)
migrations/Version20260402202809.php:
- Suppression table `order`, creation table facture
- Modification des contraintes unique sur devis et advert
(unique index supprime car ManyToOne)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relations:
- Devis → OrderNumber (OneToOne): chaque devis a un numero unique
- Devis → Advert (OneToMany): un devis peut generer plusieurs factures pro forma
- Advert → OrderNumber (OneToOne): chaque facture pro forma a un numero unique
- Advert → Devis (ManyToOne, nullable): un advert peut exister sans devis
- Advert → Order (OneToMany): un advert peut generer plusieurs factures
- Order → OrderNumber (OneToOne): chaque facture a un numero unique
- Order → Advert (ManyToOne, nullable): une facture peut exister sans advert
Chaine: Devis → Advert → Order
Si un advert genere plusieurs factures, le champ splitIndex (smallint)
ajoute un suffixe -X au numero (ex: 04/2026-00001-1, 04/2026-00001-2).
La methode getInvoiceNumber() retourne le numero complet avec suffixe.
src/Entity/Devis.php: id, orderNumber (OneToOne), createdAt, adverts (OneToMany)
src/Entity/Advert.php: id, orderNumber (OneToOne), devis (ManyToOne nullable),
createdAt, orders (OneToMany)
src/Entity/Order.php: id, orderNumber (OneToOne), advert (ManyToOne nullable),
splitIndex (smallint default 0), createdAt, getInvoiceNumber()
Table nommee `order` (mot reserve SQL, echappee avec backticks)
src/Repository/DevisRepository.php, AdvertRepository.php, OrderRepository.php
migrations/Version20260402202554.php: tables devis, advert, `order` avec
foreign keys vers order_number et relations entre elles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/OrderNumber.php (nouveau):
- id: int auto-increment
- numOrder: string(50) unique, le numero de commande
- createdAt: DateTimeImmutable, date de creation (auto dans le constructeur)
- isUsed: bool, false par defaut, marque via markAsUsed()
src/Repository/OrderNumberRepository.php (nouveau):
- Repository Doctrine standard pour OrderNumber
migrations/Version20260402201935.php:
- Table order_number avec index unique sur num_order
- created_at TIMESTAMP, is_used BOOLEAN
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>