Café Coquelicot
Étude de cas · La Rochelle · 2026 · En ligne
Boutique en ligne et back-office d'une fleuriste : paiement par carte, livraison en point relais ou à domicile, suivi des commandes.

En bref
Le prix est recalculé côté serveur à chaque commande, le paiement est confirmé par Stripe, et la préparation se suit case par case.
Ce qui a été réalisé
- Vitrine et catalogue, avec choix de taille et de coloris
- Commande et paiement par carte (Stripe), livraison en point relais ou à domicile
- Back-office : produits, suivi des commandes, tableau de bord, contenu des pages
- Jeux de tests automatiques, unitaires et dans un navigateur
Le point de départ
La boutique en ligne d'un atelier de fleurs fraîches et séchées : bouquets, compositions et vases, et des prestations pour les mariages, les événements et les entreprises. Le cahier des charges : une vitrine qui donne envie, un paiement fiable, une livraison simple, et un back-office pour préparer les commandes au quotidien.
Le projet s'étale du 10 juin au 24 septembre 2026 : première version de la boutique le 11 juin, livraison en point relais le 19 juin ; en septembre, livraison à domicile, éditeur de contenu, e-mails de commande et pages légales.
Les décisions
7 choix, expliqués simplement. Le détail technique s'ouvre d'un clic.
Un univers d'atelier plutôt qu'un gabarit de boutique
Palette lin et bordeaux, illustrations dessinées au trait, grain de papier sur la page : la boutique a l'allure d'un atelier de fleuriste.
Le prix vient de la base de données, pas du panier
À chaque paiement, le serveur refait le calcul à partir des produits enregistrés : le montant affiché dans le navigateur ne compte pas. La commande passe de « en attente » à « payée » quand Stripe le confirme.
Le détail technique
- Le navigateur n'envoie que slug, sizeId, colorId et qty ; les produits actifs sont relus en base, les frais de livraison et de carte recalculés (lib/orders.ts) et les line_items Stripe construits sur priceCentsSnapshot. Le panier vit en localStorage (coquelicot.cart.v1).
- Webhook : en-tête stripe-signature obligatoire, constructEventAsync, 400 si invalide, 500 si le secret manque. Quatre événements : checkout.session.completed et async_payment_succeeded (commande payée si payment_status vaut paid), expired et async_payment_failed (commande annulée).
- La page de confirmation refait le marquage de façon idempotente (UPDATE … WHERE status = 'pending' RETURNING) : un second passage ne rejoue ni l'étiquette ni les e-mails.
- Le webhook est testé en unitaire sur des charges fabriquées et signées avec generateTestHeaderString (8 cas). En parcours de navigateur, Stripe ne peut pas joindre localhost : c'est la page de confirmation qui marque la commande payée.
Un back-office centré sur la préparation des commandes
Les commandes payées arrivent dans un tableau de suivi à quatre colonnes : en attente, en cours de traitement, à expédier, terminée. Chaque article a sa case à cocher : deux bouquets, deux cases. Un tableau de bord additionne les ventes.
Le détail technique
- Kanban : colonnes En attente, En cours de traitement, À expédier, Terminée (lib/prep-status.ts), avec bascule vers un tableau. Une carte terminée quitte le tableau après 48 h ; la commande reste en base.
- Une case par unité (order_items.prepared_qty). Statut dérivé : aucune unité préparée donne « en attente », une partie « en cours de traitement », toutes « à expédier ».
- Tableau de bord calculé en SQL (lib/stats.ts) : chiffre d'affaires, nombre de commandes, panier moyen et articles vendus ; courbe sur 30 jours glissants, 5 meilleurs produits, 5 dernières commandes.
- Produits : tailles (libellé et prix) et coloris (cosmétiques), copiés dans la commande ; galerie de photos par produit, chaque photo pouvant être rattachée à un coloris. Au jeu d'essai, seuls deux produits (solana, lagune) ont des variantes.
Livraison en point relais ou à domicile
Au moment de commander, le client choisit un point relais Mondial Relay ou la livraison à domicile par Colissimo, en France uniquement. Le code envoie la demande d'expédition à Mondial Relay dès que le paiement est confirmé. Les deux tarifs se règlent dans les paramètres de la boutique.
Le détail technique
- Mondial Relay : widget officiel dans le checkout ; expédition créée par POST /api/shipment en XML (lib/mondial-relay/client.ts), idempotente, déclenchée par le webhook et par la page de confirmation, avec un bouton de secours dans l'admin.
- Colissimo à domicile depuis le 19 septembre 2026 : étiquette faite à la main, numéro de suivi saisi dans l'admin, aucune API Colissimo ; préfixes 97 et 98 refusés, Corse acceptée.
- Tarifs par défaut du code : 4,90 € (Mondial Relay) et 7,90 € (Colissimo), modifiables dans /admin/parametres ; 2,00 € pour la carte manuscrite. Périmètre écrit dans une spec avant le code (19 juin pour Mondial Relay, 19 septembre pour Colissimo).
Une vitrine qui démarre sans rien configurer
L'application crée elle-même sa base de données sur le poste, la met à jour et remplit le catalogue : la vitrine se parcourt tout de suite. L'administration et le paiement demandent en plus quelques réglages.
Le détail technique
- PGlite (dossier .data/pglite par défaut) quand DATABASE_URL est absent ; migration et seedIfEmpty au démarrage ; compte admin créé si ADMIN_EMAIL est défini (lib/db/client.ts). Le code prévoit Supabase en production, par le pooler transactionnel (prepare: false).
- Réglages requis pour l'administration et le paiement : un secret de session, un compte administrateur et une clé Stripe.
- Drizzle : 16 migrations versionnées (0000 à 0015) et 10 tables, lancées à la main avec npm run db:migrate ; le build ne migre pas.
- Les tests unitaires utilisent PGlite en mémoire ; les tests de parcours, une base dédiée vidée avant chaque série (.data/pglite-e2e).
Des tests écrits jusqu'au paiement
Le projet contient 500 tests unitaires (panier, commandes, statistiques, confirmation de paiement, connexion, e-mails) et 47 tests de parcours dans un navigateur, dont deux qui vont jusqu'au paiement avec le mode test de Stripe.
Le détail technique
- Parcours jusqu'au paiement : 05-checkout et 12-checkout-colissimo, page Stripe hébergée et carte de test 4242, réseau et clés Stripe de test requis. Vitest tourne sur PGlite en mémoire ; Playwright, avec un seul worker, sur le build de production (port 3105).
Sécurité et vie privée
Les pages d'administration ne s'ouvrent qu'après connexion. Les mots de passe ne sont pas stockés en clair, et une connexion est annulée quand le mot de passe change. Le bandeau de mesure d'audience peut être refusé.
Le détail technique
- Session JWT (jose, HS256, 7 jours) dans un cookie httpOnly, secure en production, sameSite lax. Mots de passe hachés avec bcrypt, avec un hash factice constant contre l'énumération de comptes. Session nulle si passwordChangedAt est postérieur à iat.
- Garde de /admin dans proxy.ts (contrôle optimiste) ; la vérité est verifySession() dans lib/auth/dal.ts, appelée page par page. Plusieurs comptes possibles (table admin_users).
- Bandeau : Consent Mode v2, tout refusé par défaut, boutons « Refuser » et « Accepter » au même niveau, choix en localStorage, réouverture par « Gérer les cookies ». Il n'apparaît que si NEXT_PUBLIC_GA_MEASUREMENT_ID est un vrai identifiant.
En chiffres
Comptés dans le code du projet.
- 500
- tests unitaires (Vitest), sur une base PostgreSQL en mémoire
- 47
- tests de parcours (Playwright), joués sur le build de production
- 4
- colonnes de suivi dans le tableau des commandes
- 16
- migrations de base de données (Drizzle), pour 10 tables
- 4
- événements Stripe écoutés par le webhook signé
Captures d'écran
Cliquez sur une capture pour l'ouvrir en grand.
(ouvre l'image en grand dans un nouvel onglet)
(ouvre l'image en grand dans un nouvel onglet)
(ouvre l'image en grand dans un nouvel onglet)Le résultat
- Une commande payée par carte arrive directement dans le tableau de préparation, avec son mode de livraison.
- Produits, prix, photos et contenu des pages se modifient depuis le back-office, sans développeur.
- Un autre développeur peut lancer la vitrine sans base de données à installer.
Ce que j'en retiens
Sur une boutique, le montant envoyé par le navigateur ne doit jamais faire foi : un prix modifié coûte cher. Et un back-office organisé autour du geste de préparation, une case par bouquet, parle plus à une fleuriste qu'un tableau d'administration générique.
