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 = -1signifie illimité.

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
- Ouvre la fiche produit → onglet Variants.
- 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.
- Section Variants : Trackily a généré le produit cartésien.
- 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).
- 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'estis_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 :
- Création d'une commande (
/api/orderPOST). Pour chaque ligne du panier, Trackily exécute :
Si la mise à jour ne retourne aucune ligne (parce queUPDATE native_product_variants SET stock = stock - $1 WHERE id = $2 AND (stock = -1 OR stock >= $1) RETURNING stock;stock < quantity), la transaction est rollback et le checkout renvoie 409 avecout_of_stock=true. Atomic — pas de race entre deux acheteurs sur la dernière unité. - Annulation (
cancel_native_order) ou refund total. La quantité est restockée :
(leUPDATE native_product_variants SET stock = stock + $1 WHERE id = $2 AND stock <> -1;stock <> -1évite de transformer une variante illimitée en stock fini.) - 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_statusest 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
- products.md — le produit qui héberge ces variantes.
- orders.md — comment une variante voyage dans
order_items(avec snapshot du nom + prix). - Landings → A/B price testing — override de prix par landing sans toucher aux variantes.
- MCP → detect_stock_alerts — alerte automatique sur variantes en sous-seuil.