At a Glance
Persona: Cost Controller (+ Cost Control Department) · Module: recipe · Scenarios: ~26
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: none at this time — Cost-Controller-internal E2E is a gap; closest adjacent coverage is indirect viatests/701-sr.spec.tsin../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Cost Controller persona (Cost Controller + Cost Control Department) directly drives in the recipe module. The Cost Controller is read across the recipe library and writes on cost / pricing columns only (per REC_AUTH_006); their primary work is cost drift monitoring, co-approval of off-target publishes (per REC_AUTH_007), and theoretical-vs-actual variance investigation. Scenarios are grouped into happy paths (cost-only edit; co-approval at off-target publish; sub-recipe cascade verification; variance dashboard; category-level target adjustment), RBAC (cost controller without recipe:edit-cost; attempted ingredient edits; auditor read-only side), validation (negative tests against REC_VAL_008 cost percentage bounds, pricing-history integrity), and edge cases around high-fanout ingredient drift, sub-recipe deep cascade, currency precision, multi-tenant target settings. Cross-persona handoffs that pivot off the Cost Controller (Scenarios 2, 3, 13 in the parent overview) live in 04-test-scenarios.md, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| CC-HP-01 | Cost-only edit on PUBLISHED recipe (price adjustment) |
Recipe House Burger is PUBLISHED with cost_per_portion = ฿99.23, selling_price = ฿310.00, target_food_cost_percentage = 32%. Cost Controller has recipe:edit-cost. |
1. Open recipe → Costing tab. 2. Change selling_price from ฿310.00 to ฿325.00 (to restore margin after a cost drift). 3. Set change_reason = "price adjustment to restore margin after vendor pricelist update". 4. Save. |
selling_price updated; actual_food_cost_percentage, gross_margin, gross_margin_percentage recomputed per REC_CALC_009–REC_CALC_010; tb_recipe_pricing_history row written with the new snapshot and change_reason; status stays PUBLISHED; no tb_recipe_version written (pricing-only edit per REC_POST_010). |
| CC-HP-02 | Co-approve off-target publish | Chef has prepared DRAFT Signature Dish with actual_food_cost_percentage = 38% vs target = 32% (drift = 6 percentage points, above tenant tolerance of 2). Tenant policy enables REC_AUTH_007. |
1. Cost Controller opens the co-approval queue. 2. Drills into Signature Dish. 3. Reviews ingredients, cost rollup, intended pricing, strategic rationale ("signature dish, accept lower margin"). 4. Clicks Co-approve. 5. Notes co-approval rationale. |
Co-approval recorded in application-layer signoff log; Chef notified that publish is now permitted; Chef clicks Publish → REC_POST_003 fires; tb_recipe_version v1 written with change_summary = "co-approved off-target by <Cost Controller name>: signature dish, accept lower margin"; tb_recipe_pricing_history row at publication snapshot. |
| CC-HP-03 | Verify sub-recipe cost cascade | Sub-recipe Burger Sauce is PUBLISHED and used as Line 4 on 3 parent recipes. Mayonnaise cost rises from ฿0.10/g to ฿0.14/g via product cost update. |
1. Cost Controller opens the cost-drift dashboard. 2. Confirms Burger Sauce.cost_per_portion re-computed. 3. Drills into the three affected parent recipes. 4. For each parent: verifies Line 4.cost_per_unit refreshed, net_cost updated, total_ingredient_cost rolled up, cost_per_portion updated. 5. Reads each parent's tb_recipe_pricing_history for the cascade entry. |
Cascade fires atomically per REC_POST_006; sub-recipe and parents all re-cost in one transaction; each affected parent recipe has a tb_recipe_pricing_history row with change_reason = "sub-recipe cost cascade from Burger Sauce"; recipes whose actual_food_cost_percentage now exceeds tolerance are flagged in the dashboard for follow-up. |
| CC-HP-04 | Adjust category-level target food-cost % | Cost Control Department decides to raise Mains category target from 32% to 33% due to industry cost pressure. |
1. Open Sysadmin → Category Master → Mains. (Cost Controller coordinates with Sysadmin who has the write authority on category masters.) 2. Update default_cost_settings.target_food_cost_percentage from 32 to 33. 3. Save. 4. Optionally trigger a bulk re-apply to existing recipes in Mains (manual operation; not automatic). |
tb_recipe_category.default_cost_settings updated; new recipes in Mains inherit 33% target; existing recipes unchanged unless explicitly re-applied; change logged with timestamp / user. Recipes in Mains whose actual_food_cost_percentage was previously at 32.5% (just over target) may now be in tolerance with the new target. |
| CC-HP-05 | Run theoretical-vs-actual variance report | Period closed; all menu sales recorded; PUBLISHED recipes drove theoretical OUT movements per REC_XMOD_003; SR / physical-count / adjustment posts created actual movements. |
1. Open variance dashboard. 2. Filter to outlet Main-Kitchen, period 2026-05. 3. View per-ingredient variance: theoretical OUT (from recipe explosion × sales) vs actual OUT (from SR / adjustment). 4. Drill into largest variances. |
Dashboard surfaces per-ingredient per-outlet variance; positive variance points to over-consumption (over-portioning, theft, spoilage, recipe under-spec); negative variance points to under-portioning or recipe over-spec. Variance investigation is collaborative; Cost Controller raises findings with Chef (recipe accuracy) and Outlet Manager (portion-control). |
| CC-HP-06 | Update target food-cost % on a single recipe | Cost Controller has recipe:edit-cost; recipe is PUBLISHED. |
1. Open recipe → Costing tab. 2. Change target_food_cost_percentage from 32 to 30 (tightening target). 3. Save. |
target_food_cost_percentage updated; suggested_price recomputed per REC_CALC_008; actual_food_cost_percentage stays the same (it's cost/price, not cost/target); tb_recipe_pricing_history row written with change_reason = "tightened target to 30%". Recipe is now off-target if actual exceeds 30% + tolerance; flagged in cost-drift dashboard for follow-up. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| CC-PERM-01 | Cost Controller edits target_food_cost_percentage, selling_price, labor_cost_percentage, overhead_percentage |
Allow. Per REC_AUTH_006. Edits succeed on DRAFT and PUBLISHED recipes; tb_recipe_pricing_history row written. |
| CC-PERM-02 | Cost Controller attempts to edit ingredient qty, wastage_percentage, sub-recipe link |
Deny — Chef-only. Cost Controller has recipe:edit-cost but not recipe:edit (full edit). UI hides the ingredient-edit controls; direct API call returns "You are not authorized to edit ingredient lines; that authority is the Chef's." |
| CC-PERM-03 | Cost Controller attempts to publish a recipe | Deny — Chef-only. Publish requires recipe:publish which is Chef-side. Cost Controller's role is co-approval, not publish; the publish action stays with the Chef per REC_AUTH_003. |
| CC-PERM-04 | Cost Controller co-approves off-target publish | Allow. Per REC_AUTH_007. Co-approval surfaces in the audit log and unblocks the Chef's publish. |
| CC-PERM-05 | Cost Control Department user attempts category-level cost-setting change | Coordinate with Sysadmin. The actual write is on tb_recipe_category.default_cost_settings which is Sysadmin-owned. Cost Control Department raises the change request; Sysadmin executes. Co-authorship is the operational pattern. |
| CC-PERM-06 | Cost Controller reads pricing history | Allow. recipe:read-history is part of the cost controller role; full tb_recipe_pricing_history access for analysis. |
| CC-PERM-07 | Cost Controller attempts to read DRAFT recipes outside their scope |
Allow (typically). Cost Controller usually has read across the library (any category) to support cross-category analysis. Tenant policy may scope; the default is open read. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| CC-VAL-01 | Invalid percentage value | Cost Controller sets target_food_cost_percentage = -5 or 150. |
Reject — REC_VAL_008. Server returns "Cost percentage values must be between 0 and 100." |
| CC-VAL-02 | selling_price ≤ cost_per_portion on a PUBLISHED recipe |
Cost Controller sets selling_price = 0 or below current cost_per_portion. |
Reject — REC_VAL_018 analogue at edit time. Server returns "Selling price must be greater than cost per portion; review pricing." (Note: schema allows nullable selling_price, so setting it to null bypasses this; setting it to a positive value below cost is rejected.) |
| CC-VAL-03 | Pricing-history change_reason empty on cost-only edit |
Cost Controller saves without change_reason. |
Reject — application-layer validation. Server returns "Change reason is required for pricing edits." (Reason text required for audit clarity per REC_POST_010.) |
| CC-VAL-04 | Cost rollup integrity check fails | tb_recipe.cost_per_portion does not match (total_ingredient_cost + labor_cost + overhead_cost) / base_yield to rounding tolerance. |
Reject at save (integrity check) — REC_VAL_013 analogue at header level. Server returns "Recipe cost rollup is inconsistent; please refresh and re-save." (Indicates an upstream cascade was not applied — recovery: refresh and re-save.) |
| CC-VAL-05 | Pricing-history rapid-fire suppression | Cost Controller saves the same value twice in 5 seconds (no actual change). | Warn — no-op. Server detects the duplicate value, optionally suppresses the second tb_recipe_pricing_history insert (or inserts with change_reason = "no-change save (idempotent)" depending on tenant policy). |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| CC-EDGE-01 | High-fanout ingredient cost drift | Product Premium Olive Oil is used in 25 recipes; cost rises 18%. |
Costing module emits drift event per REC_XMOD_006; recipe module refreshes cost_per_unit on every line referencing the product (25 recipes); writes tb_recipe_pricing_history row per affected recipe with change_reason = "cost-drift update from costing module"; recipes whose new actual_food_cost_percentage exceeds tolerance flagged in dashboard. Performance: bulk update completes in tenant-acceptable time. |
| CC-EDGE-02 | Sub-recipe deep cascade (3 levels) | Cost change on a leaf product Beef Bones cascades through Brown Stock → Sauce Au Poivre → Composite Plate (a published recipe). |
Cascade recurses 3 levels per REC_POST_006; each level recomputes cost_per_portion and writes a tb_recipe_pricing_history row with change_reason referencing the originating cascade source; the cascade is internally consistent (the final cost change on the parent reconciles to the leaf cost change × usage multiplier). |
| CC-EDGE-03 | Currency precision corner case | Cost change of ฿0.001 per unit on a high-volume ingredient (e.g. rice 10g per portion). | Stored at full 5dp precision; rolled up to net_cost at 5dp; display rounded to 2dp for currency per REC_CALC_015. The 0.001 change may not visibly change displayed cost-per-portion but rolls up over high volume in the pricing history. |
| CC-EDGE-04 | Concurrent cost-only edits | Two Cost Controllers edit the same recipe's pricing simultaneously. | Optimistic-concurrency: first save wins; second save returns conflict; second user refreshes. Each accepted save writes its own tb_recipe_pricing_history row. |
| CC-EDGE-05 | Category-level retroactive re-apply | Sysadmin changes Mains category target from 32% to 33%; Cost Control Department requests retroactive re-apply to all Mains recipes. |
Manual operation: a batch job iterates all non-soft-deleted recipes in Mains and writes the new target; per-recipe tb_recipe_pricing_history row may be written per REC_POST_010 (depending on tenant policy on bulk operations). |
| CC-EDGE-06 | Variant pricing edit | Cost Controller edits selling_price on a specific tb_recipe_yield_variant. |
Per-variant selling_price updated; variant food_cost_percentage / gross_margin / gross_margin_percentage recomputed; pricing history row may be variant-scoped (with variant_id populated on the row); base recipe pricing unaffected. |
| CC-EDGE-07 | Reading historical version state | Cost Controller drills into tb_recipe_version for a 6-month-old snapshot to investigate a margin question. |
tb_recipe_version.recipe_data / ingredients_data JSON provides the full snapshot; Cost Controller can compute historical margin at that point in time; no edit authority on historical snapshots (read-only via recipe:read-history). |
| CC-EDGE-08 | Pricing-history truncation policy | Long-lived recipe accumulates 500+ tb_recipe_pricing_history rows over its lifetime. |
All rows retained for audit; UI paginates; queries indexed appropriately on recipe_id + effective_date. Tenant may have an archival policy for very old rows (move to cold storage), but the recipe module does not delete rows by default. |
REC_VAL_008 (cost percentage bounds); Section 3 — calculation rules REC_CALC_001–REC_CALC_015 (the math the Cost Controller verifies); Section 4 — REC_AUTH_006–REC_AUTH_008 (Cost Controller's authority scope); Section 5 — REC_POST_006 (sub-recipe cascade), REC_POST_010 (pricing-only edit); Section 6 — REC_XMOD_005–REC_XMOD_006 (costing-module coupling), REC_XMOD_009 (audit / versioning).701-sr.spec.ts (variance reports indirectly).