Skip to content

Features/loan#12

Merged
eafalkens merged 4 commits into
mainfrom
features/loan
Jan 13, 2026
Merged

Features/loan#12
eafalkens merged 4 commits into
mainfrom
features/loan

Conversation

@Linsss123

@Linsss123 Linsss123 commented Jan 12, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Added "My Loans" menu option to view and manage active loans with formatted return dates
    • Books in search results now display availability status and allow logged-in users to borrow available titles directly

✏️ Tip: You can customize this high-level summary in your review settings.

DennSel and others added 4 commits January 12, 2026 11:35
…loans

Co-authored-by: Dennis Seldén <111012436+dennsel@users.noreply.github.com>
Co-authored-by: Linda Eskilsson <eskilsson.linda@hotmail.se>
Co-authored-by: Erika Falk Svendsen <alicia-fs@outlook.com>
@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a loan management feature for a library system. It updates JPA entity mappings to make Loan the owning side of the Book relationship, refactors the LoanServices API to accept entity objects instead of IDs, adds an "Mina lån" (My Loans) menu option to view and return active loans, and enhances the book search UI to display loan availability and enable book loaning.

Changes

Cohort / File(s) Summary
JPA Entity Mapping Updates
src/main/java/org/example/Book.java, src/main/java/org/example/Loan.java
Changed Book-Loan relationship ownership from Book owning (with @JoinColumn) to Loan owning (with explicit @JoinColumn(name = "bookID")). Book now uses mappedBy for the inverse side.
Loan Service Layer
src/main/java/org/example/LoanServices.java
Updated loanBook() signature from (Long bookId, Long userId) to (User user, Book book). Added activeLoans(User user) method. Enhanced transaction handling for persist/merge/detach operations. Fixed query logic for loan status checks.
Library Application UI & Menu
src/main/java/org/example/App.java
Introduced menu option 4 "Mina lån" to list active loans for the logged-in user. Added interactive loop for returning loaned books with formatted return dates. Created LoanServices instance for loan operations.
Book Search UI Enhancement
src/main/java/org/example/SearchCli.java
Integrated loan status display in book details (showing "Utlånad" or "Tillgänglig"). Added loan functionality allowing logged-in users to borrow available books via loanService.loanBook().

Sequence Diagram

sequenceDiagram
    actor User
    participant App
    participant LoanServices
    participant Database

    User->>App: Select option 4 "Mina lån"
    App->>LoanServices: activeLoans(currentUser)
    LoanServices->>Database: Query loans WHERE user_id = ? AND returnDate IS NOT NULL
    Database-->>LoanServices: Return list of active loans
    LoanServices-->>App: List<Loan>
    App->>User: Display active loans with return dates
    User->>App: Select loan to return
    App->>LoanServices: returnBook(user, book)
    LoanServices->>Database: Fetch loan & detach book
    LoanServices->>Database: Remove loan record
    Database-->>LoanServices: Loan deleted
    LoanServices-->>App: Success
    App->>User: Confirmation & updated loan list
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

  • PR #10: Both PRs modify the same loan-related entities (Book, Loan) and refactor the LoanServices API with changes to method signatures and JPA mappings.
  • PR #6: Both PRs update the JPA OneToOne relationship ownership and JoinColumn configuration between Book and Loan entities.

Poem

🐰 A library now loans with grace,
Books dance from place to place,
Loans tracked in databases bright,
Returns timed just right,
Hopping toward features complete! 📚

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Features/loan' is vague and generic, using a branch name format rather than descriptive language that communicates the actual changes made. Replace with a descriptive title that summarizes the main change, such as 'Add loan management system with active loan listing and book return functionality' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/org/example/LoanServices.java (2)

16-30: Inverted logic in isBookLoaned query.

The query WHERE l.returnDate IS NOT NULL returns loans that have a return date set. Based on loanBook() (line 47), returnDate is set to the expected return date at loan creation. This means isBookLoaned would return true for all loans (since returnDate is always set), making the check ineffective.

If returnDate represents the "expected return date" and loans are deleted when returned, the query should check for the existence of a loan record instead:

🐛 Proposed fix
     public boolean isBookLoaned(Long bookId) {

         List<Loan> loans = em.createQuery(
-            "SELECT l FROM Loan l WHERE l.book.bookId = :bookId AND l.returnDate IS NOT NULL",
+            "SELECT l FROM Loan l WHERE l.book.bookId = :bookId",
             Loan.class
         )
         .setParameter("bookId", bookId)
             .getResultList();

-        if (loans.isEmpty()) {
-            return false;
-        } else {
-            return true;
-        }
+        return !loans.isEmpty();
     }

60-68: Same query logic issue in returnBook.

