Billetterie self-hosted pour le spectacle de fin d’année de l’école de danse Desha-Moulin, au Centre Culturel de Bergerac, salle en éventail, 754 places modélisées (rangées A→Y). Pas de SaaS, pas de commission : un serveur auto-hébergé, une base SQLite, et c’est tout. Développée comme un sous-projet du site de l’école.
Principes métier verrouillés
- Pas de paiement en ligne, les familles paient au studio, l’admin marque « payé » à la main.
- Pas de choix du siège, les familles demandent N places et ne choisissent jamais leur siège.
- Anti-survente, une demande
pendingconsomme la jauge (un compteur, jamais des sièges précis) → la salle ne peut pas être survendue.
Placement et paiement
- Placement manuel et assisté, l’algo propose jusqu’à 3 suggestions, un humain valide toujours.
- Indépendance, on peut « placer maintenant, payer plus tard ».
- Règlement par versements, espèces / chèques échelonnés avec date de dépôt ; une demande est soldée quand le net atteint le dû, sinon c’est un acompte.
Billets et entrée
- Billets QR générés et scan à l’entrée via html5-qrcode.
- Emails via react-email + Brevo.
- Formulaire public protégé contre les robots par Cloudflare Turnstile.
Qualité
- 761 tests unitaires et d’intégration (Vitest), sur des bases SQLite jetables : placement, montants, versements, jauge, authentification, emails. Ils tournent en CI à chaque push sur la branche principale et sur chaque demande de fusion, avec la vérification des types.
- Suite E2E Playwright écrite à partir d’un cahier de test : chaque cas a un identifiant, une priorité (P1 à P3), des préconditions, des étapes et un résultat attendu. 58 parcours de bout en bout passent au vert en CI, et les 12 cas encore désactivés ont chacun leur raison documentée.
- Rapport Allure, les résultats E2E alimentent un tableau de bord Allure central ; le rapport HTML se consulte aussi depuis l’administration.
- Droits RGPD, droit d’accès (export du dossier d’une famille) et droit à l’effacement, traités depuis l’administration, et anonymisation automatique des demandes trente jours après la représentation, sans fausser les statistiques.
Côté produit
Contexte. Une école de danse vend les places de son spectacle de fin d’année dans une salle de 754 places. Les familles règlent au studio, en espèces ou par chèques échelonnés, et les billets sont scannés à l’entrée le soir même.
Mon rôle. Product Owner et développeur, avec des agents IA : je fixe les règles métier, j’arbitre le périmètre, j’écris le cahier de test et je livre.
Décisions.
- Placement et paiement découplés : on peut placer avant d’encaisser. « Premier payé, premier placé » reste recommandé mais n’est plus imposé, et les billets d’une demande non réglée ne partent jamais sans action explicite.
- Le placement assisté a d’abord été spécifié, avec une version naïve de référence et un simulateur pour comparer ; l’algorithme propose, un humain valide toujours.
- Deux tarifs, adulte et enfant : les places offertes se déduisent des enfants d’abord, et un tarif manquant affiche un montant « inconnu » plutôt qu’un montant faux.
- Partage d’une place : un code à dicter ouvre le billet en lecture seule, sans les coordonnées de la famille ; le billet papier reste possible et le scan ne bloque pas par défaut, une sécurité à la mesure d’un spectacle d’école.
- Clôture d’une édition : archivage réversible, lecture seule plutôt que masquage, rien n’est supprimé, et aucune modification automatique des demandes encore en attente.
- Anonymisation différée tant qu’un chèque reste à déposer : il faut alors savoir qui relancer.
Résultat. En ligne. 761 tests unitaires et d’intégration et 58 parcours E2E tournent en CI, et les droits d’accès et d’effacement sont outillés dans l’administration.
Stack Next.js App Router + Prisma / SQLite, en ligne sur billets.cours-danse-bergerac.fr, auto-hébergée.
Self-hosted ticketing for the year-end show of the Desha-Moulin dance school, at the Centre Culturel de Bergerac, fan-shaped hall, 754 seats modeled (rows A→Y). No SaaS, no commission: a self-hosted server, a SQLite database, and that’s it. Built as a sub-project of the school’s website.
Locked-in business rules
- No online payment, families pay at the studio, the admin marks “paid” by hand.
- No seat selection, families request N seats and never choose their seat.
- Oversell-proof, a
pendingrequest counts against capacity (a single counter, never specific seats) → the hall cannot be oversold.
Seating and payment
- Manual and assisted seating, the algorithm proposes up to 3 suggestions, a human always validates.
- Independence, you can “seat now, pay later”.
- Payment in installments, cash / staggered checks with deposit date; a request is settled when the net reaches the amount due, otherwise it’s a down payment.
Tickets and entry
- QR tickets generated and door scanning via html5-qrcode.
- Emails via react-email + Brevo.
- Public form protected against bots by Cloudflare Turnstile.
Quality
- 761 unit and integration tests (Vitest), on throwaway SQLite databases: seating, amounts, installments, capacity, authentication, emails. They run in CI on every push to the main branch and on every pull request, along with type checking.
- Playwright E2E suite written from a test plan: each case has an ID, a priority (P1 to P3), preconditions, steps and an expected result. 58 end-to-end journeys pass in CI, and each of the 12 cases still disabled has its reason documented.
- Allure report, E2E results feed a central Allure dashboard; the HTML report can also be viewed from the admin area.
- GDPR rights, right of access (export of a family’s record) and right to erasure, handled from the admin area, plus automatic anonymization of requests thirty days after the show, without skewing the statistics.
Product side
Context. A dance school sells the seats for its year-end show in a 754-seat hall. Families pay at the studio, in cash or with staggered checks, and tickets are scanned at the door on the night.
My role. Product Owner and developer, working with AI agents: I set the business rules, arbitrate scope, write the test plan and deliver.
Decisions.
- Seating and payment decoupled: a request can be seated before it is paid. “First paid, first seated” remains the recommended practice but is no longer enforced, and tickets for an unpaid request are never sent without an explicit action.
- Assisted seating was specified first, with a naive reference version and a simulator to compare them; the algorithm suggests, a human always validates.
- Two prices, adult and child: complimentary seats are deducted from children first, and a missing price shows an “unknown” amount rather than a wrong one.
- Sharing a seat: a code to read out opens the ticket in read-only mode, without the family’s contact details; paper tickets remain available and scanning does not block by default, security scaled to a school show.
- Closing an edition: reversible archiving, read-only rather than hidden, nothing deleted, and no automatic change to requests still pending.
- Anonymization postponed while a check is still to be deposited: the school then needs to know whom to contact.
Outcome. Online. 761 unit and integration tests and 58 E2E journeys run in CI, and the rights of access and erasure are built into the admin area.
Stack Next.js App Router + Prisma / SQLite, online at billets.cours-danse-bergerac.fr, self-hosted.