> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genuka.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration commerce

# Genuka API — Guide d'intégration e-commerce

> Public visé : développeurs front-end / full-stack qui construisent un site e-commerce

> pour une entreprise déjà inscrite sur Genuka.

> Version d'API : `2023-11` · Préfixe de toutes les routes : `/2023-11`

***

## 1. Objectif et périmètre

Ce guide décrit, **endpoint par endpoint et payload par payload**, comment bâtir une

boutique en ligne au-dessus de `genuka-api` :

catalogue · variantes · stock · collections · panier · authentification client · adresses ·

réductions · livraison & retrait (pickup) · création de commande · paiement ·

suivi de livraison · documents de commande · multi-devise · multi-boutique ·

contenu éditorial (blogs, articles, pages CMS) · réservation de services (créneaux) ·

chat storefront · factures publiques.

Tous les faits ci-dessous ont été vérifiés directement dans le code `routes/api/V202311/`,

controllers `app/Http/Controllers/V202311/`, pipeline `app/Processes/Order/`, resources

`app/Http/Resources/V202311/`). Quand un comportement est calculé côté serveur, c'est signalé.

***

## 2. Concepts clés

### 2.1 Multi-tenant

Une requête storefront est toujours **scopée à une entreprise** `Company`), et de

préférence à une **boutique** `Shop`). Le scoping passe par des en-têtes HTTP, pas par

l'URL.

\| En-tête | Obligatoire | Rôle |

\|---|---|---|

\| `X-Company: <company_id>` | **Oui** pour toutes les routes storefront / customer | Identifie l'entreprise. Middleware `check.company`. Absent → `422 { "message": "Please provide a Company Id" }`. |

\| `X-ShopId: <shop_id>` | Recommandé | Scope le catalogue, le stock, les prix et le checkout sur la boutique active. À défaut, l'API lit `X-Shop`. |

\| `Authorization: Bearer <token>` | Oui pour les routes client connecté `auth:customer`) | Jeton OAuth (Passport) renvoyé par `login` / `register` (ou par `POST /customers/orders` avec `authenticate`). |

\| `Content-Language: fr` | Optionnel | Langue des libellés / e-mails. |

> Le middleware `tenant.scope` applique automatiquement les scopes entreprise/boutique sur

> les modèles : vous ne « voyez » jamais les données d'une autre entreprise.

### 2.2 Le problème de l'amorçage (bootstrap)

`X-Company` **est** l'ID de l'entreprise — vous ne l'avez pas encore au tout premier appel.

Pour le récupérer à partir du nom de domaine de la boutique, utilisez la route racine

**hors scope** :

```text theme={null}

GET /2023-11/company/from-domain?domain=[ma-boutique.com](http://ma-boutique.com)
```

Une fois l'ID connu, placez `X-Company` sur **toutes** les requêtes suivantes.

### 2.3 Entités centrales

* `Company` — branding, devise par défaut, devises additionnelles, moyens de paiement, config storefront
* `Shop` — point de vente ; peut porter sa propre devise ; relié à des entrepôts
* `Product` / `ProductVariant` — un produit a une ou plusieurs variantes ; le prix et le stock vivent au niveau **variante**
* `Stock` (entrepôt) — relié aux boutiques via `shop_warehouse`
* `Customer`, `Address`
* `Order` — `billing` (JSON), `shipping` (JSON), `discounts`, `deliveries`, `payments`, `medias`
* `ShippingFee`, `PickupLocation`, `Delivery`, `DeliveryTracking`
* `PaymentMethod`, `Payment`
* `Collection` (catalogue groupé, imbricable) ; `Blog` / `Article` / `Page` (CMS storefront)
* `Availability` / `Unavailability` / `CalendarEvent` (réservation de services par professionnel)
* `InboxConversation` / `Message` (chat storefront)

***

## 3. Parcours storefront de référence

1. Résoudre l'entreprise `/company/from-domain`) → fixer `X-Company`
2. Charger les boutiques, choisir une boutique active → fixer `X-ShopId`
3. Lister les produits / variantes (filtres, pagination)
4. Calculer la disponibilité à partir des champs `estimated_quantity*` et `follow_stock`
5. (Optionnel) Authentifier le client — le checkout invité est possible
6. Gérer les adresses du client
7. Vérifier un code promo / charger les promos automatiques
8. Choisir livraison **ou** retrait
9. Créer la commande `POST /customers/orders`)
10. Récupérer le lien de paiement (immédiat ou différé)
11. Suivre la commande et la livraison

> **Panier** : Genuka ne stocke pas de panier côté serveur. Le panier est géré par le front

> (local/session) jusqu'à la création de la commande.

***

## 4. Référence des endpoints

### 4.1 Contexte entreprise & boutique

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/company/from-domain?domain=<domain>` | Bootstrap, **hors** `X-Company`. Accepte aussi `?companyId=<id>`. Réponse : `CompanyResource` public (en cache). |

\| `GET` | `/2023-11/companies/{company}` | Entreprise par ID, hors scope. |

\| `GET` | `/2023-11/company?domain=<domain>` | Variante scopée (nécessite `X-Company`). |

\| `GET` | `/2023-11/company/filters?companyId=<id>` | Filtres storefront configurés `storeFrontFilters`). |

\| `GET` | `/2023-11/shops` | Liste paginée des boutiques. |

\| `GET` | `/2023-11/shops/{shop}` | Détail boutique. Variante : `/shops/handle/{slug}`. |

\| `GET` | `/2023-11/shops/count` | Nombre de boutiques. |

> `GET /company/from-domain` et `GET /companies/{company}` sont **mis en cache 4 semaines**

> (clés `public_company_domain_{domain}` / `public_company_id_{companyId}`). C'est le point

> d'amorçage : un seul appel par session suffit.

*`CompanyResource` (public)*\* — champs exposés `app/Http/Resources/V202311/Public/CompanyResource.php`) :

\| Champ | Type | Description |

\|---|---|---|

\| `id` | ULID | Identifiant entreprise (= `X-Company`). |

\| `name`, `description`, `type` | string | Identité. |

\| `handle`, `company_code` | string | Slug et code public. |

\| `logoUrl` | string | URL directe du logo. |

\| `logo` | objet média | Média complet `microthumblargewebp`), ou `null`. |

\| `storefront` | objet | Config du **thème actif** `ThemeConfiguration` `is_active=1`), renvoyée telle quelle. |

\| `storeFrontFilters` | objet | Filtres pré-calculés (voir ci-dessous). |

\| `paymentMethods[]` | tableau | Moyens de paiement activés (voir ci-dessous). |

\| `currency` | `{ code, name }` | Devise par défaut. |

\| `currencies[]` | `{ code, label, value }` | Devises additionnelles + taux. |

\| `variables` | objet | Variables custom `metadata.variables`). |

\| `website` | objet domaine | Domaine principal `{ id, domain, main, is_active, … }`). |

\| `hasBlogs` | bool | L'entreprise publie-t-elle un blog. |

\| `contact` | objet | Coordonnées publiques `metadata.contact`). |

\| `address` | `AddressResource` | Adresse de l'entreprise. |

\| `business_id` | string | RCCM `metadata.rccm`). |

\| `tax_id` | string | Identifiant fiscal `metadata.fiscalID`). |

*`storeFrontFilters`*\* (aussi via `/company/filters`) contient :

```json theme={null}

{

  "collections": [ { "id", "name", "slug", "medias": [...], "sub_collections": [...] } ],

  "minPrice": 1000,

  "maxPrice": 250000,

  "options": { "Couleur": ["Rouge", "Bleu"], "Taille": ["S", "M", "L"] }

}
```

*`paymentMethods[]`*\* `PaymentMethodResource`) — un par moyen activé :

```json theme={null}

{

  "id": "<ulid>",

  "name": "Carte bancaire",

  "processor": "stripe",

  "status": true,

  "account_id": null,

  "treasury_account_id": null,

  "metadata": {},

  "configurations": { "publicKey": "pk_live_…" }

}
```

> `processor` ∈ `cash`, `notchpay`, `stripe`, `paydunya`, `taramoney`, `pawapay`, `paypal`,

> `mamoni`, `flutterwave` (enum `App\Enum\PaymentMethods`). Un type fonctionnel l'accompagne

> côté config : `card`, `mobile_money`, `bank_transfer` `App\Enum\PaymentMethodType`).

> Les valeurs secrètes des `configurations` (clés `secretprivatetoken`) sont **masquées** en sortie.

> Au checkout, `billing.method` reprend la valeur `processor` du moyen choisi (§4.8).

*`ShopResource`*\* — champs exposés `/shops`, `/shops/{shop}`) :

\| Champ | Description |

\|---|---|

\| `id`, `name`, `slug`, `description` | Identité boutique. |

\| `currency_code`, `currency_name` | Devise propre à la boutique (prioritaire sur l'entreprise). |

\| `address` | `AddressResource` (avec `latitudelongitude`). |

\| `warehouses[]` | Entrepôts rattachés `{ id, name, description }`) — base du stock. |

\| `domains[]` | Domaines de la boutique `{ domain, main, is_active }`). |

\| `logoUrl`, `logo` | Logo (URL + média). |

\| `metadata`, `company_id` | — |

`/shops` accepte `filter[search]` (nom/description) et `sort=name|currency_code|created_at|…`.

`/shops/{shop}` accepte aussi `?companyId=` pour résoudre par couple `company_idslug`.

**Route filtres** : `GET /2023-11/company/filters?companyId=<id>` renvoie directement le bloc

`storeFrontFilters` (collections + bornes de prix + options) — pratique pour bâtir la facette

de recherche sans charger toute la `CompanyResource`.

**Bonne pratique** : fixez la boutique active très tôt et propagez `X-ShopId` partout

(catalogue, stock, checkout, suivi).

***

### 4.2 Catalogue produit

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/products` | Liste paginée. |

\| `GET` | `/2023-11/products/count` | `{ "count": <int> }`. |

\| `GET` | `/2023-11/products/{product}` | Détail `ProductDetailResource`). |

\| `GET` | `/2023-11/products/handle/{handle}` | Détail par slug (réponse en cache). |

\| `GET` | `/2023-11/products/{product}/similar?limit=5` | Produits similaires. |

\| `GET` | `/2023-11/products/handle/{handle}/similar` | Similaires par slug. |

\| `GET` | `/2023-11/productVariants` | Liste de variantes paginée. |

\| `GET` | `/2023-11/productVariants/{productVariant}` | Détail variante. |

**Pagination** (standard Laravel + Spatie QueryBuilder) :

`?per_page=20` (défaut **15**, max **100**) ou `?page[size]=20&page[number]=2`.

La réponse liste est enveloppée : `{ "data": [...], "links": {...}, "meta": {...} }`.

**Filtres disponibles sur `/products`** `filter[<nom>]=...`) :

\| Filtre | Exemple | Effet |

\|---|---|---|

\| `search` | `filter[search]=tshirt` | Titre, handle, vendeur, titre/SKU/code-barres des variantes. |

\| `published` | `filter[published]=1` | Produits publiés. |

\| `type` | `filter[type]=physical` | Type de produit. |

\| `has_stocks` | `filter[has_stocks]=1` | Uniquement les produits disponibles. |

\| `priceRange` | `filter[priceRange]=1000,5000` | Fourchette `[min,max]`. |

\| `collections` | `filter[collections]=id1,id2` | Par collection. |

\| `shops` | `filter[shops]=shop_id` | Par boutique. |

\| `tags` | `filter[tags]=promo,ete` | Par tags. |

\| `optionValues` | `filter[optionValues]=Rouge` | Par valeur d'option. |

\| `created_at` / `updated_at` | `filter[created_at]=...` | Plage de dates `{ start, end }`. |

**Includes** `?include=...`) :

`variants`, `variants.stocks`, `variants.warehouseQuantities`, `collections`, `shops`,

`tags`, `options`, `supplier`, `service.professionals`…

**Tris** `?sort=...`, préfixe `-` pour décroissant) :

`price`, `-created_at`, `-updated_at`, `title`, `has_stock_first`, `total_orders`, `stocks`, `tags`.

***

### 4.3 Stock & disponibilité

Le prix et le stock vivent au niveau **variante**. Les resources variantes exposent :

* `estimated_quantity` — quantité estimée totale (toutes boutiques), ou `null` si aucune variante ne suit le stock
* `estimated_quantity_by_shop` — `{ shop_id: quantité }`
* `estimated_quantity_by_warehouse` — `{ warehouse_id: quantité }`
* `stocks` — détail par entrepôt (filtré sur la boutique active si `X-ShopId`)

**Règle métier de disponibilité** :

> Le suivi de stock se règle par variante via la colonne \*`follow_stock`\*\*.

> Si `follow_stock = false`, la variante est **toujours considérée disponible**, quelle que

> soit `estimated_quantity`. Si `follow_stock = true`, basez la disponibilité sur

> `estimated_quantity` (globale ou par boutique selon `X-ShopId`).

Au niveau produit, `ProductDetailResource` expose un booléen `follow_stock` (vrai si

au moins une variante suit le stock) et `remaining_stocks` (restant par entrepôt pour la boutique).

> ⚠️ Le stock affiché côté front est indicatif. La disponibilité est \*\*revérifiée au moment

> de la création de commande\*\* (voir §4.8). Ne vous fiez jamais au stock client comme source de vérité.

***

### 4.4 Authentification client

Routes publiques `X-Company` requis, pas de token) :

\| Méthode | Endpoint | Champs |

\|---|---|---|

\| `POST` | `/2023-11/customers/register` | `password` (requis) ; `first_name` **ou** `last_name` ; `email` **ou** `phone` (uniques par entreprise). |

\| `POST` | `/2023-11/customers/login` | `email` **ou** `phone` + `password`. |

\| `POST` | `/2023-11/customers/logout` | — |

\| `POST` | `/2023-11/customers/password/reset-link` | `email`. Renvoie un token. |

\| `POST` | `/2023-11/customers/password/forgot` | `email`. |

\| `POST` | `/2023-11/customers/password/reset` | `token`, `email`, `password` (confirmé, min 8). |

**Réponse `register` / `login`** `200`) :

```json theme={null}

{

  "token": "1|XXXXXXXXXXXXXXXXXXXX",

  "customer": { "id": "...", "first_name": "Jean", "email": "[jean@example.com](mailto:jean@example.com)" }

}
```

Le `token` est un jeton **OAuth (Passport)**. Placez-le ensuite en `Authorization: Bearer <token>`.

Mauvais identifiants → `401`.

> **Connecter le client dès le checkout** : vous pouvez aussi obtenir un token directement à la

> création de commande, sans appel `registerlogin` séparé — voir le paramètre `authenticate` en §4.8.

***

### 4.5 Profil & adresses (client connecté)

Routes `auth:customer` `Bearer` requis) :

\| Méthode | Endpoint |

\|---|---|

\| `GET` | `/2023-11/customers/profile` |

\| `PUT` | `/2023-11/customers/profile` |

\| `PUT` | `/2023-11/customers/profile/password` `current_password`, `new_password`) |

\| `GET` | `/2023-11/customers/addresses` |

\| `POST` | `/2023-11/customers/addresses` |

\| `GET` | `/2023-11/customers/addresses/{address}` |

\| `PUT` | `/2023-11/customers/addresses/{address}` |

\| `DELETE` | `/2023-11/customers/addresses/{address}` |

`CustomerResource` renvoie entre autres : `addresses[]`, `default_address`, `contacts[]`,

`tags[]`, `medias[]`, `custom_fields`.

***

### 4.6 Livraison & retrait

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/shipping-fees` | Frais de livraison disponibles (paginé ; `filter[active]`, `filter[search]`, `filter[created_at]`, `filter[updated_at]` ; `sort=name`, `orders_count`). |

\| `GET` | `/2023-11/pickup-locations` | Points de retrait (paginé ; `filter[shop]`, `filter[active]`, `filter[search]`). |

*`ShippingFeeResource`*\* — champs :

```json theme={null}

{

  "id": "<ulid>", "name": "Livraison Douala", "type": "fixed", "amount": 2000,

  "active": true, "shop_id": "<ulid>", "shop": { },

  "warehouses": [ ], "warehouse_ids": ["<ulid>"], "metadata": {},

  "created_at": "…", "updated_at": "…"

}
```

> `type` vaut toujours `fixed` (enum `ShippingFeeType`) : le `amount` est un **forfait**. Le

> ciblage géographique se fait par rattachement à des entrepôts `warehouse_ids`), pas par des

> zones tarifaires côté resource. Filtrez les frais pertinents selon la boutique / l'entrepôt active(s).

*`PickupLocationResource`*\* — mêmes champs de base `id`, `name`, `amount`, `active`, `shop`,

`warehouses`, `warehouse_ids`) **plus** :

```json theme={null}

{

  "address": { "line1": "…", "city": "…", "country": "…", "latitude": 4.05, "longitude": 9.7, "…": "…" },

  "full_address": "Rue 1.234, Douala, CM"

}
```

> Les coordonnées GPS du point de retrait sont dans `address.latitude` / `address.longitude`

> (utiles pour une carte). Les horaires d'ouverture, s'ils existent, vivent dans `metadata` /

> `address.metadata` (pas de champ dédié).

Au checkout, présentez les deux options puis renseignez la commande (voir §4.8) :

* **Livraison** : `shipping.mode = "delivery"` + `shipping_fee_id`
* **Retrait** : `shipping.mode = "pickup"` + `pickup_location_id`

> `shipping_fee_id` et `pickup_location_id` sont **mutuellement exclusifs**. Le montant de

> livraison est résolu côté serveur depuis le frais ou le point de retrait choisi ; à défaut,

> il prend `shipping.amount`.

***

### 4.7 Réductions

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/discounts?code=WELCOME10` | Vérifie un code. `200` + `DiscountResource`, ou `404` si invalide. |

\| `POST` | `/2023-11/discounts/automatic` | Promos automatiques applicables. Body : `{ "customer": { "id" } }` ou `{ "customer": { "email", "phone" } }`. |

Au moment de la commande, transmettez les remises validées dans `discounts[]` (voir §4.8).

Deux niveaux existent : **order-level** `discounts[]`) et **product-level**

`products[].discounts[]`). Le moteur applique et persiste les montants.

***

### 4.8 Création de commande

```text theme={null}

POST /2023-11/customers/orders
```

* Fonctionne en **checkout invité** (l'objet `customer` crée/retrouve le client) ou
  **connecté** `Bearer` → le client authentifié est utilisé).
* Pilotée par le pipeline `UpsertOrderProcess` (résolution boutique → client → produits →
  remises → taxes → livraison → facturation → écriture → médias → notifications).

#### ⚠️ Les totaux sont calculés par le serveur

`ManageBillingTask` **recalcule** `subtotal`, `discount`, `out_of_tax_total`,

`sum_positive_taxes`, `sum_negative_taxes`, `total` et `net_to_pay` à partir des

`products`, du shipping et des `discounts`. **N'envoyez pas ces montants** : ils seraient

écrasés. Côté client, vous fournissez les lignes, le mode de paiement, le mode de

livraison et les adresses — c'est tout.

Formule appliquée :

```text theme={null}

subtotal          = Σ (price × quantity)

out_of_tax_total  = subtotal + shipping − discount

total = net_to_pay = out_of_tax_total + taxes_positives + taxes_negatives
```

#### Payload recommandé (minimal et correct)

```json theme={null}

{

  "source": "Genuka Checkout",

  "deferred_payment": true,

  "currency_code": "XAF",

  "customer": {

    "first_name": "Jean",

    "last_name": "Dupont",

    "email": "[jean@example.com](mailto:jean@example.com)",

    "phone": "+237600000000",

    "force": true

  },

  "products": [

    { "product_id": "<product_ulid>", "variant_id": "<variant_ulid>", "quantity": 2, "price": 15000 }

  ],

  "billing": {

    "method": "cash",

    "status": "pending",

    "address_id": "<address_ulid>"

  },

  "shipping": {

    "mode": "delivery",

    "status": "pending",

    "scheduled_at": "2026-05-22T10:00:00Z",

    "address_id": "<address_ulid>"

  },

  "shipping_fee_id": "<shipping_fee_ulid>",

  "discounts": [

    { "discount_id": "<discount_ulid>", "code": "WELCOME10" }

  ],

  "metadata": { "note": "Livrer avant midi" }

}
```

> Dans cet exemple : 2 × 15 000 = 30 000 de produits + le montant du `shipping_fee_id`.

> Le `total` et le `net_to_pay` renvoyés intègrent ces frais — pas besoin de les calculer.

**Notes sur les champs :**

* `customer.force = true` : crée le client même si un email/téléphone proche existe (utile en checkout invité). À `false`, un conflit lève une erreur.
* `billing.method` : `cash`, `bank`, `stripe`, `paydunya`, `pawapay`, `paypal`, `mamoni`, `taramoney`…
* `shipping.mode` : `delivery`, `pickup`, `postal`, `online`, `other`.
* `address_id` (existante) **ou** `address` (objet `{ first_name, last_name, line1, city, country, … }`) pour billing/shipping.
* Devise résolue dans l'ordre : `currency_code` (requête) → boutique → entreprise → `XAF`.
* `authenticate` (booléen) : si `true`, un **token OAuth est émis pour le client de la commande** et renvoyé dans la réponse (voir « Connecter le client au checkout » ci-dessous). Pratique pour authentifier automatiquement un acheteur en checkout invité.

#### Livrer à un tiers (cadeau, destinataire ≠ acheteur)

L'adresse de livraison `shipping.address`) est **indépendante** de l'acheteur : elle porte

ses propres `first_name`, `last_name`, `email`, `phone`. Pour expédier à quelqu'un d'autre,

laissez l'acheteur dans `customer` (et `billing`) et décrivez le destinataire dans

`shipping.address` :

```json theme={null}

{

  "customer": {

    "first_name": "Jean", "last_name": "Dupont",

    "email": "[jean@example.com](mailto:jean@example.com)", "phone": "+237600000000", "force": true

  },

  "products": [

    { "product_id": "<product_ulid>", "variant_id": "<variant_ulid>", "quantity": 1, "price": 15000 }

  ],

  "billing": { "method": "cash", "status": "pending" },

  "shipping": {

    "mode": "delivery",

    "status": "pending",

    "is_new": true,

    "address": {

      "first_name": "Marie", "last_name": "Kamga",

      "phone": "+237699999999", "email": "[marie@example.com](mailto:marie@example.com)",

      "line1": "Rue 1.234, Bonapriso", "city": "Douala", "country": "CM"

    }

  },

  "shipping_fee_id": "<shipping_fee_ulid>"

}
```

Pièges à connaître (cf. `ManageShippingTask`) :

1. `line1`, `city`, `country` sont **obligatoires** dans `shipping.address`, sinon `400`.
2. Mettez *`"is_new": true`*\* : sinon, pour un client connecté, le serveur réutilise une
   adresse existante qui matche `line1citycountry`. `is_new` force la création d'une
   nouvelle adresse de livraison.
3. **N'utilisez pas** un `shipping.address_id` existant de l'acheteur pour livrer ailleurs :
   passer un `address_id` **avec** un objet `address` **écrase** cette adresse. Pour un tiers,
   envoyez toujours un objet `address` neuf.
4. L'adresse est enregistrée avec `is_shipping = true`, `is_billing = false`. Comme un nom de
   destinataire est fourni, elle ne devient pas l'adresse primaire de l'acheteur `is_primary`
   reste `false`) — ses propres adresses restent intactes.
5. En **retrait** `mode = "pickup"`), aucune adresse n'est requise : seul `pickup_location_id`
   compte. Indiquez le destinataire tiers via `metadata.note`.

#### Réponse

`200` + `OrderDetailResource`, avec un champ additionnel `payment_link` :

```json theme={null}

{

  "data": {

    "id": "<order_ulid>",

    "reference": "CMD-000123",

    "amount": 32000,

    "amount_due": 32000,

    "currency": "XAF",

    "billing": { "method": "cash", "status": "pending", "total": 32000, "net_to_pay": 32000, "address": { } },

    "shipping": { "mode": "delivery", "status": "pending", "amount": 2000, "address": { } },

    "products": [ ],

    "discounts": [ ],

    "deliveries": [ ],

    "payments": [ ]

  },

  "payment_link": "[https://checkout.stripe.com/](https://checkout.stripe.com/)..."

}
```

> *`payment_link`*\* est renseigné **automatiquement** quand `deferred_payment` est absent ou

> `false` (paiement immédiat). Si `deferred_payment = true`, il vaut `null` et vous générez

> le lien plus tard (§4.9).

#### Connecter le client au checkout `authenticate`)

Par défaut, `POST /customers/orders` ne renvoie **pas** de token : seuls `register` et `login`

en émettent. Pour authentifier automatiquement l'acheteur juste après le checkout (y compris en

**checkout invité**, où le client n'a pas de mot de passe), ajoutez `"authenticate": true` au payload.

La réponse contient alors un `token` OAuth et la ressource `customer`, en plus de `data` :

```json theme={null}

{

  "data": { "id": "<order_ulid>", "reference": "CMD-000123" },

  "payment_link": null,

  "token": "<oauth_access_token>",

  "customer": { "id": "<customer_ulid>", "first_name": "Jean", "email": "[jean@example.com](mailto:jean@example.com)" }

}
```

Placez ensuite ce `token` en `Authorization: Bearer <token>` pour accéder au profil, aux

adresses et à l'historique de commandes du client — sans appel `registerlogin` séparé.

> Le token est émis pour **le client de la commande** (celui résolu/créé par le pipeline). En

> checkout connecté (requête déjà porteuse d'un `Bearer`), `authenticate` renvoie simplement un

> nouveau token pour ce même client.

#### Autres routes commande

\| Méthode | Endpoint | Auth | Notes |

\|---|---|---|---|

\| `GET` | `/2023-11/customers/orders/{orderId}` | publique `X-Company`) | Détail d'une commande. |

\| `GET` | `/2023-11/customers/orders` | `Bearer` | Commandes du client connecté (paginé). |

\| `PUT` | `/2023-11/customers/orders/{order}` | `Bearer` | Mise à jour (même pipeline). |

***

### 4.9 Paiement

Deux modes :

**A. Paiement immédiat** — `deferred_payment` absent`false` à la création. Le `payment_link`

est renvoyé directement dans la réponse de `POST /customers/orders`. Redirigez le client dessus.

**B. Paiement différé** — `deferred_payment = true`, puis :

```text theme={null}

POST /2023-11/customers/orders/{order}/payments
```

```json theme={null}

{ "amount_due": 32000, "success_url": "https://...", "cancel_url": "https://..." }
```

* `amount_due` optionnel (défaut : `order.amount_due`).
* `success_url` / `cancel_url` (alias : `callback`).
* Réponse : `200 { "url": "https://..." }`, ou `400 { "message": "Payment link could not be generated" }`.

Providers supportés (sélectionnés selon `billing.method` = `processor` du moyen, §4.1) :

**Stripe, Paydunya, Pawapay, PayPal, Mamoni, Taramoney, Flutterwave, NotchPay** (+ `cash` hors ligne).

Les webhooks de chaque provider mettent à jour le statut de paiement de la commande de façon asynchrone.

> Endpoints provider directs `POST /2023-11/{stripe|paydunya|pawapay|mamoni|paypal|taramoney}/checkout`)

> existent mais sont des détails d'implémentation : passez par `payment_link` / `/payments`.

***

### 4.10 Suivi de livraison

`OrderDetailResource` inclut `delivery` (la première) et `deliveries[]`.

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/delivery/{delivery}` | Détail livraison. |

\| `GET` | `/2023-11/deliveries` | Liste paginée (filtres `tracking_number`, `order_id`, `status`, `mode`, `scheduled_at`, `search`…). |

\| `GET` | `/2023-11/delivery/{delivery}/last_position` | Dernière position connue. |

*`DeliveryResource`*\* — champs principaux :

```json theme={null}

{

  "id": "<ulid>", "tracking_number": "TRK-00123", "tracking_url": "[https://…](https://…)",

  "order_id": "<ulid>", "status": "taken_in_charge", "mode": "default",

  "scheduled_at": "…", "starts_at": null, "ends_at": null,

  "address": { },

  "products": [ { "title": "…", "quantity": 2 } ],

  "delivery_status": { "is_complete": false, "is_partial": true, "delivered_quantity": 1, "sequence": 1, "is_first": true },

  "user": { "totalDeliveries": 12 }

}
```

> `status` ∈ `idle`, `pending`, `taken_in_charge`, `partial_delivery`, `delivered`, `cancelled`

> (enum `App\Enum\DeliveryStatus`). Une commande peut être livrée en **plusieurs fois** :

> `deliveries[]` porte chaque tournée, et `delivery_status` indique l'avancement (quantité livrée,

> séquence, livraison partielle).

**Position** — réponse de `/last_position` (snapshot `DeliveryTracking`, **pas** un flux temps réel) :

```json theme={null}

{

  "delivery": { "id": "<ulid>", "status": "taken_in_charge", "order_id": "<ulid>" },

  "position": { "latitude": 4.0511, "longitude": 9.7679, "accuracy": 10, "heading": 120, "speed": 5.5, "updated_at": "…" }

}
```

> Si aucune position n'a encore été enregistrée : `{ "message": "Aucune position disponible" }`.

> Pour un suivi « live », faites du polling sur cet endpoint. Affichez `tracking_number`,

> `tracking_url` et la `position` (lat/lng + `heading` pour orienter une icône sur la carte).

***

### 4.11 Documents de commande (médias)

La tâche `ManageMediasTask` lit le tableau `medias[]` (ou son alias `images[]`) à la

création et à la mise à jour. Formats acceptés (clés exactes) :

```json theme={null}

{

  "medias": [

    { "src": "[https://exemple.com/facture.pdf](https://exemple.com/facture.pdf)" },

    { "base64": "data:application/pdf;base64,JVBER...", "name": "bon-commande.pdf" },

    { "file": "<fichier uploadé>" },

    { "id": "<media_id_existant>" }

  ]

}
```

* `{ "id": "..." }` lors d'un `PUT` conserve un média déjà attaché (les médias absents du tableau sont supprimés).
* Lecture : `OrderDetailResource` expose `medias` (publics) et `private_medias` (signature/privés).

***

### 4.12 Collections (catalogue groupé)

Les **collections** regroupent des produits (univers, catégories, sélections) et peuvent être

imbriquées `parent_collection_id` + `sub_collections`). Elles servent à bâtir la navigation et

les pages de catégorie. Le filtre `filter[collections]=id1,id2` de `/products` (§4.2) s'appuie dessus.

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/collections` | Liste paginée. |

\| `GET` | `/2023-11/collections/count` | Nombre de collections. |

\| `GET` | `/2023-11/collections/{collection}` | Détail `CollectionMinimalModel`). |

\| `GET` | `/2023-11/collections/handle/{handle}` | Détail par slug. |

\| `GET` | `/2023-11/collections/{collection}/products` | Produits de la collection `ProductDetailResource`). |

\| `GET` | `/2023-11/collections/handle/{handle}/products` | Idem par slug. |

**Filtres** `filter[…]`) : `title`, `handle`, `content` (partiels), `search`, `withProducts`

(collections non vides uniquement), `updated_at` (plage `{ start, end }`).

**Includes** : `tags`, `productsCount`, `subCollections`, `products`.

**Tris** : `title`, `handle`, `products_count`, `sub_collections_count`, `created_at`, `updated_at`.

**Champs `CollectionMinimalModel`)** : `id`, `title`, `handle`, `content`, `parent_collection_id`,

`metadata`, `medias[]`, et `products[]` (uniquement si `?include=products`).

> `/collections/{id}/products` renvoie des `ProductDetailResource` complets (prix, variantes,

> stock, médias) — c'est l'endpoint à câbler sur une page catégorie. Pour ne lister que les

> collections ayant du stock à afficher, ajoutez `filter[withProducts]=1`.

***

