Discounts
TL;DR : un code promo (table
discount_codes) est soit un pourcentage (type='percent'), soit un montant fixe (type='fixed'), soit free shipping. Tu peux le limiter à un produit, à N usages, à une plage de dates, ou à un panier minimal. Au checkout, le code est appliqué après le subtotal et avant la tax.

Concept
Un code promo dans Trackily Commerce native est un levier de conversion : tu envoies "LAUNCH20" à ta liste email pour le lancement, tu écris "SUMMER15" dans ta bio Instagram, tu offres "VIP10" à tes meilleurs clients. Le visiteur le tape au checkout, le total recalcule en live, il convertit.
Côté code, c'est une ligne discount_codes lue par /api/order au moment du checkout. La validation est server-side : pas de moyen pour un visiteur malin d'éditer le DOM pour s'offrir un -50 %.
Anatomie d'un code (discount_codes)
| Champ | Type | Rôle |
|---|---|---|
id |
SERIAL | identifiant interne |
code |
TEXT UNIQUE | le texte que le visiteur tape ("LAUNCH20"). Case-insensitive à la lecture mais stocké en l'état |
type |
TEXT | percent (default), fixed, free_shipping |
value |
NUMERIC(12,2) | pourcentage (10 = -10 %) OU montant fixe (10 = -10) — selon type |
currency |
TEXT | ISO-3 ou chaîne vide. Pour type='fixed' : la devise dans laquelle le montant est exprimé |
product_id |
INTEGER FK NULL | NULL = applicable à tout le catalogue ; non-NULL = limité à ce produit |
min_order_amount |
NUMERIC(12,2) | seuil minimum de panier pour que le code s'applique |
max_uses |
INTEGER NULL | nombre total d'utilisations possibles ; NULL = illimité |
current_uses |
INTEGER | compteur incrémenté atomiquement à chaque redemption |
valid_from |
TIMESTAMPTZ | date d'activation ; NULL = actif tout de suite |
valid_to |
TIMESTAMPTZ | date d'expiration ; NULL = pas d'expiration |
is_active |
BOOLEAN | flag d'activation manuel (override les dates) |
description |
TEXT | note interne pour toi-même |
metadata |
JSONB | extension libre |
Anatomie d'une redemption (discount_redemptions)
À chaque utilisation réussie, une ligne audit est créée :
| Champ | Rôle |
|---|---|
code_id |
FK vers discount_codes |
order_id |
FK vers orders (SET NULL si la commande est supprimée) |
discount_amount |
montant économisé par le client |
currency |
devise de la commande |
created_at |
timestamp |
Utile pour les rapports : "combien LAUNCH20 a-t-il généré en CA ?" ou "qui a utilisé VIP10 ?".
Atomicité du compteur
Le compteur current_uses n'est pas incrémenté naïvement avec UPDATE … SET current_uses = current_uses + 1. Si deux acheteurs tapent le code en même temps sur un max_uses=1, ils pourraient l'utiliser tous les deux. Trackily exécute :
UPDATE discount_codes
SET current_uses = current_uses + 1
WHERE id = $1
AND (max_uses IS NULL OR current_uses < max_uses)
RETURNING id;
Si la RETURNING ne ramène rien, le helper rollback la commande complète et renvoie "code limit reached". Idempotent et concurrence-safe.
Snapshot dans la commande
Trois colonnes ajoutées à orders :
discount_code(TEXT) — le code tapé, snapshoté (survit à la suppression du discount_code).discount_amount(NUMERIC) — le montant réellement déduit.discount_code_id(INTEGER FK NULL) — pointeur, SET NULL si suppression du code.
Les rapports peuvent grouper par discount_code (texte) même quand le code source a été supprimé.
Les trois types de codes
type='percent'
value=20 → -20 % sur le subtotal.
Exemple : panier de 100 €, code -20 % → discount_amount=20 €, total avant shipping/tax = 80 €.
Borné à 100 (un -150 % serait absurde). 100 = produit gratuit (rare cas valide pour une commande de service avec frais de port).
type='fixed'
value=10 + currency='EUR' → -10 € sur le subtotal.
Si le subtotal est inférieur au discount fixe, le discount est plafonné au subtotal (jamais de discount négatif). Ne touche pas le shipping.
type='free_shipping'
value ignoré. Au checkout, le shipping_cost est forcé à 0. Pratique si tu fais des promos genre "Livraison offerte ce week-end".
Combiner avec un min_order_amount est puissant : "Livraison gratuite à partir de 50 €" devient un upsell payant.
Comment faire (UI + MCP)
Via l'admin UI
- Commerce → Discount Codes (ou
/admin#discount-codes). - + Create code.
- Remplis :
- Code (par exemple "LAUNCH20").
- Type (Percent / Fixed / Free shipping).
- Value (20).
- Currency (si Fixed).
- Product (None = global, ou pick un produit dans le dropdown).
- Min order amount (optionnel — par exemple 30).
- Max uses (optionnel — par exemple 100).
- Valid from / Valid to (optionnels — calendrier picker).
- Active (cocher par défaut).
- Description (note interne, par exemple "Lancement Memo Mind 12 mai").
- Save. Le code apparaît dans la liste, prêt à être tapé au checkout.
L'écran liste affiche current_uses / max_uses, le total revenu généré (somme des discount_redemptions.discount_amount × orders.payment_status='paid'), et un bouton "Copy code" pour partager rapidement.
Via MCP
Lister :
{
"name": "list_discount_codes",
"arguments": {
"store_id": null,
"is_active": true,
"limit": 50
}
}
store_id=nullcible le commerce native (les codes natifs n'ont pas de store_id). Pour les codes Shopify, passe lestore_idcible.
Créer :
{
"name": "create_discount_code",
"arguments": {
"code": "LAUNCH20",
"type": "percent",
"value": 20,
"min_order_amount": 30,
"max_uses": 100,
"valid_from": "2026-05-15T00:00:00Z",
"valid_to": "2026-05-31T23:59:59Z",
"description": "Lancement Memo Mind 12 mai"
}
}
Tier-2 : preview + confirm_token, puis re-soumission avec le token. Préviens-toi avant d'émettre un code en production.
Supprimer :
{
"name": "delete_discount_code",
"arguments": { "code_id": 8 }
}
Idem Tier-2. La suppression DELETE la ligne (les redemptions historiques sont conservées via
ON DELETE SET NULL).
Exemples concrets
1. Code de lancement à durée limitée
{
"name": "create_discount_code",
"arguments": {
"code": "LAUNCH20",
"type": "percent",
"value": 20,
"max_uses": 200,
"valid_from": "2026-05-15T00:00:00Z",
"valid_to": "2026-05-22T23:59:59Z",
"description": "1ère semaine lancement"
}
}
200 redemptions ou 7 jours, whichever first. Au-delà, le code dit "Discount limit reached" ou "Discount code expired".
2. Code VIP réservé à un produit
{
"name": "create_discount_code",
"arguments": {
"code": "VIP-MEMO",
"type": "fixed",
"value": 15,
"currency": "EUR",
"product_id": 12,
"min_order_amount": 50,
"max_uses": null,
"description": "Code VIP, panier >= 50 €, sur Memo Mind seulement"
}
}
Réutilisable à l'infini (max_uses=null), mais limité au produit 12 et à un panier mini de 50 €. Le bon code à mettre en signature d'un email post-purchase.
3. Livraison gratuite ce week-end
{
"name": "create_discount_code",
"arguments": {
"code": "FREESHIP",
"type": "free_shipping",
"value": 0,
"min_order_amount": 25,
"valid_from": "2026-05-25T00:00:00Z",
"valid_to": "2026-05-26T23:59:59Z",
"description": "Free shipping week-end 25-26 mai (panier >= 25 €)"
}
}
Pas de coût direct pour toi (tu absorbes les 5 € de shipping), conversion typiquement +15 à 25 % sur les visiteurs hésitants.
4. Code one-shot personnel pour un client mécontent
{
"name": "create_discount_code",
"arguments": {
"code": "SORRY-MARIE-001",
"type": "percent",
"value": 100,
"max_uses": 1,
"valid_to": "2026-06-15T23:59:59Z",
"description": "Geste commercial Marie L. — commande 4287 cassée en transit"
}
}
Code unique, -100 %, expire dans un mois, traçable.
Stratégies discount
- Évite l'over-discount permanent — un code "WELCOME10" affiché en haut de la home apprend aux visiteurs à toujours chercher un code avant d'acheter. Préfère les codes ponctuels avec date d'expiration courte.
- Code par canal —
EMAIL15,IG10,YT20te permettent de mesurer le ROAS par canal sans dépendre d'UTM. - Code post-abandon — déclenche via l'automizer un email "Code -10 % valable 24h" envoyé J+1 aux paniers abandonnés. Conversion typique 12-18 %.
- Code first-purchase —
min_order_amountà votre AOV cible, code envoyé après inscription newsletter. Augmente l'AOV et la liste email en même temps. - Ne combine pas plusieurs codes — Trackily n'autorise qu'un code par commande. Combat les abus, garde le calcul simple.
Erreurs courantes
- "Discount code not found" — le visiteur tape "LAUNCH20" mais le code en base est "launch20". La lookup applique LOWER() des deux côtés, donc ça devrait marcher. Vérifie qu'il n'y a pas un espace en début ou fin du champ
codeen base. - "Discount code expired" —
valid_toest dans le passé, ouvalid_fromest dans le futur, ouis_active=false. - "Discount limit reached" —
current_uses >= max_uses. Pour relever la limite, editmax_uses(les utilisations passées comptent toujours). - "Minimum order amount not met" — le subtotal du panier est sous
min_order_amount. Affiché côté client comme "Add 12 € more to use this code". - "Discount not applicable to this product" — le code a un
product_idspécifique et le visiteur essaie de l'appliquer sur un autre produit. - "Discount appliqué à 0" — pour un
type='fixed'avec currency différente du panier, Trackily refuse silencieusement (pas de conversion auto entre devises). Crée un code par devise. - "Le compteur n'avance pas" — la commande est restée en
pending. Le compteur n'avance qu'à la création de la commande, pas au paiement effectif. Si tu veux la métrique "redemptions payées", regarde plutôt via JOINdiscount_redemptions × orders.payment_status='paid'.
Voir aussi
- orders.md — où vivent
discount_code,discount_amount,discount_code_idsur la commande. - Email → Sequences — déclencher un code post-abandon.
- Automizer → Rules — créer / supprimer des codes en batch.
- MCP
create_price_rule— pour les Shopify-stores, équivalent automatique (sans code).