Saltar al contenido principal

Pack Operations Troubleshooting Runbook

Operational runbook for diagnosing pack/unpack and pack-family link failures in:

  • apps/backend/src/pack-operations/
  • apps/backend/src/product-pack-links/

Use this with the domain overview in Pack Hierarchy.


Scope

Primary codepaths:

  • pack-operations/application/pack-operations.service.ts
  • pack-operations/interfaces/pack-operations.controller.ts
  • pack-operations/infrastructure/pack-operations.repository.ts
  • product-pack-links/application/product-pack-links.service.ts
  • product-pack-links/interfaces/product-pack-links.controller.ts
  • production-runs/application/production-runs.service.ts (repackaging completion pipeline)

Pack/unpack is application orchestration over an existing production-run stock pipeline. Direction (pack vs unpack) is derived from base-unit factors — clients never choose it.


1) Symptom triage

SymptomLikely layer
400 quantity must be greater than 0Client request body
400 fromVariantId and toVariantId must be differentClient selected the same pack level twice
400 Both fromVariantId and toVariantId must be levels of this product's pack familyVariants are not linked in one linear family
400 Insufficient available stockAvailable-stock guard (on-hand minus reserved/outgoing)
400 with conversion/base-unit messageNon-integer or invalid factor relationship between levels
409 A pack/unpack operation with this idempotency key is already in progressDuplicate in-flight request with same idempotencyKey
Idempotent retry returns original resultExpected: completed pack_operation row found
404 No pack link found between the specified parent and child levelsinsert-between target link missing
409 The middle variant is already part of this/another pack familyinsert-between middle variant already linked
Family stock rollup looks wrongCaller summed raw on-hand across levels instead of getFamilyStockRollup

2) Pack/unpack request checklist

Expected request (POST /pack-operations/pack-unpack, PackUnpackDTO):

{
"businessId": "<uuid>",
"productId": "<uuid>",
"locationId": "<uuid>",
"fromVariantId": "<source-level-uuid>",
"toVariantId": "<target-level-uuid>",
"quantity": 1,
"createdBy": "<user-uuid>",
"idempotencyKey": "<client-unique-key>",
"lotNumber": "LOT-2026-001",
"expirationDate": "2027-01-01T00:00:00.000Z"
}

Validate before deeper debugging:

  1. Both variants belong to the same product's resolved pack family (GET /product-pack-links/products/:productId).
  2. quantity is positive and refers to units of fromVariantId, not the target level.
  3. idempotencyKey is unique per logical operator action (reuse only for retries of the same action).
  4. Location has available stock on the source variant after subtracting reserved and in-transit-outgoing quantities.

Direction is computed in resolvePackDirection from baseUnitsPerVariant:

  • consuming a larger pack into smaller levels → unpack
  • consuming smaller levels into a larger pack → pack

3) Idempotency recovery

PackOperationsService.packUnpack inserts a pending pack_operation row keyed by (businessId, idempotencyKey).

StateBehavior
First requestInserts pending row, runs conversion, marks completed with productionRunId
Retry after successReturns the original result reconstructed from the completed production run
Retry while in progress409 Conflict — ask the client to retry shortly
Validation/runtime failurePending row is deleted so a corrected retry with the same key is not permanently blocked

If a client is stuck on 409:

  1. Wait briefly and retry with the same key (covers in-flight completion).
  2. Confirm whether a repackaging production run was created for that attempt.
  3. Only generate a new idempotencyKey when starting a new logical conversion, never to force a second apply of the same action.

4) Insufficient stock

The available-stock guard uses:

available = onHand - reservedStock - inTransitOutgoing

Common false positives:

  • stock exists on a sibling pack level (for example units exist but the case level is empty)
  • reserved stock from open sales/orders reduces availability
  • outgoing transfer quantity is committed and not available to convert
  • inventory row for the variant is missing at that location (treated as available 0)

Checks:

  1. Inspect per-level stock with GET /product-pack-links/products/:productId/stock-rollup.
  2. Confirm the client is converting from the level that actually has available quantity.
  3. Unpack a larger pack first when only the parent level has stock.

Use POST /product-pack-links for a new adjacent link, PATCH to change unitsPerParent, and DELETE to remove a link (may split the family).

Family validation requires a strict linear chain. Branching structures (one parent with two children, or cycles) are rejected by resolveChainOrThrow.

Insert middle level

Use POST /product-pack-links/insert-between when splitting an existing parent→child link into parent→middle→child.

ResponseMeaningFix
404No link between parentVariantId and childVariantIdCreate the adjacent link first, or correct the variant IDs
409 middle already in this familyMiddle variant already appears in an existing linkChoose a different middle variant
409 middle in another familyMiddle variant linked under a different product/familyUnlink it first or use another variant
400 factor does not divide cleanly into a positive middle→child factorunitsPerParent cannot derive a valid middle→child factor from the existing linkAdjust unitsPerParent so oldUnits / unitsPerParent is a positive finite factor

When to use which API:

GoalAPI
Connect two currently unlinked adjacent levelsPOST /product-pack-links
Insert a new level into an existing adjacent linkPOST /product-pack-links/insert-between
Change how many children make one parentPATCH /product-pack-links/:id
Convert on-hand stock between levelsPOST /pack-operations/pack-unpack

6) Cost / WAC surprises

Pack/unpack reuses the production-run completion path:

  1. Consume source stock at the source WAC (cost / GREATEST(quantity, 1)).
  2. Produce target stock at a converted unit cost that conserves total inventory value.
  3. Round only after quantity × unitCost in the shared production-run pipeline.

If totals drift after many conversions:

  1. Confirm both sides used the same location.
  2. Confirm no concurrent manual adjustments changed source WAC mid-flight.
  3. Prefer diagnosing through the created repackaging production run rather than inventing a parallel cost formula.

Do not sum raw on-hand quantities across pack levels for valuation. Use getFamilyStockRollup (or an equivalent multiply-once-by-base_units_per_variant rule) when reporting family totals.