Skip to content

Instantly share code, notes, and snippets.

@gohuygo
Created October 30, 2025 00:42
Show Gist options
  • Select an option

  • Save gohuygo/85125abebbad37a7773b9b7b582470c5 to your computer and use it in GitHub Desktop.

Select an option

Save gohuygo/85125abebbad37a7773b9b7b582470c5 to your computer and use it in GitHub Desktop.
import pytest
from vending import VendingMachine
class TestSelect:
def test_invalid_selection(self):
vm = VendingMachine()
with pytest.raises(Exception, match="Invalid selection"):
vm.select("INVALID")
def test_out_of_inventory(self):
vm = VendingMachine()
vm.insert_money("DOLLAR")
assert vm.credit == 100
assert vm.pending_cash["DOLLAR"] == 1
assert vm.cash["DOLLAR"] == 0
with pytest.raises(Exception, match="Out of inventory"):
vm.select("A1")
assert vm.credit == 100
assert vm.pending_cash["DOLLAR"] == 1
assert vm.cash["DOLLAR"] == 0
def test_insufficient_credit(self):
vm = VendingMachine()
vm.restock("A1", 1)
vm.insert_money("QUARTER")
assert vm.credit == 25
assert vm.pending_cash["QUARTER"] == 1
assert vm.cash["QUARTER"] == 0
with pytest.raises(Exception, match="Insufficient credit"):
vm.select("A1")
assert vm.credit == 25
assert vm.pending_cash["QUARTER"] == 1
assert vm.cash["QUARTER"] == 0
class TestInsertMoney:
def test_invalid_denomination(self):
vm = VendingMachine()
# Try inserting a fake bill
with pytest.raises(ValueError, match="Invalid denomination"):
vm.insert_money("TEN")
def test_valid_insertion_increases_credit(self):
vm = VendingMachine()
vm.insert_money("DOLLAR")
assert vm.credit == 100
vm.insert_money("QUARTER")
assert vm.credit == 125
class TestRestock:
def test_invalid_item(self):
vm = VendingMachine()
with pytest.raises(ValueError, match="Invalid item"):
vm.restock("Z9", 10) # invalid slot
def test_invalid_quantity(self):
vm = VendingMachine()
with pytest.raises(ValueError, match="Quantity must be positive"):
vm.restock("A1", 0)
with pytest.raises(ValueError, match="Quantity must be positive"):
vm.restock("A1", -5)
def test_valid_restock(self):
vm = VendingMachine()
assert vm.items["A1"]["quantity"] == 0
assert vm.items["B2"]["quantity"] == 0
assert vm.items["C3"]["quantity"] == 0
vm.restock("A1", 5)
assert vm.items["A1"]["quantity"] == 5
assert vm.items["B2"]["quantity"] == 0
assert vm.items["C3"]["quantity"] == 0
vm.restock("A1", 3)
assert vm.items["A1"]["quantity"] == 8
assert vm.items["B2"]["quantity"] == 0
assert vm.items["C3"]["quantity"] == 0
class TestLoadMoney:
def test_load_invalid_denom(self):
vm = VendingMachine()
with pytest.raises(ValueError, match="Invalid denomination"):
vm.load_money("TEN", 5) # Not a valid denomination
def test_invalid_count_zero(self):
vm = VendingMachine()
with pytest.raises(ValueError, match="Count must be positive"):
vm.load_money("DOLLAR", 0)
def test_invalid_count_negative(self):
vm = VendingMachine()
with pytest.raises(ValueError, match="Count must be positive"):
vm.load_money("DOLLAR", -3)
def test_valid_load_money(self):
vm = VendingMachine()
assert vm.cash["DOLLAR"] == 0
assert vm.cash["QUARTER"] == 0
assert vm.cash["FIVE"] == 0
# Load valid denominations
vm.load_money("DOLLAR", 5)
assert vm.cash["DOLLAR"] == 5
assert vm.cash["QUARTER"] == 0
assert vm.cash["FIVE"] == 0
vm.load_money("QUARTER", 2)
assert vm.cash["DOLLAR"] == 5
assert vm.cash["QUARTER"] == 2
assert vm.cash["FIVE"] == 0
vm.load_money("FIVE", 1)
assert vm.cash["DOLLAR"] == 5
assert vm.cash["QUARTER"] == 2
assert vm.cash["FIVE"] == 1
class TestRefund:
def test_refund_with_no_purchase(self):
vm = VendingMachine()
vm.insert_money("DOLLAR")
vm.insert_money("QUARTER")
result = vm.refund()
assert result == {"DOLLAR": 1, "QUARTER": 1}
assert all(count == 0 for count in vm.pending_cash.values())
assert vm.credit == 0
def test_refund_after_purchase_and_valid_change(self):
vm = VendingMachine()
# Setup machine with $5 for change
vm.load_money("QUARTER", 4)
vm.load_money("DOLLAR", 4)
vm.restock("A1", 1)
assert vm.cash['QUARTER'] == 4
assert vm.cash['DOLLAR'] == 4
assert vm.cash['FIVE'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
# Insert $5 for a $1 soda
vm.insert_money("FIVE")
assert vm.cash['QUARTER'] == 4
assert vm.cash['DOLLAR'] == 4
assert vm.cash['FIVE'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 1
item = vm.select("A1")
assert item == "soda"
assert vm.credit == 400 # $4 remaining credit
# Pending cash should be moved over to cash reserve
assert vm.cash['QUARTER'] == 4
assert vm.cash['DOLLAR'] == 4
assert vm.cash['FIVE'] == 1
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
# Refund $4 remaining amount
result = vm.refund()
assert result == {"DOLLAR": 4}
assert vm.credit == 0
# Should have an extra dollar from purchase (i.e. $6)
assert vm.cash['QUARTER'] == 4
assert vm.cash['DOLLAR'] == 0
assert vm.cash['FIVE'] == 1
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
def test_pending_first_then_reserve(self):
vm = VendingMachine()
vm.restock("A1", 1)
vm.load_money("DOLLAR", 1)
assert vm.cash['QUARTER'] == 0
assert vm.cash['DOLLAR'] == 1
assert vm.cash['FIVE'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
vm.insert_money("DOLLAR")
assert vm.cash['QUARTER'] == 0
assert vm.cash['DOLLAR'] == 1
assert vm.cash['FIVE'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 1
assert vm.pending_cash['FIVE'] == 0
vm.select("A1")
vm.insert_money("QUARTER")
vm.insert_money("QUARTER")
assert vm.pending_cash['QUARTER'] == 2
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
assert vm.cash['QUARTER'] == 0
assert vm.cash['DOLLAR'] == 2
assert vm.cash['FIVE'] == 0
assert vm.credit == 50
change = vm.refund()
assert change == {"QUARTER": 2}
assert vm.credit == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['FIVE'] == 0
assert vm.cash['QUARTER'] == 0
assert vm.cash['DOLLAR'] == 2
assert vm.cash['FIVE'] == 0
def test_refund_with_invalid_change(self):
vm = VendingMachine()
vm.restock("A1", 1)
# Do not load machine with cash
assert vm.cash['QUARTER'] == 0
assert vm.cash['DOLLAR'] == 0
assert vm.cash['FIVE'] == 0
# Insert $5 for a $1 soda
vm.insert_money("FIVE")
assert vm.pending_cash['FIVE'] == 1
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.cash['FIVE'] == 0
assert vm.cash['DOLLAR'] == 0
assert vm.cash['QUARTER'] == 0
item = vm.select("A1")
assert item == "soda"
assert vm.credit == 400
assert vm.pending_cash['FIVE'] == 0
assert vm.pending_cash['DOLLAR'] == 0
assert vm.pending_cash['QUARTER'] == 0
assert vm.cash['FIVE'] == 1
assert vm.cash['DOLLAR'] == 0
assert vm.cash['QUARTER'] == 0
# Machine cannot make exactly $4 with only a $5 bill available
with pytest.raises(Exception, match="Unable to make exact change"):
vm.refund()
# Credit should remain
assert vm.credit == 400
VALID_DENOMINATIONS = {
'QUARTER': 25,
'DOLLAR': 100,
'FIVE': 500,
}
class VendingMachine:
"""
Problem: OOP design problem to create a functional VendingMachine!
"""
def __init__(self):
self.items = {
"A1": {
"quantity": 0,
"price": 100,
"item": "soda"
},
"B2": {
"quantity": 0,
"price": 50,
"item": "chips"
},
"C3": {
"quantity": 0,
"price": 200,
"item": "health_bar"
}
}
# Internal system's cash reserve
self.cash = {
'QUARTER': 0,
'DOLLAR': 0,
'FIVE': 0,
}
# User's submitted cash
self.pending_cash = {
'QUARTER': 0,
'DOLLAR': 0,
'FIVE': 0,
}
self.credit = 0
def insert_money(self, denomination: str):
"""
Insert a single coin/bill into the pending bucket
[Customer operation]
"""
# Raise error if invalid bill/denomination
if denomination not in VALID_DENOMINATIONS:
raise ValueError("Invalid denomination")
# Increase current user's credit
self.pending_cash[denomination] += 1
self.credit += VALID_DENOMINATIONS[denomination]
def select(self, item_key: str):
"""
Attempt to vend an item
[Customer operation]
"""
# Check if item exists
if item_key not in self.items:
raise Exception("Invalid selection")
selected_item = self.items[item_key]
# Check to see if item can be purchased
if selected_item['quantity'] == 0:
raise Exception("Out of inventory")
# Check if we have enough credit to purchase item
if self.credit < selected_item['price']:
raise Exception("Insufficient credit")
# Update internal system cash reserve
for denom, count in self.pending_cash.items():
self.cash[denom] += count
self.pending_cash = {d: 0 for d in VALID_DENOMINATIONS.keys()}
# decrease quantity of item
selected_item['quantity'] -= 1
# reduce the credit by item price
self.credit -= selected_item['price']
return selected_item['item']
def refund(self):
"""
1) Always give back all pending inserts first.
2) If more is owed, make change from machine cash reserve.
"""
amount = self.credit
if amount == 0:
return {}
# 1) Always give back user inserts
pending_change = {denom: count for denom, count in self.pending_cash.items() if count > 0}
pending_value = sum(self.pending_cash[d] * VALID_DENOMINATIONS[d] for d in self.pending_cash)
remaining_owed = amount - pending_value
if remaining_owed < 0:
# Shouldn't happen but guard anyway
raise Exception("Invalid remaining amount owed to user")
# 2) Return remaining owed from machine cash reserve
change_from_reserve = {}
if remaining_owed > 0:
change_from_reserve, remaining = self._make_change(remaining_owed, self.cash)
if remaining != 0:
raise Exception("Unable to make exact change")
# Clear pending
self.pending_cash = {denom: 0 for denom in VALID_DENOMINATIONS}
# Deduct planned coins/bills from machine cash
for denom, count in change_from_reserve.items():
self.cash[denom] -= count
# Zero out credit
self.credit = 0
# Merge user insert change and machine reserve change for final change dict
change = pending_change
for denom, count in change_from_reserve.items():
change[denom] = change.get(denom, 0) + count
return change
def restock(self, item_key: str, quantity: int):
"""
Increase quantity in a slot (admin operation).
[ADMIN operation]
"""
# make sure we can only restock valid items
if item_key not in self.items:
raise ValueError("Invalid item")
# Make sure we dont take items out of vending during restock
if quantity <= 0:
raise ValueError("Quantity must be positive")
self.items[item_key]["quantity"] += quantity
def load_money(self, denomination: str, count: int):
"""
Add coins/bills into the machine's change inventory (admin operation).
[ADMIN operation]
"""
# make sure we can only load valid denomination
if denomination not in VALID_DENOMINATIONS:
raise ValueError("Invalid denomination")
# make sure we don't take out bills
if count <= 0:
raise ValueError("Count must be positive")
self.cash[denomination] += count
def _make_change(self, amount: int, cash_inventory: dict):
"""
Return (change, remaining_cents) using greedy change limited by cash_inventory.
Loop and start with largest bill and attempt to make as much change as possible
Repeat with a lower denomination each time
Exit early if exact change is produced
"""
change = {}
remaining = amount
for denom, value in sorted(VALID_DENOMINATIONS.items(), key=lambda x: x[1], reverse=True):
if remaining == 0:
break
available = cash_inventory.get(denom, 0)
needed = remaining // value
use = min(available, needed)
if use:
change[denom] = use
remaining -= use * value
return change, remaining
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment