Tasty Tum!
De la street food vendue les jours où le grill est allumé, payée hors de l’application et suivie jusqu’à la porte.
iOS · Android

Jours de service 
Suivi en direct 
Fiche produit 
Détail de commande
Une cuisine de rue n’est pas un restaurant avec des horaires. Elle cuisine les jours qu’elle choisit, depuis les endroits qu’elle choisit, et elle finit par tout vendre. À Maurice, on paie aussi par Juice — un virement avec une référence, pas une passerelle bancaire — donc l’argent arrive avant que quiconque ait accepté de cuisiner, et quelqu’un doit rapprocher les deux. Ces deux réalités devaient vivre dans le produit, pas être contournées par le support.
Définition du produit, design d’interface, architecture des deux moitiés, application Flutter côté client comme côté vendeur, API NestJS et modèle de données PostgreSQL, tarification et promotions, flux de vérification des paiements, tournées de livraison et suivi en direct, notifications, et la mise en production.
- App Store
- Google Play
Les deux fiches sont en cours de publication sous le nom actuel de l’application. L’application, elle, tourne déjà.
- iOS
- Android
- Application
- Flutter
- Dart
- BLoC
- go_router
- get_it
- Clean Architecture
- Backend
- NestJS
- TypeScript
- Prisma
- Argon2id
- JWT
- Socket.IO
- Données
- PostgreSQL
- Serialisable transactions
- Infrastructure
- Docker Compose
- nginx
- Firebase Cloud Messaging
- OpenStreetMap
- OSRM
- Qualité & livraison
- Jest
- Supertest
- ESLint
- Prettier
- flutter_test
- Stratégie produit
- Design UI/UX
- Design system
- Architecture technique
- Développement mobile
- Développement backend
- Conception de la base de données
- Intégrations
- Tests & assurance qualité
- Mise en production
- Maintenance
Ce que ça fait
Chaque élément répond à une tâche réelle le jour de l’épreuve, pas à une liste de fonctionnalités. Le site du produit les détaille un par un.
Des jours, pas des horaires
Rien ne peut être commandé en dehors d’un jour de service publié par le vendeur, et seulement avant l’heure limite de ce jour-là. Quand rien n’est ouvert, l’application le dit et propose la date suivante plutôt que de prendre une commande qu’elle ne pourrait pas honorer.
La carte appartient au jour
Chaque jour de service porte ses propres plats, ses propres prix, ses propres points de retrait et ses propres régions de livraison. Le prix d’hier n’engage rien, et un plat retiré un jour reste disponible le suivant.
Plusieurs points de retrait à la fois
Plusieurs points de retrait peuvent fonctionner le même jour, à des heures différentes ou qui se chevauchent, chacun avec ses créneaux et sa capacité. Un créneau affiché complet est complet.
Livré à un point, pas à une région
Le client place lui-même le point exact sur la carte et ajoute un repère et des consignes pour le livreur. Les frais découlent de la région, et certaines régions n’en ont aucun.
Paiement Juice, vérifié par une personne
Le client vire le montant par Juice, puis transmet la référence de transaction et une capture d’écran. Le vendeur la compare à son propre historique Juice et la marque vérifiée ou rejetée, motif à l’appui.
Payé et accepté restent distincts
L’arrivée de l’argent n’est pas l’accord de la cuisine. Les deux états sont suivis et affichés séparément, un paiement rejeté met la commande en attente au lieu de l’annuler, et une preuve corrigée la remet dans la file.
Des offres qui s’appliquent seules
Articles offerts, pourcentages, montants fixes et prix de lot, avec des conditions de date, de produit, de quantité et de total. Les offres éligibles s’appliquent à la validation sans code, et un code non éligible explique pourquoi.
Livraison en direct, mais bornée
Une fois la tournée lancée : position dans la file, nombre d’arrêts devant, estimation qui évolue, et la position approximative du livreur. Chaque client ne reçoit que son propre arrêt, et la position est volontairement arrondie avant d’être envoyée.
Bons de cuisine
Chaque commande devient un bon avec ses variantes, ses suppléments, ses allergènes et la note du client, pour que ce qui arrive au grill soit ce qui a été commandé.
Tournées planifiées, puis conduites
Le vendeur regroupe les livraisons d’un jour en une tournée, ordonne les arrêts, puis la conduit depuis la même application — en ne partageant sa position que tant que la tournée est réellement active.
Des événements avec leurs pré-commandes
Un festival ou un marché a ses propres dates, ses propres créneaux et sa propre carte, tarifés et plafonnés indépendamment des jours de service ordinaires.
Chiffre d’affaires, marge et coûts
Les recettes par date, par produit et par mode de retrait, mesurées contre le prix de revient enregistré au moment de la vente — la marge est donc ce qui s’est réellement passé, pas ce que suggère le tarif actuel.
Une commande, retracée
Un client ouvre l’application un jour où le grill est allumé et commande deux burgers à domicile. Voici tout ce qui se passe entre ce geste et l’arrivée du repas.
Relevé dans pricing.service.ts, orders.service.ts, payments.service.ts et tracking.gateway.ts. Chaque nom ci-dessous apparaît dans le code exactement tel quel.
- 01
Il faut d’abord déclarer un jour
Il n’y a pas de vitrine permanente. Le vendeur publie un jour de service avec ses horaires, son heure limite de commande et le fait qu’il propose le retrait, la livraison ou les deux — et tout ce que voit le client découle de cette ligne.
- WorkingDate
- orderCutoffAt
- FulfilmentCapability
Ce qui s’affiche en cas de refus
Passé l’heure limite, ou sur une date jamais publiée, la date n’est tout simplement pas commandable : l’application propose la suivante au lieu de recueillir un panier qu’elle devrait refuser.
- 02
Le prix vient du serveur
Le panier est envoyé à l’API pour être tarifé. Prix des lignes, suppléments, offres et frais de livraison y sont résolus contre la carte de ce jour-là, et l’application affiche ce qui revient plutôt que ce qu’elle aurait calculé elle-même.
- POST /cart/price
- PricedCart
Ce qui s’affiche en cas de refus
Un code promotionnel non éligible revient comme un échec nommé, le panier tarifé intact : le client voit pourquoi il ne s’est pas appliqué sans perdre sa commande.
- 03
Une livraison exige un point réel
Pour la livraison, le client place un point, pas seulement une région. La région détermine les frais et l’éventuel minimum ; le point est ce vers quoi le livreur conduit et ce autour de quoi la tournée est planifiée.
- latitude
- longitude
- DeliveryRegion
- fee
Ce qui s’affiche en cas de refus
Une adresse sans coordonnées est refusée d’emblée — « an exact delivery pin is required » — avant même de refaire le travail de tarification.
- 04
La validation retarifie ce qu’elle va écrire
Valider ne fait pas confiance aux montants que l’application a en mémoire. Le même panier est tarifé à nouveau en mode strict, dans une transaction sérialisable, et la commande est écrite à partir de ce résultat.
- price
- strict: true
- serialisable
Ce qui s’affiche en cas de refus
Tout ce qui a bougé depuis le dernier regard du client — un plat épuisé, un prix modifié, un créneau pris — échoue au passage strict et la commande n’est pas écrite.
- 05
La capacité est prise, pas vérifiée
La date, le créneau et la fenêtre de livraison sont pris chacun par une mise à jour conditionnelle qui porte son propre prédicat de capacité. Une ligne remplie entre la tarification et l’écriture ne met rien à jour.
- reserveCapacity
- updateMany
- count === 0
Ce qui s’affiche en cas de refus
Zéro ligne mise à jour, c’est le refus : « cette date est complète ». Deux clients qui se disputent le dernier créneau ne peuvent pas l’obtenir tous les deux.
- 06
Puis le client paie, hors de l’application
La commande existe avant l’argent. Le client vire par Juice et transmet la référence et une capture d’écran, conservées comme une tentative numérotée rattachée au paiement — une preuve corrigée s’ajoute au dossier au lieu de l’écraser.
- Payment
- PaymentProof
- PENDING_VERIFICATION
- 07
Une personne vérifie ; l’acceptation est autre chose
Le vendeur compare la référence à son historique Juice et marque le paiement vérifié ou rejeté. Accepter la commande est une décision distincte, prise séparément — et l’application montre les deux états au lieu de les confondre.
- PaymentStatus.VERIFIED
- OrderStatus.ACCEPTED
Ce qui s’affiche en cas de refus
Un paiement rejeté porte son motif et laisse la commande ouverte plutôt que de l’annuler ; le client renvoie une preuve et elle retourne dans la file.
- 08
La livraison est une tournée, pas un statut
Le vendeur regroupe les livraisons du jour en une tournée, ordonne les arrêts et prend la route. Le partage de position commence avec la tournée et cesse dès qu’elle est mise en pause, terminée ou annulée — il est lié à l’état de la tournée, jamais à un écran resté ouvert.
- DeliveryRun
- DeliveryStop
- DriverLocation
- 09
Chaque client ne voit que son arrêt
Le socket de suivi place chaque client dans un salon qui lui est propre, rattaché à sa commande, et construit une charge utile distincte pour chacun : sa place dans la file, les arrêts devant lui, son estimation et une position approximative du livreur.
- subscribe:order
- customerRoom
- coarsenCoordinate
Ce qui s’affiche en cas de refus
S’abonner à une commande qui n’est pas la sienne est refusé au niveau du socket, avant la moindre construction de charge utile — l’autorisation porte sur l’entrée dans le salon, pas sur ce que l’écran choisit d’afficher.
Détail technique
Relevé dans le code source du projet, pas dans une plaquette commerciale.
- Architecture
- Clean Architecture orientée fonctionnalités en Flutter — BLoC pour l’état, go_router avec shells à état, get_it et injectable pour le graphe d’objets, Retrofit sur Dio pour l’API ; 211 fichiers Dart écrits à la main
- Backend
- Sa propre API NestJS sur PostgreSQL via Prisma — 35 modèles, 22 modules fonctionnels, hachage Argon2id des mots de passe et des codes à usage unique, jetons de rafraîchissement rotatifs dont la réutilisation invalide toute la famille
- Argent
- Aucun total n’est jamais calculé sur l’appareil. Chaque montant vient de POST /cart/price, et la validation d’une commande retarifie le même panier côté serveur en mode strict avant la moindre écriture
- Confidentialité du suivi
- Salons Socket.IO par commande : un client ne peut s’abonner qu’à une commande qui lui appartient, chaque charge utile est construite pour ce client-là, et les coordonnées du livreur sont arrondies à environ 11 m avant d’être envoyées
- Capacités de l’appareil
- Rendu cartographique et placement du point (flutter_map sur OpenStreetMap), localisation (geolocator), envoi de capture d’écran comme preuve de paiement, Firebase Cloud Messaging en plus des notifications locales
- Qualité
- 100 tests unitaires et 198 tests de bout en bout sur l’API, 109 tests unitaires et de widgets côté Flutter, avec lint, vérification de types et formatage au vert des deux côtés
De la street food vendue les jours où le grill est allumé, payée hors de l’application et suivie jusqu’à la porte.
