Brief frontend DiddiFood — 2026-10-30

Ce document est le brief frontend courant pour le client DiddiFood et l'espace restaurant Pro/Cuisine. Le contrat technique de référence reste docs/api/openapi.json. Les montants sont en XOF et les dates métier utilisent Africa/Abidjan.

État de validation

Le contrat local OpenAPI a été régénéré et vérifié avec l'application actuelle. Après le redéploiement du 30 octobre, https://diddifood-backend-staging.diddifree.com/health et /health/ready retournent 200. Le contrat public vérifié est celui servi par /openapi.json sur ce même domaine.

1. Règles communes au frontend

URL et authentification

  • Définir une seule DIDDIFOOD_API_BASE_URL par environnement. En staging, l'URL attendue est https://diddifood-backend-staging.diddifree.com.
  • Envoyer Authorization: Bearer <JWT DiddiFreeID> sur toutes les routes authentifiées.
  • Envoyer un X-Request-ID UUID par opération importante et le conserver dans les logs et les écrans d'erreur Support.
  • Ne jamais mettre un secret DiddiPay, DiddiFiles ou S2S dans le frontend.

Format

  • Les identifiants sont des UUID sauf indication contraire.
  • Les montants sont des entiers XOF : 2500 signifie 2 500 XOF. Ne pas envoyer de montant flottant et ne pas convertir en centimes d'une autre devise.
  • Les dates sont ISO 8601 avec fuseau. Afficher les heures métier en Africa/Abidjan.
  • Une réponse 503 signifie qu'une dépendance backend est indisponible ou non configurée. Afficher un état d'attente ou d'erreur, jamais une réussite.
  • Après une reconnexion ou une notification, relire la commande, le paiement et la livraison par HTTP.

Portes de déploiement à respecter

DIDDIFOOD_REQUIRE_ONLINE_PAYMENT_BEFORE_PREPARATION=true
DIDDIFOOD_PICKUP_ENABLED=false
DIDDIFOOD_SUBSTITUTION_ENABLED=false
DIDDIFOOD_RESTAURANT_TOPUP_ENABLED=false

Le frontend ne doit pas afficher le retrait sur place, les substitutions ou la recharge wallet lorsque ces portes sont à false.

2. Parcours client

2.1 Catalogue

  1. Appeler GET /food/v1/restaurants.
  2. Paramètres disponibles : limit (1–100), offset, q, category, is_open, latitude, longitude, radius_km.
  3. latitude, longitude et radius_km doivent être envoyés ensemble. Le rayon est en kilomètres et doit être positif, au maximum 10.
  4. La réponse est RestaurantListOut : items, limit, offset, total.
  5. Une carte restaurant peut utiliser image_url, rating, review_count, is_open, open_hours, delivery_time_minutes et delivery_fee_minor_units. image_url peut être null.

Catégories actuellement acceptées à la création restaurant : fast_food, maquis, pizza, traiteur, ivoirien. La recherche par texte est légère et porte sur les informations indexées par Food.

2.2 Menu

Appeler GET /food/v1/restaurants/{restaurant_id}/menu.

Chaque plat expose notamment id, name, description, price_minor_units, category, is_available, unavailable_until et image_url. Une image absente doit afficher un fallback ; Food ne stocke pas le fichier lui-même, DiddiFiles en reste propriétaire.

2.3 Devis avant commande

Appeler POST /food/v1/restaurants/{restaurant_id}/quote avec :

{
  "items": [{"dish_id": "<uuid>", "quantity": 2}],
  "pickup_mode": "delivery",
  "dropoff_latitude": 5.35,
  "dropoff_longitude": -4.01
}

Le serveur renvoie subtotal_minor_units, delivery_fee_minor_units, total_minor_units, currency, is_open et calculated_at. Le frontend ne recalcule pas le total final et ne fait pas confiance à un prix conservé dans le panier.

2.4 Création de commande

