From f09494104f5489a3b2050f0e4d8c2ff9e1e3ae63 Mon Sep 17 00:00:00 2001 From: Ugur Yilmaz Date: Mon, 13 Apr 2026 22:29:22 -0400 Subject: [PATCH 1/3] deallocate and fix issues --- flask_app.py | 17 ++++++++++++++ repository.py | 11 +++++++++ services.py | 25 ++++++++++++++++++++ test_api.py | 8 +++---- test_services.py | 59 ++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 111 insertions(+), 9 deletions(-) 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/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..c0b3a919 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(): + """Use case: order ORD-42 contains multiple products (same order id, different skus). + + Allocation already ties each line to a batch by matching skus. Deallocation still + needs (orderid, sku) because the persistence layer stores one row per line item: + without sku, ``orderid`` alone is ambiguous and could pick the wrong line. + """ + 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. +# def test_trying_to_deallocate_unallocated_batch(): +# ... # TODO: should this error or pass silently? up to you. From 9b1b8380438d7034b18fb010c78f672add41ff38 Mon Sep 17 00:00:00 2001 From: Ugur Yilmaz Date: Mon, 13 Apr 2026 22:43:08 -0400 Subject: [PATCH 2/3] pin sql alchemy --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 091b983da7430da07a537b47231b11e6cc48b252 Mon Sep 17 00:00:00 2001 From: Ugur Yilmaz Date: Mon, 13 Apr 2026 23:01:53 -0400 Subject: [PATCH 3/3] unallocated batch --- test_services.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test_services.py b/test_services.py index c0b3a919..20ee83a8 100644 --- a/test_services.py +++ b/test_services.py @@ -91,12 +91,7 @@ def test_deallocate_decrements_correct_quantity(): def test_deallocate_needs_sku_when_one_order_has_several_line_items(): - """Use case: order ORD-42 contains multiple products (same order id, different skus). - Allocation already ties each line to a batch by matching skus. Deallocation still - needs (orderid, sku) because the persistence layer stores one row per line item: - without sku, ``orderid`` alone is ambiguous and could pick the wrong line. - """ 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) @@ -119,5 +114,10 @@ def test_deallocate_needs_sku_when_one_order_has_several_line_items(): assert batch_red.available_quantity == 93 -# def test_trying_to_deallocate_unallocated_batch(): -# ... # TODO: should this error or pass silently? up to you. +def test_trying_to_deallocate_unallocated_batch(): + """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