Preislisten

Gib Firmen und Kunden eigene Preise - Prozent auf den Katalogpreis plus Festpreise pro Produkt - während dein Shop den Katalog weiter cacht.

Eine Preisliste - Gold, Silver, Dealer - gibt einer Gruppe von Käufern eigene Preise. Du weist sie einer Firma oder einem Kunden zu, und cmssy berechnet Warenkorb, Checkout und Bestellungen dieses Käufers serverseitig danach. Der Katalog, den ein Besucher liest, ändert sich nicht - deine Produktseiten bleiben anonym und cachebar.

Was eine Liste enthält

  • Ein Prozentabschlag auf den Katalogpreis, 0 bis 99,99 mit höchstens zwei Nachkommastellen. Er gilt für jedes Produkt, für das die Liste keinen Festpreis hat, und für jede Mengenstaffel dieses Produkts. Gespeichert wird er in Basispunkten, gerechnet in ganzen Zahlen: kaufmännisch gerundet auf eine ganze Minor-Einheit, ein kostenloses Produkt bleibt bei 0, ein bezahltes fällt nie unter 1 Minor-Einheit.
  • Festpreise pro Produkt, in Minor-Einheiten, jeweils mit optionalen Mengenstaffeln. Ein Produkt mit Varianten bekommt einen Festpreis pro Variante, nie einen für das ganze Produkt. Ein Festpreis darf über dem Katalogpreis liegen - auch ein Vertragspreis ist ein Preis.

Ein Festpreis schlägt den Prozentabschlag; gibt es keins von beiden, zahlt der Käufer den Katalogpreis. Die Währung ist immer die des Produkts - eine Liste ändert Beträge, keine Währungen. Ein Festpreis für ein später entferntes Produkt oder eine entfernte Variante bepreist nichts; die Liste markiert ihn als entfernt, damit du ihn löschen kannst.

Wer nach welcher Liste zahlt

Eine Liste pro Käufer, nie zwei übereinander:

  • Ein Kunde, der zu einer Firma gehört, zahlt immer nach der Liste der Firma - oder den Katalogpreis, wenn die Firma keine hat. cmssy lehnt es ab, so einem Kunden direkt eine Liste zuzuweisen: weise sie der Firma zu. Eine Liste, die der Kunde vor dem Firmenbeitritt hatte, wird ignoriert, solange er dazugehört.
  • Ein Kunde ohne Firma kann eine eigene Liste haben - ein VIP oder ein einzelner Händler.
  • Ein Gast zahlt den Katalogpreis.

Listen verwalten

Listen liegen unter Commerce → Preislisten. Lege eine mit Namen und Prozentsatz an und öffne sie dann, um Festpreise pro Produkt oder Variante hinzuzufügen. Das Löschen einer Liste entfernt ihre Festpreise und hebt die Zuweisung bei allen Firmen und Kunden auf; Bestellungen behalten den Namen, mit dem sie berechnet wurden.

Zuweisen kannst du eine Liste auf der Seite des Kunden oder auf einem Firmen-Record in dessen Modell. Die Berechtigungen:

  • models:edit - Listen und ihre Festpreise anlegen, bearbeiten und löschen; models:view liest sie.
  • customers:manage - einer Firma oder einem Kunden eine Liste zuweisen. customers:view reicht, um zu sehen, welche Listen es gibt.

Ein ERP-Sync schreibt Festpreise in großen Mengen: priceList.setEntries in der Admin-API schreibt bis zu 1000 auf einmal, alles oder nichts - eine fehlerhafte Zeile lehnt den ganzen Aufruf ab, und der Fehler nennt jede fehlerhafte Zeile mit Index.

Aus Spotlight und dem MCP-Server

Spotlight und der MCP-Server (ab 0.108.0) bieten dieselben Operationen mit denselben Berechtigungen: list_price_lists, get_price_list, create_price_list, update_price_list, delete_price_list, set_price_list_entry, set_price_list_entries, remove_price_list_entries, set_customer_price_list und set_company_price_list.

Preise im Headless-Shop

Der Preis eines Käufers ist persönlich, deshalb kommt er nie in eine Produktabfrage - sonst wäre jeder Katalogabruf mitgliedsabhängig und nicht mehr cachebar. Das Muster stattdessen:

  • Katalog anonym rendern und cachen. Produktlisten und Produktseiten lesen public.model.records genau wie für einen abgemeldeten Besucher, mit Katalogpreisen. Schlüssle eine gecachte Seite nie nach dem Mitglied.
  • Nach der Anmeldung die Käuferpreise darüberlegen. Frag account.prices nach den Produkten auf dem Bildschirm - höchstens 100 Einträge pro Aufruf - mit dem Authorization: Bearer <accessToken> des Mitglieds. Ohne Mitglieds-Token wird der Aufruf abgelehnt.
  • Diese Antwort nie für andere Besucher cachen. Kein geteilter Fetch-Cache, kein CDN. Lies sie in einer dynamischen Route oder einer Server Action und setze die Preise in die bereits gerenderte Seite ein.
