Aller au contenu principal

Réservations

Base URL : /api/v1/reservations

Les réservations permettent de réserver des repas ou snacks à l'avance. Le montant est débité immédiatement à la création.


Statuts​

StatutDescription
pendingRéservée, non préparée
readyPrête à être retirée
collectedRetirée par le porteur
cancelledAnnulée (remboursée automatiquement)
refundedRemboursée
uncollectedNon retirée

Transitions autorisées​

stateDiagram-v2
[*] --> pending: Création
pending --> ready: Préparation
pending --> collected: Retrait direct
pending --> cancelled: Annulation
ready --> collected: Retrait
ready --> cancelled: Annulation
collected --> [*]
cancelled --> [*]

POST /reservations​

Crée une réservation (un ou plusieurs articles). Le montant est débité immédiatement.

Permissions : reservation.create ou reservation.manage

En-tête : Idempotency-Key (obligatoire — 422 si absente)

Contrôle IDOR

Si l'appelant est un porteur de carte, card_holder_id doit correspondre à son propre identifiant, sinon 403.

Corps de la requête​

ChampTypeRequisDescription
card_holder_iduuidOuiPorteur bénéficiaire
for_datedateOuiDate de la réservation
itemsarrayNon*Articles (product_id, quantity) — recommandé
product_iduuidOui*Produit (mode mono-article hérité)
quantityintegerOui*Quantité (mode hérité)
menu_iduuidNonMenu associé
site_iduuidNonSite
point_of_sale_iduuidNonPoint de vente
notesstringNonNotes

* product_id/quantity sont requis par la structure même si items est fourni.

POST /api/v1/reservations
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
"card_holder_id": "uuid",
"for_date": "2025-06-21",
"items": [
{ "product_id": "uuid", "quantity": 1 }
]
}

Réponse (200)​

{
"id": "uuid",
"card_holder_id": "uuid",
"product_id": "uuid",
"quantity": 1,
"unit_price_cents": 1200,
"unit_price": 1200,
"unit_price_formatted": "1 200 FCFA",
"total_price_cents": 1200,
"total_price": 1200,
"total_price_formatted": "1 200 FCFA",
"for_date": "2025-06-21",
"status": "pending",
"can_be_cancelled": true,
"card_transaction_id": "uuid",
"success": true,
"new_balance": 3800,
"notes": null,
"created_at": "2025-06-20T10:00:00Z",
"items": [
{
"id": "uuid",
"product_id": "uuid",
"quantity": 1,
"unit_price_cents": 1200,
"unit_price": 1200,
"unit_price_formatted": "1 200 FCFA",
"line_total_cents": 1200,
"line_total": 1200,
"line_total_formatted": "1 200 FCFA",
"currency": "XOF"
}
],
"currency": "XOF"
}

GET /reservations​

Liste paginée des réservations.

Permissions : reservation.read ou reservation.manage

Paramètres de query​

ParamTypeDéfautDescription
datedate—Filtrer par date (optionnel si search)
site_iduuid—Filtrer par site
statusstring—Filtrer par statut
searchstring—Code/nom porteur, notes
limitinteger20Éléments par page
offsetinteger0Décalage

Réponse (200)​

{
"items": [
{
"id": "uuid",
"card_holder_id": "uuid",
"card_holder": {
"id": "uuid",
"first_name": "Fatou",
"last_name": "Diallo",
"establishment_name": null,
"code": "CL-EL-0008"
},
"product_id": "uuid",
"quantity": 1,
"unit_price_cents": 1200,
"unit_price": 1200,
"unit_price_formatted": "1 200 FCFA",
"total_price_cents": 1200,
"total_price": 1200,
"total_price_formatted": "1 200 FCFA",
"card_transaction_id": "uuid",
"for_date": "2025-06-21",
"site_id": "uuid",
"site_name": "Buvette Principale",
"menu_id": "uuid",
"performed_by_user_id": "uuid",
"performed_by_name": "Jean Mbeki",
"notes": null,
"status": "pending",
"can_be_cancelled": true,
"created_at": "2025-06-20T10:00:00Z",
"items": [
{
"id": "uuid",
"product_id": "uuid",
"product_name": "Riz au poisson",
"quantity": 1,
"unit_price_cents": 1200,
"unit_price": 1200,
"unit_price_formatted": "1 200 FCFA",
"line_total_cents": 1200,
"line_total": 1200,
"line_total_formatted": "1 200 FCFA",
"currency": "XOF"
}
]
}
],
"total": 12,
"currency": "XOF"
}
Différence avec la création

Dans la liste, chaque items[] inclut product_name (absent de la réponse de création).


PATCH /reservations/{id}/status​

Change le statut d'une réservation.

