At a Glance
Persona: Chef (Chef / Kitchen Manager + Kitchen Staff read-only) · Module: recipe · Scenarios: ~38
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: none at this time — recipe-internal E2E is a gap; closest adjacent coverage istests/701-sr.spec.tsfor recipe-driven SR auto-create in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Chef persona (Chef / Kitchen Manager + Kitchen Staff read-only subset) directly drives in the recipe module. The Chef's involvement begins at recipe creation (DRAFT from create-time) and continues through every revision and through archive; Kitchen Staff are read-only at service time. Scenarios are grouped into happy paths (create / save / publish; in-place edit on PUBLISHED; un-publish round-trip; archive; clone; sub-recipe usage; yield variants), RBAC (chef without recipe:publish; pastry chef scoped to Desserts category; Kitchen Staff attempting edits), validation (negative tests against REC_VAL_001–REC_VAL_018 that the Chef can trigger at save or publish), and edge cases around sub-recipe cycle detection, decimal precision, concurrent edits, large recipes, and variant-scoped ingredient lines. Cross-persona handoffs that pivot off the Chef (Scenarios 1, 2, 4, 5, 7, 8, 9, 12, 14 in the parent overview) live in 04-test-scenarios.md, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| CHEF-HP-01 | Create new recipe DRAFT with minimum required fields |
Chef logged in with recipe:create on Mains category; category Mains and cuisine Italian active; products Beef Patty / Brioche Bun active and is_used_in_recipe = true. |
1. Navigate to /recipes → click New Recipe. 2. Set name = "House Burger", category = Mains, cuisine = Italian, difficulty = MEDIUM. 3. Set base_yield = 1, base_yield_unit = "portion", prep_time = 8, cook_time = 12. 4. Add Line 1: ingredient_type = product, product = Beef Patty, qty = 1, ingredient_unit = piece, cost_per_unit = 45.00, wastage_percentage = 5. 5. Click Save. |
tb_recipe row persisted at status = DRAFT; code assigned per tenant numbering; defaults inherited from tb_recipe_category.default_cost_settings (target food-cost %, labor / overhead percentages); one tb_recipe_ingredient row persisted with net_cost = 47.25, wastage_cost = 2.25; tb_recipe.total_ingredient_cost = 47.25; no tb_recipe_version row yet (versioning starts at publish). |
| CHEF-HP-02 | Save additional lines, prep steps, and yield variant | CHEF-HP-01 state. | 1. Add Lines 2–4 (Brioche Bun, Cheese with UoM conversion, Burger Sauce as sub-recipe). 2. Add 5 preparation steps with sequence, title, description, equipment, temperature. 3. Add yield variant Double Burger with conversion_rate = 1.8 and selling_price = 195.00. 4. Set tb_recipe.default_variant_id to the base or the Double variant. 5. Save. |
All rows persisted; total_ingredient_cost re-rolled-up per REC_CALC_003; labor_cost and overhead_cost computed; cost_per_portion written; UoM conversion on Cheese line writes inventory_qty = 0.030 kg from qty = 30 g × conversion_factor = 0.001; sub-recipe Line 4 sources cost_per_unit from sub-recipe cost_per_portion per REC_CALC_011. |
| CHEF-HP-03 | Publish recipe with on-target margin | DRAFT recipe with valid completeness state; cost_per_portion = ฿99.23, selling_price = ฿310.00, target_food_cost_percentage = 32% (within tolerance: actual = 32.01%). |
1. Open recipe detail. 2. Click Publish. | All publish-time rules pass (REC_VAL_015–REC_VAL_018): ≥1 ingredient, ≥1 step, valid cost rollup, selling_price > cost_per_portion; status DRAFT → PUBLISHED, published_at = now(); tb_recipe_version row v1 written with full snapshot (recipe_data, ingredients_data, steps_data, variants_data) and published = true, change_summary = "initial publication"; tb_recipe_pricing_history row written at publication snapshot. Recipe now eligible for menu-item linkage and theoretical-consumption drives per REC_POST_003. |
| CHEF-HP-04 | In-place edit on PUBLISHED recipe (tenant policy: in-place with versioning) |
Recipe House Burger at v1 PUBLISHED; tenant edit_published_with_versioning = true. |
1. Open the recipe. 2. Change Line 1 patty qty from 1 to 1.2. 3. Update change_summary = "patty qty bumped per outlet feedback". 4. Save. |
Edit applies in-place; cost rollup recomputes (net_cost on Line 1 updates, total_ingredient_cost rolls up); tb_recipe_version v2 written with published = true and the change_summary; cost change triggers a tb_recipe_pricing_history row with change_reason = "ingredient qty revision"; theoretical consumption from the next menu sale onward uses the v2 formula per REC_POST_004. |
| CHEF-HP-05 | Un-publish round-trip for major overhaul (tenant policy: un-publish required) | Recipe Seasonal Curry is PUBLISHED; tenant edit_published_with_versioning = false. |
1. Open the recipe. 2. Click Un-publish for Revision. 3. Make multiple edits (ingredients, method, variants). 4. Click Publish. | Status PUBLISHED → DRAFT (REC_POST_008); recipe excluded from menu-item linkage drives; edits applied in DRAFT; on re-publish, status DRAFT → PUBLISHED, published_at updated, new tb_recipe_version v_n written with full snapshot and change_summary = "seasonal refresh — Q3 2026". Linked menu items are flagged for review during the un-publish window. |
| CHEF-HP-06 | Archive recipe at menu retirement | PUBLISHED recipe Holiday Special; F&B Ops decision to retire; chef has recipe:archive. |
1. Open the recipe. 2. Click Archive. 3. Confirm with reason "Holiday menu retired Q1 2026". |
Status PUBLISHED → ARCHIVED per REC_POST_007; archived_at = now(); final tb_recipe_version row written with change_summary = "archived — Holiday menu retired"; recipe no longer eligible for menu-item linkage drives or theoretical-consumption fan-out on new sales; historical events in inventory ledger preserved. |
| CHEF-HP-07 | Clone existing recipe | Existing PUBLISHED recipe House Burger; chef wants to create a Spicy Burger variant on a separate recipe (not a yield variant). |
1. Open House Burger. 2. Click Clone. 3. Enter code = "RCP-SPICY-001", name = "Spicy Burger". 4. Adjust ingredients (swap sauce, add jalapeños). 5. Save. |
New tb_recipe row at status = DRAFT; header / ingredients / steps / variants copied from source; code and name set to user values; published_at and archived_at cleared; tb_recipe_version row chain starts fresh on first publish. |
| CHEF-HP-08 | Use a sub-recipe as ingredient | Sub-recipe Burger Sauce is PUBLISHED; chef adding it as Line 4 on House Burger. |
1. Open House Burger in DRAFT. 2. Add Line 4: ingredient_type = recipe, sub_recipe = Burger Sauce, qty = 15, ingredient_unit = g. 3. Save. |
Line persists with sub_recipe_id populated, product_id null per REC_VAL_010; cost_per_unit sourced from Burger Sauce.cost_per_portion translated to per-g basis per REC_CALC_011; net_cost rolls up; back-relation tb_recipe_used_in_recipes now shows House Burger as a parent of Burger Sauce. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| CHEF-PERM-01 | Chef creates and publishes recipe in own category | Allow. Chef holds recipe:create / edit / publish on Mains. CHEF-HP-01 through CHEF-HP-03 succeed. Per REC_AUTH_001 / REC_AUTH_003. |
| CHEF-PERM-02 | Pastry Chef (scoped to Desserts) attempts to create in Mains | Deny — RBAC scope. Pastry Chef has recipe:create only on Desserts. Create attempt with category = Mains is rejected at API with "You are not authorized to create recipes in category Mains."; no tb_recipe row written. |
| CHEF-PERM-03 | Kitchen Staff attempts to edit PUBLISHED recipe |
Deny — read-only. Kitchen Staff has recipe:read only. Direct API write attempts return "Kitchen Staff role is read-only on the recipe library." |
| CHEF-PERM-04 | Chef without recipe:publish attempts to publish |
Deny — RBAC. Chef has recipe:create / edit but not recipe:publish. Publish button returns "You are not authorized to publish recipes."; recipe stays DRAFT. Per REC_AUTH_003. |
| CHEF-PERM-05 | Chef attempts to edit PUBLISHED recipe without recipe:edit-published permission |
Deny — RBAC. Tenant requires recipe:edit-published for PUBLISHED edits; chef without the permission cannot save in-place. Either request the permission, or use un-publish round-trip if tenant policy permits. Per REC_AUTH_002 second clause. |
| CHEF-PERM-06 | Chef attempts to delete PUBLISHED recipe |
Deny — must archive. PUBLISHED recipes cannot be soft-deleted; must be archived first per REC_AUTH_014. Direct soft-delete API call returns "PUBLISHED recipes must be archived, not deleted." |
| CHEF-PERM-07 | Chef attempts to archive recipe with active menu-item linkages | Warn — coordinate with F&B Ops. UI surfaces a warning "This recipe is linked to N active menu items; archive will break those linkages. Coordinate with F&B Ops before proceeding." Confirm-to-proceed pattern. Underlying schema permits archive; menu-link removal is application-layer policy. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| CHEF-VAL-01 | Missing code at save |
Tenant numbering policy fails to assign code; chef clicks Save. |
Reject — REC_VAL_001. Server returns "Recipe code is required and the (code, name) pair must be unique." |
| CHEF-VAL-02 | Missing name at save |
Chef left name blank; clicks Save. |
Reject — REC_VAL_002. Server returns "Recipe name is required." |
| CHEF-VAL-03 | Missing or inactive category_id / cuisine_id |
Chef selects a category that has since been soft-deleted (UI race), or cuisine is_active = false. |
Reject — REC_VAL_003 / REC_VAL_004. Server returns "Recipe category is required and must reference an active category." / "Cuisine type is required and must reference an active cuisine." |
| CHEF-VAL-04 | base_yield = 0 or negative at publish |
Chef enters base_yield = 0; clicks Publish. |
Reject at publish — REC_VAL_005. Server returns "Recipe yield must be greater than zero and a yield unit is required." (Save may be warn-only on 0.) |
| CHEF-VAL-05 | Negative prep_time or cook_time |
Chef accidentally enters -5 for prep time. |
Reject — REC_VAL_006. Server returns "Prep time and cook time must be non-negative." |
| CHEF-VAL-06 | Invalid percentage value (> 100) on cost percentages | Chef sets target_food_cost_percentage = 150. |
Reject — REC_VAL_008. Server returns "Cost percentage values must be between 0 and 100." |
| CHEF-VAL-07 | Ingredient with qty ≤ 0 or wastage_percentage ≥ 100 |
Chef sets qty = 0 or wastage_percentage = 100 on a line. |
Reject at publish — REC_VAL_009. Server returns "Ingredient quantity must be greater than zero; cost per unit must be non-negative; wastage percentage must be in [0, 100)." (Save may be warn-only on 0 qty.) |
| CHEF-VAL-08 | Ingredient discriminator mismatch | Chef sets ingredient_type = product but populates sub_recipe_id (or vice versa). |
Reject — REC_VAL_010. Server returns "Ingredient discriminator mismatch: type 'product' requires 'product_id' populated and the referenced row to be active." |
| CHEF-VAL-09 | Sub-recipe is DRAFT or ARCHIVED at parent save / publish |
Chef references sub-recipe Burger Sauce but it has since been moved to DRAFT by another chef. |
Reject — REC_VAL_014. Server returns "Sub-recipe 'Burger Sauce' is not currently published; the parent recipe cannot reference an unpublished or archived sub-recipe." |
| CHEF-VAL-10 | Sub-recipe cycle detection | Chef attempts to add Recipe A as a sub-recipe of itself, or A → B → A loop. | Reject — REC_VAL_011. Server returns "Sub-recipe cycle detected: recipe 'A' cannot reference itself directly or through another sub-recipe." |
| CHEF-VAL-11 | UoM conversion incomplete | Chef sets ingredient_unit = g, inventory_unit = kg, but leaves conversion_factor null. |
Reject — REC_VAL_012. Server returns "Unit conversion is incomplete: when the recipe unit and inventory unit differ, a positive conversion factor is required." |
| CHEF-VAL-12 | Publish without ingredients | Chef tries to publish a recipe with no tb_recipe_ingredient rows. |
Reject at publish — REC_VAL_015. Server returns "Recipe must contain at least one ingredient line before it can be published." |
| CHEF-VAL-13 | Publish without preparation steps | Chef tries to publish a recipe with no tb_recipe_preparation_step rows. |
Reject at publish — REC_VAL_016. Server returns "Recipe must contain at least one preparation step before it can be published." |
| CHEF-VAL-14 | Publish with cost_per_portion = 0 |
Cost rollup produces zero (all ingredient costs zero, no labor / overhead). | Reject at publish — REC_VAL_017. Server returns "Recipe cost rollup is invalid or zero — verify ingredient costs, labor, overhead, and yield before publishing." |
| CHEF-VAL-15 | Publish with selling_price ≤ cost_per_portion |
Chef sets selling_price = ฿80, cost_per_portion = ฿99.23. |
Reject at publish — REC_VAL_018. Server returns "Selling price must be greater than cost per portion at publish; review pricing." |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| CHEF-EDGE-01 | Large recipe — 40 ingredients, 25 prep steps, 3 yield variants | Complex composite dish (multi-component plate). | All rows persist within Prisma Decimal(20, 5) and JSONB limits; cost rollup completes in tenant-acceptable time; tb_recipe_version snapshot JSON is large but persisted; performance regression test confirms publish takes < 2s. |
| CHEF-EDGE-02 | Sub-recipe deep nesting (3 levels) | Composite plate → mother sauce → stock → leaf product. | Cost cascade recurses 3 levels; theoretical-consumption fan-out on menu sale recurses 3 levels; OUT movement posted against the leaf product per REC_XMOD_004. No cycle (per REC_VAL_011). |
| CHEF-EDGE-03 | Decimal precision on qty and cost_per_unit |
Chef enters qty = 12.34567 (5dp) and cost_per_unit = ฿0.12345. |
Both stored at full 5dp precision per Decimal(20, 5); net_cost computed at full precision with half-up rounding to 5dp; display rounds to 3dp for qty and 2dp for currency per REC_CALC_015. |
| CHEF-EDGE-04 | Variant-scoped ingredient line | Recipe with two variants Small and Large; one ingredient (e.g. whole egg) appears only in Large (stepped quantity, not linear scale). |
Set tb_recipe_ingredient.tb_recipe_yield_variantId to the Large variant's id; ingredient applies only when the Large variant is in scope; theoretical consumption explosion respects the variant scope. |
| CHEF-EDGE-05 | Concurrent edits by two chefs on the same DRAFT |
Executive Chef and Sous Chef both open and save the same DRAFT. |
Optimistic-concurrency: first save wins; second save returns 409 conflict; second chef refreshes and resolves manually. The recipe schema does not have a doc_version column on tb_recipe directly — concurrency is enforced via updated_at timestamp check at the application layer. |
| CHEF-EDGE-06 | Clone of an ARCHIVED recipe |
Chef wants to revive a retired dish; clones the archived recipe. | New DRAFT row with header / ingredients / steps / variants copied; code and name cleared for the chef to set; archived_at = null, published_at = null on the clone; the archived original remains untouched in ARCHIVED. |
| CHEF-EDGE-07 | Yield variant with stepped quantity (non-linear scale) | Variant Large Burger uses 2 patties (not 1.8 patties from linear scale). |
Set the variant's ingredient scope on the patty line (tb_recipe_ingredient.tb_recipe_yield_variantId = Large.id) with qty = 2; the base variant uses its own patty line with qty = 1; the cost roll-up correctly differentiates per-variant cost. |
| CHEF-EDGE-08 | Re-publish after un-publish keeps version chain | Recipe was PUBLISHED (v1), un-published to DRAFT, edited, re-published. |
tb_recipe_version chain: v1 (published = true, "initial publication"), v2 (published = false, on save during DRAFT), v3 (published = true, "seasonal refresh"). Or, depending on tenant policy, DRAFT saves don't write version rows and only publishes do (v1, v2 only — both published). The audit chain is internally consistent. |
REC_VAL_001–REC_VAL_018; Section 4 — REC_AUTH_001–REC_AUTH_005 (Chef's authority scope), REC_AUTH_014 (delete authority); Section 5 — REC_POST_001–REC_POST_008 (state-transition effects).701-sr.spec.ts for the recipe-driven SR auto-create path (see 04-test-scenarios.md Section 5).cost_per_unit and is_used_in_recipe flag underpin every Chef line entry.PUBLISHED recipes on menu sale.