Commerce
Produkte sind Model-Records, der Warenkorb ist ein öffentlicher Mutations-Namespace für deinen Shop, und Bestellungen leben im Admin.
cmssy bringt Warenkorb, Bestellungen, Rabatte und eine Bestell-Pipeline mit. Nichts davon ist ein eigener Produktbereich: Ein Produkt ist ein Model-Record, der Shop spricht mit derselben Delivery-API wie alles andere, und der Bestellschreibtisch ist der Admin.
Die Fähigkeit hängt am Plan - canUseCart in get_workspace_info - lies sie also aus, statt sie anzunehmen.
Produkte sind Records
Einen Produkttyp gibt es in cmssy nicht. Du legst ein Modell an - product, plan, ticket, was immer dein Katalog ist - und der Produkt-Tab des Modells legt fest, welche seiner Felder welche Rolle spielen: Name, Preis, Bild, SKU, Steuersatz, Bestand und die Variantenachsen. Eine nicht gesetzte Rolle fällt auf das Anzeigefeld des Modells oder den konventionellen Schlüssel zurück (price, sku, inventory…), ein einfacher Katalog braucht also gar kein Mapping. Die Cart-Config des Workspace listet dann nur noch die verkäuflichen Modelle als Produktquellen.
Das Preisfeld enthält den Preis in der Standardwährung des Workspace. Preise in anderen Währungen sind kein Feld des Modells: Sie werden je Währung gesetzt (siehe Währungen unten), und ein Modell kann kein Währungsfeld mehr benennen.
Agenten lesen die aufgelösten Rollen über get_model (product.roles) und setzen Overrides über update_model.
Deshalb kann ein Katalog aus mehreren Modellen zugleich stammen, und deshalb rendert derselbe Block, der jeden anderen Record rendert, auch ein Produkt.
Den Katalog lesen
Ein Produkt kommt aus public.model.records wie jeder andere Record, mit ein paar Feldern, die nur auf einem Produkt etwas bedeuten.
variants(limit, offset)- eine Seite der Varianten des Produkts, höchstens 50;variantCountzählt alle. Ein Produkt mit mehr Varianten als eine Seite liest die nächste per Offset.variantPriceRange { min max }- die günstigste und die teuerste Variante, in kleinster Einheit (4900 = 49,00), null bei einem Produkt ohne Varianten. Eine Listenkarte mit „ab 49,00" liest das statt der Varianten.records(sku: "TEE-M")- das Produkt mit dieser SKU, als eigene oder an einer seiner Varianten. Mitvariants(sku: "TEE-M", limit: 1)kommt nur diese Variante - ein Schnellbestellformular braucht nichts weiter.available- Stück, die ein Käufer jetzt bestellen kann: Bestand minus das, was offene Warenkörbe reservieren, über die Varianten summiert. Null, wenn das Produkt keinen Bestand führt, auch wenn irgendeine seiner Varianten keinen führt.records(stockState: "in" | "low" | "out")- filtert nach derselben Zahl gegenpublic.siteConfig.lowStockThreshold: niedrig heißt mehr als null und höchstens der Schwellenwert. Färbe ein Bestands-Badge ausavailableund demselben Schwellenwert, dann kann es dem Filter nicht widersprechen.
Ein verborgenes Feld sickert nicht durch ein anderes. Die eigene SKU eines Produkts wird nur gefunden, wenn die Delivery-API das Feld ausliefert, das sie hält. Ist das Bestandsfeld verborgen, sind available und das inventory jeder Variante null, und stockState wird abgelehnt.
Was der Shop aufrufen darf
Der cart-Namespace ist öffentlich: ohne Token, sitzungsbezogen, und der einzige schreibende Teil der Delivery-API.
cart.get(workspaceId)- Positionen,subtotal,tax,shippingTotal,discountedTotal,totalGross,currency,pricesIncludeTax,availableShippingMethods,appliedDiscount,taxSummary.cart.addItem-{ recordId, quantity, variantSelections, notes }. Die Position ist eine Referenz auf den Record, keine Kopie.cart.updateItem,cart.removeItem(itemId),cart.clear.cart.applyDiscount(code),cart.removeDiscount,cart.setShippingMethod(shippingMethodId).cart.merge- führt den anonymen Sitzungswarenkorb nach dem Login mit dem des Mitglieds zusammen. Einmal aufrufen, direkt nach der Authentifizierung.cart.checkout-{ customerEmail, customerNote, poNumber, shippingAddress, customFields }macht aus dem Warenkorb eine Bestellung.
Summen kommen berechnet zurück. Addiere Positionspreise nicht im Browser: Steuermodus, Rabatte und Versand entscheidet der Server, und eine zweite Implementierung im Frontend ist eine zweite Antwort.
Eine Warenkorb-Mutation pro Request. Die Felder in cart { … } sind Felder eines Namespace-Objekts, keine Root-Felder, also führt GraphQL sie parallel aus - addItem und setShippingAddress im selben Dokument laufen um die Wette, und eine vom ersten Aufruf frisch erzeugte Sitzung ist für den zweiten nicht sichtbar. Schicke jeden Schreibvorgang als eigenen Request.
Bestellstatus ohne Konto
public.order.byToken liefert genau eine Bestellung - orderNumber, items, paymentStatus, fulfillmentStatus, amountPaid, balanceDue, customFields, invoiceUrl, shipments (Versanddienstleister, Sendungsnummer und Tracking-Link, Status) - zum beim Checkout ausgegebenen Token. So funktioniert eine „Sendung verfolgen“-Seite für Gäste.
Was der Workspace konfiguriert
public.siteConfig.publicCart ist die shop-sichere Projektion der Cart-Einstellungen: defaultCurrency, additionalCurrencies, pricesIncludeTax, taxRates, shippingMethods, productSources, maxItemsPerCart, maxQuantityPerItem, orderFields, enableSavedCarts, enableQuoteRequests. Einmal lesen und die UI davon formen lassen - Währungsformat, Mengenlimits, ob eine Angebotsanfrage überhaupt angeboten wird.
Versandregeln
Jede Versandart trägt eigene Regeln: die bedienten Länder (countries, ISO-Codes, leer = überall), ein Zwischensummen-Fenster (minSubtotal / maxSubtotal) und freeAbove, die Zwischensumme, ab der sie kostenlos ist. availableShippingMethods am Warenkorb wendet sie bereits auf Lieferadresse und Zwischensumme an - der Shop filtert Versandarten nie selbst. In einer zusätzlichen Währung nutzt eine Versandart die Beträge, die für diese Währung unter Preise in anderen Währungen gesetzt sind; eine Versandart ohne sie wird Warenkörben in dieser Währung nicht angeboten.
Bestellfelder
Eigene Felder an jeder Bestellung - eine Geschenknachricht, ein Lieferfenster, eine interne Referenz. Du definierst sie einmal unter Einstellungen → Bestellfelder (oder per update_cart_config mit orderFields), höchstens 30, jedes mit Schlüssel, Label pro Sprache, Typ (text, number, date, select, boolean, email, phone, url) und optionalen Grenzen.
Jedes Feld gehört entweder dem Kunden oder nur dem Team. publicCart.orderFields listet nur die Felder, die der Kunde setzen darf: Rendere für jedes ein Eingabefeld und schicke die Werte als customFields an cart.checkout, nach Schlüssel. Der Server prüft sie - Typ, Grenzen, Pflicht - und lehnt den Schlüssel eines Team-Felds ab; ein leerer Wert wird verworfen. Pflicht kann nur ein Feld sein, das der Kunde setzen darf, ein Team-Feld blockiert also nie einen Checkout.
Die Werte werden an der Bestellung als customFields gespeichert. public.order.byToken liefert nur die Felder des Kunden; der Admin, get_order und die order.*-Webhooks tragen alle. Das Team bearbeitet sie an der Bestellung oder per update_order_details: Ein auf null gesetzter Schlüssel leert den Wert. Das Entfernen einer Definition lässt die bereits an Bestellungen gespeicherten Werte unberührt.
Währungen
Ein Workspace hat eine Standardwährung, die Basis des Katalogs, und bis zu 9 zusätzliche Währungen. Beide setzt du unter Einstellungen → Region und Steuern oder mit update_cart_config (defaultCurrency, additionalCurrencies). Die Standardwährung lässt sich nicht ändern, solange es zusätzliche Währungen gibt - entferne sie zuerst. Eine Währung, in der ein Vertriebskanal noch verkauft, lässt sich nicht entfernen.
Jeder Vertriebskanal verkauft in einer Währung, der Standardwährung oder einer zusätzlichen (currency bei create_sales_channel und update_sales_channel). Der Käufer wählt nie eine Währung: Der Shop wählt den Kanal, und die Währung des Kanals bepreist alles. Warenkorb, Bestellung, Versand, Rabatt, Zahlungen und Erstattungen sind in dieser einen Währung. Ändert sich die Währung eines Kanals, werden die darin offenen Warenkörbe verworfen.
Nichts wird umgerechnet. cmssy kennt keine Wechselkurse. Jeden Betrag in einer zusätzlichen Währung setzt du selbst:
- Produkte - Preise je Währung an einem Produkt im Katalog: ein Preis und Mengenstaffeln je Währung, bei einem Produkt mit Varianten ein Preis je Variante. Speichern ändert nie den Bestand. Agenten nutzen
set_product_currency_prices, das eine Bestätigung verlangt, undcurrencyPricesbeiset_product_variants. - Preislisten - eine Festpreis-Zeile je Währung, und der Prozentsatz der Liste geht vom Preis in dieser Währung ab. Siehe Preislisten.
- Versandarten - Preis, Zwischensummen-Fenster und Gratis-ab je Währung, unter Preise in anderen Währungen.
- Rabattcodes - Mindestwert je Währung (
minSubtotalByCurrency) bei Prozent- und Gratisversand-Codes. Ein Code mit Mindestwert gilt nur in den Währungen, die seine Mindestwerte nennen. Ein Code mit festem Betrag hat eine Währung und gilt nur für Warenkörbe in ihr.
Ein Produkt ohne Preis in der Währung des Kanals wird in diesem Kanal nicht verkauft: Eine Preisabfrage liefert es mit price: null, ein bepreistes Listing lässt es weg, und cart.addItem lehnt es mit extensions.reason NO_PRICE_IN_CURRENCY ab. Preise in einer Währung, die du ausschaltest, bleiben erhalten; sie bepreisen nichts, bis die Währung wieder eingeschaltet ist.
Bestellungen behalten ihre Währung. Die Bestellliste hat eine Währungsspalte und einen Filter (currency bei list_orders), eine manuelle Bestellung wählt ihre Währung (currency bei create_manual_order), und Analysen und Kundenausgaben werden je Währung berichtet, nie über Währungen hinweg summiert.
Währungen im Shop
Lies die Preise eines Kanals aus public.pricing.prices und public.pricing.catalogPrices mit dem Kanal-Schlüssel, für einen angemeldeten Käufer aus account.prices und account.catalogPrices. Das Preisfeld des Records und variantPriceRange enthalten nur den Preis in der Standardwährung. Jede Preiszeile trägt currency: Formatiere jeden Betrag mit der Währung seiner eigenen Zeile, nie mit einer fest codierten, und teile durch die Zahl der Nachkommastellen dieser Währung, nicht durch 100. In einem Kanal mit zusätzlicher Währung nennt currency im Ergebnis diese Währung, auch bei einer leeren Seite. publicCart listet defaultCurrency und additionalCurrencies.
Bestellungen sind Backoffice
Alles nach dem Checkout ist autorisiert: der Admin oder der MCP-Server mit einem Token, dessen Rolle es erlaubt. Bestellungen lassen sich listen und lesen, manuell anlegen, Position für Position bearbeiten, ganz oder teilweise als bezahlt markieren, erstatten, stornieren, durch die Fulfillment-Stufen bewegen, mit einer Rechnung versehen und durch eine konfigurierbare Pipeline schieben. Jede versandte Sendung wird mit create_shipment und update_shipment an der Bestellung festgehalten - Versanddienstleister, Sendungsnummer, Tracking-Link und ein Status von pending bis delivered samt Verlauf. Eine neue Sendung markiert die Bestellung als erfüllt, bei einer Teillieferung als teilweise erfüllt. Rabatte werden genauso angelegt, aktualisiert und an- oder abgeschaltet; Produkte lassen sich in Bulk ändern, löschen oder mit Preisstaffeln versehen.
Zahlungsanbieter gehören nicht dazu: cmssy hält fest, was passiert ist - mark_order_paid, record_order_payment, refund_order - und deine Integration entscheidet, wann es passiert ist.
Eine Bestellung anonymisieren
Bei einem Löschantrag anonymisiert das Team mit orders:manage eine abgeschlossene Bestellung - storniert, erfüllt, retourniert oder erstattet - auf ihrer Seite, oder alle abgeschlossenen Bestellungen eines Kunden auf der Kundenseite. E-Mail, Lieferadresse, PO-Nummer, Notizen, Bestellfelder und Zustellnotizen des Käufers werden aus der Bestellung, ihrem Warenkorb, den gespeicherten Webhook-Zustellungen und dem Spotlight-Verlauf gelöscht, der sie gezeigt hat. Positionen, Beträge, Zahlungen, Rechnung und Bestellnummer bleiben, die Buchhaltung stimmt also weiter. Das lässt sich nicht rückgängig machen, eine laufende Bestellung wird abgelehnt, und der Webhook order.anonymized sagt Ihren Systemen, ihre Kopie zu löschen.
Eine Testbestellung löschen
Eine stornierte Bestellung ohne Zahlung und ohne Rechnung - Test oder Spam - kann das Team mit orders:manage auf ihrer Seite vollständig löschen. Die Käuferdaten verschwinden wie bei der Anonymisierung aus Warenkorb, gespeicherten Webhook-Zustellungen und Spotlight-Verlauf, und das Audit-Log behält die Bestellnummer. Jede andere Bestellung wird abgelehnt: Sobald Geld oder eine Rechnung im Spiel ist, anonymisieren Sie sie stattdessen.
Nächste Schritte
- Datenmodelle - die Records, aus denen ein Katalog besteht.
- GraphQL-Delivery-API - wie ein öffentlicher Schreibvorgang gescoped ist.
- Kunden - die Konten hinter den Bestellungen und Firmen im B2B.
- Preislisten - B2B-Preise pro Firma oder Kunde, und wie ein Shop sie anzeigt.
- Webhooks - die zehn
order.*-Events undshipment.*für Sendungen.