SeanLysten

market-spas-b2b

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.

SeanLysten 0 Updated 2mo ago

Resources

12
GitHub

Install

npx skillscat add seanlysten/market-spas-b2b

Install via the SkillsCat registry.

SKILL.md

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 -
Email Resend -

Libs interdites (ne pas ajouter) :

  • axios → utiliser fetch natif
  • moment / dayjs → utiliser date-fns ou Intl
  • lodash → utiliser les méthodes natives ES2020+
  • jsonwebtoken → l'auth est gérée par la plateforme OAuth
  • react-router / next → Wouter uniquement
  • prisma → Drizzle uniquement
  • styled-components / emotion → Tailwind uniquement

Note : framer-motion est 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_REP existe 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 (table users vs table team_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 de db.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 vatRate sur les tables products et spare_parts est 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.ts lignes 1023 et 1932 (spaUnitCount * 300)
  • Sprint 2 extraira cette logique dans computeDeposit() dans shared/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 releaseExpiredCartReservations libère les réservations expirées
  • Migration requise : le setInterval actuel 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() dans server/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.register dans routers.ts exige un invitationToken ou invitationCode valide
  • 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 admins

6.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 : view et edit
  • 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() dans server/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_MANAGER et SALES_REP sont 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_MANAGER peut 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 → COMPLETED

7.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ée

7.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.role pour le rôle global, user.partnerId pour 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 @theme blocks 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.ts ou *.test.tsx à côté du fichier source
  • pnpm vitest run pour 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 adminProcedure ou createModuleAdminProcedure
  • 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(), ou dangerouslySetInnerHTML avec 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 :

  1. git pull pour avoir la dernière version
  2. Lire ce SKILL.md pour le contexte
  3. Lire le plan actif dans docs/superpowers/plans/
  4. Identifier la tâche à faire (Sprint X, Task Y.Z)
  5. Pattern TDD :
    • Écrire le test qui échoue
    • Implémenter la solution minimale
    • Vérifier que le test passe
    • Commit avec message conventionnel
  6. Vérifier : pnpm vitest run (tous les tests passent)
  7. 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 compiler

14. 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*.ts sont 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