Skip to content

Production

Location: dg_smart_pos_api/modules/Production. Owns recipes (a versioned bill of materials for one finished stock item) and production orders (a single posted manufacturing run: consume these ingredient quantities, produce that much of the finished item). Stock is actually moved through Inventory’s InventoryAdjustmentService and StockGuard, not owned by this module; Production only decides what to consume and produce and hands that off. Jobs’ JobType has a flavor value literally named production, but that is a UI/reporting label only (see Jobs’s guardrail against branching on it) and has no code relationship to this module at all, despite the name overlap.

  • ProductionRecipe - store_id, finished_stock_id, version, is_active. Recipes are append-only: saving a new recipe for a finished item never edits the existing row, it deactivates it and inserts a new one at version = previous max + 1. Only one recipe per (store_id, finished_stock_id) is ever is_active at a time.
  • ProductionRecipeIngredient - production_recipe_id, stock_id, quantity_per_unit (nullable; a recipe line can specify an ingredient without a fixed ratio yet).
  • ProductionOrder - store_id, finished_stock_id, recipe_id (nullable; a run doesn’t strictly need a saved recipe), produced_quantity, remarks, produced_by. Immutable once created: no update or delete endpoint exists for it.
  • ProductionOrderIngredient - production_order_id, stock_id, quantity. A frozen snapshot of what that specific run actually consumed, independent of whatever the recipe says today.

ProductionOrderController::store -> ProductionService::create. Validates the finished item exists, is configured for the store (store_stocks row exists), and has use_production enabled on that row (set automatically the first time a recipe is saved for it, see below). If a recipe_id is given it must belong to the same store and finished item. Ingredient stock availability is not checked up front; it’s deliberately deferred to StockGuard::consuming(), which locks the ingredient stock positions inside the DB transaction immediately before decrementing them, closing the race window where two concurrent production runs could each read “enough” of the same ingredient and both consume it. On success it writes one StockActions::ProductionConsume adjustment per ingredient (decrement) and one StockActions::ProductionOutput adjustment for the finished item (increment) via InventoryAdjustmentService::adjustStocks(), each referencing the new ProductionOrder as reference_id/reference_type.

ProductionRecipeController::index lists a store’s active recipes; byFinishedStock returns the single active recipe for one finished item (or none). upsertByFinishedStock -> ProductionRecipeService::upsertVersioned is the only write path: it deactivates the current active recipe for that (store, finished_stock_id) pair, creates a new one at the next version number with the submitted ingredient lines, and as a side effect flips use_production = true on that item’s store_stocks row, since “someone saved a recipe for this item” is treated as consent to produce it.

All under auth:sanctum and system.config:enable_production_module, prefix production/stores/{store}, registered via modules/Production/Routes/production.php (required once from routes/tenant.php):

Method Path Controller method
GET orders ProductionOrderController::index
POST orders ProductionOrderController::store
GET orders/{order} ProductionOrderController::show
GET recipes ProductionRecipeController::index
GET recipes/by-finished-stock/{stock} ProductionRecipeController::byFinishedStock
PUT recipes/by-finished-stock/{stock} ProductionRecipeController::upsertByFinishedStock

There is no update or delete route for either ProductionOrder or ProductionRecipe; a recipe is “edited” by upserting a new version, and an order is never edited once posted.

produced_quantity and every ingredient quantity/quantity_per_unit must be a whole number when the store’s allow_decimal_stock system config is off, checked in ProductionOrderController::store before it calls the service.

  • Inventory - StockGuard/InventoryAdjustmentService actually move the stock; recipes and orders reference NewStock
  • Jobs - shares no code, but its JobType.flavor enum includes a production value purely as a label; don’t conflate the two
  • Purchasing