Commit Graph

14 Commits

Author SHA1 Message Date
Serreau Jovann
9c1ea29505 feat: systeme de logs d'activite admin avec HMAC + export PDF
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>
2026-04-02 23:11:34 +02:00
Serreau Jovann
ecc9ec82b7 feat: ajout champs Stripe Connect state sur Revendeur
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>
2026-04-02 23:04:41 +02:00
Serreau Jovann
bec008bdc1 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
Serreau Jovann
32aa5b0d78 feat: page admin tarification + TarificationService + champs PriceAutomatic
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>
2026-04-02 22:42:43 +02:00
Serreau Jovann
8136475356 feat: ajout state et raisonMessage sur Devis
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>
2026-04-02 22:37:20 +02:00
Serreau Jovann
42fe3257a1 feat: ajout totalHt, totalTva, totalTtc sur Devis
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>
2026-04-02 22:36:38 +02:00
Serreau Jovann
09148b5b33 feat: ajout champs submitter + PDFs Vich sur Devis + protection uploads
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>
2026-04-02 22:33:47 +02:00
Serreau Jovann
cdd5c656a9 feat: ajout signature HMAC SHA-256 sur Devis, Advert et Facture
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>
2026-04-02 22:32:18 +02:00
Serreau Jovann
da7f46f7e9 refactor: renommer Order en Facture + meme OrderNumber partage entre Devis/Advert/Facture
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>
2026-04-02 22:28:30 +02:00
Serreau Jovann
a6e529e643 feat: ajout entity PriceAutomatic pour les tarifs automatiques
src/Entity/PriceAutomatic.php (nouveau):
- id: int auto-increment
- type: string(50), categorie du tarif (ex: esy-web, esy-mail, ndd)
- title: string(255), intitule du tarif
- description: text nullable, detail du tarif
- priceHt: decimal(10,2), prix hors taxes

src/Repository/PriceAutomaticRepository.php (nouveau)

migrations/Version20260402202703.php:
- Table price_automatic avec colonnes type, title, description, price_ht

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:27:12 +02:00
Serreau Jovann
3bda43c72f feat: ajout entities Devis, Advert et Order liees a OrderNumber
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>
2026-04-02 22:26:15 +02:00
Serreau Jovann
423ee779e0 feat: ajout entity OrderNumber pour la gestion des numeros de commande
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>
2026-04-02 22:19:54 +02:00
Serreau Jovann
686de99909 init 2026-04-01 15:42:52 +02:00
Serreau Jovann
beb12d2b75 Add webapp packages 2026-03-30 18:52:03 +02:00