Trackily Docs

Variants

TL;DR : un produit peut avoir jusqu'à 3 axes d'options (Color, Size, Material) et autant de variantes que le produit cartésien. Chaque variante porte son propre prix, SKU, stock, image. stock = -1 signifie illimité.

Variantes dans la fiche produit

Concept

Une variante est l'unité réellement achetable. C'est elle qui a un prix, un SKU, un stock et une image, pas le produit. Cette séparation reprend le modèle Shopify (le standard de facto) : tu décris d'abord les axes ("Couleur", "Taille"), ensuite tu génères toutes les combinaisons et tu ajustes prix + stock par ligne.

Trois axes maximum (Shopify aussi). Au-delà, ton matrix devient ingérable : 4 axes × 3 valeurs = 81 SKUs à entretenir, c'est plus rentable de splitter en plusieurs produits.

Anatomie d'une variante (native_product_variants)

Champ Type Rôle
id SERIAL identifiant interne
product_id INTEGER FK rattachement au produit
option1_value TEXT valeur de l'axe 0 (par exemple "Rouge")
option2_value TEXT valeur de l'axe 1 (par exemple "L")
option3_value TEXT valeur de l'axe 2 (par exemple "Coton")
price NUMERIC(12,2) prix unitaire dans la currency du produit
sku TEXT référence interne (libre — Trackily ne l'utilise pas pour le matching)
stock INTEGER quantité en stock. -1 = illimité (digital ou dropship)
image_url TEXT image spécifique à la variante (override de l'image produit)
position SMALLINT ordre d'affichage
is_active BOOLEAN si false, masquée partout (catalog, /p/, checkout, MCP)

Le couple options + variantes

Les options vivent dans native_product_options :

Champ Rôle
name nom de l'axe ("Color", "Size")
position 0, 1 ou 2 — détermine si elle alimente option1/2/3_value
values JSONB — tableau des valeurs possibles (["Rouge", "Bleu", "Vert"])

Quand tu déclares :

  • Option 0 = Color → valeurs ["Rouge", "Bleu"]
  • Option 1 = Size → valeurs ["S", "M", "L"]

…tu peux créer jusqu'à 6 variantes (Rouge/S, Rouge/M, Rouge/L, Bleu/S, Bleu/M, Bleu/L). Tu n'es pas obligé de toutes les créer — les combinaisons absentes ne s'afficheront pas dans le sélecteur de variante (rouge/M sera grisée si tu l'as enlevée).

Convention stock = -1 : illimité

stock est un INTEGER, jamais nullable. Trois valeurs ont un sens spécial :

  • -1 → stock illimité. Pas de décrément, jamais d'out-of-stock. Utilisé pour les produits digitaux et les dropship où ton fournisseur a un stock infini de ton point de vue.
  • 0 → out of stock. La variante reste cliquable mais le checkout refuse l'achat.
  • > 0 → stock fini. Décrémenté atomiquement à la création de la commande, restocké atomiquement à l'annulation / refund.

L'admin UI affiche un badge "Unlimited" quand stock=-1. La liste produit le résume avec has_unlimited_stock=true (n'importe quelle variante illimitée fait flipper le flag).

Index unique sur la combinaison

CREATE UNIQUE INDEX idx_npv_combo
  ON native_product_variants(product_id, option1_value, option2_value, option3_value);

Tu ne peux pas créer deux variantes "Rouge/M/Coton" pour le même produit. Si tu essaies, Postgres rejette l'INSERT avec un code 23505 → l'UI te montre "Variant combination already exists".

Comment faire (UI + MCP)

Via l'admin UI

  1. Ouvre la fiche produit → onglet Variants.
  2. Section Options en haut : déclare tes axes.
    • Clique + Add option.
    • Nomme-le ("Color"), tape Enter sur chaque valeur ("Rouge", "Bleu", "Vert"), valide.
    • Jusqu'à 3 options.
  3. Section Variants : Trackily a généré le produit cartésien.
  4. Pour chaque ligne, remplis :
    • Price (obligatoire — sinon variante refusée au checkout).
    • SKU (optionnel — utile pour ton fournisseur).
    • Stock (-1 par défaut si tu ne touches pas).
    • Image URL (optionnel — override de l'image produit).
    • Active (cocher / décocher pour masquer sans supprimer).
  5. Save. L'écran se rafraîchit, l'index unique est appliqué à la sauvegarde.

Tu peux aussi réordonner les variantes par drag-and-drop (alimente position). L'ordre d'affichage sur la page publique suit ce champ.

Via MCP

Les outils variants natifs ne sont pas exposés directement (la gestion des variantes natives se fait via l'admin REST /admin/api/native-products/:id/variants). Les MCP add_product_variant / update_product_variant / delete_product_variant que tu vois dans le registre ciblent le côté Shopify.

Pour les variantes natives, le pattern recommandé est :

{
  "name": "get_native_product_detail",
  "arguments": { "product_id": 12 }
}

…qui te renvoie le tableau complet options + variants, puis tu modifies via update_product avec un patch sur variants. Exemple :

{
  "name": "update_product",
  "arguments": {
    "platform": "native",
    "product_id": 12,
    "patch": {
      "options": [
        { "name": "Durée", "position": 0, "values": ["30 jours", "60 jours", "90 jours"] }
      ],
      "variants": [
        { "option1_value": "30 jours", "price": 29.90, "sku": "MM-30", "stock": 200, "is_active": true, "position": 0 },
        { "option1_value": "60 jours", "price": 49.90, "sku": "MM-60", "stock": 150, "is_active": true, "position": 1 },
        { "option1_value": "90 jours", "price": 69.90, "sku": "MM-90", "stock": 80,  "is_active": true, "position": 2 }
      ]
    }
  }
}

Côté Shopify, le set complet des outils variants est disponible :

{
  "name": "add_product_variant",
  "arguments": {
    "product_id": 12,
    "option1": "XL",
    "price": "39.90",
    "sku": "TS-XL-RED",
    "inventory_quantity": 50
  }
}
{
  "name": "update_product_variant",
  "arguments": {
    "variant_id": 4480123456,
    "store_id": 3,
    "patch": { "price": "34.90", "inventory_quantity": 25 }
  }
}
{
  "name": "delete_product_variant",
  "arguments": {
    "product_id": 12,
    "variant_id": 4480123456
  }
}

Pour les variantes Shopify, l'API refuse de supprimer la dernière variante d'un produit. Trackily propage l'erreur. Solution : supprime le produit entier au lieu de gratter la dernière variante.

Exemples concrets

1. Supplément avec 3 durées de cure

Produit Memo Mind avec 1 option "Durée" :

option1_value price sku stock
30 jours 29.90 MM-30 200
60 jours 49.90 MM-60 150
90 jours 69.90 MM-90 80

Sur la page publique, le sélecteur de variante est un radio group. Le bloc "Économisez 15 %" est calculé à la volée par le renderer (compare le prix par jour). Bonus : la variante 90 jours étant la plus chère mais aussi celle au meilleur prix unitaire, c'est elle que generate_landing_for_product va naturellement mettre en avant.

2. T-shirt avec Color × Size

Options :

  • Option 0 = Color → ["Noir", "Blanc", "Rouge"]
  • Option 1 = Size → ["S", "M", "L", "XL"]

12 variantes possibles. Tu n'es pas obligé d'en avoir 12 — si tu n'as pas le Rouge en XL, tu ne crées simplement pas la ligne. Le sélecteur côté front filtre les combinaisons disponibles.

Prix uniforme à 24.90, SKU TS-<COLOR>-<SIZE> (ex : TS-NOIR-M), stock initial 50 par variante. Au bout d'un mois tu vois via get_native_product_stats que TS-NOIR-L représente 38 % des ventes → tu remontes son stock sans toucher aux autres.

3. Ebook digital sans variante

Pour un produit à prix unique sans déclinaison, tu peux soit :

  • Garder la variante par défaut créée à la création du produit. Elle a option1_value='' (chaîne vide), c'est is_active=true, son prix est ton prix de vente.
  • Ajouter une option "Format" avec valeurs ["PDF"] si tu veux que ça apparaisse explicitement dans le sélecteur (utile pour les variantes "PDF + Audio" plus tard).

Stock management en détail

Trois moments où le stock bouge :

  1. Création d'une commande (/api/order POST). Pour chaque ligne du panier, Trackily exécute :
    UPDATE native_product_variants
       SET stock = stock - $1
     WHERE id = $2 AND (stock = -1 OR stock >= $1)
    RETURNING stock;
    
    Si la mise à jour ne retourne aucune ligne (parce que stock < quantity), la transaction est rollback et le checkout renvoie 409 avec out_of_stock=true. Atomic — pas de race entre deux acheteurs sur la dernière unité.
  2. Annulation (cancel_native_order) ou refund total. La quantité est restockée :
    UPDATE native_product_variants
       SET stock = stock + $1
     WHERE id = $2 AND stock <> -1;
    
    (le stock <> -1 évite de transformer une variante illimitée en stock fini.)
  3. Edit manuel via UI ou MCP. Tu mets stock=42, c'est appliqué directement. Pas de validation hors les contraintes Postgres.

Le bouton "Stock alerts" dans l'admin tape sur detect_stock_alerts (MCP) pour te lister les variantes en sous-seuil → corrélé avec ta vitesse de vente (7 derniers jours).

Erreurs courantes

  • "Variant combination already exists" — l'index unique (product_id, option1_value, option2_value, option3_value) rejette le doublon. Édite la variante existante au lieu d'en créer une nouvelle.
  • "Le sélecteur de variantes est vide sur la page produit" — toutes tes variantes sont is_active=false. Coche au moins une active.
  • "Trop d'options" — si tu essaies d'ajouter une 4e option, l'UI refuse. Décompose ton produit en deux produits distincts (par exemple "Pack 30j" + "Pack 60j" en produits séparés).
  • "Out of stock alors que j'ai du stock" — la variante est out, pas le produit. Le visiteur a peut-être sélectionné une combinaison qui pointe sur une variante à stock=0. Regarde la matrice complète.
  • "Mon prix variante ne s'applique pas" — la landing peut avoir un price override (landing_pages.price_overrides) qui force un prix unifié pour le test A/B prix. Voir landings/ab-price-testing.md. Inspecte la landing avant le produit.
  • "Le restock n'a pas eu lieu après refund" — vérifie que payment_status est bien passé à 'refunded' (et pas 'partial_refund'). Le restock n'a lieu qu'à un refund complet OU un cancel. Sur un partial refund, le stock reste décrémenté (la marchandise est partiellement chez le client).

Voir aussi