diff --git a/flask_app.py b/flask_app.py index d14af59e..b1d506dd 100644 --- a/flask_app.py +++ b/flask_app.py @@ -30,3 +30,20 @@ def allocate_endpoint(): return {"message": str(e)}, 400 return {"batchref": batchref}, 201 + + + +@app.route("/deallocate", methods=["POST"]) +def deallocate_endpoint(): + session = get_session() + repo = repository.SqlAlchemyRepository(session) + + order_id = request.json["orderid"] + sku = request.json["sku"] + + try: + batchref = services.deallocate(order_id, sku,repo, session) + except Exception as e: + return {"message": str(e)}, 400 + + return {"batchref": batchref}, 201 diff --git a/repository.py b/repository.py index 73b85e31..c6c5f7f1 100644 --- a/repository.py +++ b/repository.py @@ -10,6 +10,10 @@ def add(self, batch: model.Batch): @abc.abstractmethod def get(self, reference) -> model.Batch: raise NotImplementedError + + @abc.abstractmethod + def get_line_item(self, order_id: str, sku: str) -> model.OrderLine: + raise NotImplementedError class SqlAlchemyRepository(AbstractRepository): @@ -24,3 +28,10 @@ def get(self, reference): def list(self): return self.session.query(model.Batch).all() + + def get_line_item(self, order_id: str, sku: str) -> model.OrderLine: + return ( + self.session.query(model.OrderLine) + .filter_by(orderid=order_id, sku=sku) + .one() + ) diff --git a/requirements.txt b/requirements.txt index 01a974f5..084588d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pytest -sqlalchemy +sqlalchemy==1.4.49 flask psycopg2-binary requests diff --git a/services.py b/services.py index cb007393..a86df444 100644 --- a/services.py +++ b/services.py @@ -1,4 +1,6 @@ from __future__ import annotations +import datetime +from typing import Optional import model from model import OrderLine @@ -20,3 +22,26 @@ def allocate(line: OrderLine, repo: AbstractRepository, session) -> str: batchref = model.allocate(line, batches) session.commit() return batchref + +def deallocate(order_id:str, sku:str, repo:AbstractRepository,session): + + order_line = repo.get_line_item(order_id, sku) + + if not order_line : + raise Exception("Couldnt find the order line") + + batches = repo.list() + + for batch in batches: + if order_line in batch._allocations: + batch.deallocate(order_line) + break + else: + raise Exception("Couldnt find the order line in any batch") + + session.commit() + + +def add_batch(ref:str, sku:str, qty:int, eta:Optional[datetime.date], repo:AbstractRepository, session): + repo.add(model.Batch(ref, sku, qty, eta)) + session.commit() \ No newline at end of file diff --git a/test_api.py b/test_api.py index 89fa1241..5b1bd90b 100644 --- a/test_api.py +++ b/test_api.py @@ -55,16 +55,16 @@ def test_unhappy_path_returns_400_and_error_message(): @pytest.mark.usefixtures("postgres_db") @pytest.mark.usefixtures("restart_api") -def test_deallocate(): +def test_deallocate(add_stock): sku, order1, order2 = random_sku(), random_orderid(), random_orderid() batch = random_batchref() - post_to_add_batch(batch, sku, 100, "2011-01-02") + add_stock([(batch, sku, 100, "2011-01-02")]) url = config.get_api_url() # fully allocate r = requests.post( f"{url}/allocate", json={"orderid": order1, "sku": sku, "qty": 100} ) - assert r.json()["batchid"] == batch + assert r.json()["batchref"] == batch # cannot allocate second order r = requests.post( @@ -87,4 +87,4 @@ def test_deallocate(): f"{url}/allocate", json={"orderid": order2, "sku": sku, "qty": 100} ) assert r.ok - assert r.json()["batchid"] == batch + assert r.json()["batchref"] == batch diff --git a/test_services.py b/test_services.py index 3d43f4b6..20ee83a8 100644 --- a/test_services.py +++ b/test_services.py @@ -1,3 +1,5 @@ +from datetime import date + import pytest import model import repository @@ -16,6 +18,13 @@ def get(self, reference): def list(self): return list(self._batches) + + def get_line_item(self, order_id, sku): + for batch in self._batches: + for line in batch._allocations: + if line.orderid == order_id and line.sku == sku: + return line + return None class FakeSession: @@ -61,14 +70,54 @@ def test_deallocate_decrements_available_quantity(): services.allocate(line, repo, session) batch = repo.get(reference="b1") assert batch.available_quantity == 90 - # services.deallocate(... - ... + services.deallocate(line.orderid, line.sku, repo, session) + assert batch.available_quantity == 100 def test_deallocate_decrements_correct_quantity(): - ... # TODO - check that we decrement the right sku + """Among batches with the same SKU, earliest ETA wins; deallocate must target that batch.""" + repo, session = FakeRepository([]), FakeSession() + services.add_batch("b1", "BLUE-PLINTH", 100, date(2011, 1, 1), repo, session) + services.add_batch("b2", "BLUE-PLINTH", 100, date(2011, 1, 2), repo, session) + services.add_batch("b3", "RED-PLINTH", 100, None, repo, session) + line = model.OrderLine("o1", "BLUE-PLINTH", 10) + services.allocate(line, repo, session) + batch_b1 = repo.get(reference="b1") + assert batch_b1.available_quantity == 90 + services.deallocate(line.orderid, line.sku, repo, session) + + assert batch_b1.available_quantity == 100 + + +def test_deallocate_needs_sku_when_one_order_has_several_line_items(): + + repo, session = FakeRepository([]), FakeSession() + services.add_batch("b-blue", "BLUE-PLINTH", 100, None, repo, session) + services.add_batch("b-red", "RED-PLINTH", 100, None, repo, session) + + orderid = "ORD-MULTI" + line_blue = model.OrderLine(orderid, "BLUE-PLINTH", 10) + line_red = model.OrderLine(orderid, "RED-PLINTH", 7) + + services.allocate(line_blue, repo, session) + services.allocate(line_red, repo, session) + + batch_blue = repo.get(reference="b-blue") + batch_red = repo.get(reference="b-red") + assert batch_blue.available_quantity == 90 + assert batch_red.available_quantity == 93 + + services.deallocate(orderid, "BLUE-PLINTH", repo, session) + + assert batch_blue.available_quantity == 100 + assert batch_red.available_quantity == 93 def test_trying_to_deallocate_unallocated_batch(): - ... # TODO: should this error or pass silently? up to you. + """Deallocate fails when order line is not allocated to any batch.""" + repo, session = FakeRepository([]), FakeSession() + services.add_batch("b1", "BLUE-PLINTH", 100, None, repo, session) + line = model.OrderLine("o1", "BLUE-PLINTH", 10) + with pytest.raises(Exception, match="Couldnt find the order line"): + services.deallocate(line.orderid, line.sku, repo, session) \ No newline at end of file