Skip to content

fix(recipes): read ingredient_type/ingredient_id so components keep their name - #14

Merged
obitum merged 1 commit into
mainfrom
fix/recipe-ingredient-type
Aug 15, 2026
Merged

fix(recipes): read ingredient_type/ingredient_id so components keep their name#14
obitum merged 1 commit into
mainfrom
fix/recipe-ingredient-type

Conversation

@obitum

@obitum obitum commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

What / Why

stocksmith recipes show <id> printed a literal 0 in place of every component ingredient's name.

The API (craftybase-app#5709, CU-868kqwr7q) now types each recipe ingredient and returns an id that resolves against the matching endpoint. That change was needed because a component ingredient's material_id pointed at the component's paired stock record, which the materials endpoint excludes — so it could never be looked up. material_id/material_name became deprecated aliases, null for components:

{ "id": 6001, "ingredient_type": "component", "ingredient_id": 2167942,
  "ingredient_name": "Punk IPA", "material_id": null, "material_name": null,
  "quantity": "0.5", "unit": "litres" }

RecipeIngredient.MaterialID was a non-pointer int, so those nulls unmarshalled silently to the zero value (no error), and the renderer nameOrID(ing.MaterialName, ing.MaterialID) fell through to strconv.Itoa(0). Any recipe containing a component — i.e. any multi-level BOM — lost the ingredient's name.

The change

  • RecipeIngredient reads ingredient_type, ingredient_id, ingredient_name; MaterialID becomes *int to match how ExpenseLineItem already models a nullable material.
  • New label() prefers the new fields and falls back to the deprecated aliases, so the CLI still renders correctly against an API that predates ingredient_type (relevant during rollout).
  • The ingredient table gains a TYPE column, and its first column is now INGREDIENT rather than MATERIAL — components were never materials, which is the same conflation the API fix addressed.

Before / after for a bottling recipe:

INGREDIENTS (1)                      INGREDIENTS (1)
MATERIAL  QTY  UNIT          →       INGREDIENT  TYPE       QTY  UNIT
0         0.5  litres                Punk IPA    component  0.5  litres

Testing

go test ./... -race passes. Added:

  • a component ingredient renders its name and type, and asserts no zero id leaks into the output;
  • label() falls back to material_name, then material_id, then , covering pre-ingredient_type responses.

gofmt and go vet are clean. golangci-lint isn't installed locally, so CI is the first run of it.

…heir name

The API now types each recipe ingredient and returns an id that resolves against
the matching endpoint, because a component ingredient's material_id pointed at
the component's paired stock record and could never be looked up. material_id and
material_name became deprecated aliases, null for components.

RecipeIngredient.MaterialID was a non-pointer int, so those nulls unmarshalled to
the zero value and nameOrID fell through to strconv.Itoa(0): every component
ingredient rendered as a literal "0" instead of its name.

Reads the new fields, keeping the deprecated aliases as a fallback so the CLI
still renders correctly against an API that predates ingredient_type. The
ingredient table gains a TYPE column and its first column is now INGREDIENT
rather than MATERIAL, since components were never materials.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
stocksmith-cli 78b05c6 Aug 15 2026, 07:27 AM

@obitum
obitum merged commit 1542541 into main Aug 15, 2026
3 of 4 checks passed
@obitum
obitum deleted the fix/recipe-ingredient-type branch August 15, 2026 12:31
obitum added a commit that referenced this pull request Aug 28, 2026
)

The API (CU-868kwy85j) now types purchase receipt three ways and reports
inbound stock. Previously an expense carried only a header `received`
boolean, which the app sets as an AND across the lines — so a purchase
with 3 of 5 lines arrived and one where nothing arrived both serialised
`received: false`. Integrations could not tell them apart, which is why
customers close a part-arrived purchase and re-enter the remainder as a
new one. There was also no route to inbound quantities at all: /expenses
is gated on the material-costs permission, so an operations integration
that cannot see supplier pricing could not call it.

- expenses: read `received_status` and render it as the RECEIPT column,
  replacing the RECEIVED yes/no. The rollup is a superset of the boolean,
  so a separate column would only ever restate it.
- expenses: read the per-line `received` and add a RECEIVED column to the
  line-items sub-table in `expenses show`
- expenses: add --received-status, passed through verbatim (comma-separate
  for more than one); the API validates and 400s on an unknown value
- materials: read `on_order` and add an ON ORDER column. It is reported in
  stock units, so it carries the same unit of measure as ON HAND — never
  the purchase unit, which is the easy mistake here.
- output: add FormatBoolPtr for a nullable boolean

Three back-compat guards, since --api-url can point at any deployment.
ExpenseLineItem.Received is a *bool so an API that predates per-line
receipt renders "—" rather than reporting every line as not yet arrived;
receiptStatus() falls back to the header boolean and yields only
"received" or "outstanding", never inventing "partial"; and an absent
on_order renders "—" rather than an invented zero. This is the same
failure #14 fixed, where a null unmarshalled to a zero value and rendered
as a literal "0".

Contract tests now assert on_order is a string and stays a top-level
material field rather than moving inside the gated unit_cost block, that
received_status is one of the three strings, and that the header received
boolean survives alongside it.

Verified: go build, go vet, gofmt, go test ./... -race, and the rendered
output of expenses list/show and materials list against a mock API.
golangci-lint is not installed locally; CI covers it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant