Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check-in functionality for borrowed books #29

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/main/java/example/borrow/application/CirculationDesk.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,32 @@ public HoldInformation checkout(Hold.Checkout command) {

return HoldInformation.from(
hold.checkout(command)
.then(holds::save)
);
.then(holds::save));
}

public HoldInformation checkin(Hold.Checkin command) {
var hold = holds.findById(command.holdId())
.orElseThrow(() -> new IllegalArgumentException("Hold not found!"));

if (!hold.isCheckedOut()) {
throw new IllegalArgumentException("Book is not checked out");
}

if (!hold.isHeldBy(command.patronId())) {
throw new IllegalArgumentException("Hold belongs to a different patron");
}

return HoldInformation.from(
hold.checkin(command)
.then(holds::save));
}

@ApplicationModuleListener
public void handle(Hold.BookCheckedIn event) {
books.findCheckedOutBook(new Book.Barcode(event.inventoryNumber()))
.map(Book::markAvailable)
.map(books::save)
.orElseThrow(() -> new IllegalArgumentException("Book not checked out?"));
}

@ApplicationModuleListener
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/example/borrow/application/HoldInformation.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public class HoldInformation {
private final String patronId;
private final LocalDate dateOfHold;
private final LocalDate dateOfCheckout;
private final LocalDate dateOfCheckin;
private final Hold.HoldStatus holdStatus;

private HoldInformation(String id, String bookBarcode, String patronId, LocalDate dateOfHold, LocalDate dateOfCheckout, Hold.HoldStatus holdStatus) {
private HoldInformation(String id, String bookBarcode, String patronId, LocalDate dateOfHold, LocalDate dateOfCheckout, LocalDate dateOfCheckin, Hold.HoldStatus holdStatus) {
this.id = id;
this.bookBarcode = bookBarcode;
this.patronId = patronId;
this.dateOfHold = dateOfHold;
this.dateOfCheckout = dateOfCheckout;
this.dateOfCheckin = dateOfCheckin;
this.holdStatus = holdStatus;
}

Expand All @@ -29,6 +31,9 @@ public static HoldInformation from(Hold hold) {
hold.getId().id().toString(),
hold.getOnBook().barcode(),
hold.getHeldBy().email(),
hold.getDateOfHold(), hold.getDateOfCheckout(), hold.getStatus());
hold.getDateOfHold(),
hold.getDateOfCheckout(),
hold.getDateOfCheckin(),
hold.getStatus());
}
}
}
7 changes: 6 additions & 1 deletion src/main/java/example/borrow/domain/Book.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public Book markCheckedOut() {
return this;
}

public Book markAvailable() {
this.status = BookStatus.AVAILABLE;
return this;
}

public record BookId(UUID id) implements Identifier {
}

Expand All @@ -82,4 +87,4 @@ public enum BookStatus implements ValueObject {
public record AddBook(Barcode barcode, String title, String isbn) {
}

}
}
6 changes: 5 additions & 1 deletion src/main/java/example/borrow/domain/BookRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ public interface BookRepository extends CrudRepository<Book, Book.BookId> {

Optional<Book> findByInventoryNumber(Book.Barcode inventoryNumber);

}
@Query("""
SELECT b FROM Book b WHERE b.inventoryNumber = :inventoryNumber AND b.status = 'CHECKED_OUT'
""")
Optional<Book> findCheckedOutBook(Book.Barcode inventoryNumber);
}
23 changes: 22 additions & 1 deletion src/main/java/example/borrow/domain/Hold.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.data.domain.AbstractAggregateRoot;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.function.UnaryOperator;

