Commerce

Les produits sont des enregistrements de modèle, le panier est un espace de mutations public appelé par votre boutique, et les commandes vivent dans l'admin.

cmssy embarque un panier, des commandes, des remises et un pipeline de commandes. Rien de tout cela n'est un module à part : un produit est un enregistrement de modèle, la boutique parle à la même API de diffusion que le reste, et le bureau des commandes est l'admin.

La capacité est un drapeau du plan - canUseCart dans get_workspace_info - lisez-le plutôt que de le supposer.

Les produits sont des enregistrements

Il n'existe pas de type produit dans cmssy. Vous créez un modèle - product, plan, ticket, quel que soit votre catalogue - et l'onglet Produit du modèle déclare quels champs jouent quel rôle : nom, prix, image, SKU, taux de TVA, stock et axes de variantes. Un rôle non défini se résout vers le champ d'affichage du modèle ou la clé conventionnelle (price, sku, inventory…), un catalogue simple n'a donc besoin d'aucun mapping. La configuration panier de l'espace de travail ne fait plus que lister les modèles en vente comme sources de produits.

Le champ prix contient le prix dans la devise par défaut de l'espace de travail. Les prix dans d'autres devises ne sont pas un champ du modèle : ils se définissent par devise (voir Devises plus bas), et un modèle ne peut plus désigner de champ devise.

Les agents lisent les rôles résolus via get_model (product.roles) et posent les surcharges via update_model.

C'est pourquoi un catalogue peut venir de plusieurs modèles à la fois, et pourquoi le même bloc qui affiche n'importe quel enregistrement affiche un produit.

Lire le catalogue

Un produit revient de public.model.records comme n'importe quel autre enregistrement, avec quelques champs qui n'ont de sens que sur un produit.

  • variants(limit, offset) - une page des variantes du produit, 50 au plus ; variantCount les compte toutes. Un produit qui a plus de variantes qu'une page lit la suivante par offset.
  • variantPriceRange { min max } - la variante la moins chère et la plus chère, en unités mineures (4900 = 49,00), null sur un produit sans variantes. Une carte de liste qui affiche « à partir de 49,00 » lit ce champ au lieu des variantes.
  • records(sku: "TEE-M") - le produit qui porte ce SKU, comme le sien ou sur l'une de ses variantes. Ajoutez variants(sku: "TEE-M", limit: 1) pour obtenir cette seule variante - un formulaire de commande rapide n'a besoin de rien d'autre.
  • available - les unités qu'un acheteur peut commander maintenant : le stock moins ce que les paniers ouverts réservent, additionné sur les variantes. Null quand le produit ne suit pas son stock, y compris quand l'une de ses variantes ne le suit pas.
  • records(stockState: "in" | "low" | "out") - filtre sur ce même nombre par rapport à public.siteConfig.lowStockThreshold : bas veut dire plus de zéro et pas plus que le seuil. Colorez un badge de stock à partir de available et du même seuil, et le badge ne peut pas contredire le filtre.

Un champ masqué ne fuit pas par un autre. Le SKU propre d'un produit n'est trouvé que si l'API de diffusion sert le champ qui le contient. Quand le champ de stock est masqué, available et l'inventory de chaque variante valent null, et stockState est refusé.

Ce que la boutique peut appeler

L'espace cart est public : sans jeton, porté par la session, et c'est la seule partie de l'API de diffusion qui écrit.

  • cart.get(workspaceId) - lignes, subtotal, tax, shippingTotal, discountedTotal, totalGross, currency, pricesIncludeTax, availableShippingMethods, appliedDiscount, taxSummary.
  • cart.addItem - { recordId, quantity, variantSelections, notes }. La ligne référence l'enregistrement, elle n'en est pas une copie.
  • cart.updateItem, cart.removeItem(itemId), cart.clear.
  • cart.applyDiscount(code), cart.removeDiscount, cart.setShippingMethod(shippingMethodId).
  • cart.merge - fusionne le panier de session anonyme avec celui du membre connecté. À appeler une fois, juste après l'authentification.
  • cart.checkout - { customerEmail, customerNote, poNumber, shippingAddress, customFields } transforme le panier en commande.

Les totaux reviennent calculés. N'additionnez pas les prix dans le navigateur : mode de TVA, remises et livraison se décident côté serveur, et une seconde implémentation dans votre frontend est une seconde réponse.

Une mutation panier par requête. Les champs de cart { … } sont ceux d'un objet d'espace de noms, pas des champs racine : GraphQL les exécute en parallèle. addItem et setShippingAddress dans un même document se font concurrence, et une session toute neuve créée par le premier appel n'est pas visible du second. Envoyez chaque écriture du panier dans sa propre requête.

Suivi de commande sans compte

public.order.byToken renvoie une commande - orderNumber, items, paymentStatus, fulfillmentStatus, amountPaid, balanceDue, customFields, invoiceUrl, shipments (transporteur, numéro et lien de suivi, statut) - pour le jeton émis au checkout. C'est ainsi que fonctionne une page « suivre ma commande » pour un invité.

Ce que configure l'espace de travail

public.siteConfig.publicCart est la projection sûre des réglages du panier : defaultCurrency, additionalCurrencies, pricesIncludeTax, taxRates, shippingMethods, productSources, maxItemsPerCart, maxQuantityPerItem, orderFields, enableSavedCarts, enableQuoteRequests. Lisez-la une fois et laissez-la façonner l'interface : format de devise, limites de quantité, et si une demande de devis est proposée du tout.

Règles de livraison

Chaque mode de livraison porte ses règles : les pays desservis (countries, codes ISO, vide = partout), une fenêtre de sous-total (minSubtotal / maxSubtotal) et freeAbove, le sous-total à partir duquel il est offert. availableShippingMethods sur le panier les applique déjà à l'adresse de livraison et au sous-total : la boutique ne filtre jamais les modes elle-même. Dans une devise supplémentaire, un mode utilise les montants définis pour cette devise sous Prix dans d'autres devises ; un mode qui n'en a pas n'est pas proposé aux paniers dans cette devise.

Champs de commande

Des champs à vous sur chaque commande - un message cadeau, un créneau de livraison, une référence interne. Ils se définissent une fois dans Paramètres → Champs de commande (ou via update_cart_config avec orderFields), 30 au plus, chacun avec une clé, un libellé par langue, un type (text, number, date, select, boolean, email, phone, url) et des limites facultatives.

Chaque champ appartient soit au client, soit à l'équipe seule. publicCart.orderFields ne liste que ceux que le client peut renseigner : affichez une saisie pour chacun et envoyez les valeurs comme customFields dans cart.checkout, par clé. Le serveur les valide - type, limites, caractère obligatoire - et refuse la clé d'un champ réservé à l'équipe ; une valeur vide est ignorée. Seul un champ que le client peut renseigner peut être obligatoire, un champ d'équipe ne bloque donc jamais un checkout.

Les valeurs sont enregistrées sur la commande dans customFields. public.order.byToken ne renvoie que les champs du client ; l'admin, get_order et les webhooks order.* les portent tous. L'équipe les modifie sur la commande, ou via update_order_details : une clé mise à null efface la valeur. Supprimer une définition laisse intactes les valeurs déjà enregistrées sur les commandes.

Devises

Un espace de travail a une devise par défaut, la base du catalogue, et jusqu'à 9 devises supplémentaires. Les deux se règlent dans Paramètres → Région et taxes, ou avec update_cart_config (defaultCurrency, additionalCurrencies). La devise par défaut ne peut pas changer tant qu'il existe des devises supplémentaires - retirez-les d'abord. Une devise dans laquelle un canal de vente vend encore ne peut pas être retirée.

Chaque canal de vente vend dans une devise, celle par défaut ou une supplémentaire (currency sur create_sales_channel et update_sales_channel). L'acheteur ne choisit jamais de devise : la boutique choisit le canal, et la devise du canal fixe tous les prix. Le panier, sa commande, la livraison, la remise, les paiements et les remboursements sont tous dans cette seule devise. Changer la devise d'un canal abandonne les paniers ouverts sur ce canal.

Rien n'est converti. cmssy n'a pas de taux de change. Chaque montant dans une devise supplémentaire est un montant que vous définissez :

  • Produits - Prix par devise sur un produit du catalogue : un prix et des paliers de quantité par devise, et pour un produit à variantes un prix par variante. L'enregistrement ne modifie jamais le stock. Les agents utilisent set_product_currency_prices, qui demande une confirmation, et currencyPrices sur set_product_variants.
  • Listes de prix - une ligne de prix fixe par devise, et le pourcentage de la liste s'applique au prix dans cette devise. Voir Listes de prix.
  • Modes de livraison - prix, fenêtre de sous-total et seuil de gratuité par devise, sous Prix dans d'autres devises.
  • Codes de remise - Minimum par devise (minSubtotalByCurrency) sur les codes en pourcentage et de livraison offerte. Un code avec un minimum ne s'applique que dans les devises que ses minimums nomment. Un code à montant fixe a une devise et ne s'applique qu'aux paniers dans celle-ci.

