# Inventory Ledger Foundation v20

## Source of truth

`inventory.movements` is the stock source of truth.

Every stock change is an immutable signed movement:
- positive quantity adds stock
- negative quantity removes stock

`inventory.balances` is a rebuildable materialized balance used for operational reads.

## No negative inventory

v20 blocks inventory balances below zero.

This is the default platform behavior and is enforced inside the posting transaction
before the movement is inserted.

Future controlled exceptions, if ever enabled, must be explicit settings + permissions.

## Movement types

Initial movement taxonomy:
- opening
- purchase_receipt
- sale_issue
- sale_reversal
- adjustment_in
- adjustment_out
- transfer_in
- transfer_out
- production_in
- production_out
- waste

## Immutability

Inventory movements cannot be updated or deleted.

Protection exists:
- in the model
- through PostgreSQL UPDATE/DELETE triggers

Corrections must be posted as reversing movements.

## Idempotency

Each source line may post only once.

The unique business identity is:
- tenant
- source_type
- source_public_id
- source_line_public_id

Posting commands also have a tenant-scoped idempotency key.

## Sale posting

v20 exposes an explicit boundary:

POST /api/v1/sales/{saleId}/inventory/post

Only:
- completed
- fully paid

sales may post inventory.

The source is the immutable Sale snapshot, never the mutable Cart.

The endpoint is intentionally explicit in this foundation.
A later transactional-outbox worker will automate reliable cross-domain posting.

## Balance API

GET /api/v1/inventory/balances

## Permissions

- inventory.view
- inventory.adjust
- inventory.post_sale

## Next

Next inventory increments:
- adjustment API and approval boundary
- warehouse transfer
- stock count / full warehouse freeze
- batch / expiry / FEFO
- weighted-average valuation
- transactional outbox automation from completed sales
