refactor(pricing): refonte v7.0 — 3 Cloud (Basic 189$/Essentiel 349$/Pro 549$) + DictIA Local (5998$ An1) + Pro+ soumission
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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_<PLAN>_<PERIOD> 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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user