Contexte projet pour le portail B2B Market Spas (revendeurs spas/jacuzzis FR/BE/LU). À utiliser dès qu'une tâche touche au repo market-spas-b2b — toute modification de catalogue, panier, commandes, leads, partenaires, SAV, pièces détachées, admin dashboard, carte partenaires, intégrations Meta/Mollie/Resend/supplier, ou backend tRPC/Drizzle.
Resources
12Install
npx skillscat add seanlysten/market-spas-b2b Install via the SkillsCat registry.
SKILL — market-spas-b2b
1. Mission produit
Market Spas est une marque propriétaire de spas, spas de nage et jacuzzis. Le portail B2B est la plateforme unique de gestion pour les revendeurs partenaires en France, Belgique et Luxembourg.
Points critiques :
- La marque est "Market Spas" — jamais "Wellis" (ancien fournisseur, ne doit plus apparaître)
- Le portail est exclusivement B2B : les utilisateurs sont des revendeurs professionnels, pas des consommateurs
- Les modèles de spa sont organisés par catégorie :
spa,swim_spa,accessory,spare_part - Les prix affichés sont HT (hors taxes) — la TVA est calculée dynamiquement selon le pays du partenaire
2. Architecture confirmée
Le portail est l'autorité unique pour toutes les données métier. Aucun système externe ne fait autorité.
┌─────────────────────────────────────────────────────────────┐
│ PORTAIL B2B (autorité) │
├─────────────────────────────────────────────────────────────┤
│ │
│ INTÉGRATIONS ENTRANTES (webhooks/polling) │
│ ├── Meta Ads → leads Facebook/Instagram │
│ ├── Shopify Form → leads formulaire site vitrine │
│ └── Supplier API → stock, prix, transit, numéros série │
│ │
│ INTÉGRATIONS SORTANTES (appels API) │
│ ├── Mollie → paiements (acomptes, commandes complètes) │
│ ├── Meta Graph API → récupération leads, stats ads │
│ ├── Google Analytics Data API → reporting │
│ ├── Google Ads API → performance campagnes │
│ ├── Nominatim (OpenStreetMap) → géocodage adresses │
│ ├── Resend → emails transactionnels │
│ ├── Expo Push → notifications app mobile │
│ └── S3 (storage) → fichiers, images, documents │
│ │
└─────────────────────────────────────────────────────────────┘3. Stack technique figé
| Couche | Technologie | Version |
|---|---|---|
| Frontend | React | 19.2 |
| Routing client | Wouter | 3.3 |
| State/API | tRPC (React Query) | 11.6 |
| Styling | Tailwind CSS | 4.1 |
| UI Components | shadcn/ui (Radix) | latest |
| Backend | Express + tRPC | 11.6 |
| ORM | Drizzle | 0.44 |
| Database | MySQL (PlanetScale) | 8.x |
| Build | Vite | 7.1 |
| Package manager | pnpm | 10.x |
| Tests | Vitest | latest |
| Auth | OAuth (Manus platform) | - |
| Payments | Mollie | - |
| Resend | - |
Libs interdites (ne pas ajouter) :
axios→ utiliserfetchnatifmoment/dayjs→ utiliserdate-fnsou Intllodash→ utiliser les méthodes natives ES2020+jsonwebtoken→ l'auth est gérée par la plateforme OAuthreact-router/next→ Wouter uniquementprisma→ Drizzle uniquementstyled-components/emotion→ Tailwind uniquement
Note :
framer-motionest présent dans package.json (legacy) mais ne doit pas être étendu. Préférer les animations CSS/Tailwind.
4. Architecture logicielle
market-spas-b2b/
├── client/src/ # Frontend React
│ ├── _core/ # Auth hooks, providers
│ ├── components/ # Composants réutilisables (shadcn/ui)
│ ├── hooks/ # Custom hooks
│ ├── lib/ # tRPC client, utils
│ └── pages/ # Pages (Dashboard, Catalog, Orders, Leads, SAV, admin/*)
├── server/ # Backend Express + tRPC
│ ├── _core/ # Express setup, tRPC init, Vite SSR, OAuth
│ ├── routes/ # Routes Express non-tRPC (webhooks, supplier, mobile-api)
│ ├── jobs/ # Background jobs (newsletter)
│ ├── db.ts # Fonctions DB principales (~5800 lignes)
│ ├── routers.ts # Routes tRPC (~6200 lignes)
│ ├── sav-db.ts # Fonctions DB SAV
│ ├── lead-routing.ts # Routage géographique des leads
│ ├── meta-leads.ts # Intégration Meta Ads leads
│ ├── warranty-engine.ts # Moteur de garantie SAV
│ ├── team-permissions.ts # Permissions équipe partenaire
│ └── admin-permissions.ts # Permissions admin granulaires
├── drizzle/ # Schema DB + migrations
│ └── schema.ts # 82 tables, tous les enums
├── shared/ # Types/utils partagés client+serveur
│ └── _core/ # Types OAuth, config
├── docs/ # Documentation
└── scripts/ # Scripts utilitaires (seeds, data)5. Modèle de données
82 tables dans drizzle/schema.ts. Tables principales :
| Table | Rôle |
|---|---|
users |
Comptes (admin, partenaires, commerciaux) |
partners |
Entreprises revendeurs |
team_members |
Membres d'équipe d'un partenaire |
products |
Catalogue produits (spas, accessoires) |
product_variants |
Variantes (couleurs, options) |
orders |
Commandes B2B |
order_items |
Lignes de commande |
payments |
Paiements Mollie |
leads |
Prospects (Meta, Shopify, email) |
after_sales_services |
Tickets SAV |
spare_parts |
Pièces détachées |
spa_models |
Modèles de spa (hotspots, specs) |
spa_units |
Unités individuelles (numéros de série) |
warranty_rules |
Règles de garantie (table DB) |
cart_reservations |
Réservations panier (20 min) |
notifications |
Notifications in-app |
territories |
Zones géographiques partenaires |
Enums clés :
| Enum | Valeurs | Nb |
|---|---|---|
userRoleEnum |
SUPER_ADMIN, ADMIN, SALES_MANAGER, SALES_REP, PARTNER_ADMIN, PARTNER_USER, PARTNER | 7 |
orderStatusEnum |
DRAFT → PENDING_APPROVAL → PENDING_DEPOSIT → DEPOSIT_PAID → IN_PRODUCTION → READY_TO_SHIP → PARTIALLY_SHIPPED → SHIPPED → DELIVERED → COMPLETED / CANCELLED / REFUNDED / REFUSED / PAYMENT_PENDING / PAYMENT_FAILED | 15 |
leadStatusEnum |
NEW, ASSIGNED, CONTACTED, NO_RESPONSE, QUALIFIED, NOT_QUALIFIED, MEETING_SCHEDULED, QUOTE_SENT, NEGOTIATION, CONVERTED, LOST | 11 |
savStatusEnum |
NEW, ANALYZING, INFO_REQUIRED, QUOTE_PENDING, PAYMENT_CONFIRMED, PREPARING, SHIPPED, RESOLVED, CLOSED | 9 |
teamRoleEnum |
OWNER, SALES_REP, ORDER_MANAGER, ACCOUNTANT, FULL_MANAGER | 5 |
ATTENTION naming :
SALES_REPexiste dans deux enums distincts :
userRoleEnum.SALES_REP= commercial interne Market Spas (back-office admin)teamRoleEnum.SALES_REP= vendeur dans une boutique revendeur (dashboard partenaire)
Ne jamais confondre les deux contextes (tableusersvs tableteam_members).
6. Règles métier non négociables
6.1 TVA HT-only
- Les prix sont toujours HT dans la base de données et l'interface
- La TVA est calculée dynamiquement par
getVatRateForPartner()(ligne 5574 dedb.ts) - France = 20%, Belgique/Luxembourg/autres = 0% (autoliquidation intracommunautaire)
- La valeur 21 est INTERDITE — c'est l'ancien taux belge incorrect. Si tu vois 21% quelque part, c'est un bug.
- La colonne
vatRatesur les tablesproductsetspare_partsest obsolète et sera supprimée (Sprint 1)
6.2 Acompte 300€/spa fixe
- Chaque spa ou spa de nage commandé nécessite un acompte fixe de 300€ TTC
- Les accessoires sont payés en totalité (pas d'acompte)
- Source unique de calcul :
db.tslignes 1023 et 1932 (spaUnitCount * 300) - Sprint 2 extraira cette logique dans
computeDeposit()dansshared/const.ts
6.3 Réservation panier 20 min
- Quand un partenaire ajoute un spa au panier, le stock est réservé 20 minutes
- Seuls les spas/swim_spas sont réservés (pas les accessoires)
- Le job
releaseExpiredCartReservationslibère les réservations expirées - Migration requise : le
setIntervalactuel ne fonctionne pas sur CloudRun. Sera migré vers un endpoint cron appelé par Manus Scheduled Task (Sprint 1, Task 1.10)
6.4 Routage géographique des leads
- Chaque lead entrant est automatiquement assigné au partenaire le plus proche géographiquement
- Le routage utilise les
territories(zones géographiques) et le géocodage Nominatim - Fonction principale :
findBestPartnerForLead()dansserver/lead-routing.ts - Détection automatique du pays par code postal (4 chiffres = BE/LU, 5 chiffres = FR)
6.5 Création comptes par invitation EXCLUSIVE
- Aucun compte ne peut être créé sans invitation (token ou code)
auth.registerdansrouters.tsexige uninvitationTokenouinvitationCodevalide- Les invitations sont créées par un admin (back-office) ou un PARTNER_ADMIN (son équipe)
6.6 SAV avec validation humaine 2 temps
- Le SAV suit un workflow en étapes : le revendeur soumet → l'admin analyse → décision
- Le moteur de garantie (
warranty-engine.ts) fournit un pré-diagnostic automatique - La décision finale est toujours humaine (admin Market Spas)
- 4 décisions possibles : APPROVED (garantie), REJECTED, PARTIAL_COVERAGE, COMMERCIAL_GESTURE
- Sprint 7 refondra le wizard SAV (4 étapes) et ajoutera des garde-fous anti-abus
6.7 Numéros de série via spa_units
- Chaque spa physique a un numéro de série unique
- La table
spa_units(Sprint 3) tracera : modèle, variante, n° série, statut, partenaire assigné, date installation - Le fournisseur pousse les n° série via l'API supplier (
POST /api/supplier/spa-units)
6.8 Isolation données partenaire
- Un partenaire ne voit jamais les données d'un autre partenaire
- Toutes les requêtes DB pour les partenaires filtrent par
partnerId - Les admins (SUPER_ADMIN, ADMIN) voient toutes les données
6.9 Séparation stricte des deux univers de rôles
UNIVERS 1 — BACK-OFFICE ADMIN (/admin/*)
Condition d'accès : role IN (SUPER_ADMIN, ADMIN, SALES_MANAGER, SALES_REP)
Garde frontend : AdminLayout.tsx:260
Garde backend : adminProcedure (tRPC) + createModuleAdminProcedure(module, action)
UNIVERS 2 — DASHBOARD PARTENAIRE (/dashboard, /catalog, /orders, ...)
Condition d'accès : partnerId !== null
Permissions internes : team_members.teamRole + team-permissions.ts
DUAL-ACCESS : un user avec role=SUPER_ADMIN ET partnerId=X accède aux deux univers.
→ Dashboard affiche le bouton "Ouvrir l'admin"
→ team.myPermissions retourne { role: "OWNER", isOwner: true } pour les admins6.10 Permissions admin granulaires
- 17 modules admin : dashboard, products, stock, orders, partners, marketing, territories, sav, spare_parts, resources, technical_resources, newsletter, calendar, users, settings, reports, partner_map
- Chaque module a 2 niveaux :
viewetedit - 7 presets : SUPER_ADMIN, ADMIN_FULL, ADMIN_STOCK, ADMIN_SAV, ADMIN_MARKETING, ADMIN_ORDERS, ADMIN_CUSTOM
- Stocké dans
users.adminPermissions(JSON) - Fonction de vérification :
hasAdminModuleAccess()dansserver/admin-permissions.ts - Le wrapper tRPC
createModuleAdminProcedure(module, action)existe mais n'est pas encore utilisé dans les routes (Sprint 4)
6.11 Permissions commerciaux internes
SALES_MANAGERetSALES_REPsont des commerciaux internes Market Spas- Ils accèdent au back-office admin avec des permissions limitées (Sprint 5)
- Modules autorisés : carte partenaires, candidats, tournées, médiathèque, agenda
SALES_MANAGERpeut en plus : réassigner candidats, convertir candidat → partenaire
7. Flux principaux
7.1 Commande B2B
Partenaire ajoute au panier → Réservation stock 20 min
→ Validation panier → Création commande (DRAFT)
→ Approbation admin → PENDING_DEPOSIT
→ Paiement acompte Mollie (300€/spa) → DEPOSIT_PAID
→ Envoi au fournisseur → IN_PRODUCTION
→ Livraison → SHIPPED → DELIVERED → COMPLETED7.2 Lead inbound
Source (Meta/Shopify/Email) → Webhook portail
→ Parsing + détection pays (code postal)
→ Déduplication (fenêtre 30s)
→ Routage géographique (findBestPartnerForLead)
→ Assignation partenaire + notification
→ Suivi statut (NEW → CONTACTED → QUALIFIED → CONVERTED)7.3 Onboarding partenaire
Admin crée invitation → Email avec lien
→ Partenaire s'inscrit (token obligatoire)
→ Compte créé (role=PARTNER_ADMIN, partnerId=X)
→ Accès dashboard + catalogue + commandes
→ Peut inviter son équipe (SALES_REP, ORDER_MANAGER, etc.)7.4 SAV (refonte Sprint 7)
Revendeur → Wizard 4 étapes (n° série → zone → symptôme → validation)
→ Ticket créé (status=ANALYZING)
→ Pré-diagnostic auto (warranty-engine, caché du revendeur)
→ Admin review : checklist + score risque + 4 décisions
→ Email auto au revendeur sur chaque décision
→ Si pièce nécessaire : commande spare_part liée7.5 API Supplier
Fournisseur → POST /api/supplier/* (auth API key)
├── /stock → mise à jour stock produits
├── /prices → mise à jour prix
├── /transit → arrivages en transit
├── /spa-units → numéros de série
├── /models → specs modèles
└── GET /orders/export → commandes à expédier (filtre DEPOSIT_PAID)8. Conventions code
Routing client
- Wouter uniquement (
useLocation,useRoute,<Link>,<Route>) - Pas de React Router, pas de Next.js
Auth
useAuth()hook →{ user, isAuthenticated, logout }user.rolepour le rôle global,user.partnerIdpour l'appartenance partenaire- OAuth géré par la plateforme Manus (pas de JWT custom)
WebSocket
- Rooms par partenaire pour les notifications temps réel
io.to(room).emit(event, data)
Styling
- Tailwind CSS v4 avec
@themeblocks en OKLCH - shadcn/ui pour les composants (Dialog, Card, Button, etc.)
- Pas de CSS modules, pas de styled-components
i18n
- Inline en français (pas de lib i18n)
- Les textes sont directement dans les composants
Naming
- Fichiers : kebab-case (
product-add-to-cart-dialog.tsx) - Composants : PascalCase (
ProductAddToCartDialog) - Fonctions DB : camelCase (
getProductById) - Tables DB : snake_case (
after_sales_services) - Enums : SCREAMING_SNAKE_CASE (
DEPOSIT_PAID)
Tests
- Vitest pour les tests unitaires
- Pattern TDD : test failing → implémentation → test passing → commit
- Fichiers test :
*.test.tsou*.test.tsxà côté du fichier source pnpm vitest runpour exécuter tous les tests
Logs
console.log("[Module] message")avec préfixe entre crochets- Pas de logger structuré (pino/winston) — hors scope actuel
Commits
- Conventional commits :
feat(scope): message,fix(scope): message,chore(scope): message - Scope = module concerné (cart, sav, leads, admin, supplier, etc.)
9. Sécurité checklist PR
Avant chaque merge, vérifier :
- Pas de secret/clé API en dur dans le code
- Toutes les routes admin utilisent
adminProcedureoucreateModuleAdminProcedure - Les requêtes partenaire filtrent par
partnerId(isolation données) - Les inputs sont validés avec Zod (schemas tRPC)
- Pas de SQL injection (utiliser Drizzle, pas de
sql.raw()avec des inputs user) - Les webhooks vérifient l'authenticité (signature Mollie, API key supplier)
- Pas de
eval(),Function(), oudangerouslySetInnerHTMLavec des données user - Les uploads sont validés (type MIME, taille max)
- Rate limiting sur les endpoints publics (
/api/auth/*)
10. Intégrations externes sortantes
| Service | Usage | Fichier principal | Timeout actuel |
|---|---|---|---|
| Mollie | Paiements (acomptes, commandes) | server/db.ts |
Aucun ⚠️ |
| Meta Graph API | Récupération leads, stats ads | server/meta-leads.ts |
Aucun ⚠️ |
| Google Analytics Data API | Reporting dashboard | server/routers.ts |
Aucun ⚠️ |
| Google Ads API | Performance campagnes | server/routers.ts |
Aucun ⚠️ |
| Nominatim (OSM) | Géocodage adresses | server/geo-utils.ts |
Aucun ⚠️ |
| Resend | Emails transactionnels | server/db.ts, server/jobs/ |
Aucun ⚠️ |
| Expo Push | Notifications mobile | server/routes/mobile-api.ts |
Aucun ⚠️ |
| S3 | Storage fichiers | server/_core/storage.ts |
Aucun ⚠️ |
Sprint Résilience ajoutera des timeouts (10s APIs légères, 30s Mollie) + retry avec backoff exponentiel sur Meta.
11. Intégrations entrantes (webhooks)
| Source | Route | Auth | Fichier |
|---|---|---|---|
| Meta Ads (leads) | /api/meta-leads/webhook |
Verify token | server/meta-leads.ts |
| Shopify Form | /api/inbound-leads/shopify |
API key header | server/routes/inbound-leads.ts |
| Email (leads) | /api/inbound-leads/email |
API key header | server/routes/inbound-leads.ts |
| Supplier (stock) | /api/supplier/* |
API key header | server/routes/supplier-stock.ts |
| Mollie (paiements) | /api/mollie/webhook |
Signature | server/routes/mollie-webhook.ts |
| Make.com (legacy) | /api/facebook-leads, /api/meta-ads-stats |
API key | server/webhooks.ts ⚠️ À supprimer Sprint 6 |
12. Workflow agent IA
Quand tu travailles sur ce repo :
git pullpour avoir la dernière version- Lire ce
SKILL.mdpour le contexte - Lire le plan actif dans
docs/superpowers/plans/ - Identifier la tâche à faire (Sprint X, Task Y.Z)
- Pattern TDD :
- Écrire le test qui échoue
- Implémenter la solution minimale
- Vérifier que le test passe
- Commit avec message conventionnel
- Vérifier :
pnpm vitest run(tous les tests passent) - Vérifier :
pnpm build(pas d'erreur de build)
En cas de bug :
- Écrire d'abord un test qui reproduit le bug
- Fixer le code
- Vérifier que le test passe
- Commit :
fix(scope): description du fix
13. Commandes utiles
pnpm dev # Démarre le serveur de dev (port 3000)
pnpm build # Build production (Vite + tsc)
pnpm vitest run # Exécute tous les tests
pnpm db:push # Pousse les changements de schema vers la DB (drizzle-kit generate + migrate)
pnpm tsc --noEmit # Vérifie les types sans compiler14. Ce que ce projet n'est PAS
- Pas un e-commerce B2C : les clients finaux n'ont pas accès au portail
- Pas un CMS : le contenu est géré en code, pas via un éditeur WYSIWYG
- Pas un SaaS multi-tenant : c'est un portail unique pour Market Spas
- Pas une app mobile : les fichiers
mobile-api*.tssont un legacy à supprimer (Sprint 1) - Pas un marketplace : un seul fournisseur (le supplier), un seul catalogue
- Pas un outil de comptabilité : les factures sont générées mais la compta est externe
15. Risques connus et dette technique
| Risque | Sévérité | Sprint |
|---|---|---|
mobile-api*.ts : 387 erreurs TS, code divergent (TVA 21%, acompte 30%, statut PENDING) |
Critique | Sprint 1 |
setInterval pour cron jobs : ne fonctionne pas sur CloudRun |
Haute | Sprint 1 (Task 1.10) |
getAfterSalesStatusHistory appelé mais non défini : crash garanti |
Haute | Sprint 1 (Task 1.7) |
Bug panier useSafeQuery sur objet : bouton désactivé si stockQuantity=0 |
Moyenne | Sprint 1 (Task 1.6) |
| Aucun timeout sur les 8 APIs externes | Moyenne | Sprint Résilience |
createModuleAdminProcedure défini mais jamais utilisé (190 routes non protégées) |
Moyenne | Sprint 4 |
Routes Make.com legacy encore actives (webhooks.ts) |
Basse | Sprint 6 |
vatRate colonne obsolète sur products/spare_parts |
Basse | Sprint 1 |