---
id: CAP-COM-FUL-017
title: Keep stock movements and transfers accountable
feature: FEAT-COM-FUL-04
product_area: commerce
module: fulfillment
revision: 1
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes:
  - CAP-COM-INV-011
  - CAP-COM-INV-013
  - CAP-COM-INV-014
  - CAP-COM-INV-028
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
intent_sources:
  - >-
    services-commerce:docs/migrations/inventory/README.md#D-P8I-14 (decision M,
    Andrew 2026-10-18)
purpose: Staff record physical stock changes through an immutable movement ledger.
outcomes:
  - Movement history explains physical stock changes.
  - Replenishment commits waiting backorders in purchase order.
concepts:
  - CON-COM-FUL-005
actors:
  - ACT-COM-010
  - ACT-COM-009
surfaces:
  - api
preconditions: []
postconditions: []
permissions:
  - key: commerce.inventory.read
    where_checked: api
    scope: tenant
  - key: commerce.inventory.create
    where_checked: api
    scope: tenant
  - key: commerce.inventory.update
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-COM-FUL-017-01
    proved_by: [ATS-COM-FUL-016-07, ATS-COM-FUL-017-01, ATS-COM-FUL-017-10, ATS-COM-FUL-018-13, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: >-
      Staff can enter receipts, adjustments, quarantine movements, damage, scrap
      and write-offs, while shipment, restock, transfer and count movements are
      reserved for their owning processes.
    kind: validation
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/e2e/inventory.e2e.spec.js#moves
      quarantined beside on_hand and a scrap off it; refuses what the shelf
      cannot bear (400 naming the rule), a wrong sign (400) and a type staff do
      not record (400)
    origin: none
  - id: BR-CAP-COM-FUL-017-02
    proved_by: [ATS-COM-FUL-017-01, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: >-
      Receipt, restock, transfer-in and quarantine-in quantities are positive;
      shipment, transfer-out, scrap, write-off and quarantine-out quantities are
      negative; adjustments, counts and damage permit either sign.
    kind: validation
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/db/inventory-tables.table.integration.spec.js#refuses
      a quantity whose sign the type forbids, by
      inventory_movements_quantity_sign_check, and a type outside the twelve
    origin: none
  - id: BR-CAP-COM-FUL-017-03
    proved_by: [ATS-COM-FUL-015-09, ATS-COM-FUL-016-08, ATS-COM-FUL-017-02, ATS-COM-FUL-017-08, ATS-COM-FUL-017-11, ATS-COM-FUL-017-12, ATS-COM-FUL-018-11, ATS-COM-FUL-018-14, ATS-COM-FUL-019-07, ATS-COM-FUL-020-10, TST-COM-FUL-007, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: >-
      Ledger operations alone change balances, the application role cannot write
      them directly, and database checks reject impossible movements and
      negative available stock.
    kind: invariant
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/db/inventory-tables.table.integration.spec.js#refuses
      a movement the shelf cannot bear, by inventory_items_balances_check (the
      last line of defence), leaving the balances as they were
    origin: none
  - id: BR-CAP-COM-FUL-017-04
    proved_by: [ATS-COM-FUL-017-02, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: Movement records are immutable after insertion.
    kind: invariant
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/db/inventory-tables.table.integration.spec.js#is
      append-only: the application role cannot update or delete a movement
      (42501)
    origin: none
  - id: BR-CAP-COM-FUL-017-05
    proved_by: [ATS-COM-FUL-017-03, TST-COM-FUL-057, TST-COM-FUL-066, TST-COM-FUL-067]
    statement: >-
      Repeated movement keys return the original only when type, quantity, item
      and location agree, otherwise rejecting reuse with the first movement
      identified.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js#refuses
      the receipt key reused for another SKU or quantity, 409
      idempotency_key_reused naming the first movement, writing nothing
      (DEF-COM-0452)
    origin: DEF-COM-0452 (acceptance defect)
  - id: BR-CAP-COM-FUL-017-06
    proved_by: [ATS-COM-FUL-017-01, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: >-
      Receipt can create an absent location item, while other movement types
      reject a SKU not held there.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js#refuses
      a receipt of a negative quantity and a scrap of a positive one (400), and
      a movement of a SKU nothing holds
    origin: none
  - id: BR-CAP-COM-FUL-017-07
    proved_by: [ATS-COM-FUL-012-11, ATS-COM-FUL-014-08, ATS-COM-FUL-017-04, ATS-COM-FUL-017-07, ATS-COM-FUL-018-09, TST-COM-FUL-058, TST-COM-FUL-066, TST-COM-FUL-075]
    statement: >-
      Stock arriving at a sellable location commits waiting backorders in
      oldest-order order within the receipt transaction.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js#converts
      both backordered units to committed in the receipt’s transaction, oldest
      order first, leaving one to sell
    origin: none
  - id: BR-CAP-COM-FUL-017-08
    proved_by: [ATS-COM-FUL-017-05, TST-COM-FUL-064]
    statement: >-
      Transfer atomically records linked out and in movements between locations,
      creates a destination item if necessary and moves cost layers without an
      Accounting posting.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/inventory-transfers.integration.spec.js#writes
      the pair in one transfer: a transfer_out of five on A1’s item and a
      transfer_in of five on A2’s, one id, each naming the other
    origin: >-
      Decision M, Andrew 2026-10-18 ("build the transfer now"):
      services-commerce:docs/migrations/inventory/README.md#D-P8I-14
  - id: BR-CAP-COM-FUL-017-09
    proved_by: [ATS-COM-FUL-017-06, TST-COM-FUL-057, TST-COM-FUL-064]
    statement: >-
      Transfer rejects insufficient available stock, identical locations,
      quantities below one, missing reasons, unknown or inactive destinations
      and absent source SKUs; held, allocated, quarantined and damaged units
      cannot transfer, and conflicting key reuse is rejected.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-commerce:modules/inventory/services/stock/transfer-stock.ts:transferStock
    origin: >-
      Decision M, Andrew 2026-10-18 ("build the transfer now"):
      services-commerce:docs/migrations/inventory/README.md#D-P8I-14
  - id: BR-CAP-COM-FUL-017-10
    proved_by: [ATS-COM-FUL-017-06, TST-COM-FUL-057, TST-COM-FUL-064]
    statement: >-
      Transfer into a sellable location fills waiting backorders, whose
      committed units cannot subsequently transfer.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/inventory-transfers.integration.spec.js#fills
      the two backorders in the transfer’s transaction, the units allocated on
      A1 at their cost
    origin: >-
      Decision M, Andrew 2026-10-18 ("build the transfer now"):
      services-commerce:docs/migrations/inventory/README.md#D-P8I-14
  - id: BR-CAP-COM-FUL-017-11
    proved_by: [ATS-COM-FUL-017-02, TST-COM-FUL-057, TST-COM-FUL-059, TST-COM-FUL-066]
    statement: >-
      Item settings can be edited independently, but balances remain controlled
      by movement and allocation ledgers.
    kind: invariant
    enforced_at:
      - api
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/e2e/inventory.e2e.spec.js#changes
      an item’s settings (and nothing of its balances), 404 for an item the
      tenant has not
    origin: none
  - id: BR-CAP-COM-FUL-017-12
    proved_by: [ATS-COM-FUL-017-06, TST-COM-FUL-057, TST-COM-FUL-064]
    statement: >-
      Movement and transfer creation require administrator inventory-create
      authority, while readers can inspect movements, transfers and items.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: observed
    source: 'services-commerce:modules/inventory/aws/api/inventory.routes.ts'
    origin: none
scenarios:
  - id: SC-CAP-COM-FUL-017-01
    given: Two shirts are waiting on backorder.
    when: Receive three units into sellable stock.
    then: The older backorders are committed first and one unit remains available.
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js#converts
      both backordered units to committed in the receipt’s transaction, oldest
      order first, leaving one to sell
edge_cases:
  - id: SC-CAP-COM-FUL-017-02
    given: A stored receipt key is supplied for a different SKU.
    when: Try to record the changed receipt.
    then: The conflict identifies the original movement and changes no stock.
    provenance: test
    source: >-
      services-commerce:modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js#refuses
      the receipt key reused for another SKU or quantity, 409
      idempotency_key_reused naming the first movement, writing nothing
      (DEF-COM-0452)
constraints: []
depends_on:
  - CAP-COM-FUL-016
related:
  - CAP-COM-FUL-018
  - CAP-COM-FUL-019
journeys:
  - JNY-COM-FUL-003
open_questions:
  - OQ-COM-FUL-002
evidence:
  - repository: services-commerce
    paths:
      - modules/inventory/tests/e2e/inventory.e2e.spec.js
      - >-
        modules/inventory/tests/integration/db/inventory-tables.table.integration.spec.js
      - >-
        modules/inventory/tests/integration/services/stock/staff-movements.integration.spec.js
      - >-
        modules/inventory/tests/integration/services/stock/inventory-transfers.integration.spec.js
      - modules/inventory/services/stock/transfer-stock.ts
      - modules/inventory/aws/api/inventory.routes.ts
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-CRY-001, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-002, TC-PI-003, TC-PRV-001, TC-PRV-002]
exercised_by: [FAS-COM-FUL-04-01, FAS-COM-FUL-04-02, FAS-COM-FUL-04-04, FAS-COM-FUL-04-05, FAS-COM-FUL-05-02]
suites: [TST-COM-FUL-007, TST-COM-FUL-057, TST-COM-FUL-058, TST-COM-FUL-059, TST-COM-FUL-064, TST-COM-FUL-066, TST-COM-FUL-067, TST-COM-FUL-075]
---

# Keep stock movements and transfers accountable

## Purpose

Staff record physical stock changes through an immutable movement ledger.

## How It Works

Staff record receipts and permitted corrections for a stock item. Commerce records operational movements through the process responsible for the physical event. Ledger changes determine the stored balances instead of allowing direct balance edits. Incoming sellable stock can satisfy waiting backorders. Transfers move available goods between locations while retaining their cost relationship. Invalid quantities, unavailable units and conflicting retry keys are refused. The product should show movement reasons, related work and the resulting availability in a usable stock history.

## Relationships

- Organize warehouses and stock locations supplies or consumes related delivery records.
- Allocate stock and explain what buyers can obtain supplies or consumes related delivery records.
- Reconcile shelf counts and stock findings supplies or consumes related delivery records.
