Skip to content

Commit 91d4422

Browse files
committed
Initial Domain Model with Tests
1 parent 7bfcd22 commit 91d4422

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

model.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
from datetime import date
44
from typing import Optional, List, Set
55

6+
class OutOfStock(Exception):
7+
pass
8+
9+
def allocate(line: OrderLine, batches: List[Batch]) -> str:
10+
try:
11+
batch = next(b for b in sorted(batches) if b.can_allocate(line))
12+
batch.allocate(line)
13+
14+
return batch.reference
15+
16+
except StopIteration:
17+
raise OutOfStock(f"Out of stock for sku {line.sku}")
18+
619
@dataclass(frozen=True)
720
class OrderLine:
821
orderid: str
@@ -14,10 +27,42 @@ def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
1427
self.reference = ref
1528
self.sku = sku
1629
self.eta = eta
17-
self.available_quantity = qty
30+
self._purchased_quantity = qty
31+
self._allocations = set() # type: Set[OrderLine]
32+
33+
def __repr__(self):
34+
return f"<Batch {self.reference}>"
35+
36+
def __eq__(self, other):
37+
if not isinstance(other, Batch):
38+
return False
39+
return other.reference == self.reference
40+
41+
def __hash__(self):
42+
return hash(self.reference)
43+
44+
def __gt__(self, other):
45+
if self.eta is None:
46+
return False
47+
if other.eta is None:
48+
return True
49+
return self.eta > other.eta
1850

1951
def allocate(self, line: OrderLine):
20-
self.available_quantity -= line.qty
52+
if self.can_allocate(line):
53+
self._allocations.add(line)
54+
55+
def deallocate(self, line: OrderLine):
56+
if line in self._allocations:
57+
self._allocations.remove(line)
58+
59+
@property
60+
def allocated_quantity(self) -> int:
61+
return sum(line.qty for line in self._allocations)
62+
63+
@property
64+
def available_quantity(self) -> int:
65+
return self._purchased_quantity - self.allocated_quantity
2166

2267
def can_allocate(self, line: OrderLine) -> bool:
2368
return self.sku == line.sku and self.available_quantity >= line.qty

test_allocate.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
3+
from datetime import date, timedelta
4+
from model import allocate, OrderLine, Batch, OutOfStock
5+
6+
today = date.today()
7+
tomorrow = today + timedelta(days=1)
8+
later = today + timedelta(days=10)
9+
10+
def test_prefers_current_stock_batches_to_shipments():
11+
in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
12+
shipment_batch = Batch("shipment-batch", "RETRO-CLOCK", 100, eta=tomorrow)
13+
line = OrderLine("oref", "RETRO-CLOCK", 10)
14+
15+
allocate(line, [in_stock_batch, shipment_batch])
16+
17+
assert in_stock_batch.available_quantity == 90
18+
assert shipment_batch.available_quantity == 100
19+
20+
def test_prefers_earlier_batches():
21+
earliest = Batch("speedy-batch", "MINIMALIST-SPOON", 100, eta=today)
22+
medium = Batch("normal-batch", "MINIMALIST-SPOON", 100, eta=tomorrow)
23+
latest = Batch("slow-batch", "MINIMALIST-SPOON", 100, eta=later)
24+
line = OrderLine("order1", "MINIMALIST-SPOON", 10)
25+
26+
allocate(line, [medium, earliest, latest])
27+
28+
assert earliest.available_quantity == 90
29+
assert medium.available_quantity == 100
30+
assert latest.available_quantity == 100
31+
32+
def test_returns_allocated_batch_ref():
33+
in_stock_batch = Batch("in-stock-batch-ref", "HIGHBROW-POSTER", 100, eta=None)
34+
shipment_batch = Batch("shipment-batch-ref", "HIGHBROW-POSTER", 100, eta=tomorrow)
35+
line = OrderLine("oref", "HIGHBROW-POSTER", 10)
36+
37+
allocation = allocate(line, [in_stock_batch, shipment_batch])
38+
39+
assert allocation == in_stock_batch.reference
40+
41+
def test_raises_out_of_stock_exception_if_cannot_allocate():
42+
batch = Batch("batch1", "SMALL-FORK", 10, eta=today)
43+
line = OrderLine("order1", "SMALL-FORK", 10)
44+
45+
allocate(line, [batch])
46+
47+
with pytest.raises(OutOfStock, match="SMALL-FORK"):
48+
other_line = OrderLine("order2", "SMALL-FORK", 1)
49+
50+
allocate(other_line, [batch])

test_batches.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,14 @@ def test_cannot_allocate_if_skus_do_not_match():
2323
batch = Batch("batch-001", "UNCOMFORTABLE-CHAIR", 100, eta=None)
2424
different_sku_line = OrderLine("order-123", "EXPENSIVE-TOASTER", 10)
2525
assert batch.can_allocate(different_sku_line) is False
26+
27+
def test_can_only_deallocate_allocated_lines():
28+
batch, unallocated_line = make_batch_and_line("DECORATIVE-TRINKET", 20, 2)
29+
batch.deallocate(unallocated_line)
30+
assert batch.available_quantity == 20
31+
32+
def test_allocation_is_idempotent():
33+
batch, line = make_batch_and_line("ANGULAR-DESK", 20, 2)
34+
batch.allocate(line)
35+
batch.allocate(line)
36+
assert batch.available_quantity == 18

0 commit comments

Comments
 (0)