From 7f2ab3e2c6ca9e7293a6d2245edc80f1a2ae3a30 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Tue, 18 Jun 2024 07:32:05 -0700 Subject: [PATCH] Add Checkout related events along with tests --- src/lending/book.py | 2 +- src/lending/checkout_service.py | 33 ++++++---- src/lending/holding_service.py | 2 +- src/lending/patron.py | 63 +++++++++++++++++++ .../features/check_out_a_book.feature | 1 + .../features/overdue_checkouts.feature | 2 +- .../checkouts/features/return_a_book.feature | 4 +- .../bdd/checkouts/step_defs/checkout_steps.py | 24 ++++++- .../features/overdue_checkouts_sheet.feature | 2 +- 9 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/lending/book.py b/src/lending/book.py index e284a3b..cca27d9 100644 --- a/src/lending/book.py +++ b/src/lending/book.py @@ -3,8 +3,8 @@ from protean import handle from protean.fields import String -from lending.patron import HoldPlaced from lending.domain import lending +from lending.patron import HoldPlaced class BookStatus(Enum): diff --git a/src/lending/checkout_service.py b/src/lending/checkout_service.py index 30c51f5..54a28a0 100644 --- a/src/lending/checkout_service.py +++ b/src/lending/checkout_service.py @@ -1,10 +1,10 @@ from protean import invariant -from protean.exceptions import ValidationError +from protean.exceptions import ObjectNotFoundError, ValidationError from protean.fields import Identifier -from lending.patron import Checkout, HoldStatus, Patron, PatronType from lending.book import Book, BookType from lending.domain import lending +from lending.patron import BookCheckedOut, Checkout, Patron, PatronType @lending.domain_service(part_of=[Patron, Book]) @@ -29,19 +29,30 @@ def regular_patron_cannot_place_hold_on_restricted_book(self): ) def __call__(self): - self.patron.add_checkouts( - Checkout( - book_id=self.book.id, - branch_id=self.branch_id, - ) + checkout = Checkout( + book_id=self.book.id, + branch_id=self.branch_id, ) + self.patron.add_checkouts(checkout) # Find and update hold corresponding to book if it exists + hold = None try: - hold = next(h for h in self.patron.holds if h.book_id == self.book.id) - except StopIteration: + hold = self.patron.get_one_from_holds(book_id=self.book.id) + except ObjectNotFoundError: hold = None if hold: - hold.status = HoldStatus.CHECKED_OUT.value - self.patron.add_holds(hold) + hold.checkout() + + # Raise Event + checkout.raise_( + BookCheckedOut( + patron_id=self.patron.id, + patron_type=self.patron.patron_type, + book_id=self.book.id, + branch_id=self.branch_id, + checkout_date=checkout.checkout_date, + due_date=checkout.due_date, + ) + ) diff --git a/src/lending/holding_service.py b/src/lending/holding_service.py index e32ab4e..efc792d 100644 --- a/src/lending/holding_service.py +++ b/src/lending/holding_service.py @@ -5,8 +5,8 @@ from protean.fields import Identifier from lending.book import Book, BookStatus, BookType -from lending.patron import Hold, HoldPlaced, HoldStatus, HoldType, Patron, PatronType from lending.domain import lending +from lending.patron import Hold, HoldPlaced, HoldStatus, HoldType, Patron, PatronType @lending.domain_service(part_of=[Patron, Book]) diff --git a/src/lending/patron.py b/src/lending/patron.py index e16de18..6cff379 100644 --- a/src/lending/patron.py +++ b/src/lending/patron.py @@ -81,6 +81,9 @@ class Hold: request_date = DateTime(required=True) expiry_date = Date(required=True) + def checkout(self): + self.status = HoldStatus.CHECKED_OUT.value + def expire(self): self.status = HoldStatus.EXPIRED.value @@ -125,6 +128,43 @@ class CheckoutStatus(Enum): OVERDUE = "OVERDUE" +@lending.event(part_of="Patron") +class BookCheckedOut: + """Event raised when a patron checks out a book""" + + patron_id = Identifier(required=True) + patron_type = String(required=True) + book_id = Identifier(required=True) + branch_id = Identifier(required=True) + checkout_date = DateTime(required=True) + due_date = Date(required=True) + + +@lending.event(part_of="Patron") +class BookReturned: + """Event raised when a patron returns a book""" + + patron_id = Identifier(required=True) + patron_type = String(required=True) + book_id = Identifier(required=True) + branch_id = Identifier(required=True) + checkout_date = DateTime(required=True) + due_date = Date(required=True) + return_date = DateTime(required=True) + + +@lending.event(part_of="Patron") +class BookOverdue: + """Event raised when a book is marked overdue""" + + patron_id = Identifier(required=True) + patron_type = String(required=True) + book_id = Identifier(required=True) + branch_id = Identifier(required=True) + checkout_date = DateTime(required=True) + due_date = Date(required=True) + + @lending.entity(part_of="Patron") class Checkout: """The action of a patron borrowing a book from the library @@ -145,9 +185,32 @@ def return_(self): self.status = CheckoutStatus.RETURNED.value self.return_date = datetime.now() + self.raise_( + BookReturned( + patron_id=self._owner.id, + patron_type=self._owner.patron_type, + book_id=self.book_id, + branch_id=self.branch_id, + checkout_date=self.checkout_date, + due_date=self.due_date, + return_date=self.return_date, + ) + ) + def overdue(self): self.status = CheckoutStatus.OVERDUE.value + self.raise_( + BookOverdue( + patron_id=self._owner.id, + patron_type=self._owner.patron_type, + book_id=self.book_id, + branch_id=self.branch_id, + checkout_date=self.checkout_date, + due_date=self.due_date, + ) + ) + @lending.aggregate class Patron: diff --git a/tests/lending/bdd/checkouts/features/check_out_a_book.feature b/tests/lending/bdd/checkouts/features/check_out_a_book.feature index 9df6f3e..222bd94 100644 --- a/tests/lending/bdd/checkouts/features/check_out_a_book.feature +++ b/tests/lending/bdd/checkouts/features/check_out_a_book.feature @@ -7,6 +7,7 @@ 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 Scenario: Patron checks out an available circulating book Given a circulating book is available diff --git a/tests/lending/bdd/checkouts/features/overdue_checkouts.feature b/tests/lending/bdd/checkouts/features/overdue_checkouts.feature index 253639c..f48c4be 100644 --- a/tests/lending/bdd/checkouts/features/overdue_checkouts.feature +++ b/tests/lending/bdd/checkouts/features/overdue_checkouts.feature @@ -3,4 +3,4 @@ Feature: Process Overdue Checkouts Scenario: System processes and updates the status of overdue checkouts Given the system has overdue checkouts When the system processes the overdue checkouts - Then the checkout statuses are updated to overdue + Then the checkouts are marked overdue diff --git a/tests/lending/bdd/checkouts/features/return_a_book.feature b/tests/lending/bdd/checkouts/features/return_a_book.feature index 24e3a0f..d2ec7cc 100644 --- a/tests/lending/bdd/checkouts/features/return_a_book.feature +++ b/tests/lending/bdd/checkouts/features/return_a_book.feature @@ -3,11 +3,11 @@ Feature: Return a Book Scenario: Patron returns a book on or before the due date Given a patron has checked out a book When the patron returns the book - Then the return is successfully processed + Then the book is successfully returned Scenario: Patron returns a book after the due date Given a patron has checked out a book And the book is overdue When the patron returns the book - Then the return is successfully processed + Then the book is successfully returned And the overdue status is cleared diff --git a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py index 1990405..2a3667a 100644 --- a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py +++ b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py @@ -130,6 +130,10 @@ def confirm_checkout_book(): assert len(g.current_user.checkouts) == 1 assert g.current_user.checkouts[0].book_id == g.current_book.id + assert "BookCheckedOut" in [ + event.__class__.__name__ for event in g.current_user._events + ] + @then(cfparse("the checkout has a validity of {validity_days_config}")) def confirm_checkout_expiry(validity_days_config): @@ -150,13 +154,29 @@ def confirm_returned_status(): assert g.current_user.checkouts[0].status == "RETURNED" -@then("the return is successfully processed") +@then("the book is successfully returned") def confirm_successful_return(): assert hasattr(g, "current_exception") is False + assert "BookReturned" in [ + event.__class__.__name__ for event in g.current_user._events + ] + -@then("the checkout statuses are updated to overdue") +@then("the checkouts are marked overdue") def confirm_overdue_marking(): assert g.current_patrons[0].checkouts[0].status == "OVERDUE" assert g.current_patrons[1].checkouts[0].status == "OVERDUE" assert hasattr(g, "current_exception") is False + + assert "BookOverdue" in [ + event.__class__.__name__ for event in g.current_patrons[0]._events + ] + assert "BookOverdue" in [ + event.__class__.__name__ for event in g.current_patrons[1]._events + ] + + +@then("the hold is marked as checked out") +def confirm_hold_checked_out(): + assert g.current_user.holds[0].status == "CHECKED_OUT" diff --git a/tests/lending/bdd/daily_sheet/features/overdue_checkouts_sheet.feature b/tests/lending/bdd/daily_sheet/features/overdue_checkouts_sheet.feature index 70127f2..0197b12 100644 --- a/tests/lending/bdd/daily_sheet/features/overdue_checkouts_sheet.feature +++ b/tests/lending/bdd/daily_sheet/features/overdue_checkouts_sheet.feature @@ -9,4 +9,4 @@ Feature: Generate Daily Sheets for Overdue Checkouts Scenario: System processes and updates the status of overdue checkouts Given the system has generated a daily sheet for overdue checkouts When the system processes the overdue checkouts - Then the checkout statuses are updated to overdue + Then the checkouts are marked overdue