### 4.13 Contenu éditorial — Blogs, Articles, Pages (CMS)

Genuka expose un CMS léger pour le storefront : un **blog** contient des **articles** ; les

**pages** sont des contenus statiques (À propos, CGV, FAQ…). `CompanyResource.hasBlogs` (§4.1)

indique si un blog existe.

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/blogs` | Liste des blogs `filter[search]`, `include=articles`, `sort=title`). |

\| `GET` | `/2023-11/blogs/{blog}` · `/blogs/handle/{handle}` | Détail blog. |

\| `GET` | `/2023-11/articles` | Liste d'articles (paginée). |

\| `GET` | `/2023-11/articles/count` | Nombre d'articles. |

\| `GET` | `/2023-11/articles/{article}` · `/articles/handle/{handle}` | Détail article. |

\| `GET` | `/2023-11/pages` | Liste des pages CMS. |

\| `GET` | `/2023-11/pages/count` | Nombre de pages. |

\| `GET` | `/2023-11/pages/{page}` · `/pages/handle/{handle}` | Détail page. |

**Articles** — filtres : `published`, `blog_id`, `blog_handle`, `created_at` (plage), `search`

(titre, handle, auteur, description, contenu, blog, tags). Includes : `tags`, `blog`.

Tris : `title`, `handle`, `published`, `vendor`, `created_at`, `updated_at` (défaut `title`).

Champs `ArticleResource`) : `id`, `title`, `handle`, `description`, `content`, `author`,

`published`, `blog_id`, `blog` (objet), `metadata`, `medias[]`, `created_at`, `updated_at`.

**Blogs** — filtre `search` (titre/handle + articles). Include `articles`. Champs `BlogResource`) :

`id`, `title`, `handle`, `metadata`, `medias[]`, `created_at`, `updated_at`.

**Pages** — filtre `search` (titre/handle). Champs `PageResource`) : `id`, `title`, `handle`,

`content`, `metadata`, `medias[]`, `created_at`, `updated_at`.

> **SEO / rendu** : utilisez toujours les routes `handle/{slug}` pour des URLs propres

> `/blog/mon-article`, `/p/a-propos`). `content` est du HTML/markdown éditorial à rendre tel quel ;

> `medias[]` fournit l'image de couverture (champs `microthumblargewebp`).

***

### 4.14 Réservation de services — disponibilités & créneaux

Pour les entreprises de **services** (prise de rendez-vous), un produit de type service est lié à

des professionnels `User`) qui ont des **disponibilités récurrentes** et des **indisponibilités**

(jours bloqués). Le front affiche un calendrier puis des créneaux réservables.

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/availabilities?user_id=<id>` | Horaires récurrents du professionnel. |

\| `GET` | `/2023-11/availabilities/slots?user_id=<id>&service_id=<id>&day=<date>` | **Créneaux libres** calculés pour une date. |

\| `GET` | `/2023-11/availabilities/{availability}` | Détail d'une plage récurrente. |

\| `GET` | `/2023-11/unavailabilities?user_id=<id>` | Jours d'indisponibilité (filtres `start_date`, `end_date`). |

\| `GET` | `/2023-11/unavailabilities/dates?user_id=<id>` | Tableau de dates `["2026-05-23", …]` pour griser un calendrier. |

\| `GET` | `/2023-11/unavailabilities/{unavailability}` | Détail. |

*`availabilities`*\* — champs : `user_id`, `day_of_week` (0=dimanche → 6), `start_time`,

`end_time` `H:i:s`), `timezone`, `metadata`.

*`availabilities/slots`*\* — paramètres **requis** `service_id` et `day` (date), `user_id` optionnel

(défaut = utilisateur authentifié). Réponse calculée côté serveur :

```json theme={null}

{

  "status": true,

  "data": {

    "professional_id": "<ulid>",

    "professional_name": "…",

    "unavailable": false,

    "slots": [

      { "start": "09:00", "end": "09:30", "duration": 30, "buffer": 10, "available": true }

    ]

  }

}
```

> Le moteur part des plages `availabilities` du jour, découpe en créneaux de

> `service.duration_minutes` espacés de `+ buffer_minutes`, **exclut** les rendez-vous déjà pris

> `CalendarEvent`), les créneaux passés et ceux qui débordent. Si le jour est marqué indisponible,

> `unavailable = true` et `slots = []`.

**Parcours de réservation conseillé** :

1. `unavailabilities/dates` → griser les jours bloqués du calendrier.
2. À la sélection d'un jour : `availabilities/slots` → afficher les créneaux `available: true`.
3. Le client choisit un créneau, puis vous créez la commande (§4.8) en plaçant l'heure du RDV
   dans `shipping.scheduled_at` (et/ou `metadata`).

***

### 4.15 Chat storefront (messagerie temps réel)

Genuka fournit un **webchat** embarquable : le visiteur discute avec l'entreprise depuis la

boutique. Ces routes ne nécessitent **pas** `X-Company` (l'entreprise est déduite du \*\*jeton de

conversation\*\*), mais un middleware `validate.chat.token` et un `throttle:chat`.

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/inbox/chat/{conversation}/messages` | Messages (cursor-paginés, `per_page` défaut 50). Les messages **internes** ne sont jamais renvoyés. |

\| `POST` | `/2023-11/inbox/chat/{conversation}/messages` | Envoyer un message. |

\| `POST` | `/2023-11/inbox/chat/{conversation}/typing` | Indicateur « en train d'écrire ». |

\| `POST` | `/2023-11/inbox/chat/{conversation}/read` | Marquer comme lu. |

\| `POST` | `/2023-11/inbox/chat/{conversation}/request-unlock` | Demande de déblocage (OTP) — **sans** token. |

**Envoi d'un message** :

```json theme={null}

{ "type": "text", "content": { "body": "Bonjour, ce produit est-il dispo ?" } }
```

* `type` requis ; `content.body` requis si `type=text` (max 5000 caractères). Les types
  **internes/système** sont refusés `403`).
* Réponse : la ressource `MessageResource` créée.
* La liste de messages renvoie aussi un `return_link` vers le canal d'origine (WhatsApp, Messenger,
  Instagram, Telegram) quand la conversation y est rattachée.

> Ce module est optionnel pour une boutique : utile pour le support / la conversion. Le jeton de

> conversation est émis par le widget de chat lors de l'ouverture d'une session.

***

### 4.16 Factures & documents publics

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/invoices/{orderId}` | Affiche la facture liée à une commande. |

\| `GET` | `/2023-11/invoices/{orderId}/invoice` | Version imprimable (PDF/HTML). |

Pratique pour proposer un lien « Télécharger ma facture » sur la page de confirmation / suivi de

commande, sans authentification (l`orderId` fait office de jeton d'accès — ne l'exposez pas

publiquement au-delà du client concerné).

***

### 4.17 Plans (hors boutique)

\| Méthode | Endpoint | Notes |

\|---|---|---|

\| `GET` | `/2023-11/plans` | Liste des plans d'abonnement Genuka. |

\| `GET` | `/2023-11/plans/{lookup_key}` | Détail d'un plan. |

Ces routes concernent l'abonnement **de l'entreprise à Genuka**, pas le storefront. À ignorer

pour un site e-commerce classique ; documentées ici par exhaustivité (routes publiques).

***

## 5. Multi-devise

* `CompanyResource` expose `currency` (devise par défaut) et `currencies[]` (additionnelles).
* Chaque `Shop` peut porter sa propre `currency_code` / `currency_name`.
* Devise d'une commande résolue : `currency_code` (requête) → boutique → entreprise → `XAF`.

**Recommandations front :**

1. Afficher la devise active (boutique avant entreprise).
2. Figer les prix transactionnels dans la devise de la commande.
3. Ne jamais reconvertir a posteriori une commande déjà créée.

***

## 6. Multi-boutique

* Liste des boutiques publiques via `/shops`.
* Scope via `X-ShopId` sur catalogue, stock, prix, checkout, suivi.
* Relations boutiques ↔ entrepôts pour le stock.

**Recommandations front :**

1. Imposer le choix d'une boutique au début du parcours.
2. Propager `X-ShopId` partout.
3. **Isoler le panier par boutique** (devise, stock et prix diffèrent).

***

## 7. Conventions & gestion des erreurs

\| Code | Signification | Forme |

\|---|---|---|

\| `200` | Succès | `data` ou objet métier |

\| `401` | Non authentifié (token manquant/invalide) | `{ "message": "..." }` |

\| `404` | Ressource introuvable | `{ "message": "..." }` |

\| `422` | Validation / `X-Company` manquant | `{ "message": "...", "errors": { } }` |

\| `400` | Erreur métier (stock, doublon client, lien de paiement…) | `{ "message": "..." }` |

* **Pagination** : enveloppe Laravel `{ data, links, meta }`. Contrôle via `per_page` ou `page[size]`.
* **Filtres/tris/includes** : convention Spatie QueryBuilder `filter[x]`, `sort`, `include`).
* **Idempotence** : l'API détecte les doublons `CheckDuplicateOrderTask`), mais protégez aussi
  le bouton de paiement côté front (désactivation + clé d'idempotence applicative).

***

## 8. Checklist e-commerce (à ne pas oublier)

1. Panier persistant (invité + connecté) et fusion à la connexion — géré côté front.
2. Idempotence du `POST /customers/orders` (anti double-commande).
3. Revérification serveur des prix/stock : ne jamais faire confiance au front (déjà appliqué par le pipeline).
4. Taxes complètes (produits, livraison, remises) — calculées côté serveur, à afficher fidèlement.
5. E-mails transactionnels : confirmation, paiement, expédition, échec.
6. Webhooks / polling pour synchroniser statut commande/paiement/livraison.
7. Remboursements, retours, notes de crédit `returns`, `credit_notes` dans `OrderDetailResource`).
8. SEO catalogue (slug `handle`, métadonnées, [schema.org](http://schema.org)).
9. Conformité RGPD/PII (rétention, droit à l'effacement).
10. Observabilité (correlation id dans les logs, alertes paiement/webhook).
11. Anti-abus du checkout (rate limit, captcha selon le risque).
12. Tests E2E du parcours complet (catalogue → panier → paiement → suivi).

***

## 9. Roadmap d'implémentation conseillée

\| Phase | Contenu |

\|---|---|

\| **1** | Contexte entreprise + boutique · liste/détail produits + variantes · panier local |

\| **2** | Auth client + adresses · checkout (livraison/retrait + remise) · création de commande |

\| **3** | Lien de paiement + callbacks providers · pages suivi commande/livraison · documents |

\| **4** | Multi-devise · multi-boutique · durcissement (idempotence, sécurité, monitoring) |

> ⚠️ La sécurité, l'idempotence et le monitoring (phase 4) ne sont pas optionnels : intégrez-les

> au plus tôt plutôt que de les repousser en fin de projet.
