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.
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.categoryfor 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.
ModifierGrouphas selection rules (singleormulti, with min/max).Modifiercarries a price delta and an optionalRecipeIngredientfor 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.
CartDiscountapplies an amount or percentage off when the cart meets a predicate (subtotal threshold, has-X-of-category, …).BogoPromotionhandles 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.
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /hardin/v1/categories | CRUD plus tree query. |
| POST / GET | /hardin/v1/products | CRUD plus query DSL (?categoryId=, ?attr_*=). |
| POST / GET | /hardin/v1/products/{id}/variants | Manage variants on a Product. |
| POST / DELETE | /hardin/v1/products/{id}/modifier-groups | Bind modifier groups to a Product. |
| POST / GET | /hardin/v1/modifier-groups | Manage ModifierGroups and their Modifiers. |
| POST / GET | /hardin/v1/products/{id}/composite-slots | Manage combo slots on a Product. |
| POST / GET | /hardin/v1/products/{id}/recipe | List or replace the RecipeIngredient set. |
| POST / GET | /hardin/v1/products/{id}/price-overrides | Manage scope-keyed prices. |
| POST / GET | /hardin/v1/tax-classes | Manage tax classifications. |
| PATCH | /hardin/v1/products/{id}/availability | Set 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.