Plan: task-0206 — size variants with inventory under one stock number

Recommendation

Add an opt-in variant set beneath an existing parent stock_no. The first configured axis is Size. Inventory, allocations, receiving, selling, counts, and replenishment operate on an immutable stock_variant_id; the parent stock remains the catalog identity and rolls up its variants for legacy screens and parent-level reporting. Do not create a stock number per size, overload serial number/UOM, or allow a variant-enabled product to transact against an unspecified size.

The detailed findings are in the investigation. The workflow diagram shows both old and proposed paths.

Non-negotiable invariants

  1. One parent stock_no owns product-family data; each active variant belongs to exactly one parent and is never reused for a different option combination.
  2. All quantity-changing work for a variant-enabled parent carries a valid variant ID. Parent totals are projections, not fulfillment authority.
  3. A variant cannot be selected when it is inactive, does not belong to the parent, or lacks a required option value. Database enforcement must prevent mismatches.
  4. Every variant-level movement preserves parent stock, variant, location, source document, actor, reason, and a display-label snapshot for audit/history.
  5. Existing single-size stock stays fully supported without data conversion. A null variant has one meaning only: a product that has never entered variant mode.
  6. Parent barcode scans may open a picker but cannot infer a size. Variant barcode scans identify one selectable variant directly.

Proposed workflows

Workflow Today Proposed
Product setup Create/edit one stock record and enter product identifiers/packaging dimensions. In Stock File, enable “Tracks variants”, define the Size values and order, create active variants, assign optional supplier SKU and barcode, and preview a size matrix. Parent description/price/supplier defaults are inherited. Publishing is blocked until every currently active/reserved/on-order quantity is assigned.
Purchase / PO Search one stock number; create one stock-number line. Search/select parent then choose Size. The editor stores variant ID per operational line and groups rows visually under the parent. Purchasing may split a planned quantity across sizes. Supplier SKU/barcode is shown for the chosen size.
Receiving Receive a stock-number line into cost/location inventory blocks. Receive the PO-selected size into variant inventory blocks. A mismatch opens a permission-gated “receive as different size” confirmation that preserves requested/received variant, reason, and source PO detail. Mixed-size cartons are entered as split receipts.
Adjust / count / transfer Pick stock/location; serial applies where applicable. Scan/search parent then choose/resolve Size before entering a quantity. Count import/API accepts a variant barcode or explicit variantId; parent scans with several sizes enter a pending resolution queue. Transfer preserves the same variant. A variance can never reduce a different size to satisfy a parent total.
Sales / order / fulfillment Search or scan resolves stock number, then available stock is parent total. Stock search returns parent family; sales must pick Size before add. Variant barcode skips the picker. Availability, allocation, substitute/partial fulfillment, picking, returns, and backorder status are variant-specific; parent line is only a presentation grouping.
Replenishment Demand and EOQ/reorder values are stock/location based. Calculate demand, open PO, safety/reorder, exception and recommended buy at variant/location. Show a parent rollup for planning; do not net stock of size 8 against size 11.
Reports Sales, margin, inventory, aging, stock history, and valuation group by parent stock. Default parent rollups remain familiar; an explicit “break down by size” dimension exposes variant facts. Drilldown labels include parent stock + immutable variant label. Pre-rollout history remains parent-only and is clearly labeled.

Data and schema plan

New tables

Existing data extensions

Add nullable stock_variant_id (and a snapshot label where a historical document needs it) to the current inventory blocks and transaction/document lines: inventory items/details, purchase-order and PO detail, receipt and receipt detail, order and pick detail, invoice/history writing path, stock moves, counts, variances, requisitions, EOQ/reorder facts and exports. The final technical design must map the exact legacy/foreign per-location tables before migration generation.

Use a validated foreign key plus a composite parent/variant consistency constraint or trigger. A simple FK alone is insufficient because it permits a variant for the wrong parent stock. Lock the relevant variant inventory rows in the same transaction currently used for InventoryItem detail updates; predicates must include stock_variant_id before any quantity is read or reduced.

Keep stockslc/other warehouse on_hand, allocated, and related parent fields as compatibility totals until all dependent consumers have moved. Derive them from the variant ledger (and legacy null-variant rows) atomically. Do not make two ledgers.

UI, API, import, and integration contract

Pricing, cost, accounting, and reporting policy

Parent price and discounts are inherited by default. Variant price overrides are optional future-ready records with effective dates, permission and invoice snapshot; they are not required for Size v1. Receipt-lot inventory cost and existing accounting posting remain authoritative, but the selected variant flows through the inventory detail so COGS, variance, receipt and financial-journal provenance can be filtered by size. Parent average/last cost stays a derived display rollup.

Reports must expose two deliberate grains: parent (default, stable totals) and variant (size quantities/margin/demand). Parent rollups include all variants and legacy null-variant inventory. A report cannot label pre-rollout parent-only history as a size split. Replenishment must use the variant grain and derive its targets from database-backed configuration for the selected location/date context.

Permissions and audit

Reuse existing view/create/modify inventory and purchasing permissions for routine transactions, and introduce narrowly named permissions for:

