Skip to content

feat(admin): partenaires B2B, leurs parcours et leurs QR codes - #551

Merged
PaulThiberville merged 14 commits into
mainfrom
issue/346-b2b-partners
Oct 21, 2026
Merged

PaulThiberville merged 14 commits into
mainfrom
issue/346-b2b-partners

Conversation

@Nascimento95

@Nascimento95 Nascimento95 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Résultat et problème

Les hôtels, conciergeries et offices de tourisme qui paient pour des parcours à leur nom n'existaient nulle part : ni table, ni écran, ni page. A-10 permet à l'équipe de les enregistrer, de choisir de 3 à 10 parcours dans l'ordre de leur page, et d'obtenir le lien de cette page et ses QR codes ; l'API publique donne au site de quoi la construire.

  • Base : tables b2b_clients (nom, identifiant, type, contact, adresse, statut, prix mensuel HT en centimes, début, notes) et b2b_client_routes (parcours et place, 10 au plus), migration additive 0021_add_b2b_clients. L'identifiant (slug) est vérifié par la base.
  • API admin (éditeur et admin, journalisée, jamais en cache) : GET /admin/b2b-clients (les actifs d'abord, avec les villes de leurs parcours), GET /:id, POST, PATCH /:id (jamais le slug) et PUT /:id/routes (3 à 10 parcours publiés, dans l'ordre ; 409 routes_unavailable avec les parcours refusés).
  • API publique GET /v1/public/partners/:slug : le nom, isActive et les parcours publiés dans l'ordre, sous forme de cartes pour un visiteur anonyme ; cache public d'une minute. Un partenaire suspendu, résilié ou inconnu répond 404. Ni le contact, ni le prix, ni les notes n'en sortent (testé).
  • Admin (/partenaires) : la liste, la fiche (contact, contrat, notes, parcours choisis, source de mesure b2b-<slug>), le formulaire d'ajout et de modification (prix saisi en euros, envoyé en centimes sans nombre à virgule ; l'identifiant suit le nom tant qu'on ne l'a pas tapé), le choix des parcours (recherche parmi les parcours publiés, ordre par flèches, bouton grisé hors de 3 à 10), le lien de la page /p/<slug> et deux QR codes téléchargeables (la page, et /app?src=b2b-<slug> pour la mesure).
  • Au passage : la fiche qui reste à l'écran pendant le chargement de la suivante (utilisateurs, PR feat(admin): gérer les utilisateurs et leurs rôles (A-06) #499) devient un hook partagé useCardOnScreen, utilisé aussi par les partenaires.

