Trackily Docs

Shipping

TL;DR : tu déclares des zones (groupes de country codes) et N rates par zone (Standard, Express, Free over 50). Une seule zone is_rest_of_world=true (catch-all) est autorisée. Côté checkout, le code prend le pays du visiteur, trouve la première zone qui match, présente ses rates au visiteur.

Zones de shipping

Concept

Trackily Commerce native suit le modèle Shopify pour la livraison :

  1. Tu regroupes des pays dans des zones (shipping_zones) — par exemple "European Union" = FR, DE, IT, ES, BE, NL.
  2. Pour chaque zone, tu définis un ou plusieurs rates (shipping_rates) — par exemple "Standard 5 €" + "Express 15 €".
  3. Au checkout, Trackily lit le pays du visiteur (depuis l'adresse qu'il a saisie), match la première zone qui contient ce code, présente la liste des rates de cette zone.

Le visiteur choisit. Trackily snapshote le shipping_rate_name et le prix dans la commande, pour que la commande reste lisible même si tu changes ou supprimes la rate après coup.

Anatomie d'une zone (shipping_zones)

Champ Type Rôle
id SERIAL identifiant interne
name TEXT nom affiché ("European Union", "US & Canada")
country_codes JSONB tableau de codes ISO-3166 alpha-2 (["FR", "DE", "IT"])
is_rest_of_world BOOLEAN si true, catch-all (et country_codes doit être vide)
is_active BOOLEAN si false, ignorée par le checkout
position SMALLINT ordre d'évaluation (la première qui match gagne)

Index unique partiel : une seule zone peut avoir is_rest_of_world=true :

CREATE UNIQUE INDEX idx_shipping_zones_row_unique
  ON shipping_zones(is_rest_of_world)
  WHERE is_rest_of_world = true;

Tenter d'en créer une deuxième renvoie l'erreur "A rest-of-world zone already exists".

Anatomie d'une rate (shipping_rates)

Champ Type Rôle
id SERIAL identifiant interne
zone_id INTEGER FK zone parente
name TEXT nom affiché au visiteur ("Standard", "Express", "Free over 50")
price NUMERIC(12,2) tarif fixe
currency TEXT ISO-3 ou chaîne vide (vide = s'applique à toutes les devises)
min_order_amount NUMERIC(12,2) optionnel — rate masquée si subtotal < seuil
max_order_amount NUMERIC(12,2) optionnel — rate masquée si subtotal > seuil
is_active BOOLEAN flag d'activation
position SMALLINT ordre d'affichage au checkout

Fallback : native_products.shipping_cost

Si aucune zone ne match (parce que le pays n'est dans aucune zone et qu'il n'y a pas de zone rest-of-world), Trackily retombe sur la shipping_cost flat du produit (native_products.shipping_cost). C'est le mode "simple" pour les opérateurs qui ne veulent pas se prendre la tête avec les zones — toutes les commandes payent le même shipping.

L'ordre de résolution :

1. Le visiteur a saisi un country_code
2. SELECT * FROM shipping_zones
    WHERE is_active=true
      AND ( $country = ANY(country_codes) OR is_rest_of_world = true )
    ORDER BY is_rest_of_world ASC, position ASC, id ASC LIMIT 1
3. Si une zone est trouvée → lister ses shipping_rates actives filtrées
   par currency + min/max_order_amount → laisser le visiteur choisir
4. Sinon → fallback sur native_products.shipping_cost (et fallback
   sur free_shipping_above si le subtotal dépasse le seuil)

Snapshot dans la commande

À la création de l'order, on persiste dans la ligne orders :

  • shipping_cost (NUMERIC) — le montant final
  • shipping_rate_name (TEXT) — le nom de la rate, dupliqué pour ne pas dépendre de la table source
  • shipping_zone_id (INTEGER FK, SET NULL) — pour les rapports

Tu peux donc supprimer une rate ou rebaptiser une zone sans casser les commandes historiques.

Comment faire (UI + MCP)

Via l'admin UI

  1. Settings → Shipping zones (ou /admin#shipping-zones).
  2. + Add zone pour créer une nouvelle zone.
  3. Remplis :
    • Name ("European Union").
    • Countries (tape les codes ou sélectionne dans le picker).
    • Rest of world — coche si c'est ta zone catch-all (et laisse Countries vide).
  4. Save. La zone apparaît dans la liste.
  5. Clique sur la zone → tab "Rates" → + Add rate.
  6. Pour chaque rate, remplis :
    • Name ("Standard", "Express", "Free over 50 €").
    • Price (5.00).
    • Currency (EUR — ou vide pour s'appliquer à toutes les devises).
    • Min order / Max order (optionnels).
    • Position (ordre d'affichage).
  7. Save.

Via MCP

Lister :

{
  "name": "list_shipping_zones",
  "arguments": {}
}

Récupérer une zone avec ses rates :

{
  "name": "get_shipping_zone",
  "arguments": { "id": 3 }
}

Créer une zone avec ses rates inline (un seul appel — c'est plus rapide que zone + 2 rates en 3 appels) :

{
  "name": "create_shipping_zone",
  "arguments": {
    "name": "European Union",
    "country_codes": ["FR", "DE", "IT", "ES", "BE", "NL", "LU", "PT", "AT", "IE", "FI"],
    "is_rest_of_world": false,
    "is_active": true,
    "rates": [
      { "name": "Standard", "price": 5.00, "currency": "EUR" },
      { "name": "Express", "price": 15.00, "currency": "EUR" },
      { "name": "Free over 75 €", "price": 0.00, "currency": "EUR", "min_order_amount": 75.00 }
    ]
  }
}

Tier-2 : première réponse confirmation_required avec preview + confirm_token. Tu re-soumets avec le token pour exécuter.

Update :

{
  "name": "update_shipping_zone",
  "arguments": {
    "id": 3,
    "name": "Union européenne",
    "country_codes": ["FR", "DE", "IT", "ES", "BE", "NL", "LU", "PT", "AT", "IE", "FI", "SK"],
    "is_active": true
  }
}

Delete (cascade les rates) :

{
  "name": "delete_shipping_zone",
  "arguments": { "id": 3 }
}

Ajouter une rate à une zone existante :

{
  "name": "create_shipping_rate",
  "arguments": {
    "zone_id": 3,
    "name": "Express 24h",
    "price": 19.90,
    "currency": "EUR",
    "min_order_amount": 0,
    "position": 2
  }
}

Update / delete d'une rate :

{
  "name": "update_shipping_rate",
  "arguments": { "id": 14, "price": 6.00 }
}
{
  "name": "delete_shipping_rate",
  "arguments": { "id": 14 }
}

Exemple réel : 3 zones (EU, US, ROW)

C'est la config minimale que je recommande pour un opérateur qui vend à l'international depuis l'Europe.

Zone 1 : European Union — tarifs en EUR

{
  "name": "create_shipping_zone",
  "arguments": {
    "name": "European Union",
    "country_codes": ["FR", "DE", "IT", "ES", "BE", "NL", "LU", "PT", "AT", "IE", "FI", "SK", "SI", "GR", "DK", "SE", "CZ", "PL", "HU", "RO", "BG", "HR", "CY", "MT", "LT", "LV", "EE"],
    "position": 0,
    "rates": [
      { "name": "Standard", "price": 5.00,  "currency": "EUR", "position": 0 },
      { "name": "Express",  "price": 15.00, "currency": "EUR", "position": 1 },
      { "name": "Free over 75 €", "price": 0.00, "currency": "EUR", "min_order_amount": 75.00, "position": 2 }
    ]
  }
}

Zone 2 : United States — tarifs en USD

{
  "name": "create_shipping_zone",
  "arguments": {
    "name": "United States",
    "country_codes": ["US"],
    "position": 1,
    "rates": [
      { "name": "Standard", "price": 10.00, "currency": "USD", "position": 0 },
      { "name": "Express",  "price": 25.00, "currency": "USD", "position": 1 }
    ]
  }
}

Zone 3 : Rest of world — catch-all

{
  "name": "create_shipping_zone",
  "arguments": {
    "name": "Rest of world",
    "is_rest_of_world": true,
    "country_codes": [],
    "position": 99,
    "rates": [
      { "name": "International standard", "price": 20.00, "currency": "" }
    ]
  }
}

currency: "" signifie "applique-toi à n'importe quelle devise" — la rate s'affiche que le visiteur paye en EUR, USD, GBP ou autre. Pratique pour les zones "international" où tu factures un tarif unique converti à la volée par Stripe.

Résultat au checkout

  • Un visiteur français qui achète 30 € voit "Standard 5 €" + "Express 15 €".
  • Le même visiteur qui dépasse 75 € voit "Free over 75 €" en plus, déjà à 0.
  • Un visiteur US voit "Standard $10" + "Express $25".
  • Un visiteur japonais (pas dans EU ni US) voit "International standard 20" (devise = celle du produit, par exemple EUR).

Stratégies de pricing shipping

Quelques patterns qui marchent bien chez les opérateurs Trackily :

  • Free shipping over X — la mécanique de loin la plus efficace pour augmenter le panier moyen. Régle ton seuil à ~1.5× ton prix unitaire moyen.
  • Express comme upsell visible — afficher l'option Express à côté du Standard transforme 10 à 15 % des acheteurs en payeurs Express, marge nette.
  • Shipping inclus dans le prix — au lieu de facturer 5 € de shipping sur un produit à 25 €, vends-le à 30 € avec "Livraison gratuite" en argument hero. Conversion +10-20 % observée sur les supplements.
  • Zones par bandes tarifaires — au lieu de 1 zone EU avec 1 rate à 5 €, fais 2 zones (Zone EU-proche : FR/BE/DE à 4 € ; Zone EU-loin : ES/PT/GR/PL à 8 €). Plus de réalisme, moins de pertes sur les longues distances.

Erreurs courantes

  • "A rest-of-world zone already exists" — il ne peut y en avoir qu'une. Édite l'existante au lieu d'en créer une deuxième.
  • "Le visiteur ne voit aucune rate" — la zone qui match a 0 rate active dans la bonne currency, OU les filtres min/max_order_amount excluent toutes les rates pour ce subtotal. Le fallback native_products.shipping_cost prend alors le relais (si configuré).
  • "Mon shipping reste à 0 alors que j'ai configuré une rate"free_shipping_above sur le produit est sans doute déclenché. Ou le min_order_amount d'une rate "free" est dépassé.
  • "Plusieurs zones contiennent le même pays" — la première qui match gagne (position ASC, id ASC). Réordonne en jouant sur position. Évite si possible — c'est trompeur.
  • "La currency vide ne fonctionne pas pour ma rate" — vérifie que ton produit a bien une currency setté (USD/EUR…) ; les rates en currency vide héritent de la currency du produit pour le calcul du total, ne se substituent pas à l'absence de currency.
  • "delete_shipping_zone me dit qu'il a tout supprimé mais les commandes historiques affichent encore la rate" — c'est attendu, les colonnes shipping_rate_name + shipping_zone_id sont snapshotées sur la commande. Le zone_id passe à NULL mais le name est conservé.

Voir aussi

  • products.mdshipping_cost et free_shipping_above au niveau produit (fallback).
  • orders.md — comment shipping_cost et shipping_rate_name sont persistés sur la commande.
  • Admin UI → Settings — l'onglet Shipping zones dans Settings.
  • MCP list_shipping_zones / create_shipping_zone / create_shipping_rate — référence complète des outils Tier-2.