At a Glance
Rule families:PRD_VAL_*validation ·PRD_AUTH_*permission ·PRD_CALC_*calc ·PRD_LIFE_*lifecycle ·PRD_XMOD_*cross-module
Rule count: approximately 65 rules
Audience: Test author + developer — every rule ID is anchored from04-test-scenarios*pages
Status lifecycle: Section 5.1 (where present) carries the Live UI vs BRD discrepancy callouts
This page captures the operational business rules that govern the product master module — the catalogue every other module references. Unlike the transactional modules (PR, PO, GRN, SR, inventory), product is master-data, so the rule set looks different. There is no document doc_status workflow with draft → in_progress → completed transitions, no posting event that fires journal entries, no period-end window that locks the record. The lifecycle is simpler — create → active → deprecated (inactive) → soft-deleted — and the interesting rules are concentrated on validation at create/update (uniqueness, classification consistency, unit / conversion validity), calculation of inherited defaults (deviation tolerances, tax profile cascading from category through item-group to product), authorization (who creates, who deactivates, who can import / export, who deletes), lifecycle guards (cannot deactivate a product on an open PR / PO / SR; cannot delete a product with non-zero inventory or referenced by a published recipe), and cross-module read-side rules (consumers see the active catalogue, snapshots survive product re-coding).
Two structural points colour every rule below and are worth stating up front. First, the catalogue is the dependency root — every transactional table downstream of inventory references the product via product_id. So most lifecycle rules below are guarded against in-use state in downstream modules rather than against state local to the product. Second, the costing method is not on the product — it lives on tb_business_unit.calculation_method (platform schema). Per-product rules about FIFO vs Weighted-Average appearing in carmen/docs are therefore not enforceable at the product layer; the product's standard_cost is the reference cost used by count-variance valuation (enum_physical_count_costing_method = standard) and by recipe baselining, not by the FIFO / WA cost-pick engine. See product/01-data-model § 5 for the full divergence catalogue.
Rule IDs follow PRD_VAL_NNN. Validation runs at create and update on the product master and its supporting tables (tb_unit, tb_unit_conversion, tb_product_category, tb_product_sub_category, tb_product_item_group, tb_product_location, tb_product_tb_vendor).
| Rule ID | Condition | When enforced | Error / behaviour |
|---|---|---|---|
PRD_VAL_001 |
tb_product.code is non-empty, non-whitespace, and unique within (code, name, deleted_at) per product_code_name_u. Application enforces the looser convention of "code unique per live row" as well — duplicate live codes are rejected even if names differ. |
Create / Update | Reject with "Product code <code> already exists. Choose a different code or restore the existing soft-deleted product." 409 Conflict. |
PRD_VAL_002 |
tb_product.name is non-empty and non-whitespace. Together with code and deleted_at, the (code, name, deleted_at) tuple is unique. |
Create / Update | Reject with "Product name is required." 400 Bad Request. |
PRD_VAL_003 |
tb_product.inventory_unit_id references an active, non-soft-deleted tb_unit. The unit cannot be changed once the product has any tb_inventory_transaction_detail or tb_inventory_transaction_cost_layer row (business rule — no schema constraint). |
Create / Update | Reject with "Inventory unit cannot be changed for a product with existing inventory history." 409 Conflict. For create with a deleted/inactive unit: "Selected inventory unit is inactive or deleted." 400 Bad Request. |
PRD_VAL_004 |
tb_product.product_item_group_id (when set) references an active, non-soft-deleted tb_product_item_group. Production catalogue requires the item-group; null is allowed in the schema for import staging but rejected at user-edit submit. |
Create / Update | Reject with "Item group is required (or selected item group is inactive/deleted)." 400 Bad Request. |
PRD_VAL_005 |
tb_product.barcode (when set) is unique within (barcode, deleted_at). No schema constraint — application-enforced per product/01-data-model § 5 item 5. Format selection (UPC, EAN, CODE128) is not validated at the schema; application may check format-by-length. |
Create / Update | Reject with "Barcode <barcode> is already assigned to product <other_code>." 409 Conflict. |
PRD_VAL_006 |
tb_product.price_deviation_limit and tb_product.qty_deviation_limit are in [0, 100] (percentage). Default 0 (no tolerance). Negative values rejected at submit. |
Create / Update | Reject with "Deviation limits must be between 0 and 100 percent." 400 Bad Request. |
PRD_VAL_007 |
tb_product.standard_cost is ≥ 0. Reference cost is non-negative; zero is permitted (e.g. for FOC / promotional items). |
Create / Update | Reject with "Standard cost cannot be negative." 400 Bad Request. |
PRD_VAL_008 |
tb_product_category.code and tb_product_item_group.code are non-empty and unique per productcategory_code_u / productitemgroup_code_name_product_subcategory_u. tb_product_sub_category.code defaults to "" per schema (see product/01-data-model § 5 item 10) — the application UI requires it but the schema permits empty. |
Create / Update | Reject with "Category / sub-category / item-group code is required and must be unique within its scope." 400 / 409 as appropriate. |
PRD_VAL_009 |
tb_product_sub_category.product_category_id references an active parent category; tb_product_item_group.product_subcategory_id references an active parent sub-category. Cycles are impossible by schema construction (no self-reference). |
Create / Update | Reject with "Parent category is inactive or deleted." 400 Bad Request. |
PRD_VAL_010 |
tb_unit_conversion row has from_unit_id ≠ to_unit_id (no self-conversion), from_unit_qty > 0, to_unit_qty > 0, and a unique (product_id, unit_type, from_unit_id, to_unit_id, deleted_at) tuple per unitconversion_product_unit_type_from_unit_to_unit_deletedat_u. |
Create / Update | Reject with "Conversion factor must reference two distinct units and have positive qty on both sides." 400 / 409. |
PRD_VAL_011 |
Bidirectional consistency — for any (product_id, unit_type) group, the set of tb_unit_conversion rows must round-trip: if A → B = k and B → C = m, then A → C (if defined) must equal k × m within tenant tolerance (default ±0.001). Detected at conversion-row save when a transitive path exists. Circular conversions (A → B, B → A with different factors) are rejected. |
Create / Update | Reject with "Conversion factor is inconsistent with existing conversions for this product. Expected <calculated>, got <entered>." 409 Conflict. Application-layer check per product/01-data-model § 5 item 9. |
PRD_VAL_012 |
tb_product_location row has unique (product_id, location_id, deleted_at). min_qty, max_qty, re_order_qty, par_qty are all ≥ 0; max_qty ≥ min_qty (when both non-zero); re_order_qty ≥ min_qty (when both non-zero, recommended but not strict). |
Create / Update | Reject with "Min must not exceed max; reorder qty should be at or above min." 400 Bad Request. Duplicate row: 409 Conflict. |
PRD_VAL_013 |
tb_product_tb_vendor row has unique (vendor_id, product_id, deleted_at) per product_vendor_vendor_product_u. Both FKs must reference active, non-deleted rows. |
Create / Update | Reject duplicate with "Vendor <vendor> is already mapped to product <product>." 409 Conflict. |
PRD_VAL_014 |
tb_product.tax_profile_id (when set) references an active tb_tax_profile. Unset (null) is permitted — the inheritance rule (Section 3, PRD_CALC_002) walks up the classification chain to find a non-null value. |
Create / Update | Reject with "Selected tax profile is inactive or deleted." 400 Bad Request. |
PRD_VAL_015 |
tb_product.product_status_type transitions are restricted to the three enum values per product/01-data-model § 4: active → inactive and active → discontinued permitted (subject to in-use guards PRD_LIFE_*); inactive → active and discontinued → active permitted (re-activation, with discontinued → active discouraged operationally per § 4); inactive ↔ discontinued permitted (recategorising a frozen product). Any value outside the enum is rejected. |
Update | Reject invalid value: "Product status must be 'active', 'inactive', or 'discontinued'." 400 Bad Request. |
PRD_VAL_016 |
tb_product.info, tb_product.dimension, and tb_product.certification are valid JSON. No schema-level shape validation; the application MAY enforce tenant-specific shape rules (e.g. info.allergens is an array of allergen codes from a known list) but the schema only validates JSONB syntax. |
Create / Update | Reject malformed JSON with 400 Bad Request. Shape rule violations are application-layer. |
PRD_VAL_017 |
tb_unit deletion is blocked when the unit is referenced by any non-deleted tb_product.inventory_unit_id, tb_unit_conversion.from_unit_id / .to_unit_id, or any downstream document line's unit columns. Soft-delete sets deleted_at; the in-use guard runs before the soft-delete. |
Delete | Reject with "Unit <unit_name> is in use by <N> products / <M> conversions / <K> document lines and cannot be deleted." 409 Conflict. |
PRD_VAL_018 |
tb_product_category, tb_product_sub_category, and tb_product_item_group deletion is blocked when any descendant is non-deleted or when any product is still classified beneath. |
Delete | Reject with "Category cannot be deleted: <N> sub-categories / <M> item-groups / <K> products still classified beneath it." 409 Conflict. |
Rule IDs follow PRD_CALC_NNN. The product master has no posted ledger and no per-document calculation, but it owns inheritance / cascading default rules and derived-field rules that downstream modules consume.
| Rule ID | Formula |
|---|---|
PRD_CALC_001 (Classification path) |
The product's classification path is tb_product_category.name / tb_product_sub_category.name / tb_product_item_group.name resolved by walking up tb_product.product_item_group_id → tb_product_item_group.product_subcategory_id → tb_product_sub_category.product_category_id. Surfaced as a single readable string for display ("Beverages / Hot / Coffee Beans"). Re-computed on every read; no persisted column. |
PRD_CALC_002 (Tax-profile inheritance) |
The effective tax profile for a product is the first non-null value walking up the classification chain: tb_product.tax_profile_id ⟶ tb_product_item_group.tax_profile_id ⟶ tb_product_sub_category.tax_profile_id ⟶ tb_product_category.tax_profile_id. If all four are null, no tax applies (effective rate 0%, "exempt"). Application reads the chain at every PR/PO/GRN line create to determine the tax profile to apply. |
PRD_CALC_003 (Deviation-tolerance inheritance) |
Same cascade as PRD_CALC_002 for price_deviation_limit and qty_deviation_limit — the first non-zero value walking up product → item-group → sub-category → category. Zero is treated as "not set" (fall through). Floor is 0%, ceiling 100%. Procurement / receiving rules (PR_VAL_*, GRN_VAL_*) read this effective value. |
PRD_CALC_004 (is_used_in_recipe inheritance) |
Same cascade for is_used_in_recipe and is_sold_directly. The product-level value, if set, wins; otherwise inherit from item-group → sub-category → category. (Note: null is fall-through; the default true / false on the column is not the same as inheritance — the application distinguishes "user set false" from "unset".) |
PRD_CALC_005 (Conversion factor) |
The base-to-target conversion factor for a tb_unit_conversion row is to_unit_qty / from_unit_qty. Used to translate a qty on a downstream document line back to the product's base inventory unit (tb_product.inventory_unit_id). Resolution order at document-line save: (a) if the document's unit equals the base unit, factor = 1; (b) else look up tb_unit_conversion with matching (product_id, unit_type, from_unit_id, to_unit_id); (c) if no row exists for the requested unit pair, the document-line save is rejected. |
PRD_CALC_006 (Multi-hop conversion) |
When the requested unit pair has no direct tb_unit_conversion row but a transitive path exists (e.g. KG → G and G → MG defined, no direct KG → MG), the engine composes the factors along the path. Subject to PRD_VAL_011 bidirectional consistency. Cycles or ambiguous paths flag for review. |
PRD_CALC_007 (Effective base unit display precision) |
The display precision for a quantity in the base inventory unit is read from tb_unit.decimal_place (default 2). Underlying storage is Decimal(20, 5). Display rounding is half-up. |
PRD_CALC_008 (Last receiving cost — derived) |
"Last receiving cost" surfaced on the product detail view is derived, not persisted (per product/01-data-model § 5 item 12). Resolved as the cost_per_unit of the most recent inbound tb_inventory_transaction_cost_layer row at (product_id, *) with transaction_type ∈ {good_received_note, adjustment_in, transfer_in}. Source GRN, vendor, date are read by joining back to the source-document via the polymorphic inventory_doc_type / inventory_doc_no. Shown alongside standard_cost for comparison. |
PRD_CALC_009 (Current on-hand — derived, not on product) |
The product detail view's "current on-hand by location" is derived from the inventory cost-layer ledger (see inventory/01-data-model § 5 item 1). Per location: last_period_snapshot.closing_qty + Σ (cost_layer.in_qty − cost_layer.out_qty) since the snapshot, scoped to (product_id, location_id, lot_no). No tb_stock_balance model exists; the product page renders this read-model. |
PRD_CALC_010 (Cost-tier inheritance) |
When a product has no standard_cost set (= 0), the recipe-baseline cost falls back to the most recent inbound cost-layer's cost_per_unit (same as PRD_CALC_008). When even that is absent (product never received), the recipe-baseline cost is 0 — recipes including the product show a cost-incomplete warning. |
Rule IDs follow PRD_AUTH_NNN. The product module's authorization is RBAC-driven with three primary persona roles: Product Administrator (owner — full CRUD on master data), Purchaser (lookup only — reads catalogue for PR / PO composition; can flag stale entries via comments), and Store Keeper (lookup only — scans / reads catalogue during receiving / picking / counting). Audit / System Administrator roles are implicit (read-only audit access; system-level configuration).
| Rule ID | Subject | Right | Constraint |
|---|---|---|---|
PRD_AUTH_001 |
Product Administrator | Create / Update / Soft-delete tb_product rows |
Full CRUD across all product-master tables (tb_product, classification chain, tb_unit, tb_unit_conversion, tb_product_location, tb_product_tb_vendor, comments). Subject to validation rules PRD_VAL_* and lifecycle guards PRD_LIFE_*. Above-threshold edits (e.g. cost changes >tenant-configurable %, deactivation of products with sizeable on-hand) may route for Cost Controller / Finance sign-off in the activity log — but this is typically advisory rather than a hard gate at the product layer. |
PRD_AUTH_002 |
Product Administrator | Configure tb_unit and tb_unit_conversion |
Master-data on units and conversions. Subject to PRD_VAL_017 in-use guard on unit deletion and PRD_VAL_010 / PRD_VAL_011 on conversion validity. |
PRD_AUTH_003 |
Product Administrator | Run import / export of products, categories, units, conversion factors | Bulk-load via Excel / CSV with row-level validation, dry-run preview, downloadable error report. The import job runs under the Product Administrator's user_id (not under a system user); each row insert / update records created_by_id / updated_by_id accordingly. Export of sensitive fields (standard_cost, vendor mapping) is subject to the same role authority — no separate export-permission. |
PRD_AUTH_004 |
Product Administrator | Activate / Deactivate products (product_status_type transitions) |
Subject to in-use guards PRD_LIFE_001 (cannot deactivate with open PR / PO / SR lines), PRD_LIFE_002 (cannot deactivate when referenced by a published recipe — soft-block with override option), and the audit-log requirement that every status transition records actor / timestamp / reason. |
PRD_AUTH_005 |
Purchaser | Read product catalogue, search, filter, view detail | Read-only across all live products. Sees the "active" subset (default filter excludes product_status_type = inactive) but can flip the filter to see inactive products for historical reference. No write authority on the product master; can post comments (tb_product_comment) to flag stale entries or request updates — comment routes back to Product Administrator. |
PRD_AUTH_006 |
Purchaser | Read tb_product_tb_vendor and tb_pricelist joins via the product |
Read-only. The purchaser uses the product–vendor mapping and the pricelist to compose PR / PO lines; no write authority. Vendor-product mapping changes route through the Product Administrator. |
PRD_AUTH_007 |
Store Keeper | Read product catalogue with barcode lookup | Read-only, scoped to the products mapped to their tb_user_location (or unrestricted at most installations — the location scope applies to inventory, not product master). Barcode scan resolves to tb_product via barcode lookup; the mobile app surfaces the resolved product to the count / receiving flow. |
PRD_AUTH_008 |
Store Keeper | Read per-location stock policy (tb_product_location) |
Read-only. Sees min / max / par / reorder for products at the locations they have scope on. Cannot edit — replenishment-policy edits are Inventory Controller authority per inventory/02-business-rules INV_AUTH_004. |
PRD_AUTH_009 |
All transactional roles | Read product master via the picker on every downstream document | Read-only. Pickers filter to product_status_type = active, is_active = true, and deleted_at IS NULL by default. Showing inactive / deleted products is a deliberate "advanced" filter the user must toggle; selecting an inactive product on a new document is rejected per PRD_XMOD_001. |
PRD_AUTH_010 |
System Administrator | Configure RBAC roles for product-module access; manage integration endpoints (e-commerce sync, vendor catalog feed, sustainability database) | System-level configuration. Cannot directly edit product master (separation of duties — Sysadmin configures, Product Administrator operates). Audit log records every RBAC change. |
PRD_AUTH_011 |
Auditor | Read-only access to all product-master tables and audit log (including soft-deleted rows) | Read scope across tb_product, classification chain, unit, conversion, location-mapping, vendor-mapping, and comments. Sees soft-deleted rows for historical traceability. Export of sensitive fields (standard_cost, vendor mapping) requires secondary approval per the parent-module audit pattern. |
PRD_AUTH_012 |
Segregation of duties — Approver of price change ≠ creator | The user who approves a standard_cost change above the tenant SoD threshold MUST NOT be the same user who submitted the change. Approval is captured in the activity log; for installations without a hard approval workflow, the audit log + Finance review is the soft control. |
Enforced at the change-approval action when threshold-driven workflow is enabled. |
Product is master-data — the lifecycle is (none) → active → inactive → soft-deleted with the option to re-activate from inactive. There is no document workflow; the lifecycle rules below are the transition guards that gate moves between states.
Rule IDs follow PRD_LIFE_NNN.
| Rule ID | Transition / Event | Effects / Guards |
|---|---|---|
PRD_LIFE_001 |
(none) → active (create) |
Insert tb_product with product_status_type = active, is_active = true, validation rules PRD_VAL_001–PRD_VAL_016 all pass. Inheritance defaults (PRD_CALC_002–PRD_CALC_004) read at first display; no persisted snapshot. Activity log records { event: 'create', by: <user>, at: <ts> }. |
PRD_LIFE_002 |
active → inactive (deactivate) |
Validation: no open PR / PO / SR line with doc_status ∉ {completed, cancelled, voided} may reference this product_id. Soft-block (Product Administrator may override with reason text in activity log) if the product is referenced by a published recipe — recipe re-costing and theoretical-consumption explosions would break. On override, the application MAY auto-flag the affected recipes for review. On pure deactivation (no open documents, no published recipe references), the update sets product_status_type = inactive; is_active is not changed. Activity log records the transition. |
PRD_LIFE_003 |
inactive → active (re-activate) |
Validation: classification chain is still valid (parent item-group / sub-category / category not deleted). Inheritance values recomputed at next read. Activity log records re-activation; the recipe-review flags from PRD_LIFE_002 are cleared. |
PRD_LIFE_004 |
active | inactive → soft-deleted (delete) |
Hard guards: non-zero current on-hand at any location (derived per PRD_CALC_009) blocks the delete with "Cannot delete product with non-zero on-hand. Drain inventory first via stock-out / write-off, then re-attempt." 409 Conflict. Any non-soft-deleted document line referencing the product (PR / PO / GRN / SR / count / spot-check / credit-note / pricelist / recipe-ingredient) blocks with "Product is referenced by <N> documents and cannot be deleted." 409 Conflict. Any non-soft-deleted recipe ingredient referencing the product blocks similarly. When all guards pass: set deleted_at, deleted_by_id; the (code, name) becomes available for re-use after the soft-delete (unique constraint is scoped to deleted_at). |
PRD_LIFE_005 |
is_active = true → false (hard-disable) |
Distinct from product_status_type = inactive — is_active = false removes the product from all pickers including admin views (per product/01-data-model § 5 item 14). Subject to the same guards as PRD_LIFE_002 plus an additional rule: is_active = false implies product_status_type = inactive (the application enforces this combination; the schema permits but does not require it). |
PRD_LIFE_006 |
Bulk import — create/update | Bulk import via Excel / CSV runs row-level validation per PRD_VAL_* and dry-run preview. Rows that fail validation are surfaced in the downloadable error report; rows that pass are inserted / updated in a single transaction. Partial-success mode is the default (passing rows commit, failing rows are skipped and reported); strict mode (all-or-nothing) is a per-job option. created_by_id / updated_by_id records the Product Administrator running the import. |
PRD_LIFE_007 |
Bulk import — soft-delete | Mass soft-delete via import (column action = 'delete') is permitted but each row is subject to PRD_LIFE_004 guards; failing rows reported individually in the error report. No "force delete" bypass — the in-use guards are absolute at the product layer. |
PRD_LIFE_008 |
Scheduled status change | The application MAY support scheduling a status transition to take effect on a future date (e.g. effective_at = 2026-06-01 deactivate). Implementation is a scheduled job that runs the transition under the originally-requesting user's authority; no schema-level effective_at column on tb_product. Subject to the same lifecycle guards at the moment of execution (so the scheduled deactivation may fail if open documents exist at run time — the scheduled job logs the failure and notifies the requester). |
PRD_LIFE_009 |
Restore from soft-delete | The Product Administrator MAY restore a soft-deleted product (clear deleted_at and deleted_by_id). Validation re-runs at restore — if the (code, name) has been re-used by another live product in the meantime, restore is rejected with "A live product with this code/name already exists. Restore is blocked." 409 Conflict. |
PRD_LIFE_010 |
Classification re-organisation | Moving a product between item-groups (i.e. updating tb_product.product_item_group_id) is permitted but updates the inherited defaults for tax-profile and deviation-tolerance per PRD_CALC_002 / PRD_CALC_003. The change is prospective — open documents that snapshotted the old tax-profile keep their snapshot; new documents read the new inherited value. The activity log records the reclassification. |
State diagram (simple, three-state):
[*] ──create──► active ◄──reactivate── inactive
│ │
├──deactivate──────────────┘
│
└──delete──► soft-deleted ──restore──► active
(soft-deleted is terminal in normal use; restore is an exceptional Product Administrator action)
Rule IDs follow PRD_XMOD_NNN.
| Rule ID | Related module | Rule |
|---|---|---|
PRD_XMOD_001 |
purchase-request / purchase-order / good-receive-note / store-requisition / recipe | Every line that references a product validates the product is active, is_active = true, and non-soft-deleted at the moment of save. Picker filters enforce this default; direct API submission with an inactive / deleted product_id is rejected with "Product <code> is inactive or deleted and cannot be added to new transactions." 400 Bad Request. Existing line items on documents that were created when the product was active remain valid; subsequent edits to those lines may be blocked depending on the source-module rule. |
PRD_XMOD_002 |
inventory | Every tb_inventory_transaction_detail.product_id and tb_inventory_transaction_cost_layer.product_id (no @relation — application-resolved) must resolve to a tb_product row at post time. The inventory module enforces this on transaction post (INV_VAL_002). Soft-deleted products may still appear in historical inventory transactions (the row survives the product's delete because there is no FK cascade); reading the product through the join may yield a tombstone row. |
PRD_XMOD_003 |
costing | Costing reads tb_product.standard_cost for the standard count-costing method (enum_physical_count_costing_method = standard); see costing/01-data-model § 2.6. Costing does not read the costing method from the product — the method lives at tb_business_unit.calculation_method (per product/01-data-model § 5 item 7). The product's role in costing is the reference-cost source and the recipe-baseline source, not the cost-flow selector. |
PRD_XMOD_004 |
vendor-pricelist | tb_pricelist_detail.product_id references the product. Pricelist creation validates the product is active. Vendor mapping (tb_product_tb_vendor) constrains which vendors may appear on pricelists for the product — application convention; not strictly enforced at the schema level. |
PRD_XMOD_005 |
recipe | Recipe ingredient lines reference products via tb_recipe_ingredient.product_id; the picker filters to is_used_in_recipe = true (effective value per PRD_CALC_004). Deactivating a product referenced by a published recipe triggers a soft-block per PRD_LIFE_002 with override + recipe-review flag; deleting is hard-blocked. |
PRD_XMOD_006 |
purchase-request / purchase-order | Procurement lines may reference an order-unit via tb_unit_conversion (with unit_type = order_unit). The qty on the PR / PO line is stored in the document's unit; the system converts to the base inventory unit at receiving (good-receive-note) per PRD_CALC_005. Missing conversion factors block the line save with "No conversion factor defined for unit <X> → <inventory_unit> on product <code>." |
PRD_XMOD_007 |
good-receive-note | GRN's accepted_qty and cost_per_unit populate tb_inventory_transaction_cost_layer.in_qty and .cost_per_unit per inventory/02-business-rules INV_XMOD_001. The product's price_deviation_limit and qty_deviation_limit (effective values per PRD_CALC_003) gate whether the GRN line is auto-approved or requires above-threshold approval. |
PRD_XMOD_008 |
store-requisition | SR lines reference products; the issuing store's tb_product_location.par_qty may drive automated requisition sizing. SR posting consumes from inventory at the product's (location, lot) per the FIFO / WA cost-pick rule (which is read from tb_business_unit.calculation_method, not from the product). |
PRD_XMOD_009 |
physical-count / spot-check | Count documents reference products via product_id. Count-variance valuation reads either tb_product.standard_cost or the cost-layer per enum_physical_count_costing_method per costing/01-data-model § 2.6. The product's classification (category → item-group) drives count-sweep grouping for full physical counts. |
PRD_XMOD_010 |
inventory-adjustment | Manual stock-in / stock-out documents reference products; the in-use guard for product delete (PRD_LIFE_004) sees these documents as references when their doc_status is not terminal. |
PRD_XMOD_011 |
Activity log / audit | Every change to tb_product (create, update, status transition, soft-delete, restore) writes an activity-log entry. Comment-table entries (tb_product_comment) carry user-driven discussion; the activity log is system-driven. Both surfaces are queryable by Auditor (PRD_AUTH_011). |
PRD_XMOD_012 |
External integrations (POS, e-commerce, vendor catalog feed, sustainability DB) | Outbound: the product master is read by POS / e-commerce for menu / catalog publishing (filtered to is_sold_directly = true for POS). Inbound: vendor catalog feeds may auto-create / update tb_product_tb_vendor rows under a system user; sustainability DBs may update tb_product.certification JSON. Integration endpoints are configured by Sysadmin per PRD_AUTH_010; the integration's writes are subject to the same validation rules PRD_VAL_*. |
../carmen/docs/product-management/PROD-PRD.md — primary PRD; validation, calculation, and lifecycle rules above are realigned to the canonical Prisma model (no variant table, no typed attribute table, no media table, no per-product costing method, three-level classification). See product/01-data-model § 5 for the divergence catalogue.../carmen/docs/product-management/product-master-prd.md — product-master PRD describing UI structure; cross-referenced for PRD_CALC_008 (last-receiving-cost derivation) and the "Latest Purchase" tab pattern.../carmen/docs/product-management/PROD-API-Endpoints-Products.md / PROD-API-Endpoints-Categories.md / PROD-API-Endpoints-Units.md / PROD-API-Endpoints-Locations.md / PROD-API-Endpoints-Import-Export.md — REST surface for the rules above; useful for cross-reference but not the source of authority.enum_product_status_type (three values: active, inactive, discontinued), the classification chain shape (three levels, not five), and the divergence catalogue that underpins Section 5 and 6 above.../carmen-turborepo-backend-v2/apps/ — the product service module is the implementation hook for these rules (uniqueness check, inheritance resolver, conversion-factor consistency validator, in-use guard for delete, import/export job).PRD_XMOD_002), costing (PRD_XMOD_003), vendor-pricelist (PRD_XMOD_004), recipe (PRD_XMOD_005), purchase-request / purchase-order (PRD_XMOD_006), good-receive-note (PRD_XMOD_007), store-requisition (PRD_XMOD_008), physical-count / spot-check (PRD_XMOD_009), inventory-adjustment (PRD_XMOD_010).