Permissions :

  • cancelled → reservation.cancel ou reservation.manage
  • autres → reservation.validate ou reservation.manage

En-tête : Idempotency-Key (obligatoire)

Corps de la requête​

ChampTypeRequisDescription
statusstringOui"ready", "collected" ou "cancelled"
reasonstringNonMotif (conservé en cas d'annulation)
PATCH /api/v1/reservations/{id}/status
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440002
Content-Type: application/json

{
"status": "ready"
}

Réponse (200)​

Objet réservation enrichi, incluant refund_transaction_id et refund_reason en cas d'annulation.

Remboursement automatique

Passer une réservation à cancelled déclenche automatiquement le remboursement sur la carte et la restauration du stock.


POST /reservations/multi-day​

Crée plusieurs réservations (une par jour) avec une sélection de produits propre à chaque journée.

Permissions : reservation.create ou reservation.manage (avec contrôle IDOR)

En-tête : Idempotency-Key (obligatoire)

Corps de la requête​

{
"card_holder_id": "uuid",
"site_id": "uuid",
"notes": "Semaine du 21 juin",
"days": [
{
"date": "2025-06-21",
"items": [{ "product_id": "uuid", "quantity": 1 }]
},
{
"date": "2025-06-22",
"items": [{ "product_id": "uuid", "quantity": 2 }]
}
]
}

Réponse (200)​

{
"reservations": [
{
"id": "uuid",
"card_holder_id": "uuid",
"product_id": "uuid",
"quantity": 1,
"total_price_cents": 1200,
"total_price": 1200,
"total_price_formatted": "1 200 FCFA",
"for_date": "2025-06-21",
"status": "pending",
"can_be_cancelled": true,
"currency": "XOF"
}
],
"count": 2,
"currency": "XOF"
}

GET /reservations/by-card/{code}​

Retourne les réservations du jour (statuts pending et ready) pour une carte scannée.

Permissions : reservation.read

Le paramètre code accepte indifféremment un code carte, un numéro de carte, un ancien hash QR ou un code porteur.

GET /api/v1/reservations/by-card/CL-EL-0008

Réponse (200)​

{
"card": {
"id": "uuid",
"code": "CL-EL-0008",
"card_number": "6287940000123456",
"status": "active",
"balance_cents": 3800,
"balance": 3800,
"balance_formatted": "3 800 FCFA",
"currency": "XOF"
},
"holder": {
"id": "uuid",
"first_name": "Fatou",
"last_name": "Diallo",
"establishment_name": null,
"code": "CL-EL-0008"
},
"today": "2025-06-21",
"reservations": [ ],
"total": 0,
"currency": "XOF"
}

GET /reservations/availability​

Vérifie si une date donnée est réservable et détaille la disponibilité de chaque produit.

Permissions : reservation.read

Toujours 200 — une date non réservable renvoie can_reserve: false avec un motif, sans erreur HTTP.

Paramètres de query​

ParamTypeRequisDescription
datedateOuiDate à vérifier
site_iduuidNonSite

Réponse (200)​

{
"date": "2025-06-21",
"can_reserve": true,
"reason": "ok",
"message": null,
"has_menu": true,
"menu_id": "uuid",
"menu_name": "Menu du 21 juin",
"deadline": {
"min_advance_days": 1,
"earliest_date": "2025-06-21",
"creation_deadline_time": "09:00",
"max_reservation_days_ahead": 30,
"max_date": "2025-07-21"
},
"products": [
{
"product_id": "uuid",
"name": "Riz au poisson",
"code": "RZP",
"meal_type": "Fish",
"can_reserve": true,
"reason": "ok",
"message": null,
"reservation_deadline_days_before": 1,
"reservation_deadline_time": "09:00",
"is_active": true,
"available_on_date": true,
"stock_quantity": 50
}
]
}

Valeurs de reason (niveau date)​

ok, past_date, below_min_advance, creation_deadline_passed, beyond_horizon, no_menu.

Valeurs de products[].reason (niveau produit)​

ok, product_inactive, product_unavailable_day, product_deadline, product_deadline_time, product_out_of_stock.


GET /reservations/availability/range​

Vérifie la disponibilité sur une plage de jours consécutifs.

Permissions : reservation.read

Paramètres de query​

ParamTypeRequisDescription
startdateOuiPremier jour
daysintegerOuiNombre de jours (1 à 366)
site_iduuidNonSite

Réponse (200)​

Un tableau d'objets DateAvailability (même forme que /availability), un par jour consécutif à partir de start.

[
{ "date": "2025-06-21", "can_reserve": true, "reason": "ok", "products": [] },
{ "date": "2025-06-22", "can_reserve": true, "reason": "ok", "products": [] }
]

Erreurs​

CodeDescription
422days hors bornes (0 ou > 366)