The query WHERE l.returnDate IS NOT NULL has the same issue as isBookLoaned. Since loans are deleted upon return (line 78), simply find the loan by user and book:

🐛 Proposed fix
         Loan loan = em.createQuery(
-            "SELECT l FROM Loan l WHERE l.user = :user AND l.book = :book AND l.returnDate IS NOT NULL",
+            "SELECT l FROM Loan l WHERE l.user = :user AND l.book = :book",
             Loan.class
         )
🤖 Fix all issues with AI agents
In @src/main/java/org/example/App.java:
- Line 43: The code instantiates LoanServices inside the main loop causing
repeated creation of LoanServices and its EntityManager; move the instantiation
of LoanServices loanServices = new LoanServices(EMFactory.getEntityManager())
out of the loop and create it once before entering the loop alongside searchCli
and userCli so the same LoanServices/EntityManager is reused for each iteration.
- Around line 58-92: This code path lacks a login guard and doesn't handle
non-numeric input: check SessionManager.getCurrentUser() for null before calling
loanServices.activeLoans() (if null, print a message asking the user to log in
and break/return to the menu), and wrap the Integer.parseInt(inputString) call
in a try-catch for NumberFormatException (on catch, print an "invalid input"
message and continue the while loop without crashing); keep using
loans.remove(...) and the existing logic when valid.
🧹 Nitpick comments (5)
src/main/java/org/example/Loan.java (1)

58-60: JPA mapping looks correct, but consider cascade and orphanRemoval.

The @OneToOne with @JoinColumn correctly establishes Loan as the owning side. However, there's no cascade or orphanRemoval configured. If a Loan is removed, the Book reference remains intact (which is likely desired), but ensure this aligns with your persistence strategy in LoanServices.

Also, note the inconsistent column naming convention: bookID vs loan_user (line 55). Consider standardizing to snake_case (e.g., book_id).

src/main/java/org/example/SearchCli.java (2)

115-115: Consider injecting LoanServices instead of creating new instances.

Creating a new LoanServices instance on every call to printBookDetails is inefficient. Consider injecting LoanServices via the constructor or as a class field, similar to how em and searchService are handled.

