Specification · inventory @ 0.1

inventory — Stock and What It Is Worth · Area Specification

last conformance run: 5/5 scenarios pass · 6 acts mapped 2026-10-24 18:56 UTC

Spec

Status: 0.1. What the stock on hand is worth and what each sale cost: every stock move kept, the moving average cost derived from what was paid, cost of goods sold on each shipment, physical counts with reasons, and a valuation that ties to the books. It completes the cost side that the p2p area's receipts start (P2P-5, P2P-13).

1. Calibration: who this is for

The same small business as o2c and p2p: one stockroom, one currency, a few hundred items at most, stock bought on purchase orders and sold on orders. The owner wants to know what the shelves are worth, what a sale actually earned, and where the missing units went. Their accountant wants the inventory balance and cost of goods sold to come from the record, not from a spreadsheet at year end.

2. Scope

In: the stock move record, the moving average cost, the cost of opening stock, physical counts, cost of goods sold per shipment, valuation as of a date, and their postings.

Out (§9): FIFO and specific identification, landed cost, several stockrooms, lots and serial numbers, reorder points, and standard costing.

3. Entities and lifecycles

4. The acts

Writes: set_opening_cost, count_stock.

Reads: stock (every item: on hand, average cost, value), item_stock (one item's moves with the running quantity, average and value), valuation (as of a date, tied to the books), cost_of_sales (each shipment line in a period: sold at, cost, margin).

Moves themselves are written by the acts that move stock: p2p's receive_goods, o2c's ship_order and record_return, core's receive_stock, and count_stock here.

5. Invariants

6. Required read models

stock (with the uncosted quantity per item), item_stock, valuation (per item and in total, with the journal's Inventory balance beside it), cost_of_sales (per shipment line, with the totals).

7. Contract vs freedom

Contract: moves only through core; the moving average; costs derived except the opening cost; uncosted stock named; valuation ties to the journal. Freedom: how a count is reported, how the pages sort.

8. Conformance (scenarios)

01 the average moves with receipts and shipments leave at it · 02 opening stock without a cost is named and left out until its cost is stated, then everything derives · 03 a count records the shrinkage at the average · 04 a return comes back at the average · 05 valuation ties to the journal's Inventory balance.

9. Deferred, with reasons

ItemWhy deferred, not rejected
FIFO, specific identificationMoving average first; a method choice needs a design partner with a reason (tax or industry).
Landed costFreight allocated across items needs its own design; freight posts as a service line today.
Several stockroomsTransfers and per-location stock; one stockroom fits the calibration.
Lots, serial numbers, expiryTraceability for regulated goods; a different customer.
Reorder pointsAdvice, not record. The record says what is on hand and on order.
Standard costingA manufacturer's tool; the calibration buys and resells.

---

Change log: 0.1 (2026-10-24) — first version: stock moves, moving average, opening cost, counts, cost of goods sold, valuation tied to the books.

Acts

ActKindRequired
set_opening_costwriteitem unit_cost reason
count_stockwriteitem counted reason
stockreadnone
item_stockreaditem
valuationreadnone
cost_of_salesreadnone

Invariants

IdInvariant
STK-1Money is integer cents; one currency.
STK-2Every change to the quantity on hand is a stock move, written by core's stock door; the moves add up to on hand.
STK-3Cost is derived (moving average), never typed, except the opening cost of stock that came in without one.
STK-4Each unit leaves at the average of that moment; cost of goods sold is that cost, per shipment line.
STK-5Stock without a cost is named with its quantity, never valued at zero in silence.
STK-6A count records what was counted, with a reason; the difference is valued at the average.
STK-7Valuation as of a date equals the journal's Inventory balance for that date.
STK-8The journal derives balanced: shipments to cost of goods sold, counts to adjustments, opening stock to equity.
STK-9Every write is a logged command with an actor; refusals are logged too.

Scenarios

Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.

the average moves with what was paid; shipments leave at it pass

STK-3/4/7: 40 at $48 and 20 at $54 average $50; ten ship at $50 each; what is left is worth what the books say.
Environment: create_customer, create_item
#ActExpectWhy
1create_pook
2issue_pook
3receive_goodsok40 arrive at $48.00: a costed inflow
4create_pook
5issue_pook
6receive_goodsok20 arrive at $54.00: a costed inflow
7item_stockok qty=60 average_cost=5000 value=300000
8create_orderok
9confirm_orderok
10ship_orderokeach unit leaves at the average of this moment
11cost_of_salesok revenue=80000 cost=50000 margin=30000sold at $80, cost $50
12valuationok value=250000 ties=trueSTK-7 the 50 left, worth $2,500, tie to Inventory in the books

stock without a cost is named until its opening cost is stated pass

STK-3/5: ten on hand before the record have no cost; a sale of them is left out of cost of goods sold, then everything derives once a person states what one was worth.
Environment: create_customer, create_item
#ActExpectWhy
1item_stockok cost_known=false uncosted_qty=10
2create_orderok
3confirm_orderok
4ship_orderokeach unit leaves at the average of this moment
5cost_of_salesok count=1 cost=0STK-5 left out and listed, never costed at zero in silence
6set_opening_costrefused reason is required
7set_opening_costok cost_known=true value=27000
8cost_of_salesok cost=18000 margin=14000the earlier sale is now costed: derived, not re-entered
9valuationok ties=true

a count records the shrinkage at the average pass

STK-6: thirty received at $48, twenty-eight counted; the two missing are $96 of Inventory Adjustments. A matching count moves nothing.
Environment: create_item
#ActExpectWhy
1create_pook
2issue_pook
3receive_goodsok30 arrive at $48.00: a costed inflow
4count_stockok difference=-2 difference_value=-9600
5count_stockok difference=0
6count_stockrefused zero or more
7valuationok value=134400 ties=true

a return comes back at the average pass

STK-4/8: twenty at $50, five shipped at $50; two come back sellable at the average, reversing their cost of goods sold.
Environment: create_customer, create_item
#ActExpectWhy
1create_pook
2issue_pook
3receive_goodsok20 arrive at $50.00: a costed inflow
4create_orderok
5confirm_orderok
6ship_orderokeach unit leaves at the average of this moment
7record_returnok
8item_stockok qty=17 average_cost=5000 value=85000
9valuationok ties=true

valuation on a past day ties to the books on that day pass

STK-7: two deliveries on different days; the stock on the day between them is worth the first one, and so is Inventory in the books for that day.
Environment: create_item
#ActExpectWhy
1create_pook
2issue_pook
3receive_goodsok
4receive_goodsok
5valuationok value=144000 ties=true
6valuationok value=240000 ties=true

These pages are generated from the files in specs/ in the repository; scenarios are executed by src/conformance.js on every build. Written and maintained by Peter Varga, who also runs Portland AI Works. github.com/alviso/saybooks, AGPL-3.0.