Appeler POST /food/v1/restaurants/{restaurant_id}/orders avec :

{
  "items": [{"dish_id": "<uuid>", "quantity": 1}],
  "payment_method": "cash_on_delivery",
  "pickup_mode": "delivery",
  "dropoff_address": "Cocody, Abidjan",
  "dropoff_latitude": 5.35,
  "dropoff_longitude": -4.01,
  "recipient_name": "Nom du destinataire",
  "recipient_phone": "+2250700000000"
}

Valeurs de paiement : cash_on_delivery ou online. Pour la livraison, l'adresse, les coordonnées et le contact du destinataire sont nécessaires. pickup_mode=pickup reste fermé en MVP par DIDDIFOOD_PICKUP_ENABLED=false.

La réponse contient status=placed, respond_by, les lignes, les totaux et les horodatages. Le frontend doit afficher le compte à rebours vers respond_by et traiter l'expiration comme une annulation avec cancellation_reason=restaurant_unresponsive.

2.5 Paiement en ligne

La règle est : pas de paiement confirmé, pas de préparation.

Flux obligatoire :

  1. Créer la commande ; elle reste placed.
  2. Appeler POST /food/v1/orders/{order_id}/payments/prepare avec {} ou {"customer_email":"..."}.
  3. Lire PaymentOut.status.
  4. Si status=requires_action, ouvrir next_action_url dans le parcours de paiement sécurisé.
  5. Attendre le webhook DiddiPay et relire GET /food/v1/orders/{order_id}/payment ou appeler POST /food/v1/orders/{order_id}/payment/refresh.
  6. Le restaurant peut accepter uniquement après status=succeeded.

customer_email est optionnel. Si le client n'en fournit pas, Food dérive une valeur technique à partir du téléphone du payeur authentifié. Le téléphone du destinataire ne doit jamais être utilisé.

États de paiement : pending, requires_action, succeeded, failed, cancelled, expired, collected_cash, refunded, partially_refunded. Un retour navigateur ne prouve jamais le paiement à lui seul.

Pour cash_on_delivery, afficher que la collecte aura lieu dans le parcours DiddiSend. Le client ne confirme pas lui-même la collecte.

2.6 Suivi de commande et livraison

  • GET /food/v1/orders liste les commandes du client (limit, offset).
  • GET /food/v1/orders/{order_id} relit une commande.
  • GET /food/v1/orders/{order_id}/delivery/tracking est la surface stable de suivi frontend.
  • POST /food/v1/orders/{order_id}/delivery/refresh demande une nouvelle lecture de la projection quand nécessaire.

DeliveryOut expose status, les coordonnées de départ et d'arrivée, ready_at_estimate, last_synced_at, failure_code et failure_reason, et peut contenir tracking avec coursier public, position, ETA et dernière mise à jour.

Les statuts de livraison sont requested, assigned, picked_up, in_transit, delivered et failed. DiddiSend reste propriétaire du coursier, de la position, de l'ETA réelle, de la preuve de remise et de la décision opérationnelle après échec. Food ne doit pas proposer un bouton de nouvelle tentative ou de remboursement automatique.

2.7 Annulation et remboursement

  • POST /food/v1/orders/{order_id}/cancel est disponible avant acceptation.
  • POST /food/v1/orders/{order_id}/refund-requests crée une demande avec un reason texte obligatoire.
  • GET /food/v1/orders/{order_id}/refund-requests suit la demande.

Statuts de demande : submitted, approved, rejected, processing, refunded, failed. Support/Backoffice prend la décision ; DiddiPay exécute le remboursement, y compris pour une commande initialement cash, vers le bénéficiaire approprié. Le frontend ne propose jamais un remboursement cash manuel séparé.

2.8 Notation

  • Après delivered, afficher le formulaire POST /food/v1/orders/{order_id}/rating avec stars de 1 à 5 et un commentaire facultatif de 1 000 caractères maximum.
  • Avant d'afficher le formulaire, appeler GET /food/v1/orders/{order_id}/rating.
  • 404 RATING_NOT_FOUND signifie « aucune note encore donnée », pas une erreur réseau.

2.9 Notifications et temps réel

Le frontend peut enregistrer un appareil avec POST /food/v1/devices/register et le retirer avec POST /food/v1/devices/unregister. Les notifications ne remplacent pas la lecture HTTP. Après reconnexion WebSocket, relire les données de la commande.

3. Parcours restaurant Pro et Cuisine

3.1 Sélection du restaurant et onboarding

  • GET /food/v1/me/restaurants renvoie les restaurants liés à l'utilisateur et leur rôle (manager ou kitchen).
  • Le MVP conserve l'invitation par user_id DiddiFreeID.
  • POST /food/v1/restaurants crée le dossier restaurant.
  • GET /food/v1/restaurants/{restaurant_id}/documents lit les pièces et les motifs de refus.
  • PUT /food/v1/restaurants/{restaurant_id}/documents/{document_type} dépose ou remplace une pièce refusée.
  • Les types MVP sont manager_identity et business_activity.
  • POST /food/v1/restaurants/{restaurant_id}/submit soumet le dossier.

États onboarding : draft, pending_review, approved, rejected. Après un refus, afficher review_reason et rejection_reason, permettre le remplacement des pièces refusées, puis une nouvelle soumission.

3.2 Profil, horaires et zone

Le manager peut modifier le profil, le logo, les horaires hebdomadaires, les quartiers nommés et le service_radius_km. Les horaires sont interprétés en Africa/Abidjan. Le frontend ne décide pas lui-même si un restaurant est ouvert : il affiche is_open fourni par Food.

3.3 Menu Pro et disponibilité Cuisine

  • Manager : GET/POST /food/v1/restaurants/{restaurant_id}/dishes.
  • Modification : PATCH /food/v1/restaurants/{restaurant_id}/dishes/{dish_id}.
  • Disponibilité : PATCH /food/v1/restaurants/{restaurant_id}/dishes/{dish_id}/availability.
  • Cuisine : GET /food/v1/restaurants/{restaurant_id}/kitchen/dishes.

Pour la création d'un plat, envoyer une Idempotency-Key stable pendant toute la tentative. Même clé + même corps = même résultat ; même clé + corps différent = 409 IDEMPOTENCY_CONFLICT.

La vue Cuisine reçoit DishAvailabilityOut sans prix mais avec image_url, is_available et unavailable_until. Les prix ne doivent jamais être affichés ni déduits côté rôle kitchen.

3.4 File de commandes et préparation

GET /food/v1/restaurants/{restaurant_id}/orders accepte le filtre répété status, par exemple ?status=placed&status=preparing.

  • POST .../{order_id}/accept avec prep_minutes : passe à preparing.
  • Pour une commande online, l'API renvoie 409 PAYMENT_REQUIRED_BEFORE_PREPARATION tant que DiddiPay n'est pas succeeded.
  • POST .../{order_id}/ready : passe à ready.
  • POST .../{order_id}/reject avec un motif texte : passe à rejected.
  • POST .../{order_id}/delivery/request permet de relancer une demande DiddiSend après une indisponibilité technique.

Le frontend Cuisine n'affiche pas les montants financiers. Le manager peut consulter la commande et la ventilation.

3.5 Finances restaurant

  • GET /food/v1/restaurants/{restaurant_id}/weekly-summary affiche les gains de la semaine en XOF et le fuseau Africa/Abidjan.
  • GET /food/v1/restaurants/{restaurant_id}/orders/{order_id}/financial-breakdown affiche la ventilation connue de la commande.
  • GET /food/v1/restaurants/{restaurant_id}/wallet affiche pending_balance, available_balance, reserved_balance, le minimum de retrait et les frais.
  • GET .../wallet/entries liste les mouvements ; le filtre order_id permet de relier un gain à une commande.
  • GET .../wallet/withdrawal-quote vérifie les frais et le montant net.
  • POST .../wallet/withdrawals demande un retrait avec une Idempotency-Key et le quote_id retourné par le devis confirmé. Le backend refuse un devis expiré ou un montant différent.
  • GET .../wallet/withdrawals et GET .../wallet/withdrawals/{withdrawal_id} suivent le retrait.

