# Storefront API TikTak PRO — Phase 1 (MVP) Contrat public pour un front custom (Claude, Cursor, un développeur) qui affiche le catalogue TikTak et prend des commandes COD (paiement à la livraison). Le backoffice TikTak reste la source de vérité (produits, stock, transporteurs, upsells, réglages boutique). Le site généré ne fait que consommer ces endpoints. Base URL production : `https://api.tiktakpro.com/api/v1/` Auth Phase 1 : aucune (lecture publique + création de commande). Toujours passer `company` (hashid boutique). --- ## Périmètre | Inclus | Exclu (phases suivantes) | |--------|--------------------------| | Bootstrap boutique | Compte client, login, VIP | | Catalogue, catégories, recherche | Paiement en ligne (Flouci, Konnect, …) | | Homepage builder (optionnelle) | Zones de livraison dynamiques signées | | Panier côté client | Panier abandonné, CAPI Facebook | | Codes promo / auto-remise | Compte, adresses, wishlist | | Checkout COD + confirmation | MCP / dashboard | | Upsell post-achat COD | | Le store officiel `store-nuxt` reste l'implémentation de référence. Un site custom peut ignorer le builder homepage et n'utiliser que le catalogue. --- ## Conventions ### Identifiants | Entité | Type d'id | Exemple | |--------|-----------|---------| | Boutique (`company`) | Hashid string | `8GPmlML` | | Produit, variante, catégorie, transport, page | Entier | `4218` | | Upsell | Hashid string | `XyZ1` | | Commande (côté store) | `order_code` string = `order_ref` | `K7P2M` | Ne jamais inventer d'id. Toujours les lire dans les réponses. ### Pagination (listes DRF) Query : `?page=1&size=20` (`size` max 1000, défaut 10). ```json { "count": 84, "total_pages": 5, "current_page": 1, "next": "https://…?page=2", "previous": null, "results": [] } ``` `website/menus-read/` et `website/informations-read/` ne sont pas paginés (tableau / objet unique). ### Filtres produits fréquents Toujours : `company={hashid}` + `active=true`. | Paramètre | Rôle | |-----------|------| | `no_parent=true` | Uniquement produits parents (pas les variantes) | | `show-children=false` | N'embarque pas `declinaisons[]` (listes) | | `show-children=true` | Inclut les variantes (fiche produit) — défaut API = `true` | | `ids_in=1,2,3` | Produits ciblés | | `ids_not_in=1` | Exclusion | | `has_category=12` ou `12,15` | Catégorie principale ou M2M | | `search=` | Nom / référence / code-barres | | `ordering=` | `created_at`, `updated_at`, `price`, `sold`, `name`, `reference`, `order` (préfixe `-` = desc) | | `discount__gte=1` | Produits en promo | | `has_attributs=` | Filtre facette (ids d'attributs) | Le queryset public force déjà `active=True` et `display_on_website=True`. --- ## Logique globale ``` Domaine / slug │ ├─ GET get-store/ → company.id, currency, works_with_transport └─ GET website/informations-read/ → store_settings (checkout, COD, formulaire) │ ├─ menus, homepage (optionnel), catalogue │ Panier = localStorage (aucun endpoint panier) │ ├─ GET transports-read/ si works_with_transport └─ POST website/get-auto-discount │ POST create-fast-order/ payement_type = CASH │ ├─ upsell_id → GET upsell + page offre → POST update-order-upsell | refuser └─ sinon → confirmation │ GET get-order-by-idref/{order_ref}/{company} ``` Le backend recalcule les totaux. Le `total_amount` envoyé est indicatif. Prix, remise, transport, stock sont repris côté serveur. --- ## 1. Bootstrap Identifier la boutique par slug (`boutique.tiktak.space` → `boutique`) ou domaine custom (`?server=www.client.tn`). ### `GET get-store/?slug={slug}` ou `?server={host}` Réponse `CompanyWebsiteSerializer` : `id` (hashid), `name`, `slug`, `logo`, `phone`, `email`, `city`, `country`, `currency`, `currency_display`, `currency_text`, `currency_locale`, `works_with_transport`, `store_activated`, `config_payment_status`. 403 si introuvable. ### `GET website/informations-read/?slug={slug}` ou `?server={host}` Objet `Website` (pas une liste). Champs utiles Phase 1 : | Champ | Usage | |-------|--------| | `store_settings.cash_on_delivery` | Afficher COD | | `store_settings.online_payment` | Ignorer en Phase 1 | | `store_settings.default_payement_type` | `CASH` (défaut) ou `ONLINE` | | `store_settings.formCheckout` | Champs checkout dynamiques (`slug`, `display`, `required`) | | `store_settings.checkout_message` | HTML sous le « merci » | | `store_settings.payment_provider` | Inutile en COD | | `seo_settings` | Title / meta | | `css_settings` | Couleurs du thème officiel (optionnel) | | `maintain_settings` | Maintenance, modules | | `stock_settings` | Comportement rupture | Slugs `formCheckout` → payload commande : | `slug` formulaire | Champ `order.*` | |-------------------|-----------------| | `name` | `name` | | `phone` | `phone` | | `phone_extra` | `phone_extra` | | `email` | `email` | | `address` | `address` | | `gouv` | `gouvernorat` | | `city` | `delegation` | | `code` | `code_postal` | | `country` | `country` (défaut `TN`) | | `comment` | `comment` | | `create_account` | racine `create_account` (bool) | | `terms` | validation front uniquement | | `discount_code` | racine `promo_code` | Si `formCheckout` est vide ou tous les champs cœur sont `display: false`, afficher un formulaire défaut : nom, téléphone, adresse, gouvernorat. ### `GET website/menus-read/?company={id}&position=header&active=true` Tableau (non paginé). Prendre `[0].menus`. Chaque item : `label`, `type` (`category` | `page` | `extern` | …), `slug`, `href`, `cat_id`, `submenu[]`, `clickable`, `is_home`. `position=footer` pour le pied. --- ## 2. Catalogue ### Listes — `GET products-read/` ``` # Grille catégorie products-read/?company={id}&page=1&size=20&active=true&no_parent=true&show-children=false&has_category={cat_id}&ordering=-created_at # Recherche products-read/?company={id}&no_parent=true&active=true&show-children=false&search={q} # IDs (homepage, wishlist, upsell) products-read/?company={id}&active=true&no_parent=true&ids_in=1,2,3&size=30&show-children=false ``` En liste, `show-children=false` : plus léger. Sur la fiche, `show-children=true` (défaut). ### Fiche — id ou slug - `GET products-read/{id}/?company={id}` si l'URL est un entier - `GET product-by-slug?seo_slug={slug}&company={id}` sinon (parent uniquement, `display_on_website`) Champs produit utiles : | Champ | Rôle | |-------|------| | `id`, `parent` | Variante : `parent` = id du produit parent | | `name`, `description`, `photo`, `photo_thumb`, `images[]` | Médias | | `price`, `discount`, `discount_type` | `fixed_amount` ou `percent` / `percentage` | | `formula[]` | Palier qty : `{ quantity, discount, discount_type }` | | `declinaisons[]` | Variantes (`active`, `_attributs`, stock, prix) | | `declinaison` | Bool « a des variantes » | | `stock`, `total_stock`, `active_stock`, `order_without_stock` | Stock | | `seo_slug`, `seo_title`, `seo_description` | URL / SEO | | `_category` | Catégorie principale | | `custom_delivery_price`, `delivery_price` | Port spécifique produit | | `reference` | SKU | Prix unitaire affiché : ``` si discount_type == "fixed_amount": price - discount si "percent" | "percentage": price - (price * discount / 100) si formula et qty >= palier: appliquer le palier le plus haut dont quantity <= qty ``` Le backend recalcule à la commande (`getPriceOfProduct` serveur). Variantes : l'utilisateur choisit une déclinaison ; c'est `declinaisons[i].id` qui part dans `product_id`. `product_parent_id` = `parent` de la variante, sinon `id` du produit. Stock : si `active_stock` et pas `order_without_stock`, refuser qty > stock de la ligne commandée (variante, pas le parent). ### Mise en page fiche — `GET product-extra-read/?product={id}&company={id}` Tableau ; prendre `[0]`. `data[]` = blocs (`title`, `price`, `variants`, `description`, `checkout_system`, `avis`, …). `data_full_page[]` = sections longues. ### Catégorie - `GET categories-read/{id}/` — nom, image, `subcategories[]`, `seo_slug` - `GET category-breadcrumb/{cat_id}/{company}` — `[{ id, name, seo_slug }, …]` racine → feuille --- ## 3. Homepage (optionnelle) `GET website/page-read/?page_type=home&company={id}` → `{ results: [ page ] }`. `GET website/page-read/{id}/` → objet page. `main_content` = JSON du builder Nuxt (sections). Un site custom n'est pas obligé de le rendre. --- ## 4. Panier — pas d'API Le panier vit dans `localStorage` (clé `cart` sur le store officiel). ```json { "_details": [], "transport": null, "transport_id": null, "transport_price": 0, "transport_list": [], "discount": 0, "subtotal": 0, "total": 0, "promoCode": "", "promoCodeDiscount": 0 } ``` Ligne (`_details`) : ```json { "product_id": 4219, "product_parent_id": 4218, "product_name": "T-shirt", "product_ref": "TS-001", "category_id": 12, "quantity": 2, "active_stock": true, "price_ttc": 49.9, "price_ttc_after_discount": 39.9, "final_price": 79.8, "discount": 10, "product_attrs": "Rouge M", "product_thumb": "https://…", "_product": {} } ``` - Fusionner deux lignes si même `product_id` (+ mêmes options). - Recalculer `final_price` à chaque changement. - `subtotal` = somme des `final_price`. - `total` = `subtotal - discount + transport_price`. --- ## 5. Livraison Si `company.works_with_transport` : `GET transports-read/?company={id}` → paginé, `use_for_web=true` et `active=true`. Champs : `id`, `name`, `price`, `default`, `gouvernorates` (JSON). Choisir le transporteur `default === true`, sinon laisser l'utilisateur choisir. Envoyer `order.transport_id`. Prix custom produit : si tous les articles ont `custom_delivery_price`, le port = `max(delivery_price)`. Si au moins un n'en a pas : `max(custom…, transporteur.price)`. Phase 1 : ne pas implémenter `delivery_quote_token` / `transport_key=delivery_zone_dynamic`. --- ## 6. Remises `POST website/get-auto-discount` ```json { "slug": "ma-boutique", "details": [ { "product_id": 4218, "product_parent_id": 4218, "quantity": 1, "final_price": 39.9 } ], "code": "", "delivery_price": 7 } ``` - `code` vide → remise automatique boutique. - `code` renseigné → code promo. Réponse Phase 1 : nombre = montant de remise (pas un %). Si franco, ce montant peut égaler `delivery_price`. Au checkout, n'envoyer `promo_code` que pour un vrai code saisi (auto → `""`). --- ## 7. Checkout COD ### `POST create-fast-order/` `Content-Type: application/json`. Idéal : proxy serveur pour injecter l'IP client dans `meta_data.client_ip`. ```json { "company": "8GPmlML", "source": "SITE CUSTOM", "promo_code": "", "create_account": false, "provider": null, "meta_data": { "client_ip": "41.x.x.x", "fingerprint": { "id": "fp_…" }, "visit_source": null }, "order": { "name": "Ahmed Ben Ali", "phone": "22123456", "phone_extra": "", "email": "", "address": "12 rue …", "gouvernorat": "Tunis", "delegation": "Tunis", "code_postal": "1000", "country": "TN", "comment": "", "payement_type": "CASH", "transport_id": 3, "total_amount": 86.8, "_details": [] } } ``` Règles : - `payement_type` : `CASH` (COD). Pas `ONLINE` en Phase 1. - `_details` obligatoire, non vide. - `phone` : espaces retirés côté serveur. - 429 = trop de commandes IP/fingerprint → page succès factice (ne pas révéler le blocage). - Challenge checkout OBLIGATOIRE : un front custom doit avoir un challenge checkout en place avant d'appeler `create-fast-order/` (fingerprint navigateur dans `meta_data.fingerprint.id`, IP réelle dans `meta_data.client_ip` via proxy serveur, rate-limit par IP + fingerprint). Une requête sans fingerprint ni challenge est non conforme et peut être rejetée. - 400 : `{ "detail": "…" }` (`_details is required`, `No step configured.`, …). Succès : ```json { "order_ref": "K7P2M", "payment": {}, "upsell_id": "XyZ1" } ``` `upsell_id` peut être `null` ou `""`. Routage : ``` CASH + upsell_id → /upsell/{upsell_id}/{order_ref} CASH sans upsell → /checkout-confirmation/{order_ref} ONLINE → hors Phase 1 (URL dans payment.payUrl | paymentUrl | link) ``` Vider le panier seulement après succès (sauf 429). Sécurité : la création de commande doit passer par une route serveur de la boutique, qui seule ajoute la clé de sécurité. Ne jamais exposer cette clé dans le bundle front. --- ## 8. Upsell (COD uniquement) Le serveur ne propose un upsell que si `payement_type != ONLINE`. Matching : `product_parent_id` des lignes ∩ `Upsell.product_to_buy`, `active=True`, meilleur `priority`. Un seul upsell. `discount` = montant devise boutique, pas un %. 1. `GET website/upsell/{company}/{upsell_id}` — `name`, `text_upsell` (HTML), `text_footer`, `product_to_propose`, `discount`, `quantity`, `free_shipping`, `confirm_button`, `cancel_button` 2. `GET get-order-by-idref/{order_ref}/{company}` — commande déjà créée 3. `GET products-read/?ids_in={product_to_propose}&company=…` — prix barré = `product.price` ; prix offre = `price - upsell.discount` 4. Accepter : `POST update-order-upsell` `{ "upsell", "order": "", "company" }` → `"Success"` 5. Refuser : aller à la confirmation sans POST Si le produit proposé est vide / erreur → confirmation directe. --- ## 9. Confirmation `GET get-order-by-idref/{order_ref}/{company}` Afficher : `order_code` / `order_number`, `created_at`, `name`, `phone`, `address`, `gouvernorat`, `delegation`, `payement_type`, `details[]` (`product_name`, `product_thumb`, `quantity`, `final_price`, `is_upsell`), `transport_price`, `discount`, `total_after_discount`, `checkout_message` du store. 404 si ref inconnue. --- ## Catalogue des 17 endpoints | # | Méthode | Chemin | Rôle | |---|---------|--------|------| | 1 | GET | `get-store/` | Boutique | | 2 | GET | `website/informations-read/` | Réglages store | | 3 | GET | `website/menus-read/` | Navigation | | 4 | GET | `website/page-read/` | Homepage / pages (`page_type` ou id) | | 5 | GET | `products-read/` | Liste / recherche | | 6 | GET | `products-read/{id}/` | Fiche par id | | 7 | GET | `product-by-slug` | Fiche par `seo_slug` | | 8 | GET | `product-extra-read/` | Blocs fiche | | 9 | GET | `categories-read/{id}/` | Catégorie | | 10 | GET | `category-breadcrumb/{id}/{company}` | Fil d'Ariane | | 11 | GET | `transports-read/` | Transporteurs web | | 12 | POST | `website/get-auto-discount` | Remise | | 13 | POST | `create-fast-order/` | Créer commande | | 14 | GET | `get-order-by-idref/{ref}/{company}` | Lire commande | | 15 | GET | `website/upsell/{company}/{id}` | Config upsell | | 16 | POST | `update-order-upsell` | Accepter upsell | | 17 | GET | `website/page-read/{id}/` | Page CMS par id | Ne pas appeler d'autres routes (`products/`, `orders/`, `login-jwt/`, stats, Shopify, MCP, …). --- ## Pièges (ne pas réinventer) 1. `company` est un hashid, pas un entier. 2. Pas d'endpoint panier — localStorage / Pinia / cookie. 3. `payement_type` s'écrit avec un seul `e` (pas `payment_type`). 4. `order_ref` = `order_code`, pas l'id numérique interne. 5. Upsell jamais après ONLINE. 6. `discount` upsell = montant, pas un pourcentage. 7. Totaux recalculés serveur — ne pas faire confiance au front pour le prix final. 8. Listes : `no_parent=true` + `show-children=false` ; fiche : `show-children=true`. 9. Pagination : `size`, pas `page_size` / `limit`. 10. Challenge checkout exigé pour tout front custom : `meta_data.fingerprint` + IP client + rate-limit. Le HMAC Nuxt (`X-Checkout-Challenge`) reste spécifique au store officiel. --- ## Référence code (store officiel) | Sujet | Fichier | |-------|---------| | Bootstrap | `store-nuxt/src/server/api/company-data.get.js`, `store-info.get.js` | | Commande | `store-nuxt/src/composables/useOrderService.js` | | Panier | `store-nuxt/src/composables/services/cartService.js` | | Prix | `store-nuxt/src/composables/services/helpers.js` → `getPriceOfProduct` | | Upsell page | `store-nuxt/src/pages/upsell/[upsell_id]/[order_ref]/index.vue` | | Confirmation | `store-nuxt/src/pages/checkout-confirmation/[order_ref]/index.vue` | | Django order | `shopmanager/api/order/views.py` → `SecureWebsiteOrder` | --- Documentation web : https://tiktakpro.com/api-documentation