All conversion, mismatch, override, and retirement actions require reason text and the current actor. PaperTrail/PosVersion should receive variant key and label in tracked attributes or audit metadata; document/fact snapshots make old history readable after labels change. Permission checks belong in controllers/services and the API contract—not just disabled UI buttons.

Migration and rollout

  1. Foundation, dark read: deploy schema, models, composite integrity, positions projection, permissions, and telemetry with all product UI behind a feature flag. Null variants remain legacy and no stock enters variant mode.
  2. Read-only pilot: enable a small selected-location cohort to configure/display size matrices and compare projected parent totals against existing parent totals. Alert on any difference; do not permit transactions yet.
  3. Transaction pilot: choose a small, pre-cleaned set of parent stocks. Reconcile every active/reserved/on-order/on-PO quantity to explicit variants, run dry-run count and receive flows, then enable PO/receive/order/count/transfer for that cohort. Block conversion if any quantity cannot be classified.
  4. Reporting/replenishment: move demand, EOQ, exception and operational reports for pilot families to variant grain; retain parent total parity dashboards.
  5. Controlled expansion: batch enable only after reconciliation, staff training, integration capability confirmation, and rollback rehearsal. Keep a kill switch that disables new variant creation/selection while preserving already posted variant transactions; never “roll back” by erasing variant history.

Conversion must be resumable/idempotent, record source rows and decision actor, and use an exception queue. It must not mass-update closed historical invoices merely to create appealing reports. Backfill known mappings only with evidence; leave unknown history parent-only.

Performance and reliability

Phased implementation slices

Slice Deliverable Exit evidence
A — Domain foundation Schema/migrations, models, constraints, audit metadata, flags, permissions, positions API/read model. Database constraints and parent-total parity tests pass; no legacy flow behavior changes.
B — Stock setup and identifiers Stock File variant configuration, size matrix, barcode/SKU resolution, label/scan behavior. Accessibility/browser tests; ambiguous parent barcode requires choice; identifier uniqueness tests.
C — Purchase and receiving Variant PO lines, split/mismatch receiving, inventory block creation and accounting provenance. Receive/unreceive/cost tests and audited mismatch workflow pass.
D — Sales, allocation, transfers and counts Size selection/search, allocation/pick/return/backorder, moves, adjustments, cycle/full counts and mobile/API payloads. Concurrency and negative-availability tests; count/transfer cannot cross sizes.
E — Replenishment, reports and integrations Variant demand/reorder/EOQ, reports/exports, OpenAPI/imports/events, integration versioning. Parent rollup parity and variant drilldown verification on pilot data.
F — Pilot and rollout Conversion service, reconciliation dashboard, training/manuals, observability, staged enablement. Dry run, production-read-only parity, pilot acceptance, rollback/kill-switch rehearsal.

This is intentionally a multi-slice story; no branch, worktree, product-code build, CI, push, or PR is authorized until the user approves a build after planning.

Test and verification plan

Risks and mitigations

Risk Mitigation
Parent total lets the wrong size sell Every mutating flow validates variant availability; parent totals are display-only for variant parents.
Partial conversion creates “unknown size” inventory Hard conversion gate and exception queue; no variant mode until all active/reserved/on-order quantities are resolved.
New schema slows stock search and reports Indexed exact identifier lookup, lazy matrix fetch, query plans/load tests, and parent aggregate compatibility view.
Legacy integrations drop size Capability/version flag, additive reads, reject unsafe writes, staged partner validation and monitoring.
Barcode duplicates or parent ambiguity Normalized unique identifier registry and required chooser/error path.
Cost/financial audit loses traceability Carry variant through inventory detail and document snapshots; parity/reconciliation before rollout.
Staff selects the wrong size Size-first UI, scan confirmation, mismatch reason/permission, labels, guided count and receiving workflows.
Scope expands into color/style matrix immediately Ship only Size in v1 atop a generic model; require a separate decision before enabling extra axes.

Product decisions required

These decisions are intentionally not assumed:

  1. Variant attributes for v1: recommended: Size only, with generic internals ready for a later Color/Width decision. Should Color or Width be included now?
  2. Barcode policy: recommended: every active sellable size gets a unique variant barcode where supplier data permits; parent barcode opens a required size chooser. Is a default-size parent scan ever acceptable?
  3. Pricing: recommended: inherit parent price in v1; defer variant price overrides until a concrete business case. Are size-specific prices required now?
  4. Replenishment: recommended: variant-level reorder/EOQ for enabled families, beginning with pilot locations. Do buyers require per-size target quantities from the first pilot or a parent target split manually?
  5. Migration policy: recommended: pilot only products with a clean physical size allocation; unresolved quantities block activation. Is an explicit temporary “unclassified inventory” quarantine acceptable, or must every pilot item be manually assigned before release?
  6. Customer document presentation: recommended: show parent stock number plus size on one sales/packing/invoice line, grouping parent lines only in planning views. Is that the desired customer-facing document behavior?

Planning state

Keep task-0206 in planning pending these product decisions and a user review of the migration scope. The work must transition to ready only after decisions are recorded and before explicit build approval.