Un produit sans prix dans la devise du canal n'est pas vendu sur ce canal : une lecture de prix le renvoie avec price: null, une liste tarifée l'omet, et cart.addItem le refuse avec extensions.reason NO_PRICE_IN_CURRENCY. Les prix d'une devise que vous désactivez sont conservés ; ils ne fixent aucun prix tant que la devise n'est pas réactivée.

Les commandes gardent leur devise. La liste des commandes a une colonne et un filtre de devise (currency sur list_orders), une commande manuelle choisit sa devise (currency sur create_manual_order), et les analyses et les dépenses des clients sont présentées par devise, jamais additionnées entre devises.

Les devises dans la boutique

Lisez les prix d'un canal depuis public.pricing.prices et public.pricing.catalogPrices avec la clé du canal, ou depuis account.prices et account.catalogPrices pour un acheteur connecté. Le champ prix de l'enregistrement et variantPriceRange ne contiennent que le prix dans la devise par défaut. Chaque ligne de prix porte currency : formatez chaque montant avec la devise de sa propre ligne, jamais une devise codée en dur, et divisez par le nombre de décimales de cette devise, pas par 100. Sur un canal dans une devise supplémentaire, le currency du résultat la nomme, même sur une page vide. publicCart liste defaultCurrency et additionalCurrencies.

Les commandes sont du back-office

Tout ce qui suit le checkout est autorisé : l'admin, ou le serveur MCP avec un jeton dont le rôle le permet. Les commandes se listent et se lisent, se créent à la main, s'éditent ligne par ligne, se marquent payées en totalité ou en partie, se remboursent, s'annulent, avancent dans le fulfillment, reçoivent une facture et se déplacent le long d'un pipeline configurable. Chaque colis expédié est enregistré sur la commande avec create_shipment et update_shipment : transporteur, numéro de suivi, lien de suivi et un statut de pending à delivered avec son historique. Ajouter un colis marque la commande comme expédiée, ou partiellement expédiée pour une livraison fractionnée. Les remises se créent, se modifient et s'activent de la même façon ; les produits se modifient ou se suppriment en masse, ou reçoivent des paliers de prix.

Les prestataires de paiement n'en font pas partie : cmssy enregistre ce qui s'est passé - mark_order_paid, record_order_payment, refund_order - et votre intégration décide quand cela s'est passé.

Anonymiser une commande

Pour une demande d'effacement, l'équipe disposant de orders:manage anonymise une commande terminée - annulée, expédiée, retournée ou remboursée - depuis sa page, ou toutes les commandes terminées d'un client depuis la page du client. L'e-mail, l'adresse de livraison, le numéro de bon de commande, les notes, les champs de commande et les notes de livraison de l'acheteur sont effacés de la commande, de son panier, des livraisons de webhooks enregistrées et de l'historique Spotlight qui les affichait. Les articles, montants, paiements, la facture et le numéro de commande sont conservés : la comptabilité reste juste. Action irréversible ; une commande en cours est refusée, et le webhook order.anonymized indique à vos systèmes d'effacer leur copie.

Supprimer une commande de test

Une commande annulée sans paiement ni facture - test ou spam - peut être supprimée entièrement depuis sa page par l'équipe disposant de orders:manage. Les données de l'acheteur sont effacées du panier, des livraisons de webhooks enregistrées et de l'historique Spotlight comme pour l'anonymisation, et le journal d'audit conserve le numéro de commande. Toute autre commande est refusée : dès qu'un paiement ou une facture existe, anonymisez-la.

Étapes suivantes

  • Modèles de données - les enregistrements dont un catalogue est fait.
  • API de diffusion GraphQL - comment une écriture publique est cadrée.
  • Clients - les comptes derrière les commandes, et les entreprises en B2B.
  • Listes de prix - des prix B2B par entreprise ou client, et comment une boutique les affiche.
  • Webhooks - les dix événements order.*, et shipment.* pour les colis.