Un gain cash reste pending jusqu'à l'événement DiddiSend delivery.cash_remitted. Le frontend ne doit pas afficher ce montant comme disponible avant cette régularisation. La recharge du wallet restaurant reste hors MVP.

La disponibilité d'un plat est manuelle : envoyez is_available=true|false. Le champ historique until_end_of_day est refusé et ne doit pas être utilisé; Food ne réactive jamais automatiquement un plat.

4. Surfaces backend à ne pas appeler depuis le frontend public

Les routes suivantes sont réservées au Backoffice, Pilotage ou Support avec JWT S2S et scopes dédiés :

  • /food/v1/internal/backoffice/*
  • /food/v1/internal/pilotage/*
  • /internal/v1/support/order/{order_id}/context

Le frontend public ne doit pas stocker de client_secret, fabriquer un JWT S2S ou appeler directement DiddiPay, DiddiSend ou DiddiFiles.

5. Gestion des erreurs côté interface

Lire error.code, error.message et error.request_id.

Code Affichage recommandé
401 / UNAUTHORIZED Reconnecter via DiddiFreeID.
403 / FORBIDDEN Compte sans rôle pour ce restaurant ou cette action.
404 RATING_NOT_FOUND Aucune note encore enregistrée.
409 PAYMENT_REQUIRED_BEFORE_PREPARATION Paiement en attente ; relire le paiement, puis réessayer l'acceptation.
409 ORDER_INVALID_STATE Actualiser la commande avant de proposer une action.
409 IDEMPOTENCY_CONFLICT Ne pas réutiliser la clé avec un autre corps.
503 Dépendance indisponible ; garder l'action en attente et afficher le request_id.

6. Ce qui est disponible, conditionnel ou hors MVP

Surface État actuel
Catalogue, recherche légère, pagination, rayon GPS Exposé par l'API ; le rayon doit être activé/configuré avec le déploiement.
Commande livraison cash/online MVP.
Paiement online avant préparation MVP, garde activé par défaut.
Livraison et suivi Projection Food de DiddiSend ; synchronisation dépend de la configuration du worker.
Remboursements Demande client puis décision Support/Backoffice, exécution DiddiPay.
Onboarding R7 Exposé : pièces, soumission, revue et remplacement.
Wallet et retrait restaurant Exposé ; payout dépend de DiddiPay et cash de delivery.cash_remitted.
Pickup Route présente mais porte désactivée par défaut.
Substitution Route présente mais porte désactivée par défaut.
Recharge wallet restaurant Hors MVP.
Surfaces Backoffice/Pilotage/Support Backend S2S, jamais frontend public.

7. Vérification frontend minimale avant recette

  1. Vérifier /health puis /health/ready.
  2. Vérifier le catalogue, une image null et une image résolue.
  3. Créer une commande cash et vérifier placed → preparing → ready.
  4. Créer une commande online et vérifier que l'acceptation avant succeeded renvoie PAYMENT_REQUIRED_BEFORE_PREPARATION.
  5. Vérifier la préparation DiddiPay, le webhook et l'acceptation après succeeded.
  6. Vérifier le suivi DiddiSend et le rendu d'un échec projeté.
  7. Vérifier une demande de remboursement et ses statuts.
  8. Vérifier la notation existante et RATING_NOT_FOUND.
  9. Vérifier l'onboarding avec pièce refusée puis remplacement.
  10. Vérifier wallet pending → available après cash_remitted, puis le retrait idempotent.