♻️ Suggested refactor
 public class SearchCli {
         static EntityManager em;
         static BookSearch searchService = new BookSearch();
+        static LoanServices loanServices;
         static Scanner sc;

         // Constructor?
         public SearchCli(Scanner sc, EntityManager em) {
             this.sc = sc;
             this.em = em;
+            this.loanServices = new LoanServices(em);
         }

Then use the field in printBookDetails instead of instantiating a new one.


156-162: Clean up commented-out code and TODO comments.

These lines contain dead code and incomplete TODO items. The TODO mentions redirecting non-logged-in users to the login page, but the current implementation simply doesn't show the loan option to guests. Either implement the TODO or remove these comments if the current behavior is intentional.

Would you like me to help implement the redirect-to-login flow, or should these comments be removed?

src/main/java/org/example/LoanServices.java (2)

46-47: Consider renaming returnDate to dueDate for clarity.

Setting returnDate at loan creation (line 47) suggests this is the due date, not the actual return date. This naming confusion affects the entire codebase. Consider renaming to dueDate or expectedReturnDate to better reflect its purpose.


39-52: Add exception handling with transaction rollback.

If an exception occurs after begin() but before commit(), the transaction remains open. Consider wrapping in try-catch with rollback in the catch/finally block.

♻️ Suggested pattern
try {
    em.getTransaction().begin();
    // ... operations ...
    em.getTransaction().commit();
} catch (Exception e) {
    if (em.getTransaction().isActive()) {
        em.getTransaction().rollback();
    }
    throw e; // or handle appropriately
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84f6fdf and 28ecdc7.

📒 Files selected for processing (5)
  • src/main/java/org/example/App.java
  • src/main/java/org/example/Book.java
  • src/main/java/org/example/Loan.java
  • src/main/java/org/example/LoanServices.java
  • src/main/java/org/example/SearchCli.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/org/example/SearchCli.java (1)
src/main/java/org/example/user/SessionManager.java (1)
  • SessionManager (5-33)
src/main/java/org/example/App.java (2)
src/main/java/org/example/EMFactory.java (1)
  • EMFactory (7-30)
src/main/java/org/example/user/SessionManager.java (1)
  • SessionManager (5-33)
🔇 Additional comments (4)
src/main/java/org/example/SearchCli.java (1)

127-154: LGTM!

The loan status display and interactive loan flow are well-structured. The login check before presenting the loan option is appropriate.

src/main/java/org/example/Book.java (1)

84-85: LGTM!

The mappedBy = "book" correctly establishes Book as the inverse side of the bidirectional relationship with Loan. This is consistent with Loan.java owning the relationship via @JoinColumn.

src/main/java/org/example/App.java (1)

23-24: LGTM!

The DateTimeFormatter is appropriately initialized for displaying loan return dates.

src/main/java/org/example/LoanServices.java (1)

83-91: LGTM!

Given that loans are deleted upon return, this query correctly returns all active loans for the user.


System.out.print("Menyval: ");
String choice = scanner.nextLine();
LoanServices loanServices = new LoanServices(EMFactory.getEntityManager());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move LoanServices instantiation outside the loop.

Creating a new LoanServices (and thus potentially a new EntityManager reference) on every iteration of the main loop is inefficient. This should be instantiated once before the loop, similar to searchCli and userCli.

♻️ Suggested refactor
         SearchCli searchCli = new SearchCli(scanner, EMFactory.getEntityManager());
+        LoanServices loanServices = new LoanServices(EMFactory.getEntityManager());
         DateTimeFormatter formatter =
             DateTimeFormatter.ofPattern("yyyy-MM-dd");
         
         // ...
         
             switch (choice) {
-            LoanServices loanServices = new LoanServices(EMFactory.getEntityManager());
🤖 Prompt for AI Agents
In @src/main/java/org/example/App.java at line 43, The code instantiates
LoanServices inside the main loop causing repeated creation of LoanServices and
its EntityManager; move the instantiation of LoanServices loanServices = new
LoanServices(EMFactory.getEntityManager()) out of the loop and create it once
before entering the loop alongside searchCli and userCli so the same
LoanServices/EntityManager is reused for each iteration.

Comment on lines +58 to +92
case "4" -> {
System.out.println("Dina aktiva lån:");
List<Loan> loans = loanServices.activeLoans(SessionManager.getCurrentUser());

boolean running = true;
while (running) {
if (!loans.isEmpty()) {
int counter = 0;
for (Loan loan : loans) {
counter += 1;
System.out.println(counter + ". Titel: " + loan.getBook().getTitle() + " - Åter: " + loan.getReturnDate()
.format(formatter));
}
System.out.println("Välj nummer för den bok du vill lämna tillbaka (0 = tillbaka): ");
String inputString = scanner.nextLine();
Integer input = Integer.parseInt(inputString);
if (input >= 1 && input <= loans.size()) {
loanServices.returnBook(
SessionManager.getCurrentUser(),
loans.get(input - 1).getBook()
);
loans.remove(input - 1);
System.out.println("Du har lämnat tillbaka din bok!");
} else if (input == 0) {
running = false;
} else {
System.out.println("ogiltigt val.");
}

} else {
System.out.println("Inga aktiva lån.");
running = false;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add login check and handle NumberFormatException.

Two issues:

  1. Missing login guard: While menu option "4" is only displayed when logged in, a user can still type "4" when logged out. SessionManager.getCurrentUser() would return null, causing issues in activeLoans().

  2. Unhandled NumberFormatException: Line 73 uses Integer.parseInt() without try-catch. Non-numeric input will crash the application.

🐛 Proposed fix
                 case "4" -> {
+                    if (!SessionManager.isLoggedIn()) {
+                        System.out.println("Du måste logga in först.");
+                        break;
+                    }
                     System.out.println("Dina aktiva lån:");
                     List<Loan> loans = loanServices.activeLoans(SessionManager.getCurrentUser());

                     boolean running = true;
                     while (running) {
                         if (!loans.isEmpty()) {
                             int counter = 0;
                             for (Loan loan : loans) {
                                 counter += 1;
                                 System.out.println(counter + ". Titel: " + loan.getBook().getTitle() + " - Åter: " + loan.getReturnDate()
                                     .format(formatter));
                             }
                             System.out.println("Välj nummer för den bok du vill lämna tillbaka (0 = tillbaka): ");
                             String inputString = scanner.nextLine();
-                            Integer input = Integer.parseInt(inputString);
+                            int input;
+                            try {
+                                input = Integer.parseInt(inputString);
+                            } catch (NumberFormatException e) {
+                                System.out.println("Skriv en siffra.");
+                                continue;
+                            }
                             if (input >= 1 && input <= loans.size()) {
🤖 Prompt for AI Agents
In @src/main/java/org/example/App.java around lines 58 - 92, This code path
lacks a login guard and doesn't handle non-numeric input: check
SessionManager.getCurrentUser() for null before calling
loanServices.activeLoans() (if null, print a message asking the user to log in
and break/return to the menu), and wrap the Integer.parseInt(inputString) call
in a try-catch for NumberFormatException (on catch, print an "invalid input"
message and continue the while loop without crashing); keep using
loans.remove(...) and the existing logic when valid.

@eafalkens
eafalkens merged commit 832a829 into main Jan 13, 2026
2 checks passed
This was referenced Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants