My Fruitopia
La cueillette du matin, commandée avant l’heure limite et suivie jusqu’à la porte.
iOS · Android

Liste du jour 
Suivi en direct 
Fiche produit 
Mes commandes
Vendre des fruits et des légumes à la journée n’a rien du commerce de détail. Ce qui existe change chaque matin, cela se vend au kilo ou à la pièce, et cela doit arriver sur un bureau ou devant une porte tant que cela vaut encore la peine d’être acheté. Un catalogue est la mauvaise forme pour ça. Une liste valable un jour est la bonne.
Définition du produit, design d’interface, architecture applicative, application Flutter côté client comme côté vendeur, backend et modèle de données Supabase, règles de commande, de stock et de livraison, suivi en direct, notifications — et le travail de mise en production en cours.
- 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
- Cubit
- go_router
- get_it
- Clean Architecture
- Backend
- Supabase
- PostgreSQL
- Row Level Security
- Postgres RPC
- Supabase Realtime
- Infrastructure
- Firebase Cloud Messaging
- Supabase Edge Functions
- Supabase Storage
- 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.
La liste du jour
Le vendeur publie ce qui est réellement arrivé le matin, avec sa date de livraison, son heure limite, les régions desservies et les frais de livraison. Rien n’y est en rupture, puisque la liste est le stock.
Tarifé dans son unité
Au kilo, au demi-kilo, par paliers de 100 g, ou à la pièce, à la boîte, au paquet, à la douzaine ou au bouquet. Chaque article garde l’unité dans laquelle il se vend au lieu d’être ramené à une seule.
Des commandes qui se ferment
L’heure limite et la date de livraison sont à l’écran dès le premier geste. Une fois l’heure passée, la liste n’est plus proposée : personne ne remplit un panier pour une camionnette déjà partie.
Une commande par jour de livraison
Commander à nouveau pour le même jour complète la commande déjà ouverte au lieu d’en créer une seconde — une livraison pour le client, un colis pour le vendeur, et une ligne par article sur la liste de préparation.
Domicile ou bureau
Une région de livraison, et un immeuble de bureaux lorsque c’est la destination. Choisi une fois et retenu ensuite ; sur une commande fusionnée, c’est le dernier passage en caisse qui décide où elle va.
Suivi de livraison en direct
Une fois la camionnette partie : sa position sur une carte, les arrêts restants et une estimation qui bouge avec elle — sans rien rafraîchir.
Historique des commandes
Commandes en cours et livrées, avec leurs articles, leurs totaux et leurs dates, pour que recommander l’habituel prenne quelques secondes.
Notifications push
La liste du jour qui paraît, une commande confirmée, une livraison qui démarre — on est prévenu plutôt que d’aller voir. Le vendeur choisit les alertes qu’il souhaite.
Diffusions
Un message à tous les clients d’une région choisie, rédigé et envoyé depuis le téléphone du vendeur.
Stock & prix de revient
Produits, photos, catégories et prix de revient, tenus derrière la liste que voit le client.
Préparation de la tournée
Les commandes du jour rassemblées en une tournée par région et par immeuble, puis parcourues arrêt par arrêt, la progression étant enregistrée au fur et à mesure.
Chiffre d’affaires & marge
Les recettes, la marge calculée sur le prix de revient figé au moment de la commande, et ce qui s’est réellement vendu.
Un panier, retracé
Un client met une mangue dans son panier et valide sa commande. Voici tout ce qui se passe entre ce geste et la camionnette qui arrive à sa porte.
Relevé dans supabase_checkout_data_source.dart, place_customer_order dans supabase/schema.sql et order_tracking_cubit.dart. Chaque nom ci-dessous apparaît dans le code exactement tel quel.
- 01
La liste est le stock
Chaque jour de livraison, le vendeur publie une liste : ce qui a été cueilli, à quel prix, ce qu’il en reste, les régions desservies, les frais de livraison et l’heure de clôture. L’accueil du client, c’est cette ligne et les articles qui y sont rattachés — pas un catalogue filtré sur ce qui se trouve être disponible.
- daily_lists
- daily_list_items
- order_deadline
- delivery_fee
- 02
La commande se ferme à l’heure
L’heure limite et la date de livraison sont à l’écran dès le premier geste. Une liste dont l’heure est passée est écartée au moment où la vitrine est chargée : elle cesse d’être proposée plutôt que d’échouer à la fin. Personne ne remplit un panier qu’il ne pourra pas valider.
- order_deadline
- delivery_date
- 03
La validation est un appel, pas une insertion
L’application n’écrit jamais dans la table des commandes. Elle confie le panier entier à une seule fonction security definer et laisse la base décider — ce qui fait que la commande, ses lignes et le stock sont soit tous faits, soit aucun.
- place_customer_order
- security definer
Ce qui s’affiche en cas de refus
« Not authenticated ». La fonction lit le client depuis auth.uid() et refuse net s’il n’y a personne, quoi que l’application ait envoyé.
- 04
Deux gestes ne peuvent pas scinder une commande
Avant toute lecture, la transaction prend un verrou consultatif indexé sur le client et la date de livraison. Deux validations simultanées pour le même jour verraient sinon toutes deux « aucune commande ouverte » et en inséreraient chacune une — précisément la scission que la fonction existe pour empêcher. La seconde attend, puis trouve la première.
- pg_advisory_xact_lock
- 05
Une commande ouverte est complétée, jamais doublée
Une commande en attente et impayée, pour le même vendeur et le même jour, est celle à compléter ; ce qui est livré ou déjà réglé est une affaire close et en ouvre une nouvelle. Le même produit commandé deux fois vient grossir sa ligne au lieu de la répéter, pour que la liste de préparation garde une entrée par article — et l’adresse suit le dernier passage en caisse, parce qu’une commande fusionnée est une seule dépose.
- orders
- order_items
- fulfillment
- payment
- 06
Le stock se retire de la liste au fil de l’eau
Chaque ligne décrémente la liste du jour, plancher à zéro pour qu’une course ne la fasse pas passer en négatif. Le « plus que 8 » que lira le client suivant est le nombre que cette commande vient de déplacer.
- decrement_daily_list_stock
- daily_list_items
- stock
- 07
Les totaux sont recalculés, jamais rafistolés
Une fois les lignes en place, le sous-total est resommé depuis les propres lignes de la commande et les frais de livraison ajoutés une seule fois. Une commande fusionnée ne peut jamais contredire ce qu’elle contient, et la livraison n’est jamais facturée deux fois.
- subtotal
- delivery_fee
- total
- 08
Le vendeur en est averti
L’insertion de la commande déclenche un trigger et son complément un autre, chacun écrivant une notification — mais seulement si le vendeur a activé cette alerte. L’insertion de cette ligne appelle ensuite une edge function avec son seul identifiant : une requête forgée ne peut choisir ni le destinataire ni le texte, puisque la fonction lit les deux dans la table elle-même.
- notify_seller_new_order
- on_order_topped_up
- notifications
- push-on-notification
- 09
Puis le client la regarde arriver
L’écran de suivi s’abonne à deux flux de changements Postgres filtrés sur ce vendeur — la position de la camionnette et la progression de la tournée. La carte bouge quand elle bouge, et un arrêt terminé raccourcit l’estimation immédiatement plutôt qu’au prochain déplacement significatif.
- seller_locations
- delivery_progress
- onPostgresChanges
Détail technique
Relevé dans le code source du projet, pas dans une plaquette commerciale.
- Architecture
- Clean Architecture orientée fonctionnalités, gestion d’état par Cubit, shells go_router et localisateur de services get_it ; la couche domaine est en Dart pur et renvoie une valeur Result au lieu de lever une exception à la frontière
- Backend
- Supabase — PostgreSQL avec sécurité au niveau des lignes, fonctions security definer pour la commande et le stock, et une edge function qui envoie le push
- Modèle de données
- 18 migrations versionnées par-dessus le schéma de base, couvrant les listes du jour, les tournées et leur progression, les prix de revient, les frais de livraison, les rappels de panier et le moyen de paiement
- Temps réel
- Les flux de changements Postgres sur seller_locations et delivery_progress alimentent la carte du client ; la position de la camionnette et les arrêts restants se mettent à jour sans rafraîchissement
- Capacités de l’appareil
- Rendu cartographique (flutter_map), localisation de l’appareil (geolocator), prise et recadrage de photos produit, notifications locales en complément du push
- Diffusion
- Version 0.0.15, build 16 à ce jour ; une seule base Flutter pour iOS et Android, en anglais et en français, en thème clair et sombre
La cueillette du matin, commandée avant l’heure limite et suivie jusqu’à la porte.
