Audit « ce qu'il reste » — 2026-06-15 (post #303→#306)
Reconciliation après merge des PRs #298/#303/#305/#306. Trois inspecteurs : (1) dette restante vs main, (2) robustesse du tunnel onboarding→transfert→ activation, (3) scan frais du code récent (#304/#305). Tout vérifié…
Reconciliation après merge des PRs #298/#303/#305/#306. Trois inspecteurs : (1) dette restante vs
main, (2) robustesse du tunnel onboarding→transfert→ activation, (3) scan frais du code récent (#304/#305). Tout vérifié par lecture du code (fichier:ligne). Les items des PRs mergées sont exclus.
Verdict
Plus aucun critique de correctness/sécurité « classique » (les 5 du précédent audit sont corrigés). Le risque s'est déplacé vers la couche conversion/monétisation du tunnel : le moteur de provisioning est production-grade, mais les étapes qui transforment un store fini en revenu ont des trous critiques. Plus un vrai bug d'argent sur les payouts escrow.
🔴 CRITIQUE — bloque directement l'objectif d'activation
F1 — Le cockpit ne peut PAS atteindre « Prêt à vendre » (plafond à 79 %)
- Où :
launch-checklist.tsx:198-204,222+launch/route.ts:308-342+wizard-skills/registry.ts:40-54 - Problème : 6 jalons
auto(preferences-configurator,consent-native,markets-configurator,tax-configurator,shipping-configurator,search-configurator) n'ont pas d'exécuteur mais sont comptés au dénominateur (readyTotal = auto + human). Run parfait =23/29 = 79 %. L'entête (pct >= 100 ? "Ready to sell" : "Preparing your store") reste toujours « Preparing… 79 % », avec TVA/consentement RGPD figés en « Planned ». Le CTA « Prêt à vendre → Transférer » exige 100 % → ne s'affiche jamais → étouffe transfert + activation. - Correctif (~2 lignes, meilleur ROI du repo) : exclure du dénominateur les
autosans exécuteur (ou reclasser les 6 en checkpointshumanguidés avec deep-links Shopify admin). Mirroir danslaunch/route.tscounts.
F2 — Les stores « waitlisted » (pool vide) sont perdus à jamais
- Où :
wizard/store/provision/route.ts:79-113(branche waitlist) +launch-tick/route.ts:197(if (!connectedSet.has(storeId)) continue) - Problème : pool vide →
status:"waitlisted", email ops, « le flux continue » : mais le Store n'a pas d'IntegrationConnectionactive, donc launch-tick le saute. Quand l'ops ajoute de la capacité, le nouveau row est justeAVAILABLE: aucun job ne re-claim pour les stores waitlistés. Le lead le plus chaud (wizard terminé) restequeuedà vie. La doc sous-entend un backfill auto : aucun code ne l'implémente. - Correctif : passe de backfill dans launch-tick (ou au register pool) : pour
chaque Store avec activité
launch.provision.waitlistedsans connexion shopify active → tenterclaimDevStore, binder la connexion, purger le row.
🟠 ÉLEVÉ
B1 — Double-paiement escrow possible (fenêtre d'idempotency Stripe 24h)
- Où :
cron/marketplace-payouts/route.ts:83-93+services/stripe/connect.ts:193-206 - Problème : le transfer Stripe est créé AVANT le stamp DB
(
order.updateMany(stripeTransferId)). Non atomique. Si le transfer réussit mais le stamp échoue (blip DB / timeout après le retour Stripe), l'order restePAID, stripeTransferId=nullet est re-sélectionné. Seul garde-fou = clé d'idempotency Stripetransfer:${orderId}, qui expire après 24h, or le cron tourne quotidiennement. Re-sélection au tick suivant (~24h) = hors fenêtre → second payout réel au vendeur. Perte d'argent réelle. - Correctif : claim AVANT Stripe (
updateManygardé :stripeTransferId='pending:<uuid>'oupayoutClaimedAt), et/ou clé d'idempotency stable persistée réutilisée.
F3 — ✅ FAIT : relances auto J+2/5/6 pendant la fenêtre de transfert
- Où :
shopify-partners.ts:161-217,303-360+modules/email/index.ts:148-155+chat-banners.tsx - Problème : 2 emails seulement (
…TransferRequested→ ops,…TransferAccepted→ user après acceptation). Entre les deux (fenêtre où commission + plan se gagnent) : aucune relance client.pollTransferAcceptancepasse àexpiredpuis retombe enCLAIMEDsilencieusement. Lead refroidi → perdu. - Correctif : dans le poll launch-tick, pour les
TRANSFER_REQUESTEDpendants à J+2/J+5/J+6 → email de relance (idempotent via dedupeKeytransfer:reminder:{day})- bannière
invite_pendingavec compte à rebourstransferExpiresAt. La data existe.
- bannière
H1 — Route vocale EVI : français codé en dur (contourne next-intl)
- Où :
evi/chat/completions/route.ts:353(refus parlé),:496-507(overlay),:514-524(fallback) - Correctif : résoudre la locale (store/org déjà chargés dans
resolveStoreVoiceContext) + ressources par-locale.
H2 — ✅ FAIT (dédup) : une seule bannière crédits s'affiche (i18n par-bannière = reste)
- Où :
credits-limit-banner.tsx:45,73,102(FR, montéai-chat.tsx:983) +chat-banners.tsx:141(EN, montéai-chat.tsx:1261) - Correctif : router les deux via
useTranslations, n'afficher qu'une bannière.
🟡 MOYEN
F4 — ✅ PARTIEL : marqueur d'attribution commission posé (réconciliation = Partner API externe)
- Où :
shopify-partners.ts(tout le fichier, aucun enregistrement de commission) - Problème : la thèse « le coût est couvert par la commission » est non
mesurée : aucune attribution (storeId/orgId/planName/acceptedAt), pas de
réconciliation contre le payout Shopify, pas de détection de downgrade
post-transfert (
TRANSFERREDest terminal). - Correctif : sur
accepted, écrire un row d'attribution + cron de réconciliation mensuelle.
M1 — ✅ FAIT : dismiss en state React (plus de getElementById/remove)
- Où :
chat-banners.tsx:94,114-121(getElementById("error-banner")+el.remove()+setTimeoutsans cleanup ; id codé en dur collisionne) - Correctif : dismiss en state React + rendu conditionnel.
M2 — ⏸️ REPORTÉ (sciemment) : Revenu KPI brut, un chiffre FAUX vaut pire qu'un stub
- Où :
services/jobs/kpi-snapshot.ts:105-107(grossCents=0,creditCents=0) - Correctif : sommer depuis StripeEvent/payment-events depuis
since(MRR/ARR déjà câblés).
M3 — Runner de workflow durable = stub
- Où :
api/workflow/[id]/run/route.ts:137-147(queue le row, 202, n'exécute jamais) - Correctif : câbler Vercel Workflow DevKit (graph.nodes → DurableAgent steps + SSE).
🟢 FAIBLE
- B2 —
wizard/store/launchgate sur accès brut, passtore.update(launch/route.ts:87-90) → un viewer matérialise 29 rows + écrit StoreContext. Correctif : ajouter le gatestore.update(commeskill/run/checkpoint). - B3 — launch-tick
plan-selectedauto-approve = deny-list → un plan Shopify inconnu auto-flippe en « payé » (launch-tick/route.ts:93-96,117-128). Correctif : allow-list de plans payants connus. - F5 — ✅ NON-PROBLÈME :
maxDuration300s (5min) <STALE_RUNNING_MS15min → Vercel tue un milestone avant qu'il puisse être faussement reclaimé. Pas de correctif nécessaire. - L1 — EVI : floor mais pas de réservation (race dépense concurrente),
evi/.../route.ts:350-351. Correctif :reserveCreditsForChannel("voice"). - L2 —
domain-verification.tsx:31setTimeout sans cleanup. - L3 —
connectors-popover.tsx:79erreur en couleur seule (le trigger est désormais étiqueté ✅).
♿ A11Y — ~5 boutons restants
shared/credits/dropdown.tsx:95,166(2× ArrowLeft retour),file-manager.tsx:503(MoreVertical),web-preview/buttons.tsx:200-214(boutons device sans icône). (Le gros des ~30 est fait.)
🧹 Ménage
- DC1 — Roster fictif 7 personas (
execution-planner/agent-avatar.tsx:8-44, consommé parstep-card.tsx/message-plan-part.ts) → remplacer par Atlas+Maya/Marco/Otis/Faye/Sam, ou marquer décoratif. - DC2 —
web-preview/buttons.tsx:85,173,223,240,277: 5×(globalThis as any).browserTabalors quelib/electron-bridge.tsexiste pour ça. - DC3 —
~/billing/page.tsx:145:(authClient as any).stripeà typer.
🧪 Tests manquants
- Crypto token round-trip (
lib/security/crypto.ts),trackUsagehold→usage swap, pré-stream cost gate / cost-guard, clés API (lib/security/api-keys.ts). - ✅ RBAC
tenant-auth+permissions: couvert (permissions.test.ts).
Ordre recommandé
- ✅ F1 (déblocage 100 % cockpit), FAIT : les 6 jalons orphelins reclassés en étapes guidées humaines (deep-links Shopify admin), le cockpit atteint 100 %.
- ✅ F2 (backfill waitlist), FAIT : passe
0.5dans launch-tick claim+bind les stores waitlistés dès que le pool a de la capacité (helperbindClaimedDevStore). - ✅ B1 (double-payout escrow), FAIT : claim-before-pay (marqueur
pending:gardé) avant le transfer Stripe, revert sur échec. - F3 + F4 (relances transfert + tracking commission), la moitié monétisation.
- H1/H2 (i18n EVI + double bannière).
- M1-M3, puis B2/B3/F5/L*, a11y, ménage, tests.