query BuyerPrices($workspaceId: ID!, $items: [PriceQueryItem!]!) {
  account {
    prices(workspaceId: $workspaceId, items: $items) {
      recordId
      variantKey
      currency
      price
      priceMax
      listed
      tiers { minQty price }
    }
  }
}

Jeder Eintrag ist { recordId, variantKey }, wobei variantKey die ProductVariant.id der Delivery-API ist. Lass ihn bei einem Produkt mit Varianten weg, um eine Spanne zu bekommen: price ist der niedrigste, priceMax der höchste Preis, tiers ist leer - der "ab"-Preis einer Listing-Karte. Beträge sind in Minor-Einheiten. listed ist true, wenn eine Liste den Preis gesetzt hat, für ein "Dein Preis"-Label. Ein unbekanntes oder verborgenes Produkt fehlt in der Antwort - ordne Zeilen über recordId und variantKey zu, nicht über die Position.

Für ein Badge wie "Deine Preise: Dealer Gold" liest du den Namen der Liste aus dem Profil des Mitglieds. Er ist null, wenn keine Liste gilt, und ein Mitglied sieht immer nur seine eigene:

query MyPriceList($workspaceId: ID!) {
  account {
    profile(workspaceId: $workspaceId) {
      priceList { name }
    }
  }
}

Mit @cmssy/core 19.11.0 oder neuer schickt getBuyerPrices die Preisabfrage für dich.

Warenkorb und Checkout

Der Warenkorb wird für den berechnet, der ihn liest. Bei jedem Lesen und Schreiben berechnet cmssy die Positionen neu, die für eine andere Liste berechnet wurden - ein Gast meldet sich an, ein Mitglied meldet sich ab, eine Liste wird zugewiesen oder entfernt. unitPrice ist der Betrag, der berechnet wird; snapshot.basePrice ist der Katalogpreis vor Liste und Mengenstaffeln, für einen durchgestrichenen "statt"-Preis. Er ist null bei einer Position, die vor den Preislisten hinzugefügt wurde.

query CartPrices($workspaceId: ID!) {
  cart {
    get(workspaceId: $workspaceId) {
      items {
        id
        quantity
        unitPrice
        currentPrice
        priceMismatch
        snapshot { price basePrice currency }
      }
      subtotal
    }
  }
}

Ändern sich Prozentsatz oder Festpreise einer Liste, werden Warenkörbe, die bereits danach berechnet sind, nicht neu berechnet. Die Position zeigt den neuen Preis in currentPrice mit priceMismatch: true, und der Checkout berechnet, was im Warenkorb steht.

Wenn der Checkout ablehnt

Hat die Neuberechnung den Warenkorb verändert, seit der Käufer ihn zuletzt gesehen hat, lehnt cart.checkout ab, statt einen Betrag zu berechnen, den niemand angezeigt hat. Der Grund steht in extensions.reason des GraphQL-Fehlers:

  • CART_REPRICED (Code BAD_USER_INPUT) - die Preise des Käufers haben sich geändert. Der Warenkorb ist bereits mit den neuen Preisen gespeichert: lies ihn erneut, zeig die neuen Summen, lass den Käufer bestätigen und ruf cart.checkout noch einmal auf.
  • CART_CURRENCY_CHANGED (Code CONFLICT) - eine Position würde jetzt in einer anderen Währung als der Warenkorb berechnet; extensions.itemId nennt sie. Entferne die Position und füge das Produkt erneut hinzu.

Mit @cmssy/core 19.11.0 oder neuer liefert checkoutErrorReason(error) beide Gründe.

Rabattcodes wirken zusätzlich zur Liste: Ein Code wird von der nach Liste berechneten Zwischensumme abgezogen, und auch der Mindestbestellwert eines Codes wird an ihr gemessen.

Bestellungen

Eine Bestellung friert die Liste ein, nach der sie berechnet wurde. Ihr Name steht an der Bestellung im Admin und in get_order und bleibt, auch wenn die Liste umbenannt oder gelöscht wird. Auf jeder Bestellposition:

  • price - der berechnete Stückpreis, mit Liste und Mengenstaffel.
  • listPrice - der Katalog-Stückpreis vor Liste und Mengenstaffeln. Ohne Liste ist es der Preis vor Mengenstaffeln, wie bisher; bei einem Vertragspreis über dem Katalog kann er unter price liegen.

Bearbeitest du eine Bestellung im Admin, werden ihre Produktpositionen mit den aktuellen Werten der eingefrorenen Liste neu berechnet, oder zu Katalogpreisen, wenn die Liste gelöscht wurde. Eine manuelle Bestellung wird nach der Liste des gewählten Kunden berechnet.

Nächste Schritte

  • Commerce - Warenkorb, Checkout und Bestellungen, die eine Liste bepreist.
  • Kunden - Kunden und Firmen, denen du eine Liste zuweist.
  • Mitglieder-Authentifizierung - Anmeldung und das Access-Token, das account.prices braucht.