Projet client · Réf. CW-03

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.

Plateformes

iOS · Android

  • Le prochain jour de service — mardi 15 septembre, 11h00 à 21h00, marqué ouvert, clôture des commandes dans quatre heures — avec les plats populaires tarifés en roupies, un événement à venir et les autres dates publiées en dessous.
    Jours de service
  • Livraison en direct de la commande TT-2397 sur une carte de Curepipe : la position approximative du livreur, l’arrêt du client épinglé, « vous êtes l’arrêt 2, il y a 1 livraison avant la vôtre », une arrivée estimée à 19h40, et une mention rappelant que les autres clients restent anonymes.
    Suivi en direct
  • Le Double Smash Burger à 320 Rs : sa composition, ses allergènes, un choix de taille obligatoire avec le triple à +90 Rs, ses suppléments, et un bouton d’ajout portant le total courant.
    Fiche produit
  • La commande TT-2397 en livraison : le paiement vérifié de 470 Rs avec sa référence Juice et la mention « paiement et commande sont suivis séparément », au-dessus d’une frise en cinq étapes, de la soumission à la mise en livraison.
    Détail de commande
Écrans de l’application en fonctionnement sur un appareil Android, connectée à sa propre API sur une base amorcée en local. Plats, commandes et tournée inventés : jamais de données de production.

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.

Le rôle de StyloTech

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.

Bientôt sur les stores
  • 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à.

Site du produittasty-tum.stylotech.mu
Plateformes
  • iOS
  • Android
Construit avec
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
Ce que StyloTech a livré
  • 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.

01

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.

02

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.

03

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.

04

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.

05

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.

06

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.

07

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.

08

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.

09

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

10

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.

11

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.

12

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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
  7. 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.

  8. 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
  9. 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.

Tasty Tum! — Projet client — StyloTech