diff --git a/poetry.lock b/poetry.lock index 6b4d2c7..4896ca1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1076,7 +1076,7 @@ sqlite = ["sqlalchemy (>=2.0.30,<2.1.0)"] type = "git" url = "https://github.com/proteanhq/protean.git" reference = "main" -resolved_reference = "97a2dded06ff76df8f4328e2d7cd4e0bbf4a8954" +resolved_reference = "0e50a2bc77b611ce91d8b9a766412a4e4968b19e" [[package]] name = "psycopg2" diff --git a/src/lending/__init__.py b/src/lending/__init__.py index ff883d2..fa4ffc7 100644 --- a/src/lending/__init__.py +++ b/src/lending/__init__.py @@ -19,6 +19,7 @@ from lending.app.dailysheet import DailySheet # isort:skip from lending.app.patron.hold import PlaceHold # isort:skip +from lending.app.patron.checkout import CheckoutBook # isort:skip __all__ = [ @@ -29,6 +30,7 @@ "HoldStatus", "HoldPlaced", "Checkout", + "CheckoutBook", "CheckoutStatus", "Book", "BookStatus", diff --git a/src/lending/app/patron/checkout.py b/src/lending/app/patron/checkout.py new file mode 100644 index 0000000..1fb4618 --- /dev/null +++ b/src/lending/app/patron/checkout.py @@ -0,0 +1,23 @@ +from protean import current_domain, handle +from protean.fields import Identifier + +from lending import Book, Patron, checkout +from lending.domain import lending + + +@lending.command(part_of="Patron") +class CheckoutBook: + patron_id = Identifier(required=True, identifier=True) + book_id = Identifier(required=True) + branch_id = Identifier(required=True) + + +@lending.command_handler(part_of="Patron") +class CheckoutCommandHandler: + @handle(CheckoutBook) + def handle_checkout_book(self, command: CheckoutBook) -> None: + patron = current_domain.repository_for(Patron).get(command.patron_id) + book = current_domain.repository_for(Book).get(command.book_id) + + checkout(patron, book, command.branch_id)() + current_domain.repository_for(Patron).add(patron) diff --git a/src/lending/app/patron/hold.py b/src/lending/app/patron/hold.py index 38e1ac1..bef5c65 100644 --- a/src/lending/app/patron/hold.py +++ b/src/lending/app/patron/hold.py @@ -16,7 +16,7 @@ class PlaceHold: @lending.command_handler(part_of="Patron") class HoldCommandHandler: @handle(PlaceHold) - def handle_PlaceHold(self, command: PlaceHold) -> None: + def handle_place_hold(self, command: PlaceHold) -> None: patron = current_domain.repository_for(Patron).get(command.patron_id) book = current_domain.repository_for(Book).get(command.book_id) diff --git a/tests/lending/app/bdd/checkout_handlers/__init__.py b/tests/lending/app/bdd/checkout_handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/lending/app/bdd/checkout_handlers/features/check_out_a_book.feature b/tests/lending/app/bdd/checkout_handlers/features/check_out_a_book.feature new file mode 100644 index 0000000..5db905f --- /dev/null +++ b/tests/lending/app/bdd/checkout_handlers/features/check_out_a_book.feature @@ -0,0 +1,10 @@ +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 \ No newline at end of file diff --git a/tests/lending/app/bdd/checkout_handlers/step_defs/__init__.py b/tests/lending/app/bdd/checkout_handlers/step_defs/__init__.py new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..2fcccb1 --- /dev/null +++ b/tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py @@ -0,0 +1,75 @@ +from datetime import date, timedelta + +import pytest +from protean import current_domain, g +from pytest_bdd import given, then, when +from pytest_bdd.parsers import cfparse + +from lending import CheckoutBook, HoldStatus, HoldType, Patron, PlaceHold + + +@pytest.fixture(autouse=True) +def reset_globals(): + yield + + if hasattr(g, "current_user"): + delattr(g, "current_user") + if hasattr(g, "current_book"): + delattr(g, "current_book") + if hasattr(g, "current_exception"): + delattr(g, "current_exception") + + +@given("a circulating book is available") +def circulating_book_available(book): + g.current_book = book + + +@given("a patron is logged in") +def patron_logged_in(regular_patron): + g.current_user = regular_patron + + +@given("the patron has a hold on the book") +def patron_has_hold_on_book(): + command = PlaceHold( + patron_id=g.current_user.id, + book_id=g.current_book.id, + branch_id="1", + hold_type=HoldType.CLOSED_ENDED.value, + ) + current_domain.process(command) + + +@when("the patron checks 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) + + +@then("the checkout is successfully completed") +def checkout_completed(): + message = current_domain.event_store.store.read_last_message( + f"library::patron-{g.current_user.id}" + ) + assert message.metadata.type == "Library.BookCheckedOut.v1" + + +@then(cfparse("the checkout has a validity of {validity_days_config}")) +def checkout_validity(validity_days_config): + patron = current_domain.repository_for(Patron).get(g.current_user.id) + checkout = patron.checkouts[0] + assert checkout.due_on == date.today() + timedelta( + days=current_domain.config["custom"][validity_days_config] + ) + + +@then("the hold is marked as checked out") +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 diff --git a/tests/lending/app/bdd/checkout_handlers/test_checkouts.py b/tests/lending/app/bdd/checkout_handlers/test_checkouts.py new file mode 100644 index 0000000..cfd0ad1 --- /dev/null +++ b/tests/lending/app/bdd/checkout_handlers/test_checkouts.py @@ -0,0 +1,5 @@ +from pytest_bdd import scenarios + +from .step_defs.checkout_steps import * # noqa: F403 + +scenarios("./features")