From 1c4cafaf69eae40855398f8d677367c4ee6a3405 Mon Sep 17 00:00:00 2001 From: Allison Date: Tue, 28 Apr 2026 21:06:12 -0400 Subject: [PATCH] =?UTF-8?q?refactor(pricing):=20refonte=20v7.0=20=E2=80=94?= =?UTF-8?q?=203=20Cloud=20(Basic=20189$/Essentiel=20349$/Pro=20549$)=20+?= =?UTF-8?q?=20DictIA=20Local=20(5998$=20An1)=20+=20Pro+=20soumission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace l'ancien pricing (DictIA 8 / 16 / Cloud) par la nouvelle structure canonique v7.0 : 4 forfaits + 1 sentinel quote-only. Changements clés : - pricing_card.html : signature étendue (badge, recommended, capacity_audio, capacity_storage, gpu, yearly_renewal, cta_label) + format prix server-side avec NBSP OQLF (5998 -> 5 998 $) - _pricing_tiers.html : 4 cards (Cloud Basic 189$, Cloud Essentiel 349$, Cloud Pro 549$+485$ RECOMMANDÉ, DictIA Local 5998$ An1) + chip Pro+ soumission -> /contact?pro-plus=1 - plans.py : refonte complète avec yearly_renewal_env (DictIA Local An 2+ = 500$/an) + is_quote_only sentinel (Pro+ -> redirect /contact, jamais Stripe) - routes.py : Pro+ intercepté avant le flow Stripe Checkout - env.stripe.example : nouveau naming STRIPE_CLOUD_BASIC|ESSENTIEL|PRO_* + STRIPE_DICTIA_LOCAL_SETUP/RENEWAL_YEARLY - tarifs.html : header "Quatre forfaits", matrice comparative 4 colonnes, FAQ enrichie (7 questions incluant DictIA Local + onboarding Pro + Pro+) - fonctionnalites.html : section Architecture refondue (4 cards v7.0) - landing.html : ROI footnote + cycle "189$" + wave "189$/mois" actualisés - roi_calculator.js : recalibrage sur Cloud ESSENTIEL 349$ × 12 = 4188$/an - routes.py marketing : FAQ "DictIA 8 et 16" -> "DictIA LOCAL" - contact.html : "déploiements DictIA 16" -> "Cloud PRO" + "DictIA LOCAL" Tests : - test_marketing_landing_template.py : assertions prix v7.0 (189/349/549/5998), 4 slugs (cloud-basic, cloud-essentiel, cloud-pro, dictia-local), Pro+ chip, capacity chips, RECOMMANDÉ sur Cloud PRO - test_marketing_secondary_pages.py : 4 cards + Pro+ chip + matrice 4 col + FAQ 7 questions - test_stripe_checkout.py : env vars v7.0, slugs cloud-basic/cloud-pro/ dictia-local + nouveau test pro-plus -> /contact + tests setup pour Cloud PRO et DictIA Local - test_stripe_webhook.py : plan_slug metadata cloud-basic Status : 28/28 Stripe checkout + 17/17 webhook + 93/98 marketing pass (les 5 marketing failures sont pré-existantes, non liées au pricing : test_landing_has_main_nav et test_footer_links_complete = /blog manquant ; test_trust_bar_has_eyebrow_factual_phrasing + 2 tests conformite = casing eyebrow + entité é — vérifié par git stash baseline). Co-Authored-By: Claude Opus 4.7 (1M context) --- config/env.stripe.example | 43 +++-- src/billing/plans.py | 138 +++++++++++---- src/billing/routes.py | 4 + src/marketing/routes.py | 4 +- static/css/marketing.css | 23 ++- static/js/roi_calculator.js | 7 +- templates/macros/pricing_card.html | 123 +++++++++++--- .../marketing/_partials/_pricing_tiers.html | 127 +++++++++++--- templates/marketing/contact.html | 2 +- templates/marketing/fonctionnalites.html | 108 ++++++------ templates/marketing/landing.html | 10 +- templates/marketing/tarifs.html | 60 ++++--- tests/test_marketing_landing_template.py | 82 +++++---- tests/test_marketing_secondary_pages.py | 44 +++-- tests/test_stripe_checkout.py | 160 ++++++++++++------ tests/test_stripe_webhook.py | 14 +- 16 files changed, 648 insertions(+), 301 deletions(-) diff --git a/config/env.stripe.example b/config/env.stripe.example index 23e8ddf..000b2d7 100644 --- a/config/env.stripe.example +++ b/config/env.stripe.example @@ -1,5 +1,5 @@ ############################################################################### -# Stripe — Checkout + Subscriptions (B-2.7 / B-2.8) +# Stripe — Checkout + Subscriptions (B-2.7 / B-2.8) — v7.0 pricing ############################################################################### # # Required for the /checkout/ flow and the /webhooks/stripe receiver. @@ -15,30 +15,34 @@ # STRIPE_WEBHOOK_SECRET=whsec_... # for B-2.8 webhook signature verification ############################################################################### -# Price IDs — one per plan, period, and (for hardware plans) setup fee. +# Price IDs — v7.0 (Cloud Basic / Essentiel / Pro + DictIA Local) ############################################################################### # # Format: price_xxxxxxxxxxxxxxxxxxxxxxxxxx # Naming convention in this codebase: STRIPE__ -# PLAN = DICTIA_8 | DICTIA_16 | DICTIA_CLOUD -# TYPE = SETUP (one-time, hardware only) | MONTHLY | YEARLY +# PLAN = CLOUD_BASIC | CLOUD_ESSENTIEL | CLOUD_PRO | DICTIA_LOCAL +# TYPE = SETUP (one-time) | MONTHLY | YEARLY | RENEWAL_YEARLY (DictIA Local An 2+) # # Yearly Price = Monthly Price × 12 × 0.85 (15 % discount). Configure both -# Prices in the Stripe Dashboard for each plan. +# Prices in the Stripe Dashboard for each Cloud plan. +# Pro+ is quote-only — NO Stripe Price IDs (the route redirects to /contact). -# DictIA 8 (8-channel hardware bundle): 3 450 $ setup + 173 $/mo -# STRIPE_DICTIA_8_SETUP=price_xxx -# STRIPE_DICTIA_8_MONTHLY=price_xxx -# STRIPE_DICTIA_8_YEARLY=price_xxx +# Cloud BASIC : 189 $/mo (no setup) — solopreneur, petite équipe, ~165 h audio/mo +# STRIPE_CLOUD_BASIC_MONTHLY=price_xxx +# STRIPE_CLOUD_BASIC_YEARLY=price_xxx -# DictIA 16 (16-channel hardware bundle): 5 750 $ setup + 201 $/mo -# STRIPE_DICTIA_16_SETUP=price_xxx -# STRIPE_DICTIA_16_MONTHLY=price_xxx -# STRIPE_DICTIA_16_YEARLY=price_xxx +# Cloud ESSENTIEL : 349 $/mo (no setup) — cabinet en croissance, ~330 h audio/mo +# STRIPE_CLOUD_ESSENTIEL_MONTHLY=price_xxx +# STRIPE_CLOUD_ESSENTIEL_YEARLY=price_xxx -# DictIA Cloud (SaaS-only, no hardware): 369 $/mo -# STRIPE_DICTIA_CLOUD_MONTHLY=price_xxx -# STRIPE_DICTIA_CLOUD_YEARLY=price_xxx +# Cloud PRO : 549 $/mo + 485 $ onboarding (one-time) — usage intensif, ~660 h audio/mo +# STRIPE_CLOUD_PRO_SETUP=price_xxx +# STRIPE_CLOUD_PRO_MONTHLY=price_xxx +# STRIPE_CLOUD_PRO_YEARLY=price_xxx + +# DictIA LOCAL : 5 998 $ An 1 (one-time matériel + 1ère année logiciel) puis 500 $/an dès An 2 +# STRIPE_DICTIA_LOCAL_SETUP=price_xxx +# STRIPE_DICTIA_LOCAL_RENEWAL_YEARLY=price_xxx ############################################################################### # Required Stripe Dashboard configuration @@ -56,10 +60,13 @@ # Apple Pay requires verifying the dictia.ca domain via the Stripe-hosted # `.well-known/apple-developer-merchantid-domain-association` file. # -# 4. For each plan, create: +# 4. For each Cloud plan, create: # - One recurring monthly Price (CAD, billing_scheme=per_unit) # - One recurring yearly Price (CAD, = monthly × 12 × 0.85) -# For DictIA 8 and DictIA 16, also create a one-time Price for the setup fee. +# For Cloud PRO, also create a one-time Price for the 485 $ setup fee. +# For DictIA LOCAL, create: +# - One one-time Price for 5 998 $ (An 1 — matériel + logiciel) +# - One recurring yearly Price for 500 $ (renewal — MAJ + support dès An 2) # # 5. Create a webhook endpoint (B-2.8) pointing at # https://your-domain.example/checkout/webhooks/stripe diff --git a/src/billing/plans.py b/src/billing/plans.py index 8b8f683..a1997b9 100644 --- a/src/billing/plans.py +++ b/src/billing/plans.py @@ -1,14 +1,25 @@ -"""DictIA pricing plans (B-2.7). +"""DictIA pricing plans — v7.0 (B-2.7 refonte 2026-04-27). Centralized plan registry. Stripe Price IDs are resolved from environment variables — set STRIPE__ env vars in production. The slug -(`dictia-8`, `dictia-16`, `dictia-cloud`) is the canonical identifier -used throughout the codebase (URL params, webhook metadata, audit logs). +(`cloud-basic`, `cloud-essentiel`, `cloud-pro`, `dictia-local`) is the +canonical identifier used throughout the codebase (URL params, webhook +metadata, audit logs). -Pricing reference (CAD, pre-tax — TPS/TVQ added by Stripe automatic_tax): -- DictIA 8: 3 450$ setup (one-time) + 173$/mo recurring (or yearly = 173 × 12 × 0.85) -- DictIA 16: 5 750$ setup (one-time) + 201$/mo recurring (or yearly = 201 × 12 × 0.85) -- DictIA Cloud: 369$/mo recurring (or yearly = 369 × 12 × 0.85) +v7.0 pricing reference (CAD, pre-tax — TPS/TVQ added by Stripe automatic_tax): +- Cloud BASIC : 189 $/mo recurring (no setup) + yearly = 189 × 12 × 0.85 ≈ 1 928 $/an +- Cloud ESSENTIEL : 349 $/mo recurring (no setup) + yearly = 349 × 12 × 0.85 ≈ 3 559 $/an +- Cloud PRO : 549 $/mo recurring + 485 $ one-time onboarding setup + yearly = 549 × 12 × 0.85 ≈ 5 600 $/an (+ 485 $ setup) +- DictIA LOCAL : 5 998 $ one-time (An 1 = matériel + 1ʳᵉ année logiciel) + puis 500 $/an dès An 2 (renewal yearly only — no monthly) + +Pro+ is a sentinel plan — no Stripe Price IDs, the route redirects to +/contact?pro-plus=1 instead of opening Stripe Checkout. It exists in PLANS +so other code (URL routing, navigation) can identify it; `is_configured()` +always returns False so the route falls through to the contact redirect. """ import os from dataclasses import dataclass @@ -17,20 +28,31 @@ from typing import Dict, List, Optional @dataclass(frozen=True) class Plan: - """A DictIA subscription plan. + """A DictIA subscription plan (v7.0). Stripe Price IDs are resolved lazily from environment variables — the Plan instance itself only stores the variable names. This lets the application boot without Stripe credentials (CI, dev branches) and keeps secrets out of source control. + + Three pricing shapes are supported: + - Cloud Basic / Essentiel : monthly + yearly (no setup, no renewal) + - Cloud Pro : monthly + yearly + setup (one-time onboarding) + - DictIA Local : setup (An 1) + yearly_renewal (dès An 2) + — no monthly Price ID + + The Pro+ plan has all *_env fields set to None — the route checks + `is_quote_only` and redirects to /contact instead of opening Checkout. """ slug: str name: str description_fr: str has_setup_fee: bool - monthly_env: str - yearly_env: str - setup_env: Optional[str] = None # only set for plans with a setup fee + monthly_env: Optional[str] = None + yearly_env: Optional[str] = None + setup_env: Optional[str] = None # Cloud Pro setup OR DictIA Local An 1 + yearly_renewal_env: Optional[str] = None # DictIA Local An 2+ renewal + is_quote_only: bool = False # True for Pro+ (no Stripe — redirect to contact) def setup_price_id(self) -> Optional[str]: if not self.has_setup_fee or not self.setup_env: @@ -38,47 +60,91 @@ class Plan: return os.environ.get(self.setup_env) def monthly_price_id(self) -> Optional[str]: + if not self.monthly_env: + return None return os.environ.get(self.monthly_env) def yearly_price_id(self) -> Optional[str]: + if not self.yearly_env: + return None return os.environ.get(self.yearly_env) + def yearly_renewal_price_id(self) -> Optional[str]: + if not self.yearly_renewal_env: + return None + return os.environ.get(self.yearly_renewal_env) + def is_configured(self) -> bool: - """True when all required Stripe Price IDs are set in the environment.""" + """True when all required Stripe Price IDs are set in the environment. + + - Quote-only plans (Pro+) are never configured (always redirect to /contact). + - Cloud plans require monthly + yearly Price IDs. + - Cloud Pro additionally requires the one-time setup Price ID. + - DictIA Local requires setup (An 1) + yearly_renewal (dès An 2). + """ + if self.is_quote_only: + return False + # DictIA Local (one-shot + yearly renewal — no monthly) + if self.setup_env and self.yearly_renewal_env and not self.monthly_env: + return bool(self.setup_price_id() and self.yearly_renewal_price_id()) + # Cloud plans (monthly + yearly required, + setup if Pro) if self.has_setup_fee and not self.setup_price_id(): return False return bool(self.monthly_price_id() and self.yearly_price_id()) def price_id_for_period(self, period: str) -> Optional[str]: + """Resolve the Price ID for the given billing period. + + For DictIA Local (no monthly), 'monthly' falls back to the yearly_renewal + Price ID — Stripe Checkout will display the recurrence in the session UI. + """ + if not self.monthly_env and self.yearly_renewal_env: + # DictIA Local — only a yearly renewal exists + return self.yearly_renewal_price_id() return self.yearly_price_id() if period == 'yearly' else self.monthly_price_id() PLANS: Dict[str, Plan] = { - 'dictia-8': Plan( - slug='dictia-8', - name='DictIA 8', - description_fr='Boîtier 8 canaux + transcription IA locale (poste de travail).', - has_setup_fee=True, - setup_env='STRIPE_DICTIA_8_SETUP', - monthly_env='STRIPE_DICTIA_8_MONTHLY', - yearly_env='STRIPE_DICTIA_8_YEARLY', - ), - 'dictia-16': Plan( - slug='dictia-16', - name='DictIA 16', - description_fr='Boîtier 16 canaux + transcription IA locale (salle de réunion).', - has_setup_fee=True, - setup_env='STRIPE_DICTIA_16_SETUP', - monthly_env='STRIPE_DICTIA_16_MONTHLY', - yearly_env='STRIPE_DICTIA_16_YEARLY', - ), - 'dictia-cloud': Plan( - slug='dictia-cloud', - name='DictIA Cloud', - description_fr='Transcription IA hébergée au Québec, 100% conforme Loi 25.', + 'cloud-basic': Plan( + slug='cloud-basic', + name='Cloud BASIC', + description_fr='Cloud souverain QC — 189 $/mo · solopreneur, petite équipe.', has_setup_fee=False, - monthly_env='STRIPE_DICTIA_CLOUD_MONTHLY', - yearly_env='STRIPE_DICTIA_CLOUD_YEARLY', + monthly_env='STRIPE_CLOUD_BASIC_MONTHLY', + yearly_env='STRIPE_CLOUD_BASIC_YEARLY', + ), + 'cloud-essentiel': Plan( + slug='cloud-essentiel', + name='Cloud ESSENTIEL', + description_fr='Cloud souverain QC — 349 $/mo · cabinet en croissance.', + has_setup_fee=False, + monthly_env='STRIPE_CLOUD_ESSENTIEL_MONTHLY', + yearly_env='STRIPE_CLOUD_ESSENTIEL_YEARLY', + ), + 'cloud-pro': Plan( + slug='cloud-pro', + name='Cloud PRO', + description_fr='Cloud souverain QC — 549 $/mo + 485 $ onboarding · usage intensif multi-postes.', + has_setup_fee=True, + setup_env='STRIPE_CLOUD_PRO_SETUP', + monthly_env='STRIPE_CLOUD_PRO_MONTHLY', + yearly_env='STRIPE_CLOUD_PRO_YEARLY', + ), + 'dictia-local': Plan( + slug='dictia-local', + name='DictIA LOCAL', + description_fr='100 % hors-ligne — 5 998 $ An 1 (matériel + logiciel) puis 500 $/an dès An 2.', + has_setup_fee=True, + setup_env='STRIPE_DICTIA_LOCAL_SETUP', + yearly_renewal_env='STRIPE_DICTIA_LOCAL_RENEWAL_YEARLY', + # No monthly_env / yearly_env — local plan is one-shot + yearly renewal + ), + 'pro-plus': Plan( + slug='pro-plus', + name='Pro+', + description_fr='Soumission personnalisée — > 660 h audio/mois, multi-sites, SLA 99,9 %, SOC 2.', + has_setup_fee=False, + is_quote_only=True, ), } diff --git a/src/billing/routes.py b/src/billing/routes.py index 88c9009..cfc10c5 100644 --- a/src/billing/routes.py +++ b/src/billing/routes.py @@ -42,6 +42,10 @@ def checkout(plan): flash('Forfait inconnu.', 'danger') return redirect(url_for('marketing.tarifs')) + # Pro+ — soumission personnalisée (no Stripe Checkout, redirect to /contact) + if plan_obj.is_quote_only: + return redirect(url_for('marketing.contact') + '?pro-plus=1') + period = request.args.get('period', 'monthly') if period not in VALID_PERIODS: period = 'monthly' diff --git a/src/marketing/routes.py b/src/marketing/routes.py index ed57499..805942e 100644 --- a/src/marketing/routes.py +++ b/src/marketing/routes.py @@ -37,7 +37,7 @@ TESTIMONIALS = [ FAQ = [ { 'q': 'Comment fonctionne la transcription?', - 'a': 'DictIA utilise WhisperX Large-v3, le moteur de transcription de pointe d\'OpenAI, exécuté directement sur le GPU local (DictIA 8 et 16) ou sur un GPU cloud dédié au Québec (DictIA Cloud). Vous téléversez un fichier audio ou vidéo, et la transcription est générée automatiquement avec identification des locuteurs. Pour la conformité Loi 25, l\'audit trail (art. 3.5 LPRPSP), le registre des consentements (art. 14) et l\'EFVP (art. 3.3) sont fournis par défaut.', + 'a': 'DictIA utilise WhisperX Large-v3, le moteur de transcription de pointe d\'OpenAI, exécuté soit sur un GPU dédié au Québec (forfaits Cloud BASIC, ESSENTIEL, PRO — OVH Beauharnois) soit directement sur votre GPU local (DictIA LOCAL — RTX 5070 Ti chez vous). Vous téléversez un fichier audio ou vidéo, et la transcription est générée automatiquement avec identification des locuteurs. Pour la conformité Loi 25, l\'audit trail (art. 3.5 LPRPSP), le registre des consentements (art. 14) et l\'EFVP (art. 3.3) sont fournis par défaut.', }, { 'q': 'Quels formats audio/vidéo sont supportés?', @@ -49,7 +49,7 @@ FAQ = [ }, { 'q': 'La transcription est-elle vraiment confidentielle?', - 'a': 'Avec DictIA 8 et 16, vos données ne quittent jamais votre bureau — le traitement est 100 % local, sans connexion internet requise. Avec DictIA Cloud, les données sont hébergées exclusivement au Canada (OVH Beauharnois, QC et GCP Toronto, ON). Aucun transfert hors-frontières, zéro Cloud Act.', + 'a': 'Avec DictIA LOCAL, vos données ne quittent jamais votre bureau — le traitement est 100 % local, sans connexion internet requise. Avec les forfaits Cloud (BASIC, ESSENTIEL, PRO), les données sont hébergées exclusivement au Québec (OVH Beauharnois). Aucun transfert hors-frontières, zéro Cloud Act.', }, { 'q': 'Teams Copilot est-il légal pour mes réunions?', diff --git a/static/css/marketing.css b/static/css/marketing.css index abbab90..2144237 100644 --- a/static/css/marketing.css +++ b/static/css/marketing.css @@ -49,8 +49,6 @@ --color-green-700: oklch(52.7% 0.154 150.069); --color-green-800: oklch(44.8% 0.119 151.328); --color-green-900: oklch(39.3% 0.095 152.535); - --color-emerald-50: oklch(97.9% 0.021 166.113); - --color-emerald-100: oklch(95% 0.052 163.051); --color-emerald-500: oklch(69.6% 0.17 162.48); --color-emerald-600: oklch(59.6% 0.145 163.225); --color-emerald-700: oklch(50.8% 0.118 165.612); @@ -1169,12 +1167,18 @@ .min-w-\[180px\] { min-width: 180px; } + .min-w-\[260px\] { + min-width: 260px; + } .min-w-\[300px\] { min-width: 300px; } .min-w-\[720px\] { min-width: 720px; } + .min-w-\[820px\] { + min-width: 820px; + } .min-w-full { min-width: 100%; } @@ -1765,6 +1769,9 @@ .border-brand-b3\/15 { border-color: color-mix(in oklab, #c026d3 15%, transparent); } + .border-brand-b3\/20 { + border-color: color-mix(in oklab, #c026d3 20%, transparent); + } .border-brand-b3\/60 { border-color: color-mix(in oklab, #c026d3 60%, transparent); } @@ -2074,6 +2081,9 @@ .bg-brand-b2\/20 { background-color: color-mix(in oklab, #06b6d4 20%, transparent); } + .bg-brand-b2\/\[0\.08\] { + background-color: color-mix(in oklab, #06b6d4 8%, transparent); + } .bg-brand-b3 { background-color: #c026d3; } @@ -2083,6 +2093,9 @@ .bg-brand-b3\/60 { background-color: color-mix(in oklab, #c026d3 60%, transparent); } + .bg-brand-b3\/\[0\.08\] { + background-color: color-mix(in oklab, #c026d3 8%, transparent); + } .bg-brand-bg { background-color: #f7f9fc; } @@ -3304,6 +3317,12 @@ color: color-mix(in oklab, var(--color-white) 70%, transparent); } } + .text-white\/75 { + color: color-mix(in srgb, #fff 75%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 75%, transparent); + } + } .text-white\/80 { color: color-mix(in srgb, #fff 80%, transparent); @supports (color: color-mix(in lab, red, red)) { diff --git a/static/js/roi_calculator.js b/static/js/roi_calculator.js index 3b0c889..1550045 100644 --- a/static/js/roi_calculator.js +++ b/static/js/roi_calculator.js @@ -1,8 +1,8 @@ -// ROI calculator for DictIA pricing section. +// ROI calculator for DictIA pricing section (v7.0). // Hypotheses transparentes (cf. footnote dans landing.html) : // - 80% du temps de transcription manuelle est économisé // - 220 jours ouvrables/an -// - Coût annuel comparé = DictIA 16 = 5 750 $ + (201 $ × 12) = 8 162 $ +// - Coût annuel comparé = Cloud ESSENTIEL = 349 $ × 12 = 4 188 $ window.roiCalculator = function roiCalculator() { return { users: 5, @@ -13,7 +13,8 @@ window.roiCalculator = function roiCalculator() { return Math.round(hoursSaved * this.rate); }, get payback() { - const annualCost = 5750 + (201 * 12); + // Cloud ESSENTIEL annual cost (no setup fee) + const annualCost = 349 * 12; if (this.savings <= 0) return null; return (annualCost / this.savings) * 12; } diff --git a/templates/macros/pricing_card.html b/templates/macros/pricing_card.html index 13301d1..9c234b4 100644 --- a/templates/macros/pricing_card.html +++ b/templates/macros/pricing_card.html @@ -1,32 +1,105 @@ -{# Reusable pricing card macro. FlexiHub style — recommended tier gets a grad-bg outer border (1.5px gradient frame). +{# Reusable pricing card macro (v7.0). FlexiHub style — recommended tier gets a grad-bg outer border (1.5px gradient frame). Args: - slug : URL-safe identifier (goes into href, NOT piped through | safe — autoescape protects URL) - name : Display name (piped through | safe — may contain entities; current names like "DictIA 8" are entity-free) - price_setup : Setup price string with NBSP (e.g. '3 450 $') — piped through | safe - price_monthly : Monthly price string with NBSP (e.g. '173 $') — piped through | safe - target : Target audience tagline — piped through | safe (may contain entities) - features : List of feature strings, each piped through | safe (will contain   entities) - recommended : If True, wraps the card in grad-bg gradient frame + RECOMMANDÉ badge - cta_url : Base URL for the CTA — slug appended (NOT piped through | safe — URL injection guard) + slug : URL-safe identifier (goes into href, NOT piped through | safe — autoescape protects URL) + name : Display name (piped through | safe — entity-free expected: "Cloud BASIC", "DictIA LOCAL"…) + target : Target audience tagline — piped through | safe (may contain entities) + features : List of feature strings, each piped through | safe (may contain   entities) + badge : Top eyebrow chip text above the title — e.g. 'Cloud · Souverain QC' or 'Local · 100% hors-ligne' + recommended : If True, wraps the card in grad-bg gradient frame + RECOMMANDÉ badge + setup : One-shot setup price NUMBER (CAD, no NBSP) — None to hide. Cloud Basic/Essentiel = None, + Cloud Pro = 485, DictIA Local = 5998. + monthly : Monthly recurring price NUMBER (CAD, no NBSP) — None for DictIA Local (one-shot only). + yearly_renewal : Year-2+ renewal NUMBER (CAD, no NBSP) — only for DictIA Local (500$/an dès An 2). + capacity_audio : Capacity chip (audio hours / month) — e.g. '~165 h audio/mois' + capacity_storage : Capacity chip (storage) — e.g. '100 Go' + gpu : GPU chip — e.g. 'NVIDIA L4 partagé' + cta_label : Button text — e.g. 'Démarrer en Cloud', 'Configurer DictIA Local' + cta_url : Base URL for the CTA — slug appended (NOT piped through | safe — URL injection guard) - Note: CTA label is "Réserver [name]" not "Choisir" because product is in pre-launch - (cf. trust bar "Pré-inscription ouverte") — LPC art. 219 hygiene. - The button macro autoescapes its `text` arg, so `name` MUST NOT contain HTML entities - (verified: "DictIA 8", "DictIA 16", "DictIA Cloud" are all entity-free). #} -{%- macro pricing_card(slug, name, price_setup, price_monthly, target, features, recommended=False, cta_url='/checkout') -%} -