The capstone combines the course’s pieces into one useful contract. StockItem owns and validates one SKU’s changing quantity. Inventory owns a collection of StockItems and coordinates multi-SKU requests. Frozen StockSnapshot records report current state, while frozen Allocation records report what a successful reservation took. Keep these responsibilities distinct when choosing methods.
A reservation containing two lines introduces a failure the single-item model did not have. The first SKU may have enough stock while the second is missing, invalid or short. The public promise is all-or-nothing: a rejected request must leave every live quantity unchanged. Merely catching the error after subtracting the first line does not fulfill that promise.
Stage a separate object graph
A shallow dictionary copy creates a new container but still refers to the same mutable children. Staging against those children would change live stock. One clear solution creates fresh StockItems with the same current values, performs all takes on them, constructs the receipt, then publishes the staged mapping. The internal identities are deliberately outside the public interface.
class Count:
def __init__(self, value):
self.value = value
def take(self, units):
if units <= 0 or units > self.value:
raise ValueError("cannot take")
self.value -= units
live = {"a": Count(5), "b": Count(1)}
staged = {sku: Count(item.value) for sku, item in live.items()}
try:
for sku, units in {"a": 2, "b": 2}.items():
staged[sku].take(units)
except ValueError:
print("rejected")
print(live["a"].value, live["b"].value)This example demonstrates the isolation mechanism with valid integer inputs; the graded classes add precise count/SKU validation, immutable records and a successful commit path. A two-pass approach can also validate every request before changing live objects under the single-threaded contract. Whichever approach you choose, prove what happens when the last line fails.
Observations must outlive later commands
Returning internal StockItems from snapshot would expose mutable entities. Instead construct frozen records containing strings and integers. An old snapshot then remains a reliable observation after a new reservation or restock. Receipt units describe the amounts taken, not remaining stock; use separate record types because these values answer different questions.
The task uses exact integers and raises explicit errors. Validate each request count before its SKU lookup, and preserve supplied request order when deciding which error occurs first. Sort only the successful receipt and snapshots. An empty inventory, empty request, zero starting stock and exact depletion are meaningful boundaries, not exceptional machinery failures.
This model is local and single-threaded. Publishing one Python attribute is not a database commit, durable storage or concurrency control. Staging copies the inventory and therefore uses additional time and memory proportional to stored SKUs. Those limits are honest tradeoffs for a small exercise, and its failure tests remain useful if you later choose a different implementation.
Use the concept checks to trace ownership and error timing. Repair the shallow staging and wrong-receipt bugs, fill reserve, then complete the build. Test a late failure, a repeated success and another independent inventory. Passing the capstone demonstrates the stated in-memory object design, while a later game, parser or persistent service adds its own tools and preparation.