[
{"name": "an issue spans two receipts, a later one spans the rest of the second and a third", "args": [[{"date": "2026-01-01", "quantity": 100, "unitCost": {"minor": 500, "currency": "GBP"}}, {"date": "2026-01-05", "quantity": 50, "unitCost": {"minor": 600, "currency": "GBP"}}, {"date": "2026-01-10", "quantity": -120, "unitCost": null}, {"date": "2026-01-15", "quantity": 100, "unitCost": {"minor": 550, "currency": "GBP"}}, {"date": "2026-01-20", "quantity": -60, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 70, "closingValue": {"minor": 38500, "currency": "GBP"}, "costOfSales": {"minor": 96500, "currency": "GBP"}, "issueCosts": [{"minor": 62000, "currency": "GBP"}, {"minor": 34500, "currency": "GBP"}], "layers": [{"date": "2026-01-15", "quantity": 70, "unitCost": {"minor": 550, "currency": "GBP"}}]}},
{"name": "an issue that exactly empties the oldest layer removes it", "args": [[{"date": "2026-02-01", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-02-02", "quantity": 10, "unitCost": {"minor": 200, "currency": "GBP"}}, {"date": "2026-02-03", "quantity": -10, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 10, "closingValue": {"minor": 2000, "currency": "GBP"}, "costOfSales": {"minor": 1000, "currency": "GBP"}, "issueCosts": [{"minor": 1000, "currency": "GBP"}], "layers": [{"date": "2026-02-02", "quantity": 10, "unitCost": {"minor": 200, "currency": "GBP"}}]}},
{"name": "issuing everything leaves no layers and nothing to value", "args": [[{"date": "2026-02-01", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-02-02", "quantity": 5, "unitCost": {"minor": 120, "currency": "GBP"}}, {"date": "2026-02-03", "quantity": -15, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 0, "closingValue": {"minor": 0, "currency": "GBP"}, "costOfSales": {"minor": 1600, "currency": "GBP"}, "issueCosts": [{"minor": 1600, "currency": "GBP"}], "layers": []}},
{"name": "receipts only: every layer is still there, oldest first", "args": [[{"date": "2026-03-01", "quantity": 3, "unitCost": {"minor": 999, "currency": "GBP"}}, {"date": "2026-03-01", "quantity": 2, "unitCost": {"minor": 1001, "currency": "GBP"}}], "GBP"], "expect": {"closingQuantity": 5, "closingValue": {"minor": 4999, "currency": "GBP"}, "costOfSales": {"minor": 0, "currency": "GBP"}, "issueCosts": [], "layers": [{"date": "2026-03-01", "quantity": 3, "unitCost": {"minor": 999, "currency": "GBP"}}, {"date": "2026-03-01", "quantity": 2, "unitCost": {"minor": 1001, "currency": "GBP"}}]}},
{"name": "same-day lines are taken in ledger order: a receipt then an issue on one day", "args": [[{"date": "2026-03-01", "quantity": 4, "unitCost": {"minor": 250, "currency": "GBP"}}, {"date": "2026-03-01", "quantity": -1, "unitCost": null}, {"date": "2026-03-02", "quantity": 4, "unitCost": {"minor": 300, "currency": "GBP"}}, {"date": "2026-03-02", "quantity": -5, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 2, "closingValue": {"minor": 600, "currency": "GBP"}, "costOfSales": {"minor": 1600, "currency": "GBP"}, "issueCosts": [{"minor": 250, "currency": "GBP"}, {"minor": 1350, "currency": "GBP"}], "layers": [{"date": "2026-03-02", "quantity": 2, "unitCost": {"minor": 300, "currency": "GBP"}}]}},
{"name": "a free receipt (a sample) is a layer at zero cost", "args": [[{"date": "2026-04-01", "quantity": 2, "unitCost": {"minor": 0, "currency": "GBP"}}, {"date": "2026-04-02", "quantity": 2, "unitCost": {"minor": 700, "currency": "GBP"}}, {"date": "2026-04-03", "quantity": -3, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 1, "closingValue": {"minor": 700, "currency": "GBP"}, "costOfSales": {"minor": 700, "currency": "GBP"}, "issueCosts": [{"minor": 700, "currency": "GBP"}], "layers": [{"date": "2026-04-02", "quantity": 1, "unitCost": {"minor": 700, "currency": "GBP"}}]}},
{"name": "restocking after running out starts a fresh layer", "args": [[{"date": "2026-05-01", "quantity": 5, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-05-02", "quantity": -5, "unitCost": null}, {"date": "2026-05-03", "quantity": 5, "unitCost": {"minor": 130, "currency": "GBP"}}, {"date": "2026-05-04", "quantity": -2, "unitCost": null}], "GBP"], "expect": {"closingQuantity": 3, "closingValue": {"minor": 390, "currency": "GBP"}, "costOfSales": {"minor": 760, "currency": "GBP"}, "issueCosts": [{"minor": 500, "currency": "GBP"}, {"minor": 260, "currency": "GBP"}], "layers": [{"date": "2026-05-03", "quantity": 3, "unitCost": {"minor": 130, "currency": "GBP"}}]}},
{"name": "an empty ledger is nothing, in the given currency", "args": [[], "EUR"], "expect": {"closingQuantity": 0, "closingValue": {"minor": 0, "currency": "EUR"}, "costOfSales": {"minor": 0, "currency": "EUR"}, "issueCosts": [], "layers": []}},
{"name": "an issue larger than the stock on hand is an error", "args": [[{"date": "2026-01-01", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-01-02", "quantity": -11, "unitCost": null}], "GBP"], "expectError": "insufficient stock: an issue of 11 on 2026-01-02 exceeds the 10 on hand"},
{"name": "an issue before any receipt is an error", "args": [[{"date": "2026-01-02", "quantity": -1, "unitCost": null}], "GBP"], "expectError": "insufficient stock"},
{"name": "a ledger out of date order is an error", "args": [[{"date": "2026-01-05", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-01-04", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}], "GBP"], "expectError": "movements must be in date order: 2026-01-04 comes after 2026-01-05"},
{"name": "a receipt with no unit cost is an error", "args": [[{"date": "2026-01-01", "quantity": 10, "unitCost": null}], "GBP"], "expectError": "a receipt needs a unitCost"},
{"name": "an issue with a unit cost is an error", "args": [[{"date": "2026-01-01", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}, {"date": "2026-01-02", "quantity": -1, "unitCost": {"minor": 100, "currency": "GBP"}}], "GBP"], "expectError": "its unitCost must be null"},
{"name": "a zero quantity is an error", "args": [[{"date": "2026-01-01", "quantity": 0, "unitCost": null}], "GBP"], "expectError": "quantity must be a non-zero whole number"},
{"name": "a fractional quantity is an error", "args": [[{"date": "2026-01-01", "quantity": 1.5, "unitCost": {"minor": 100, "currency": "GBP"}}], "GBP"], "expectError": "quantity must be a non-zero whole number"},
{"name": "a negative unit cost is an error", "args": [[{"date": "2026-01-01", "quantity": 10, "unitCost": {"minor": -100, "currency": "GBP"}}], "GBP"], "expectError": "unitCost must not be negative"},
{"name": "a cost in another currency is an error", "args": [[{"date": "2026-01-01", "quantity": 10, "unitCost": {"minor": 100, "currency": "EUR"}}], "GBP"], "expectError": "currency mismatch"},
{"name": "an impossible date is an error", "args": [[{"date": "2026-02-30", "quantity": 10, "unitCost": {"minor": 100, "currency": "GBP"}}], "GBP"], "expectError": "is not a real calendar date"}
]