Design a Cash Register with Inventory and Profit Tracking
SIG (Susquehanna)Software Engineer · OnsiteLast updated
Design an object-oriented cash register that manages multi-lot product inventory using FIFO accounting, executes atomic multi-item sales, and accurately tracks realized gross profit.
Problem Statement
Financial trading firms and retail engines alike require exact accounting and transactional guarantees. Design a cash register system that tracks product inventory across multiple purchase batches (lots), processes multi-product customer purchases atomically, and calculates realized gross profit using First-In, First-Out (FIFO) cost accounting.
All monetary values must be represented as integer cents to avoid floating-point rounding errors.
Requirements
Implement the CashRegister class with the following methods:
-
add_inventory(product_id: str, quantity: int, unit_cost_cents: int) -> None- Adds a new lot of
quantityunits ofproduct_idpurchased atunit_cost_centsper unit.
- Adds a new lot of
-
checkout(transaction_id: str, items: list[tuple[str, int, int]]) -> int- Processes a sale transaction.
itemsis a list of tuples:(product_id, quantity, unit_price_cents). - Atomicity: Either all items in the basket are fulfilled or the transaction fails entirely. If any product has insufficient total inventory, the method must raise an
InsufficientInventoryError(or return-1if custom exceptions are not used), leaving all inventory lots completely unchanged. - FIFO Costing: Depletes stock starting from the oldest available lot for each product.
- Idempotency: If a transaction with an already-processed
transaction_idis submitted again, do not re-deduct inventory. Return the previously computed realized profit for that transaction. - Returns the realized gross profit for the transaction:
total_revenue - total_cost_of_goods_sold.
- Processes a sale transaction.
-
get_total_realized_profit() -> int- Returns the cumulative realized gross profit across all successful checkouts in cents.
Example
register = CashRegister()
# Buy 10 widgets at $1.00 each, then 10 widgets at $1.50 each
register.add_inventory("widget", 10, 100)
register.add_inventory("widget", 10, 150)
# Sell 15 widgets at $2.00 each in transaction "tx-001"
# Revenue: 15 * 200 = 3000 cents
# COGS (FIFO): (10 * 100) + (5 * 150) = 1000 + 750 = 1750 cents
# Realized Profit: 3000 - 1750 = 1250 cents ($12.50)
profit1 = register.checkout("tx-001", [("widget", 15, 200)])
assert profit1 == 1250
# Idempotent retry returns the same profit without deducting stock
assert register.checkout("tx-001", [("widget", 15, 200)]) == 1250
# Remaining widget inventory: 5 units from the second lot (cost 150)
# Attempting to sell 10 widgets should fail atomically
try:
register.checkout("tx-002", [("widget", 10, 200)])
except InsufficientInventoryError:
pass
# Cumulative profit
assert register.get_total_realized_profit() == 1250
Constraints
1 <= quantity <= 10^50 <= unit_cost_cents, unit_price_cents <= 10^7- Up to
10^4calls toadd_inventoryandcheckout. product_idandtransaction_idare non-empty alphanumeric strings.
Hints
Hint 1
Always represent currency as integer cents (or a fixed-point decimal) to prevent floating-point inaccuracies.
Hint 2
Use a queue (e.g., `collections.deque`) of lots per product to consume inventory in First-In, First-Out order.
Hint 3
To guarantee atomicity, verify that available stock across all requested products is sufficient before mutating any lot queues.
Follow-up questions
- How would you ensure thread-safety for concurrent checkout requests without locking the entire register with a single coarse-grained lock?
- How should the system handle customer returns or refunds under FIFO accounting: should returned items be restored to their original lots or added as new lots?
- How would you store an immutable audit log of every sale such that historical inventory state and profit calculations can be reconstructed from scratch?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.