At a Glance
Persona: Product Administrator · Module: product · Scenarios: ~59
Categories: Happy Path · Permission · Validation · Lifecycle · Bulk · Edge Case
E2E coverage: maps to100-product.spec.ts,101-product-category.spec.tsin../carmen-inventory-frontend-e2e/(CRUD surface largely manual / planned)
This page captures the test scenarios the Product Administrator persona directly drives in the product module. They own full CRUD on tb_product, classification (tb_product_category / tb_product_sub_category / tb_product_item_group), units (tb_unit), conversions (tb_unit_conversion), location mapping (tb_product_location), vendor mapping (tb_product_tb_vendor), and run bulk import / export. Their catalogue-side ownership begins at create and ends at soft-delete (or restore, exceptionally). Scenarios are grouped into happy paths (single create, classification CRUD, unit and conversion definition, location mapping, vendor mapping, bulk import success), RBAC (Product Administrator scope vs Cost Controller / Inventory Controller scope, segregation of duties on standard-cost changes), validation (negative tests against PRD_VAL_001–PRD_VAL_018), lifecycle (deactivate / re-activate / soft-delete / restore with in-use guards PRD_LIFE_*), bulk operations (import dry-run, error report, partial-success, strict-commit), and edge cases (concurrent edits, inheritance overrides, classification re-org, restore-vs-recreate, hard-disable). Cross-persona handoffs that pivot off the Product Administrator (Scenarios 1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14, 16 in the parent overview) live in 04-test-scenarios.md, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| PA-HP-01 | Create a single new product end-to-end | Product Administrator pa@blueledgers.com logged in; classification chain exists (Beverages → Hot → Coffee Beans); base unit KG exists; tax profile set on Beverages category. |
1. Products → New. 2. Enter code = COF-001, name = Arabica Beans Premium, local_name, description. 3. Pick inventory_unit_id = KG. 4. Assign item group "Coffee Beans"; verify inherited tax profile shows in greyed form. 5. Set is_used_in_recipe = true, is_sold_directly = false. 6. Set standard_cost = ฿450.00, price_deviation_limit = 10, qty_deviation_limit = 5. 7. Save. |
tb_product row inserted with product_status_type = active, is_active = true. Activity log: { event: 'create', by: pa@blueledgers.com, at: <ts> }. Validation rules PRD_VAL_001–PRD_VAL_016 all pass. Product immediately visible on PR / PO / GRN / SR pickers per PRD_AUTH_009. Maps to parent Scenario 1. |
| PA-HP-02 | Define order-unit conversion for the new product | PA-HP-01 state; vendor invoices in CASE; need 1 CASE = 5 KG. |
1. Open product COF-001 detail → Conversions tab. 2. New → unit_type = order_unit, from_unit = CASE, from_unit_qty = 1, to_unit = KG, to_unit_qty = 5, is_default = true. 3. Save. |
tb_unit_conversion row inserted with the values. unit_conversion_product_unit_type_from_unit_to_unit_deletedat_u unique constraint enforced. Default flag means the PR / PO picker surfaces CASE first for this product. Bidirectional consistency check passes (no transitive conflicts). |
| PA-HP-03 | Define location mapping for the new product | PA-HP-01 state; two stores LOC-A (main warehouse) and LOC-B (outlet). |
1. Open product detail → Location Assignment tab. 2. New mapping for LOC-A: min_qty = 5, max_qty = 50, par_qty = 20, re_order_qty = 10. 3. Repeat for LOC-B: min_qty = 2, max_qty = 15, par_qty = 8, re_order_qty = 5. 4. Save. |
Two tb_product_location rows inserted (product_id × location_id unique per PRD_VAL_012). Constraint max_qty ≥ min_qty honored. Product can now hold balance at the mapped locations; replenishment alerts will fire when on-hand crosses thresholds per inventory. Note: per PRD_AUTH_004 / INV_AUTH_004, the Product Administrator creates the mapping (enables the location) but the Inventory Controller typically fine-tunes the numeric values. |
| PA-HP-04 | Define vendor mapping with vendor-product-code cross-reference | PA-HP-01 state; vendor "Coffee Imports Co." has the product in their catalog under their own code CIC-AR-PREM. |
1. Open product detail → Vendor Mapping tab. 2. New → pick vendor "Coffee Imports Co.", enter vendor_product_code = CIC-AR-PREM, vendor_product_name = "Arabica Premium - Roasted". 3. Save. |
tb_product_tb_vendor row inserted; unique per (vendor_id, product_id) per PRD_VAL_013. Product now appears on vendor pricelist pickers and on PO line pickers when the vendor scope is set. Maps to parent Scenario 16. |
| PA-HP-05 | Create a category with cascading defaults | Need new category "Pastries"; should inherit tax-profile TAX-VAT-7, set deviation tolerances 12% / 5%, mark as recipe-eligible. |
1. Categories → New → enter code = PAST, name = Pastries, description. 2. Set tax_profile_id = TAX-VAT-7, price_deviation_limit = 12, qty_deviation_limit = 5, is_used_in_recipe = true. 3. Save. |
tb_product_category row inserted; productcategory_code_u and productcategory_code_name_u unique constraints honored. Future products / sub-categories beneath this category inherit the tax profile and tolerances per PRD_CALC_002 / PRD_CALC_003. |
| PA-HP-06 | Create a unit with conversion to existing units | Need new unit PUNNET (for fresh produce); existing unit KG. |
1. Units → New → name = PUNNET, description = "Plastic clamshell — 250g standard", decimal_place = 0. 2. Save. 3. (Per-product) On product P-BERRY, define conversion 1 PUNNET = 0.25 KG with unit_type = order_unit. |
tb_unit row inserted; unit_name_deletedat_u honored. tb_unit_conversion row inserted with the conversion. Future products may also reuse PUNNET by defining their own product-specific conversion. |
| PA-HP-07 | Bulk import — dry-run with errors, fix, strict-commit | Excel file with 500 product rows; mix of valid and invalid (12 rows have duplicate code, 8 rows have invalid inventory_unit_id, 5 rows have empty name). |
1. Products → Import → upload file → run dry-run. 2. Download error report — lists 25 failing rows with per-row reason. 3. Open source file, fix the 25 errors (rename codes, fix unit references, supply names). 4. Re-upload → dry-run → all rows pass. 5. Run strict-mode commit. | All 500 rows inserted in one transaction. created_by_id is the Product Administrator. Activity log records the import job with row counts and timestamp. Maps to parent Scenario 2. |
| PA-HP-08 | Bulk import — partial-success mode | Same as PA-HP-07 but the Administrator opts for partial-success on a tight deadline. | 1. Products → Import → upload file → partial-success mode → commit. | 475 rows commit (passing rows); 25 rows reported in the error file with per-row failure reason. Administrator iterates on the failing 25 separately. Activity log records the partial-success commit. Maps to parent Scenario 3. |
| PA-HP-09 | Standard-cost edit below SoD threshold — immediate commit | Active product with standard_cost = ฿100; SoD threshold 15%; new cost ฿110 (10% increase — below threshold). |
1. Open product → edit standard_cost = ฿110. 2. Save. |
Edit commits immediately. Activity log: { event: 'standard_cost_changed', from: ฿100, to: ฿110, by: pa@blueledgers.com }. No SoD routing per PRD_AUTH_012 because below threshold. |
| PA-HP-10 | Reply to and resolve an inbound comment from Purchaser | Open tb_product_comment thread on a product: "Standard cost out of date — vendor quote ฿140 vs master ฿100." |
1. Navigate to product → Comments tab. 2. Read thread, review attached vendor quote. 3. Update standard_cost = ฿140 (subject to SoD routing if above threshold). 4. Reply on the thread: "Standard cost updated based on new vendor pricing." 5. Mark thread as resolved (tenant convention). |
Standard cost updated; comment thread updated with reply; thread state = resolved. Maps to parent Scenario 17. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| PA-PERM-01 | Product Administrator CRUD on product master | Allow. Per PRD_AUTH_001, full CRUD on tb_product rows. Subject to validation rules PRD_VAL_* and lifecycle guards PRD_LIFE_*. |
| PA-PERM-02 | Product Administrator configures tb_unit and tb_unit_conversion |
Allow. Per PRD_AUTH_002. Subject to PRD_VAL_017 in-use guard on unit deletion. |
| PA-PERM-03 | Product Administrator runs import / export | Allow. Per PRD_AUTH_003. The import job runs under the Administrator's user_id; export of sensitive fields (standard_cost, vendor mapping) is permitted to this role. |
| PA-PERM-04 | Product Administrator approves their own standard-cost change above SoD threshold | Deny — second signature required. Per PRD_AUTH_012, the approver of a standard_cost change above the tenant SoD threshold MUST NOT be the same user who submitted it. Submission stages the change; approval requires Cost Controller or Finance. Direct API submission attempting to bypass returns "Self-approval of standard-cost change above SoD threshold is not permitted." |
| PA-PERM-05 | Product Administrator edits tb_product_location numeric policy (min / max / par / reorder) |
Allowed at schema level — but conventionally Inventory-Controller authority. Per PRD_AUTH_004 Product Administrator can create the mapping row to enable a product at a location; the numeric values (min_qty / max_qty / par_qty / re_order_qty) are conventionally maintained by Inventory Controller per inventory/02-business-rules INV_AUTH_004. Tenant workflow may enforce this with a UI gate; the schema permits PA to write the values. |
| PA-PERM-06 | Product Administrator attempts to approve a PR / PO / GRN line | Deny — out of scope. Product Administrator's authority is on the product master, not on transactional documents. Approval of PR / PO / GRN / SR requires the respective module's role (Purchaser / Approver, Receiver, etc.). |
| PA-PERM-07 | Product Administrator attempts to lock an accounting period | Deny — Finance Manager required. Per inventory/02-business-rules INV_AUTH_006, period lock is Finance Manager authority. Product Administrator has no period-management authority. |
| PA-PERM-08 | Auditor read-only access to soft-deleted products | Allow read-only. Per PRD_AUTH_011, Auditor sees soft-deleted rows for traceability. No write authority. |
| PA-PERM-09 | System Administrator configures RBAC for product module | Allow. Per PRD_AUTH_010, Sysadmin configures roles and integration endpoints. Cannot directly edit product master (separation of duties). |
| PA-PERM-10 | Product Administrator views audit log | Allow. Per PRD_XMOD_011, activity log is queryable by Product Administrator and by Auditor. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| PA-VAL-01 | Duplicate product code (PRD_VAL_001) |
Create new product with code = COF-001 while another live product with that code exists. |
Reject at submit — server returns "Product code COF-001 already exists. Choose a different code or restore the existing soft-deleted product." 409 Conflict. Product Administrator picks a different code or investigates the soft-deleted product. |
| PA-VAL-02 | Empty product name (PRD_VAL_002) |
Create new product with empty / whitespace name. |
Reject at submit — "Product name is required." 400 Bad Request. |
| PA-VAL-03 | Change inventory unit on product with history (PRD_VAL_003) |
Existing product with 10 GRN receipts on KG; attempt to change inventory_unit_id to LB. |
Reject at submit — "Inventory unit cannot be changed for a product with existing inventory history." 409 Conflict. The Administrator must (rarely) drain inventory, soft-delete, and recreate under a different code with the new unit. |
| PA-VAL-04 | Inactive / deleted classification reference (PRD_VAL_004, PRD_VAL_009) |
Create new product picking a soft-deleted tb_product_item_group_id. |
Reject at submit — "Item group is required (or selected item group is inactive/deleted)." 400 Bad Request. Picker normally filters out inactive / deleted nodes; direct API submission re-checks. |
| PA-VAL-05 | Barcode collision (PRD_VAL_005) |
Existing product has barcode = '8851234567890'; create a new product with the same barcode. |
Reject at submit — "Barcode 8851234567890 is already assigned to product <other_code>." 409 Conflict. Application-enforced (no schema @unique). |
| PA-VAL-06 | Out-of-range deviation tolerance (PRD_VAL_006) |
Set price_deviation_limit = 150 (above 100%). |
Reject at submit — "Deviation limits must be between 0 and 100 percent." 400 Bad Request. |
| PA-VAL-07 | Negative standard cost (PRD_VAL_007) |
Set standard_cost = -50. |
Reject at submit — "Standard cost cannot be negative." 400 Bad Request. |
| PA-VAL-08 | Self-conversion in unit-conversion row (PRD_VAL_010) |
Define tb_unit_conversion with from_unit_id = KG, to_unit_id = KG. |
Reject at submit — "Conversion factor must reference two distinct units and have positive qty on both sides." 400 Bad Request. |
| PA-VAL-09 | Bidirectional inconsistency in conversion (PRD_VAL_011) |
Existing conversion 1 KG = 1000 G; attempt to add 1 KG = 1100 G for the same product / unit_type. |
Reject at submit — "Conversion factor is inconsistent with existing conversions for this product. Expected 1000, got 1100." 409 Conflict. Application-layer check across rows. |
| PA-VAL-10 | Min > max on location policy (PRD_VAL_012) |
Set min_qty = 50, max_qty = 20 on a tb_product_location row. |
Reject at submit — "Min must not exceed max; reorder qty should be at or above min." 400 Bad Request. |
| PA-VAL-11 | Duplicate vendor mapping (PRD_VAL_013) |
Existing tb_product_tb_vendor row for (vendor_id = V1, product_id = P1); attempt to add another. |
Reject at submit — "Vendor V1 is already mapped to product P1." 409 Conflict. |
| PA-VAL-12 | Inactive tax profile (PRD_VAL_014) |
Set tb_product.tax_profile_id to a soft-deleted tb_tax_profile. |
Reject at submit — "Selected tax profile is inactive or deleted." 400 Bad Request. Picker normally filters. |
| PA-VAL-13 | Invalid status transition (PRD_VAL_015) |
Direct API call attempts product_status_type = 'archived' (any value outside the three-member enum). |
Reject at submit — "Product status must be 'active', 'inactive', or 'discontinued'." 400 Bad Request. The three enum values are canonical per product/01-data-model § 4. (discontinued was added in the May 2026 enum-cleanup pass and is now a valid value — earlier wiki revisions that called it out as invalid are out of date.) |
| PA-VAL-14 | Malformed JSON in info / dimension / certification (PRD_VAL_016) |
API submission with malformed JSON in info. |
Reject at submit — 400 Bad Request (JSONB syntax error). Shape rule violations are application-layer. |
| PA-VAL-15 | Delete in-use unit (PRD_VAL_017) |
Attempt to soft-delete tb_unit "POUND" referenced by 12 products. |
Reject at submit — "Unit POUND is in use by 12 products / 30 conversions / N document lines and cannot be deleted." 409 Conflict. Maps to parent Scenario 14. |
| PA-VAL-16 | Delete category with children (PRD_VAL_018) |
Attempt to soft-delete tb_product_category "Beverages" with 5 sub-categories beneath. |
Reject at submit — "Category cannot be deleted: 5 sub-categories / N item-groups / M products still classified beneath it." 409 Conflict. |
| # | Scenario | Trigger | Expected behaviour |
|---|---|---|---|
| PA-LIFE-01 | Deactivate active product with no in-use blockers | Active product; no open PR / PO / SR line referencing it; no recipe references. | Allow. Per PRD_LIFE_002. Sets product_status_type = inactive; is_active unchanged. Picker exclusion takes effect immediately. Activity log records the transition. |
| PA-LIFE-02 | Deactivate blocked by open PR line | Active product; one tb_purchase_request_detail line at doc_status = in_progress referencing the product. |
Reject per PRD_LIFE_002 with "Product is referenced by 1 open PR line and cannot be deactivated." Maps to parent Scenario 5. Administrator coordinates with Purchaser; after PR completes / cancels, retry succeeds. |
| PA-LIFE-03 | Deactivate soft-blocked by published recipe — override | Active product; one published recipe references it as ingredient. | Soft-block per PRD_LIFE_002 with override option. Administrator enters reason text and confirms; deactivation commits. Affected recipe auto-flagged for review (recipe module notification). Activity log records override + reason. Maps to parent Scenario 6. |
| PA-LIFE-04 | Re-activate inactive product | Product at product_status_type = inactive; classification chain still valid. |
Allow per PRD_LIFE_003. Sets product_status_type = active; inheritance values recomputed at next read; recipe-review flags cleared. |
| PA-LIFE-05 | Soft-delete with zero on-hand and no document references | Inactive product; current on-hand = 0 at all locations (derived); no non-soft-deleted document line; no recipe ingredient reference. | Allow per PRD_LIFE_004. Sets deleted_at, deleted_by_id. The (code, name) becomes re-usable. |
| PA-LIFE-06 | Soft-delete blocked by non-zero on-hand | Inactive product; on-hand = 25 at LOC-A (derived per PRD_CALC_009). |
Reject per PRD_LIFE_004 with "Cannot delete product with non-zero on-hand at LOC-A: 25 units. Drain inventory first via stock-out / write-off, then re-attempt." Maps to parent Scenario 7. Administrator coordinates drain via inventory stock-out, then retries. |
| PA-LIFE-07 | Soft-delete blocked by completed-document reference | Inactive product; one completed tb_good_received_note_detail line referencing the product. |
Reject per PRD_LIFE_004 with "Product is referenced by 1 document(s) and cannot be deleted." Completed history retains the reference; the product cannot be deleted while history exists. The (code, name) is therefore effectively permanent for any product that has ever been transacted on. |
| PA-LIFE-08 | Hard-disable (is_active = false) |
Compliance-mandated removal; supplier delisted. | Allow per PRD_LIFE_005. Sets is_active = false; implicitly sets product_status_type = inactive. Product hidden from all pickers (including admin views). Reversible. Maps to parent Scenario 13. |
| PA-LIFE-09 | Restore soft-deleted product — code unique | Soft-deleted product with code = 'BVR-001'; no live product with that code. |
Allow per PRD_LIFE_009. Clears deleted_at / deleted_by_id; product returns to active state. |
| PA-LIFE-10 | Restore blocked by code re-use | Soft-deleted BVR-001; another product has since been created with code = BVR-001. |
Reject per PRD_LIFE_009 with "A live product with code BVR-001 already exists. Restore is blocked." Maps to parent Scenario 12. |
| PA-LIFE-11 | Classification re-organisation — prospective propagation | Move sub-category to a different parent category; 50 products beneath. | Allow per PRD_LIFE_010. Move commits; inherited defaults (tax profile, deviation tolerances) propagate to new reads. Open document snapshots untouched. Maps to parent Scenario 11. |
| PA-LIFE-12 | Bulk import — soft-delete via import | Import file with action = 'delete' column for 10 rows; 3 rows have non-zero on-hand. |
Per PRD_LIFE_007: 7 rows soft-deleted; 3 rows reported in error file with "Cannot delete product with non-zero on-hand". No "force delete" override available. |
| PA-LIFE-13 | Scheduled status change | Schedule deactivate to fire on a future date for product P1; product becomes referenced by an open PR before the date. |
Per PRD_LIFE_008: scheduled job runs at the date; deactivation fails with in-use guard; failure logged, requester notified. Product remains active. |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| PA-EDGE-01 | Concurrent edits to the same product header | Two Administrators open product P1 at the same time; both edit description; A saves at T; B saves at T+5s with stale data. |
The product's audit columns include updated_at / updated_by_id but no version field (per product/01-data-model — no optimistic-concurrency column on tb_product). Last-write-wins; B's edit overwrites A's description field. Best practice: tenant convention enforces collaborative review via comments before edit; the audit log preserves both changes. |
| PA-EDGE-02 | Inheritance override at product level | Category has tax_profile_id = TAX-7; product overrides with tax_profile_id = TAX-0 (exempt). |
Effective tax profile per PRD_CALC_002 reads product-level first; result is TAX-0. Activity log records the override. |
| PA-EDGE-03 | All-classification-level inherited values are null | Product has no tax_profile_id; item-group has none; sub-category has none; category has none. |
Effective tax profile is null per PRD_CALC_002; effective rate is 0% ("exempt"). Procurement / receiving lines compute tax at 0%. |
| PA-EDGE-04 | Multi-hop conversion via transitive path | Conversions defined: 1 BAG = 5 KG and 1 KG = 1000 G; no direct BAG → G row. PR line entered in BAG. |
Per PRD_CALC_006: engine composes 1 BAG = 5 KG × 1000 G/KG = 5000 G. Conversion succeeds. If a direct BAG → G row exists with a different factor, the inconsistency is flagged per PRD_VAL_011. |
| PA-EDGE-05 | Decimal precision on unit conversion | from_unit_qty = 1, to_unit_qty = 0.12345 (5dp). |
Stored at 5dp on Decimal(20, 5). Effective conversion factor 0.12345. Display rounded per tb_unit.decimal_place (typically 2dp → 0.12); intermediate computation carries 5dp. |
| PA-EDGE-06 | Hard-disable with product_status_type still active |
Direct API sets is_active = false but leaves product_status_type = active. |
Application convention enforces is_active = false → product_status_type = inactive per PRD_LIFE_005. The combination is permitted at the schema level but the application normalises on read (product effectively inactive). Best practice: backend service hooks the column-pair to keep them consistent. |
| PA-EDGE-07 | Sub-category with empty string code (schema quirk) | Sub-category code = "" per product/01-data-model § 5 item 10. |
Permitted at the schema level (@default("")). Application UI requires a non-empty code on create; legacy / migrated rows may have empty codes. The productsubcategory_code_name_u unique constraint scopes uniqueness on (code, name, deleted_at) so two sub-categories with empty code are permitted if their names differ. |
| PA-EDGE-08 | Restore of soft-deleted product with classification chain partially deleted | Soft-deleted product P1; its parent item-group has since been soft-deleted (no other products referenced it, so the delete succeeded). | Restore per PRD_LIFE_009 validates the classification chain. If the parent item-group is still soft-deleted, restore is rejected with "Parent item group is inactive or deleted." Administrator must restore the item-group first, then the product. |
| PA-EDGE-09 | Bulk export with sensitive fields | Administrator exports the catalogue including standard_cost and vendor mapping for an audit submission. |
Per PRD_AUTH_003, the Administrator role permits export of sensitive fields. The export job logs the request; audit log records { event: 'export', fields: [...], by: pa@blueledgers.com, at: <ts> }. Auditor's parallel export per PRD_AUTH_011 requires secondary approval per tenant convention. |
| PA-EDGE-10 | Activity log volume on a frequently-edited product | Single product receives 50 standard-cost updates / month from supplier feed integration. | All 50 activity-log entries are retained; no auto-pruning of the activity log within the standard data-retention window. Audit query per PRD_AUTH_011 returns the full history. Tenant may configure activity-log archival per retention policy. |
PRD_VAL_001 (code unique), PRD_VAL_002 (name required), PRD_VAL_003 (inventory unit immutable), PRD_VAL_004 / PRD_VAL_009 (classification reference), PRD_VAL_005 (barcode unique), PRD_VAL_006 (deviation range), PRD_VAL_007 (non-negative cost), PRD_VAL_010 / PRD_VAL_011 (conversion validity), PRD_VAL_012 (location policy), PRD_VAL_013 (vendor unique), PRD_VAL_014 (tax profile), PRD_VAL_015 (status enum), PRD_VAL_017 / PRD_VAL_018 (in-use guards); authorization PRD_AUTH_001–PRD_AUTH_004, PRD_AUTH_010, PRD_AUTH_011, PRD_AUTH_012; lifecycle PRD_LIFE_001–PRD_LIFE_010; calculation rules PRD_CALC_001–PRD_CALC_010 (inheritance, conversion).../carmen-inventory-frontend-e2e/tests/101-product-category.spec.ts — partial coverage of category view / CRUD. Most scenarios above are manual / planned — there is no direct 100-product.spec.ts. Happy-path fixture aligns with purchase@blueledgers.com (multi-role — Product Manager / Sysadmin); permission-denial uses requestor@blueledgers.com.tb_product_location policy fine-tuning is Inventory Controller authority per INV_AUTH_004; on-hand derivation per PRD_CALC_009.standard_cost source; SoD routing for standard-cost changes per PRD_AUTH_012.PRD_LIFE_002.tb_product_tb_vendor makes products visible on pricelist pickers.