Expand Down Expand Up @@ -43,6 +44,8 @@ public class Hold extends AbstractAggregateRoot<Hold> {

private LocalDate dateOfCheckout;

private LocalDate dateOfCheckin;

@Enumerated(EnumType.STRING)
private HoldStatus status;

Expand Down Expand Up @@ -70,6 +73,12 @@ public Hold checkout(Checkout command) {
return this;
}

public Hold checkin(Checkin command) {
this.status = HoldStatus.RETURNED;
this.dateOfCheckin = command.dateOfCheckin();
return this;
}

public Hold then(UnaryOperator<Hold> function) {
return function.apply(this);
}
Expand All @@ -78,11 +87,15 @@ public boolean isHeldBy(PatronId patronId) {
return this.heldBy.equals(patronId);
}

public boolean isCheckedOut() {
return status == HoldStatus.ACTIVE;
}

public record HoldId(UUID id) implements Identifier {
}

public enum HoldStatus {
HOLDING, ACTIVE, COMPLETED
HOLDING, ACTIVE, RETURNED
}

///
Expand All @@ -96,6 +109,8 @@ public record Checkout(HoldId holdId, LocalDate dateOfCheckout, PatronId patronI

}

public record Checkin(HoldId holdId, LocalDate dateOfCheckin, PatronId patronId) {}

///
// Events
///
Expand All @@ -111,4 +126,10 @@ public record BookPlacedOnHold(UUID holdId,
String inventoryNumber,
LocalDate dateOfHold) {
}

@DomainEvent
public record BookCheckedIn(UUID holdId,
String inventoryNumber,
LocalDateTime dateOfCheckin) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ ResponseEntity<HoldInformation> checkoutBook(@PathVariable("id") UUID holdId, @A
return ResponseEntity.ok(hold);
}

@PostMapping("/borrow/holds/{id}/checkin")
ResponseEntity<HoldInformation> checkinBook(@PathVariable("id") UUID holdId, @Authenticated UserAccount userAccount) {
var command = new Hold.Checkin(new Hold.HoldId(holdId), LocalDate.now(), new PatronId(userAccount.email()));
var hold = circulationDesk.checkin(command);
return ResponseEntity.ok(hold);
}

@GetMapping("/borrow/holds/{id}")
ResponseEntity<HoldInformation> viewSingleHold(@PathVariable("id") UUID holdId) {
return circulationDesk.locate(holdId)
Expand Down
14 changes: 13 additions & 1 deletion src/test/java/example/borrow/CirculationDeskControllerIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@ void checkoutBookRestCall() throws Exception {
.andExpect(jsonPath("$.dateOfCheckout").isNotEmpty())
.andExpect(jsonPath("$.holdStatus", equalTo("ACTIVE")));
}
}

@Test
void checkinBookRestCall() throws Exception {
mockMvc.perform(post("/borrow/holds/018dc74a-9c4e-743f-916f-e152f190d13e/checkin")
.with(jwt().jwt(jwt -> jwt.claim("email", "[email protected]"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", equalTo("018dc74a-9c4e-743f-916f-e152f190d13e")))
.andExpect(jsonPath("$.bookBarcode", equalTo("55667788")))
.andExpect(jsonPath("$.patronId", equalTo("[email protected]")))
.andExpect(jsonPath("$.dateOfCheckin").isNotEmpty())
.andExpect(jsonPath("$.holdStatus", equalTo("RETURNED")));
}
}
41 changes: 41 additions & 0 deletions src/test/java/example/borrow/CirculationDeskTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,45 @@ void bookStatusUpdatedWhenCheckoutBook() {
// Assert
assertThat(book.getStatus()).isEqualTo(ISSUED);
}

@Test
void patronCanCheckinBook() {
// Arrange
var patronId = new PatronId("[email protected]");
var book = Book.addBook(new Book.AddBook(new Book.Barcode("12345"), "Test Book", "1234567890"));
var hold = Hold.placeHold(new Hold.PlaceHold(book.getInventoryNumber(), LocalDate.now(), patronId));
hold.checkout(new Hold.Checkout(hold.getId(), LocalDate.now(), patronId));

var checkinCommand = new Hold.Checkin(hold.getId(), LocalDate.now(), patronId);

when(holdRepository.findById(hold.getId())).thenReturn(Optional.of(hold));
when(holdRepository.save(any())).thenReturn(hold);

// Act
var holdInformation = circulationDesk.checkin(checkinCommand);

// Assert
assertThat(holdInformation.getId()).isEqualTo(hold.getId().id().toString());
assertThat(holdInformation.getDateOfCheckin()).isNotNull();
assertThat(hold.getStatus()).isEqualTo(Hold.HoldStatus.RETURNED);
}

@Test
void patronCannotCheckinBookHeldBySomeoneElse() {
// Arrange
var patronId = new PatronId("[email protected]");
var otherPatronId = new PatronId("[email protected]");
var book = Book.addBook(new Book.AddBook(new Book.Barcode("12345"), "Test Book", "1234567890"));
var hold = Hold.placeHold(new Hold.PlaceHold(book.getInventoryNumber(), LocalDate.now(), patronId));
hold.checkout(new Hold.Checkout(hold.getId(), LocalDate.now(), patronId));

var checkinCommand = new Hold.Checkin(hold.getId(), LocalDate.now(), otherPatronId);

when(holdRepository.findById(hold.getId())).thenReturn(Optional.of(hold));

// Act & Assert
assertThatIllegalArgumentException()
.isThrownBy(() -> circulationDesk.checkin(checkinCommand))
.withMessage("Hold belongs to a different patron");
}
}
10 changes: 6 additions & 4 deletions src/test/resources/borrow.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ INSERT INTO borrow_books (id, version, title, barcode, isbn, status)
VALUES ('018dc771-7b96-776b-980d-caf7c6b2c00b', 0, 'Sapiens', '13268510', '9780062316097', 'AVAILABLE'),
('018dc771-6e03-7f3b-adc1-0b9f9810bde4', 0, 'Moby-Dick', '64321704', '9780763630188', 'AVAILABLE'),
('018dc771-97e4-7e1e-921f-50d3397d6b32', 0, 'To Kill a Mockingbird', '49031878', '9780446310789', 'ON_HOLD'),
('018dc771-bd5f-71c5-b481-e9b9e8268c6c', 0, '1984', '37040952', '9780451520500', 'ISSUED');
('018dc771-bd5f-71c5-b481-e9b9e8268c6c', 0, '1984', '37040952', '9780451520500', 'ISSUED'),
('018dc771-ce5a-7d2f-b591-fa9f98375c1d', 0, 'The Great Gatsby', '55667788', '9780743273565', 'ISSUED');

INSERT INTO borrow_holds (id, version, book_barcode, patron_id, date_of_hold, status)
VALUES ('018dc74a-4830-75cf-a194-5e9815727b02', 0, '49031878', '[email protected]', '2023-03-11', 'HOLDING'),
('018dc74a-8b3d-732e-806f-d210f079c0cc', 0, '37040952', '[email protected]', '2023-03-24', 'ACTIVE');
INSERT INTO borrow_holds (id, version, book_barcode, patron_id, date_of_hold, date_of_checkout, status)
VALUES ('018dc74a-4830-75cf-a194-5e9815727b02', 0, '49031878', '[email protected]', '2023-03-11', null, 'HOLDING'),
('018dc74a-8b3d-732e-806f-d210f079c0cc', 0, '37040952', '[email protected]', '2023-03-24', null, 'ACTIVE'),
('018dc74a-9c4e-743f-916f-e152f190d13e', 0, '55667788', '[email protected]', '2023-03-24', '2023-03-25', 'ACTIVE');