Primitive spec

Hardin

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

Status: Entity model and HTTP surface designed; controllers and entities exist for Category, Product, ProductVariant, Modifier, ModifierGroup, CompositeSlot, PriceOverride, RecipeIngredient, and TaxClass. The read-optimised /menu resolver and price-scope expansion are the next slices.

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", "18-hole Round". 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. v0 scopes are location (sub-tenant) and daypart (Seldon TimeScheme). Tier / channel / segment scopes are deferred. Resolution is most-specific wins, ties by priority.
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.

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.