Primitive spec

Hardin

The catalog primitive. Hardin holds the sellable concept and its rules — products, variants, modifiers, recipes, categories, dynamic pricing, and tax classes — that POS and online ordering surfaces price and order against.

Status: Catalog model live across Category, Product, ProductVariant, Modifier, ModifierGroup, CompositeSlot, RecipeIngredient, and TaxClass. Dynamic pricing engine now multi-tier: PriceOverride scopes (tier / channel / minimum quantity / Magnifico audience), PricingRule (raw CEL fallback), YieldRule (yield / surge / off-peak), CartDiscount + BogoPromotion (cart-shape promotions), BundlePromotion (fixed-price combos), and customer-redeemable promo codes wired into Hober redemption at order close. The read-optimised /menu resolver is the remaining slice.

What it owns

Hardin owns the sellable concept. Trantor owns the physical thing that gets allocated or consumed. A burger is not allocated — its ingredients get consumed; Hardin's RecipeIngredient links a variant to Trantor inventory items that decrement at sale.

Hardin does not decrement stock, evaluate daypart windows, or render UI. It owns the model; the resolver answers "what is sellable, at what price, right now, at this location" by walking it.

Concepts

Category
A hierarchical menu section. Drives display order, routing rules (Daneel reads Product.category for kitchen routing), and combo-eligibility shapes.
Product
A sellable concept — "Bacon Cheeseburger", "T-Shirt", "Spa Day Pass". A Product is never ordered directly; orders reference its variants.
ProductVariant
A SKU. Every Product has at least one. Carries the orderable price, barcode, vendor codes, and the recipe that drives Trantor consumption on sale.
ModifierGroup & Modifier
Flat option lists that decorate a variant. ModifierGroup has selection rules (single or multi, with min/max). Modifier carries a price delta and an optional RecipeIngredient for inventory-affecting choices ("+ bacon" consumes two strips).
CompositeSlot
A combo slot. Distinct from a ModifierGroup because the choice is a Product reference, not a flat option — "Pick a side" lets the customer choose any Product whose category is sides; the chosen product carries its own modifiers, recipe, and price.
PriceOverride
A scope-keyed price for a variant. Scopes include location (sub-tenant), daypart (Seldon TimeScheme), tier (Terminus tier group), channel (online vs in-person vs kiosk …), minimum quantity, and Magnifico audience membership. Resolution is most-specific wins, ties by priority.
PricingRule
A raw-CEL fallback for everything PriceOverride scopes can't express. A CEL expression evaluated against the cart context returns a price; lets a tenant author pricing logic that doesn't fit the scope vocabulary without forking the primitive.
YieldRule
Yield / surge / off-peak adjustments. A YieldRule modifies the resolved price up or down based on capacity utilisation, time of day, or any tenant-defined factor — the same primitive an airline uses to push fares up as a flight fills, applied to hotel rooms, classes, appointments, or anything Seldon books.
CartDiscount & BogoPromotion
Cart-shape promotions evaluated at order time. CartDiscount applies an amount or percentage off when the cart meets a predicate (subtotal threshold, has-X-of-category, …). BogoPromotion handles buy-one-get-one style shapes ("buy any 2 mains, get cheapest side free"). Both compose with PriceOverride and YieldRule resolution.
BundlePromotion
Fixed-price combo. "Burger + fries + drink for $11" is a BundlePromotion that swaps in the bundle price when the customer's cart matches the bundle's required slots. Distinct from CompositeSlot (which is a single-product configuration) because a bundle's components can be added independently and the discount applies retroactively.
PromoCode & Hober redemption
Customer-redeemable codes. Issued by Magnifico (audience-scoped, expiring, single-or-multi-use); redeemed at the Hober Order checkout, where Hardin resolves the discount against the current cart state. Tying campaigns to specific orders without operator data entry.
RecipeIngredient
The link from a variant or modifier to a Trantor inventory item that gets consumed at sale. Keyed on (sourceType, sourceId, trantorResourceTypeId, quantity, unit).
TaxClass
A jurisdiction-aware tax classification. Mallow reads it on every OrderLine at close to compute line tax.

API surface

Endpoints are versioned under /hardin/v1/. The /menu resolver is the read path for POS UIs; everything else is admin / write.

Hardin endpoints in the Foundation API reference OpenAPI 3.1 schema for Hardin with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST / GET/hardin/v1/categoriesCRUD plus tree query.
POST / GET/hardin/v1/productsCRUD plus query DSL (?categoryId=, ?attr_*=).
POST / GET/hardin/v1/products/{id}/variantsManage variants on a Product.
POST / DELETE/hardin/v1/products/{id}/modifier-groupsBind modifier groups to a Product.
POST / GET/hardin/v1/modifier-groupsManage ModifierGroups and their Modifiers.
POST / GET/hardin/v1/products/{id}/composite-slotsManage combo slots on a Product.
POST / GET/hardin/v1/products/{id}/recipeList or replace the RecipeIngredient set.
POST / GET/hardin/v1/products/{id}/price-overridesManage scope-keyed prices.
POST / GET/hardin/v1/tax-classesManage tax classifications.
PATCH/hardin/v1/products/{id}/availabilitySet manuallyDisabled (the "86" flag) or the daypart TimeScheme.
GET/hardin/v1/menu?locationId=&at=Read-optimised resolver. Filtered, priced, available at this instant. Planned for v2.

Example: a burger with a temperature modifier

POST /hardin/v1/products
{
  "categoryId": "cat_hot_food",
  "name": "Bacon Cheeseburger",
  "taxClassId": "tax_prepared_food",
  "variants": [
    {
      "name": "Single",
      "priceCents": 1495,
      "recipe": [
        { "trantorResourceTypeId": "rt_beef_patty", "quantity": 1, "unit": "each" },
        { "trantorResourceTypeId": "rt_bun",        "quantity": 1, "unit": "each" },
        { "trantorResourceTypeId": "rt_bacon",      "quantity": 2, "unit": "strip" }
      ]
    }
  ]
}
POST /hardin/v1/modifier-groups
{
  "name": "Temperature",
  "selectionRule": "single",
  "min": 1, "max": 1,
  "modifiers": [
    { "name": "Medium-rare", "priceDeltaCents": 0 },
    { "name": "Medium",      "priceDeltaCents": 0 },
    { "name": "Well done",   "priceDeltaCents": 0 }
  ]
}

How it fits with the rest

flowchart LR
  Ops[Operator authoring] --> Ha(Hardin)
  Ha -. recipe .-> T[Trantor inventory]
  Ha -. daypart .-> S[Seldon TimeScheme]
  Ho[Hober POS] -- read --> Ha
  M[Mallow] -. taxClassId .-> Ha
            

Hober snapshots Hardin variants and modifiers at add-time onto OrderLines — the price the customer sees is the price they pay even if the catalog changes mid-order. Trantor is consumed via RecipeIngredient on Order close; Hardin itself never decrements. Seldon TimeSchemes back daypart availability. Mallow reads taxClassId per OrderLine to compute line tax. Daneel kitchen routing reads Product.category to pick a station.