diff --git a/.vscode/launch.json b/.vscode/launch.json index a45d466..4dff089 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "module": "pytest", "justMyCode": false, "args": [ - "tests/lending/model/bdd/holds/test_holds.py::test_regular_patron_tries_to_place_an_openended_hold", + "tests/lending/app/bdd/checkout_handlers/test_checkouts.py::test_patron_returns_a_book_after_the_due_date", ] }, ] diff --git a/src/lending/__init__.py b/src/lending/__init__.py index feafc08..750d356 100644 --- a/src/lending/__init__.py +++ b/src/lending/__init__.py @@ -19,7 +19,7 @@ from lending.app.dailysheet import DailySheet # isort:skip from lending.app.patron.hold import CancelHold, PlaceHold # isort:skip -from lending.app.patron.checkout import CheckoutBook # isort:skip +from lending.app.patron.checkout import CheckoutBook, ReturnBook # isort:skip __all__ = [ @@ -31,6 +31,7 @@ "HoldPlaced", "Checkout", "CheckoutBook", + "ReturnBook", "CheckoutStatus", "Book", "BookStatus", diff --git a/src/lending/app/patron/checkout.py b/src/lending/app/patron/checkout.py index 1fb4618..7950c72 100644 --- a/src/lending/app/patron/checkout.py +++ b/src/lending/app/patron/checkout.py @@ -12,6 +12,12 @@ class CheckoutBook: branch_id = Identifier(required=True) +@lending.command(part_of="Patron") +class ReturnBook: + patron_id = Identifier(required=True, identifier=True) + book_id = Identifier(required=True) + + @lending.command_handler(part_of="Patron") class CheckoutCommandHandler: @handle(CheckoutBook) @@ -21,3 +27,10 @@ def handle_checkout_book(self, command: CheckoutBook) -> None: checkout(patron, book, command.branch_id)() current_domain.repository_for(Patron).add(patron) + + @handle(ReturnBook) + def handle_return_book(self, command: ReturnBook) -> None: + patron = current_domain.repository_for(Patron).get(command.patron_id) + + patron.return_book(command.book_id) + current_domain.repository_for(Patron).add(patron) diff --git a/tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py b/tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py index 2fcccb1..dfe7439 100644 --- a/tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py +++ b/tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py @@ -1,11 +1,22 @@ from datetime import date, timedelta import pytest -from protean import current_domain, g +from protean import UnitOfWork, current_domain, g from pytest_bdd import given, then, when from pytest_bdd.parsers import cfparse -from lending import CheckoutBook, HoldStatus, HoldType, Patron, PlaceHold +from lending import ( + Book, + Checkout, + CheckoutBook, + CheckoutStatus, + DailySheetService, + HoldStatus, + HoldType, + Patron, + PlaceHold, + ReturnBook, +) @pytest.fixture(autouse=True) @@ -25,7 +36,13 @@ def circulating_book_available(book): g.current_book = book +@given("a restricted book is available") +def restricted_book(restricted_book): + g.current_book = restricted_book + + @given("a patron is logged in") +@given("a regular patron is logged in") def patron_logged_in(regular_patron): g.current_user = regular_patron @@ -41,14 +58,116 @@ def patron_has_hold_on_book(): current_domain.process(command) +@given("a patron has checked out a book") +def patron_with_checkout(regular_patron, book): + g.current_user = regular_patron + g.current_book = book + + command = CheckoutBook( + patron_id=g.current_user.id, + book_id=g.current_book.id, + branch_id="1", + ) + try: + current_domain.process(command) + except Exception as e: + g.current_exception = e + + +@given("the system has overdue checkouts") +def system_has_overdue_checkouts(): + patron1 = Patron() + current_domain.repository_for(Patron).add(patron1) + patron2 = Patron() + current_domain.repository_for(Patron).add(patron2) + + book1 = Book(isbn="1234567890123") + current_domain.repository_for(Book).add(book1) + book2 = Book(isbn="1234567890124") + current_domain.repository_for(Book).add(book2) + book3 = Book(isbn="1234567890125") + current_domain.repository_for(Book).add(book3) + + patron1 = current_domain.repository_for(Patron).get(patron1.id) + patron2 = current_domain.repository_for(Patron).get(patron2.id) + + patron1.add_checkouts( + [ + Checkout( + book_id=book1.id, + branch_id="1", + ), + Checkout( + book_id=book2.id, + branch_id="1", + ), + ] + ) + # Manually expire a checkout + patron1.checkouts[0].due_on = date.today() - timedelta(days=1) + + patron2.add_checkouts( + Checkout( + book_id=book3.id, + branch_id="1", + ) + ) + # Manually exipre a checkout + patron2.checkouts[0].due_on = date.today() - timedelta(days=1) + + current_domain.repository_for(Patron).add(patron1) + current_domain.repository_for(Patron).add(patron2) + + g.current_patrons = [patron1, patron2] + g.patron1_checkout_overdue_id = patron1.checkouts[0].id + g.patron2_checkout_overdue_id = patron2.checkouts[0].id + + +@given("the book is overdue") +def mark_checkout_overdue(): + patron = current_domain.repository_for(Patron).get(g.current_user.id) + + patron.checkouts[0].due_on = date.today() - timedelta(days=1) + patron.checkouts[0].status = CheckoutStatus.OVERDUE.value + + current_domain.repository_for(Patron).add(patron) + + @when("the patron checks out the book") +@when("the patron tries to check out the book") def patron_checks_out_book(): command = CheckoutBook( patron_id=g.current_user.id, book_id=g.current_book.id, branch_id="1", ) - current_domain.process(command) + try: + current_domain.process(command) + except Exception as e: + g.current_exception = e + + +@when("the system processes the overdue checkouts") +def process_overdue_checkouts(): + with UnitOfWork(): + patron1 = current_domain.repository_for(Patron).get(g.current_patrons[0].id) + patron2 = current_domain.repository_for(Patron).get(g.current_patrons[1].id) + DailySheetService(patrons=[patron1, patron2]).run() + + current_domain.repository_for(Patron).add(patron1) + current_domain.repository_for(Patron).add(patron2) + + +@when("the patron returns the book") +def patron_returns_book(): + command = ReturnBook( + patron_id=g.current_user.id, + book_id=g.current_book.id, + ) + try: + current_domain.process(command) + except Exception as e: + g.current_exception = e @then("the checkout is successfully completed") @@ -73,3 +192,56 @@ def hold_marked_checked_out(): patron = current_domain.repository_for(Patron).get(g.current_user.id) hold = patron.holds[0] assert hold.status == HoldStatus.CHECKED_OUT.value + + +@then("the checkout is rejected") +def checkout_rejected(): + assert isinstance(g.current_exception, Exception) + assert ( + str(g.current_exception) + == "{'restricted': ['Regular patron cannot place a hold on a restricted book']}" + ) + + +@then("the checkouts are marked overdue") +def confirm_overdue_marking(): + patron1 = current_domain.repository_for(Patron).get(g.current_patrons[0].id) + patron2 = current_domain.repository_for(Patron).get(g.current_patrons[1].id) + + patron1_checkout = next( + ( + checkout + for checkout in patron1.checkouts + if checkout.id == g.patron1_checkout_overdue_id + ), + None, + ) + patron2_checkout = next( + ( + checkout + for checkout in patron2.checkouts + if checkout.id == g.patron2_checkout_overdue_id + ), + None, + ) + assert patron1_checkout.status == "OVERDUE" + assert patron2_checkout.status == "OVERDUE" + + if hasattr(g, "current_exception"): + print(g.current_exception.messages) + assert hasattr(g, "current_exception") is False + + +@then("the book is successfully returned") +def book_successfully_returned(): + book = current_domain.repository_for(Book).get(g.current_book.id) + assert book.status == "AVAILABLE" + + assert hasattr(g, "current_exception") is False + + +@then("the overdue status is cleared") +def overdue_status_cleared(): + patron = current_domain.repository_for(Patron).get(g.current_user.id) + checkout = patron.checkouts[0] + assert checkout.status != "OVERDUE" diff --git a/tests/lending/features/checkouts/check_out_a_book.feature b/tests/lending/features/checkouts/check_out_a_book.feature index 5db905f..454896c 100644 --- a/tests/lending/features/checkouts/check_out_a_book.feature +++ b/tests/lending/features/checkouts/check_out_a_book.feature @@ -7,4 +7,16 @@ Feature: Check Out a Book When the patron checks out the book Then the checkout is successfully completed And the checkout has a validity of CHECKOUT_PERIOD - And the hold is marked as checked out \ No newline at end of file + And the hold is marked as checked out + + Scenario: Patron checks out an available circulating book + Given a circulating book is available + And a patron is logged in + When the patron checks out the book + Then the checkout is successfully completed + + Scenario: Regular Patron tries to check out a restricted book without holding + Given a restricted book is available + And a regular patron is logged in + When the patron tries to check out the book + Then the checkout is rejected diff --git a/tests/lending/model/bdd/checkouts/features/overdue_checkouts.feature b/tests/lending/features/checkouts/overdue_checkouts.feature similarity index 100% rename from tests/lending/model/bdd/checkouts/features/overdue_checkouts.feature rename to tests/lending/features/checkouts/overdue_checkouts.feature diff --git a/tests/lending/model/bdd/checkouts/features/return_a_book.feature b/tests/lending/features/checkouts/return_a_book.feature similarity index 100% rename from tests/lending/model/bdd/checkouts/features/return_a_book.feature rename to tests/lending/features/checkouts/return_a_book.feature diff --git a/tests/lending/model/bdd/checkouts/features/check_out_a_book.feature b/tests/lending/model/bdd/checkouts/features/check_out_a_book.feature deleted file mode 100644 index 454896c..0000000 --- a/tests/lending/model/bdd/checkouts/features/check_out_a_book.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: Check Out a Book - - Scenario: Patron checks out a book on hold - Given a circulating book is available - And a patron is logged in - And the patron has a hold on the book - When the patron checks out the book - Then the checkout is successfully completed - And the checkout has a validity of CHECKOUT_PERIOD - And the hold is marked as checked out - - Scenario: Patron checks out an available circulating book - Given a circulating book is available - And a patron is logged in - When the patron checks out the book - Then the checkout is successfully completed - - Scenario: Regular Patron tries to check out a restricted book without holding - Given a restricted book is available - And a regular patron is logged in - When the patron tries to check out the book - Then the checkout is rejected diff --git a/tests/lending/model/bdd/checkouts/test_checkouts.py b/tests/lending/model/bdd/checkouts/test_checkouts.py index cfd0ad1..6bff1dd 100644 --- a/tests/lending/model/bdd/checkouts/test_checkouts.py +++ b/tests/lending/model/bdd/checkouts/test_checkouts.py @@ -2,4 +2,4 @@ from .step_defs.checkout_steps import * # noqa: F403 -scenarios("./features") +scenarios("../../../features/checkouts")