Ce que cette PR ne tient pas (décision de l'opérateur, Marco, 2026-10-20) :

  • Les clics et installations « depuis PostHog » de la fiche : aucun événement de l'app ni du site ne porte la source b2b-<slug>, le wiki ne dit pas quels événements comptent comme un clic ou une installation, et la lecture demande une clé PostHog de lecture côté API, à créer par Ilan ou Paul. La fiche dit « pas encore branché ». Questions déposées sur l'Inbox.
  • La page publique du site elle-même est Site web : page d'un partenaire B2B (/p/:slug) et ses QR codes #347 : le critère « un partenaire actif avec 5 parcours a sa page sur le site » n'est tenu que côté API et QR codes.

Choix à relire

  • Le slug ne change plus une fois créé : il finit l'adresse de la page et la source de mesure, et des QR codes imprimés cesseraient de mesurer.
  • La colonne ville n'a pas de champ dans le modèle de données : la liste montre la ou les villes des parcours choisis.
  • Seuls les parcours publiés peuvent être choisis. Un parcours dépublié plus tard sort de la page publique mais reste dans la fiche, marqué « Hors ligne ».
  • Un partenaire prospect est servi (isActive: false) pour que le site garde sa page hors des moteurs de recherche ; suspendu et résilié répondent 404 (ticket du site).
  • Un éditeur ne voit dans la recherche de parcours que ceux de l'équipe (règle de GET /admin/routes) ; un admin voit tous les parcours publiés.
  • Rien n'impose qu'un partenaire « actif » ait ses 3 parcours : le wiki ne le dit pas.
  • NEXT_PUBLIC_SITE_URL (origine du site, sans chemin) est à régler au déploiement ; sans elle, la fiche le dit et ne donne aucun lien. Les QR codes sont tracés dans le navigateur avec la bibliothèque qrcode, déjà présente.
  • packages/ui : une hauteur et une largeur qr (96 px) au preset, « à reporter dans tokens.json » ; ConfirmDialog gagne isConfirmDisabled.

Fixes #346
Écrans : A-10
Dépendance : PR empilée sur #550 (ticket #528), elle-même sur #501, #500, #499, #479, #398, #365 et #363 (base issue/528-recent-second-factor).

Comment vérifier

Montage local de la pile, base locale migrée (pnpm --filter @widoo/api db:migrate), compte admin connecté, et dans apps/admin/.env.local : NEXT_PUBLIC_SITE_URL=http://localhost:3000.

  1. Partenaires B2B : ajouter « Hôtel du Nord » (l'identifiant devient hotel-du-nord), prospect, avec un contact et un prix « 49,50 » : la fiche montre « 49,50 € HT » et la source b2b-hotel-du-nord.
  2. Refus : un e-mail faux, un identifiant avec majuscules, puis un identifiant déjà pris (« Un partenaire a déjà cet identifiant »).
  3. « Choisir les parcours » : le bouton est grisé sous 3 parcours ; en ajouter 4, changer l'ordre, en retirer un, enregistrer à 3 ; la fiche et la colonne « Ville » suivent.
  4. « Page et QR codes » : deux QR codes, les liens http://localhost:3000/p/hotel-du-nord et …/app?src=b2b-hotel-du-nord, et « Télécharger le QR code » qui donne un PNG.
  5. « Modifier » : statut Actif (« La page est en ligne »), puis Suspendu (« La page n'est plus accessible »).
  6. curl localhost:8080/v1/public/partners/hotel-du-nord : actif, le JSON avec "isActive":true et les 3 parcours dans l'ordre, sans e-mail, téléphone ni prix ; suspendu, not_found.
  7. Compte moderator : pas de section Partenaires B2B, 403 de l'API.

Tests : 12 tests d'API (admin-b2b.test.ts : accès par rôle, création et journal, slug pris ou invalide, modification et journal avant et après, slug refusé, choix des routes et leur ordre, remplacement, refus sous 3, doublon, parcours non publiés ; public-partners.test.ts : l'ordre, un parcours dépublié, prospect, suspendu, résilié, inconnu, aucun champ privé) ; schémas, formulaire, prix, choix des parcours, liens et variable d'environnement côté shared et admin.

Testé par Marco le 2026-10-20, en local, sur le commit 467fd5a (base locale, Firebase de staging) : la liste et la fiche ; l'ajout et les refus ; le choix des 3 parcours dans l'ordre ; les statuts prospect, actif et suspendu ; la réponse de l'API publique pour un partenaire actif (nom, actif, 3 parcours) et sa réponse « introuvable » pour un suspendu. Rien n'a été testé sur un environnement déployé, ni avec un compte éditeur.

  • pnpm lint && pnpm typecheck && pnpm test verts en local (chaque commit passe lint et types)

Formatage mécanique : non

Risque

Niveau : 2 — migration additive (deux tables), nouveaux endpoints authentifiés qui écrivent, un endpoint public en lecture.

Taille : tous les commits sous 400 lignes utiles (le plus gros : 367). Fichiers générés à part : migration 0021_add_b2b_clients.sql et son instantané meta/0021_snapshot.json, générés par drizzle-kit.

Revue OWASP (Top 10, édition en cours)

  • A01 Contrôle d'accès : les routes admin passent par requireAdminSection('b2bClients') (éditeur et admin ; un modérateur reçoit 403, une requête sans jeton 401, testé). L'endpoint public ne lit que ce qui est destiné à la page : nom, drapeau actif et cartes de parcours publiés ; aucun champ de contact, de prix ni de notes (testé sur le corps de la réponse). Un partenaire suspendu ou résilié répond 404.
  • A03 Injection : requêtes paramétrées ; corps stricts (champ inconnu refusé), textes bruts bornés sans balise, e-mail validé, identifiant limité à [a-z0-9-] par le schéma et par une contrainte de la base ; l'identifiant est encodé dans les liens et les QR codes.
  • A04 Conception non sécurisée : l'identifiant, source de mesure des QR codes imprimés, ne change plus ; 3 à 10 parcours publiés, dont la validité est revérifiée dans la transaction avec le partenaire verrouillé (for update).
  • A09 Journalisation : création, modification (avant et après) et choix des parcours (avant et après) écrivent admin_actions dans la transaction de l'écriture.
  • Données personnelles : le contact d'un partenaire est une personne morale ou un professionnel ; il n'est lu que par l'équipe et n'est pas dans l'export RGPD d'un compte (aucun lien avec users).

Nouvelle dépendance : aucune (qrcode est déjà dans l'admin).

Retour arrière

Le revert de la PR suffit pour le code. La migration 0021 est additive : après un revert, les tables b2b_clients et b2b_client_routes restent en base sans effet ; les supprimer demande une migration à part et perd les partenaires enregistrés, leurs parcours choisis et leurs lignes de journal ne sont pas touchées. La route publique répond alors 404.

🤖 Generated with Claude Code

@Nascimento95 Nascimento95 added the level:2 Risque niveau 2 (SECURITY.md) label Oct 20, 2026
@Nascimento95 Nascimento95 mentioned this pull request Oct 20, 2026
1 task
@Nascimento95
Nascimento95 force-pushed the issue/528-recent-second-factor branch from f4bb245 to 256a22d Compare October 20, 2026 07:19
@Nascimento95
Nascimento95 force-pushed the issue/346-b2b-partners branch from 46f0ce0 to a8418f9 Compare October 20, 2026 07:19
@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise sur la base de la PR #398 corrigée (test instable de admin-catalog-places). Aucun commit n'est ajouté ni modifié ici : les commits sont les mêmes, seuls leurs identifiants changent. Lint, types et tests verts en local.

@PaulThiberville

PaulThiberville commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

📋 MANAGER, 2026-10-21 — relecture de la pile admin pour Paul (tête a8418f9). Récapitulatif : Inbox.

Verdict : B, fusionnable, avec une question à Ilan. Niveau 2, cohérent.

Vérifié :

  • requireAdminSection('b2bClients'). Choix des parcours revérifié sous verrou.
  • GET /v1/public/partners/:slug ne sert ni le contact, ni le prix, ni les notes (testé sur le corps de la réponse).
  • Migration 0021 additive, slug contraint par la base.

Points :

  • Le critère de #346 n'est tenu que côté API et QR codes. La page du site est #347.
  • Les clics et installations PostHog ne sont pas branchés : il faut d'abord décider quels événements comptent (questions déjà sur l'Inbox). Déjà suivi par #576 (needs-design), pas de nouveau ticket.
  • Question à Ilan : un partenaire prospect est servi publiquement avec isActive: false. Le nom d'un prospect en négociation est donc lisible par qui devine le slug. Faut-il plutôt un 404, comme pour suspendu et résilié ? Si oui, c'est un fix d'une ligne.
  • Labels type:feature, area:api et area:admin absents.

@PaulThiberville
PaulThiberville force-pushed the issue/528-recent-second-factor branch from 256a22d to aa4e44a Compare October 21, 2026 16:17
Base automatically changed from issue/528-recent-second-factor to main October 21, 2026 16:23
Nascimento95 and others added 14 commits October 21, 2026 18:23
…d their public page (A-10)

Types of the partners, their statuses, the 3 to 10 routes of a page, the card, the body of each write and the public page the site reads. The slug never changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
b2b_clients and b2b_client_routes, with a unique slug that is checked in the database, a price bounded in cents and at most 10 routes in order.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET /admin/b2b-clients and /:id, and POST: the active partners first, the cities of their routes, a source of measure b2b-<slug>, a journal line on each write.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PATCH /admin/b2b-clients/:id changes the fields given and journals the before and after. The slug is refused: the QR codes printed with it would stop measuring.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PUT /admin/b2b-clients/:id/routes replaces the routes of a partner, 3 to 10, in the order of its page. Only published routes can be chosen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ontact or its price (A-10)

GET /public/partners/:slug serves the name and the published routes of an active or prospect partner as cards, cached a minute. A suspended or ended partner is a 404, and no contact, price or note ever leaves.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
getB2bClients, getB2bClient, createB2bClient, updateB2bClient and setB2bClientRoutes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a panel

The card that stays on screen while the next one loads moves to a hook shared by the users and the partners.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A-10 opens on the list of the partners and the card of the one the address names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…A-10)

The price is typed in euros and sent in cents without a float, the slug follows the name until it is typed, and only the fields that changed are sent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The window that registers a partner or changes one, with its toasts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A window with a search among the published routes, an order set by arrows, and a Save button that waits for 3 to 10 routes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y its statistics are not wired (A-10)

The card gives the link of the page and the link of the stores with the source of measure, each as a QR code to download, from NEXT_PUBLIC_SITE_URL. The statistics say they are not wired yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
journalOf had no order by, so the lines written by one partner could come back in any
order and fail the test at random, as the one of the users did.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
widoo-web Ready Ready Preview Oct 21, 2026 4:32pm UTC

@PaulThiberville
PaulThiberville merged commit 95260bf into main Oct 21, 2026
9 of 11 checks passed
@PaulThiberville
PaulThiberville deleted the issue/346-b2b-partners branch October 21, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:2 Risque niveau 2 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin : partenaires B2B (A-10)

2 participants