Aller au contenu principal

Proposal v2 + Add-ons par SessionType (Plan + État d'implémentation)

Résumé

Cette fonctionnalité introduit un flux de proposal public par token dédié (/proposal/:token) permettant :

  • de proposer des add-ons issus de user_rates
  • filtrés par compatibilité user_rate_session_types / sessions.user_session_type_id
  • sélectionnables par le client avec preview pricing
  • appliqués au devis (quote_items) sous forme de lignes ADDON traçables
  • validés ensuite via acceptation de la proposal (et du devis)

État actuel (février 2026)

✅ Implémenté (Phase v3: builder dédié + email système + UX publique premium)

  • Builder Proposal dédié (/quotes/:quoteId/proposal)
    • stepper back-office (Story & Cover / Add-ons / Public Preview / Share & Send)
    • slug éditable (avec suggestion + unicité backend)
    • cover upload en dropzone (preview + suppression overlay) avec compression image côté client
    • mode read-only complet si devis ACCEPTED
    • bouton retour vers le devis
  • Email Proposal système
    • template système new-proposal (FR/EN)
    • endpoints preview-email / send-email dédiés proposal
    • variables {proposal_link} / {proposalLink} (+ alias FR)
    • envoi Proposal marque le devis SENT
    • migration 0188_add_new_proposal_default_email_template
  • Proposal public premium /proposal/:token|:slug
    • expérience immersive full-screen en étapes
    • navigation clavier ← / → + stepper bas de page
    • slug public (lecture sans login) + fallback token sécurisé
    • auto-update sélection + auto-apply au devis (moins de clics client)
    • verrouillage lecture seule si devis/proposal déjà confirmé
    • étape Confirm masquée en navigation quand proposal déjà acceptée
    • invitation compte client en fin de flow (auto + resend manuel)
  • Custom proposal items (hors catalogue tarifs)
    • ajoutables/supprimables/réordonnables dans le builder add-ons
    • support complet public selection/apply/quote lines
    • fix backend JSONB + IDs custom-* + pricing TTC-only
    • option allowQty (quantité éditable ou forcée à 1)
  • Proposal panel sur fiche devis
    • colonne droite sticky
    • statut Proposal dans le bloc (Draft, Shared, Read-only)
    • CTA simplifié (Open proposal builder, lien public slug-first)
    • blocage si devis non prêt (session type + au moins un item réel requis)
  • Proposal defaults (user settings)
    • defaults globaux (cover, intro label, templates titre/sous-titre, flow flags)
    • préremplissage builder via variables ({{contactNames}}, {{sessionDate}}, etc.)
    • migration 0189_add_proposal_defaults_to_user_settings
    • defaults de Proposal story section (titre + texte) en settings
    • migration 0190_add_proposal_story_defaults_to_user_settings

✅ Implémenté (socle backend + route publique MVP)

  • Migration DB 0187 :
    • extensions user_rates (flags add-on)
    • extension quote_items (ADDON, user_rate_id, line_origin, line_metadata)
    • nouvelles tables proposal_*
    • quote_revisions
  • Changelog Liquibase mis à jour (infra/liquibase/changelog-master.yaml)
  • Backend user-rates :
    • nouveaux champs add-on supportés dans CRUD
    • endpoint GET /api/user-rates/addons-catalog
  • Backend proposals (module Nest) :
    • GET/PUT /api/proposals/:quoteId/upsells
    • POST /api/proposals/:quoteId/link
    • GET /api/public/proposals/:token
    • PUT /api/public/proposals/:token/selection
    • POST /api/public/proposals/:token/apply-selection
    • POST /api/public/proposals/:token/accept
  • Quote item patch/recalc helpers :
    • replaceUpsellAddonItems(...)
    • recalculateQuoteAmountFromDbItems(...)
  • OpenAPI :
    • QuoteItemType inclut ADDON
    • schémas quote_items / user_rates enrichis
    • schémas/paths proposals + user-rates/addons-catalog
    • types backend/frontend régénérés (openapi:generate)
  • Frontend public MVP :
    • route /proposal/$token
    • hooks public-proposals
    • page MVP (sélection / apply / accept)
  • Builder back-office (MVP+) :
    • panel Proposal/Add-ons dans QuoteViewPage
    • configuration add-ons (offer/recommended/defaultQty/badge)
    • ordre persistant des add-ons (drag/drop + move up/down fallback, sauvegardé via ordre JSON des offers)
    • génération / copie du lien public proposal

✅ Correctifs importants livrés (post-implémentation)

  • Résolution publique Proposal accessible sans login (/proposal/* déclaré comme route publique frontend)
  • Résolution publique par slug ou token côté backend
  • GET /api/public/proposals/:token|slug reste accessible en lecture même si devis accepté (mutations bloquées seulement)
  • Validation publique selection compatible IDs custom (custom-*)
  • updatePublicSelection / applySelection filtrent les IDs non-UUID avant requêtes SQL user_rates
  • proposal_upsell_offers / proposal_selections / presentations sérialisés explicitement en jsonb (fix invalid input syntax for type json)
  • EmailTemplatesService fix récursion sur auto-création template système absent (email_new-proposal)
  • Emails Proposal (preview/send + confirmation après acceptation) utilisent l’URL slug en priorité si configurée
  • Expiration des liens Proposal alignée sur quote.valid_until (fin de journée pour les dates sans heure)
  • Share & Send : état Link active/expired, régénération lien, édition date de validité du devis (+14/+30 jours)
  • Bloc URL du builder basé sur le slug sauvegardé (plus de confusion avec le champ local non sauvegardé)
  • Total footer public proposal aligné avec le total courant configuré (et non ancien devis)
  • Auto-refetch supprimé sur update/apply selection pour éviter flicker/reload du background cover
  • Auto-update + auto-apply publics (add-ons) avec blocage temporaire de la navigation et des champs pendant recalcul
  • Step Review : correction des montants (Current total base + Optional additions total après apply)
  • Step FINAL protégé (non affiché tant que la quote est seulement SENT)
  • Step Confirm enrichi + wording simplifié ; Overview allégée (blocs dashboard retirés)
  • Guard builder Proposal: devis non prêt => blocage frontend + backend (session type + items requis)
  • Fallback sessionTypeId depuis ligne quote_items.item_type = SESSION_TYPE si session liée incomplète
  • Fallback contract_template_id en édition de devis depuis default_contract_template_id du SessionType (session liée ou ligne SESSION_TYPE)
  • Upload cover Proposal : normalisation multipart booléens (usePublicUrl) + couverture stable via coverImageKey
  • Emails : footer Aaperture cliquable (wrappers user + système) + signatures texte brut formatées en HTML (retours ligne + liens)

✅ Vérifications effectuées

  • npm run openapi:generate exécuté après ajout des schemas/paths Proposal
  • npm run type-check --prefix backend (OK)
  • lint backend ciblé sur src/proposals/* et DTOs (OK)
  • lint frontend ciblé Proposal / Quote panel / Builder / public proposal (OK)
  • OpenAPI : le paramètre de path {token} des endpoints /public/proposals/{token} est documenté comme « token ou slug » (résolution côté API).
  • Tests backend ciblés Proposal (OK):
    • backend/src/__tests__/proposals/proposals-pricing.service.spec.ts
    • backend/src/__tests__/proposals/proposals-apply.service.spec.ts
    • backend/src/__tests__/proposals/proposals-email.service.spec.ts (preview + send proposal email, planification, contact sans email, quote not found)
    • couvertures actuelles:
      • accept refuse DIRTY avant transaction
      • accept idempotent quand proposal déjà acceptée / quote déjà ACCEPTED
      • accept envoie l’invitation compte client quand autoInviteAfterAccept activé ; skippe quand désactivé
      • applySelection orchestre patch add-ons, recalcul, quote_revisions, et passe la sélection en APPLIED
      • applySelection stable sur double exécution avec la même sélection (idempotence pratique)
      • sendClientPortalInvitation : refus si proposal non acceptée ; resend si invitation déjà en attente ; SKIPPED si contact sans email ; ALREADY_HAS_ACCOUNT si compte existant
  • Scénario E2E Playwright ajouté (exécution conditionnelle via E2E_PROPOSAL_TOKEN):
    • frontend/e2e/public-proposal.smoke.spec.ts
  • Scénario E2E Playwright full-flow ajouté (seed via APIs auth, nécessite E2E_EMAIL / E2E_PASSWORD):
    • frontend/e2e/public-proposal.full-flow.spec.ts
  • Scripts npm dédiés Proposal E2E:
    • npm run test:e2e:proposal
    • npm run test:e2e:proposal:smoke
    • npm run test:e2e:proposal:full-flow
  • GitLab CI:
    • job dédié test:e2e:frontend:proposal ajouté (full-flow Proposal)
    • auto si E2E_BASE_URL + E2E_EMAIL + E2E_PASSWORD, manuel sinon en MR
  • Factorisation accept quote partagée livrée:
    • backend/src/quotes-invoices/quote-client-actions.service.ts
    • utilisée par client-access et proposals

QA UX Proposal (checklist)

  • Design tokens : écran « builder unavailable » utilise min-h-[var(--size-empty-state-min-h)] (token --size-empty-state-min-h: 40dvh dans design-tokens.css).
  • États read-only / confirmé : page publique proposal en mode confirmé a aria-readonly="true" et aria-label (clé proposals.public.aria.confirmedReadOnly) sur la section principale pour les lecteurs d’écran.
  • Copy : statuts Proposal dans le panel devis (Draft / Shared / Read-only) et messages publics (Overview, Options, Review, Confirm, Portal) en EN/FR via i18n.
  • Copie du lien : builder et panel Proposal (fiche devis) utilisent un fallback document.execCommand("copy") lorsque navigator.clipboard.writeText échoue (contexte non sécurisé, permission), pour éviter « Unable to copy link » en HTTP ou environnements restreints.
  • Mobile : à valider manuellement (touch targets, stepper, boutons Apply/Accept, step FINAL).

⚠️ Hors scope (ou restant partiel)

  • UI premium/luxe finale polish (copy/visual QA fine, animations avancées)

🚧 Restant (phase suivante)

  • Tests backend invitation compte client (auto/retry) en fin de flow proposalcomplété : proposals-apply.service.spec.ts couvre auto-invite à l’accept (activé/désactivé), retry (resend when pending), refus avant accept, contact sans email (SKIPPED), contact déjà compte (ALREADY_HAS_ACCOUNT).
  • E2E builder dédié (/quotes/:quoteId/proposal) + envoi email Proposal depuis le builder — ajouté : scénario dans public-proposal.full-flow.spec.ts (step Share & Send + Send Proposal + vérif statut SENT).
  • QA UX premium (copy, alignment, states confirmed/read-only, mobile) — démarrage : token --size-empty-state-min-h pour écran builder indisponible, aria aria-readonly + aria-label sur la page publique en mode confirmé (lecture seule), clés i18n proposals.public.aria.confirmedReadOnly (EN/FR).
  • Suivi post-cut du legacy /client (monitoring, comms, nettoyage résiduel)

Schéma métier (source de vérité)

Tarifs / Add-ons

  • Tarifs = user_rates
  • Compatibilité SessionType = user_rate_session_types
  • Champs add-on:
    • is_addon
    • is_active
    • addon_display_order
    • addon_cover_image_url (optionnel)
    • addon_badge (optionnel)

Proposal

  • proposal_presentations
  • proposal_links (token dédié, chiffré)
  • proposal_upsell_offers
  • proposal_selections
  • quote_revisions

Quote lines add-ons

  • quote_items.item_type = 'ADDON'
  • quote_items.line_origin = 'UPSELL'
  • quote_items.user_rate_id = <user_rates.id>
  • quote_items.line_metadata pour provenance (proposalLinkId, tariffId, etc.)

Endpoints (implémentés)

Auth user

  • GET /api/user-rates/addons-catalog?quoteId=...|sessionTypeId=...
  • GET /api/proposals/:quoteId/upsells
  • PUT /api/proposals/:quoteId/upsells
  • POST /api/proposals/:quoteId/link
  • GET /api/proposals/:quoteId/builder
  • PUT /api/proposals/:quoteId/presentation
  • POST /api/proposals/:quoteId/preview-email
  • POST /api/proposals/:quoteId/send-email

Public token

  • GET /api/public/proposals/:token
  • PUT /api/public/proposals/:token/selection
  • POST /api/public/proposals/:token/apply-selection
  • POST /api/public/proposals/:token/accept
  • POST /api/public/proposals/:token/send-client-portal-invitation

OpenAPI (source de vérité)

Components

  • openapi/components/proposals.yaml
  • openapi/components/quotes-invoices.yaml
  • openapi/components/user-rates.yaml
  • openapi/components/schemas.yaml

Paths

  • openapi/paths/proposals.quoteId.upsells.yaml
  • openapi/paths/proposals.quoteId.link.yaml
  • openapi/paths/public.proposals.token.yaml
  • openapi/paths/public.proposals.token.selection.yaml
  • openapi/paths/public.proposals.token.apply-selection.yaml
  • openapi/paths/public.proposals.token.accept.yaml
  • openapi/paths/public.proposals.token.send-client-portal-invitation.yaml
  • openapi/paths/proposals.quoteId.builder.yaml
  • openapi/paths/proposals.quoteId.presentation.yaml
  • openapi/paths/proposals.quoteId.preview-email.yaml
  • openapi/paths/proposals.quoteId.send-email.yaml
  • openapi/paths/user-rates.addons-catalog.yaml

Règle de maintenance

  • Modifier la spec OpenAPI avant les types générés, puis exécuter npm run openapi:generate
  • Ne pas patcher backend/src/_generated/types.gen.ts / frontend/src/_generated/types.gen.ts à la main

Règles importantes (actuelles)

  • Validation server-side des tariffId contre les offres configurées
  • Preview pricing calculé côté serveur
  • accept refuse l’état DIRTY (sélection non appliquée)
  • accept marque aussi le devis quotes.status = ACCEPTED
  • apply-selection remplace uniquement les lignes add-on upsell (ADDON + UPSELL)
  • selection / apply-selection publics refusés si devis déjà accepté (proposal en lecture seule / read-only)
  • GET public proposal reste accessible en lecture même après acceptation
  • Builder Proposal interdit si devis non prêt (session type + item réel requis)

Fichiers clés (implémentation)

Backend

  • backend/src/proposals/proposals.module.ts
  • backend/src/proposals/proposals.service.ts
  • backend/src/proposals/proposals-apply.service.ts
  • backend/src/proposals/proposals-token.service.ts
  • backend/src/proposals/proposals-pricing.service.ts
  • backend/src/proposals/proposals-repository.ts
  • backend/src/proposals/proposals-email.service.ts
  • backend/src/user-rates/user-rates.controller.ts
  • backend/src/user-rates/user-rates-crud.service.ts
  • backend/src/quotes-invoices/quote-client-actions.service.ts
  • backend/src/quotes-invoices/quotes-invoices-items.service.ts
  • infra/liquibase/changes/0187_create_proposals_v2_and_quote_addons/up.sql
  • infra/liquibase/changes/0188_add_new_proposal_default_email_template/up.sql
  • infra/liquibase/changes/0189_add_proposal_defaults_to_user_settings/up.sql
  • infra/liquibase/changes/0190_add_proposal_story_defaults_to_user_settings/up.sql

OpenAPI

  • openapi/components/proposals.yaml
  • openapi/components/quotes-invoices.yaml
  • openapi/components/user-rates.yaml
  • openapi/paths/proposals.quoteId.upsells.yaml
  • openapi/paths/public.proposals.token.yaml
  • openapi/index.yaml

Frontend (MVP → v3)

  • frontend/src/client/public-proposals/usePublicProposal.ts
  • frontend/src/client/proposals/useProposals.ts
  • frontend/src/pages/public-proposal/PublicProposalPage.tsx
  • frontend/src/pages/proposals/ProposalBuilderPage/ProposalBuilderPage.tsx
  • frontend/src/pages/proposals/ProposalBuilderPage/components/SendProposalEmailModal.tsx
  • frontend/src/routes/public.routes.tsx
  • frontend/src/router.tsx

Plan de rollout (mis à jour)

  1. ✅ Schéma DB + types + OpenAPI source
  2. ✅ Backend user-rates add-ons catalog + flags
  3. ✅ Backend proposals (auth + public token flow)
  4. ✅ Route publique frontend /proposal/:token (MVP)
  5. ✅ Builder back-office dédié Proposal (v3)
  6. 🚧 Tests backend + E2E (v3 email/invitation à compléter)
  7. 🚧 UI premium/luxe (polish final)
  8. ✅ Migration/suppression legacy /client (hard cut frontend)
  9. ✅ Suppression exposition /api/client-access/* (controller legacy retiré)

Backlog immédiat (prochain PR recommandé)

  1. Tests backend Proposal v3 (preview-email, send-email, send-client-portal-invitation, paths confirmés/locked)
  2. E2E builder Proposal dédié + envoi email (template new-proposal)
  3. QA UX premium finale (mobile/desktop, states locked/accepted, alignements/actions)
  4. Stabiliser l’environnement CI E2E (données/stack) pour réduire les exécutions allow_failure

Commandes utiles

  • Générer types depuis OpenAPI :
    • npm run openapi:generate
  • Type-check backend :
    • npm run type-check --prefix backend
  • Lint ciblé proposal backend :
    • cd backend && npx eslint src/proposals/*.ts src/proposals/dto/*.ts --max-warnings=0

Notes de maintenance

  • Ne pas modifier backend/src/_generated/types.gen.ts ou frontend/src/_generated/types.gen.ts à la main sans patcher la source OpenAPI correspondante.
  • Toute évolution des endpoints proposals doit être répercutée dans :
    • openapi/components/proposals.yaml
    • openapi/paths/public.proposals.*.yaml
    • openapi/paths/proposals.*.yaml
  • Le cut legacy /client / /api/client-access/* est désormais non rétrocompatible; les liens publics historiques peuvent échouer.