Skip to content

feat: item master — a SKU's sellable item + category taxonomy (ADR 0022) - #5

Merged
DanMat merged 1 commit into
mainfrom
feat/item-master
Aug 30, 2026
Merged

DanMat merged 1 commit into
mainfrom
feat/item-master

Conversation

@DanMat

@DanMat DanMat commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Foodmart Slice 1. For retail (a grocery sells its inventory as-is), a SKU can now optionally carry a sellable item record — name, price, unit, description, image, active/featured — alongside its ledger stock, plus a two-level category taxonomy. Additive: a SKU can have stock with no item and vice-versa, so a pure-ledger user is unaffected. Keyed by the opaque sku_code with no FK into core (image is a soft media-id ref resolved at render).

Evolves — via ADR 0022 — the "catalog lives in content" stance: content collections stay the catalog for editorial items; the item master is for operational stock you sell.

What's here

  • Schema: inventory_item + inventory_category (migration 003_items); supersedes the catalog-in-content docblocks.
  • Catalog service: item/category CRUD with the pinned security controls — store raw / escape on render, non-negative decimal price, media-id soft ref, field allow-list (no over-posting), category parent-must-exist + two-level depth (cycles impossible), delete blocked while referenced.
  • MCP: inventory_item_set/get, inventory_category_set/get, inventory_categories — write-gated on inventory:write, reads on :read. (Deletes stay admin-only — a recorded deferral.)
  • Admin: a 📇 Catalog page (item + category management), inventory:write + CSRF, one nonce'd <style> block (admin CSP), data-label reflow tables for mobile.
  • Guide updated to teach the item master.

Reviews

Both nimbus-review-loop and nimbus-security-review run before build. Classification: official plugin, additive — not core. Drift guard passes on the generic-fields constraint (no grocery-specific columns). Security-green: store-raw/escape-on-render contract set now for Slice 2.

Verification

PHPStan (max) clean · 68 tests / 187 assertions green · php-cs-fixer clean. Live 375px browser check to follow on deploy (reuses Phase 1/2a responsive primitives).

🤖 Generated with Claude Code

For retail (a grocery sells its inventory as-is), a SKU can now optionally
carry a sellable *item* record — name, price, unit, description, image,
active/featured flags — alongside its ledger stock, plus a two-level
category taxonomy. Additive: a SKU can have stock with no item and vice
versa, so a pure-ledger user is unaffected. Keyed by the opaque sku_code
with no FK into core (image is a soft media-id ref resolved at render).

- Schema: inventory_item + inventory_category (migration 003_items),
  superseding the "catalog lives in content" docblocks (ADR 0022).
- Catalog service: item/category CRUD with the pinned controls — store
  raw / escape on render, non-negative decimal price, media-id soft ref,
  field allow-list (no over-posting), category parent-must-exist + two-
  level depth (cycles impossible), delete blocked while referenced.
- MCP: inventory_item_set/get + category_set/get + categories, gated
  inventory:write/read (deletes stay admin-only — a recorded deferral).
- Admin: a Catalog page (item + category management), inventory:write +
  CSRF, one nonce'd <style> block (admin CSP), mobile-reflow tables.
- Guide updated to teach the item master.

Tests: item override, raw storage, price/media-id/category validation,
content-token-cannot-write, over-posting ignored, two-level depth,
delete-block, slug allow-list, admin escaping + CSP-clean + reflow.
PHPStan L max, 68 tests, cs-fixer all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DanMat
DanMat merged commit f649a82 into main Aug 30, 2026
2 checks passed
@DanMat
DanMat deleted the feat/item-master branch August 30, 2026 23:35
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