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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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?',
|
||||
|
||||
Reference in New Issue
Block a user