diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..4bbab6cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+.idea/
+*.iml
+*.ipr
+*.iws
+
+.classpath
+.project
+.settings/
+
+data/
+*.log
+
+.DS_Store
+Thumbs.db
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..c61c0f5e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+
+ com.ironhack
+ ironlibrary
+ 1.0.0
+ IronLibrary
+ Library Management System - IronHack Unit 3 Homework
+
+
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/src/main/java/com/ironhack/Application.java b/src/main/java/com/ironhack/Application.java
new file mode 100644
index 00000000..2d78ae8d
--- /dev/null
+++ b/src/main/java/com/ironhack/Application.java
@@ -0,0 +1,12 @@
+package com.ironhack;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/src/main/java/com/ironhack/controller/LibraryMenuController.java b/src/main/java/com/ironhack/controller/LibraryMenuController.java
new file mode 100644
index 00000000..81905305
--- /dev/null
+++ b/src/main/java/com/ironhack/controller/LibraryMenuController.java
@@ -0,0 +1,243 @@
+package com.ironhack.controller;
+
+import com.ironhack.dto.request.AddBookRequest;
+import com.ironhack.dto.request.IssueBookRequest;
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.model.MenuOption;
+import com.ironhack.service.LibraryService;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Scanner;
+
+@Component
+@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
+ name = "ironlibrary.menu.enabled",
+ havingValue = "true",
+ matchIfMissing = true
+)
+public class LibraryMenuController implements CommandLineRunner {
+
+ private final LibraryService libraryService;
+ private final Scanner scanner;
+
+ public LibraryMenuController(LibraryService libraryService) {
+ this.libraryService = libraryService;
+ this.scanner = new Scanner(System.in);
+ }
+
+ @Override
+ public void run(String... args) {
+ boolean running = true;
+
+ while (running) {
+ displayMenu();
+ try {
+ int choice = readMenuChoice();
+ MenuOption option = MenuOption.fromValue(choice);
+
+ if (option == null) {
+ System.out.println("Invalid choice. Please select a number from the menu.");
+ continue;
+ }
+
+ running = handleMenuOption(option);
+ } catch (Exception exception) {
+ System.out.println("Error: " + exception.getMessage());
+ }
+ }
+
+ System.out.println("Thank you for using IronLibrary. Goodbye!");
+ }
+
+ public void displayMenu() {
+ System.out.println();
+ System.out.println("===== IronLibrary Menu =====");
+ System.out.println("1. Add a book");
+ System.out.println("2. Search book by title");
+ System.out.println("3. Search book by category");
+ System.out.println("4. Search book by Author");
+ System.out.println("5. List all books along with author");
+ System.out.println("6. Issue book to student");
+ System.out.println("7. List books by usn");
+ System.out.println("8. List books to be returned today");
+ System.out.println("9. Exit");
+ System.out.print("Enter your choice: ");
+ }
+
+ public int readMenuChoice() {
+ while (!scanner.hasNextInt()) {
+ scanner.next();
+ System.out.print("Invalid input. Please enter a number: ");
+ }
+ int choice = scanner.nextInt();
+ scanner.nextLine();
+ return choice;
+ }
+
+ public boolean handleMenuOption(MenuOption option) {
+ return switch (option) {
+ case ADD_BOOK -> {
+ handleAddBook();
+ yield true;
+ }
+ case SEARCH_BY_TITLE -> {
+ handleSearchByTitle();
+ yield true;
+ }
+ case SEARCH_BY_CATEGORY -> {
+ handleSearchByCategory();
+ yield true;
+ }
+ case SEARCH_BY_AUTHOR -> {
+ handleSearchByAuthor();
+ yield true;
+ }
+ case LIST_ALL_BOOKS -> {
+ handleListAllBooks();
+ yield true;
+ }
+ case ISSUE_BOOK -> {
+ handleIssueBook();
+ yield true;
+ }
+ case LIST_BOOKS_BY_USN -> {
+ handleListBooksByUsn();
+ yield true;
+ }
+ case LIST_BOOKS_DUE_TODAY -> {
+ handleListBooksDueToday();
+ yield true;
+ }
+ case EXIT -> false;
+ };
+ }
+
+ private void handleAddBook() {
+ System.out.print("Enter isbn : ");
+ String isbn = scanner.nextLine();
+ System.out.print("Enter title : ");
+ String title = scanner.nextLine();
+ System.out.print("Enter category : ");
+ String category = scanner.nextLine();
+ System.out.print("Enter Author name : ");
+ String authorName = scanner.nextLine();
+ System.out.print("Enter Author mail : ");
+ String authorEmail = scanner.nextLine();
+ System.out.print("Enter number of books : ");
+ int quantity = readPositiveInteger();
+
+ libraryService.addBook(new AddBookRequest(isbn, title, category, authorName, authorEmail, quantity));
+ System.out.println("Book added successfully.");
+ }
+
+ private void handleSearchByTitle() {
+ System.out.print("Enter title : ");
+ String title = scanner.nextLine();
+ printBookTable(libraryService.searchBooksByTitle(title), false);
+ }
+
+ private void handleSearchByCategory() {
+ System.out.print("Enter category : ");
+ String category = scanner.nextLine();
+ printBookTable(libraryService.searchBooksByCategory(category), false);
+ }
+
+ private void handleSearchByAuthor() {
+ System.out.print("Enter name : ");
+ String authorName = scanner.nextLine();
+ printBookTable(libraryService.searchBooksByAuthor(authorName), false);
+ }
+
+ private void handleListAllBooks() {
+ printBookTable(libraryService.listAllBooksWithAuthors(), true);
+ }
+
+ private void handleIssueBook() {
+ System.out.print("Enter usn : ");
+ String usn = scanner.nextLine();
+ System.out.print("Enter name : ");
+ String name = scanner.nextLine();
+ System.out.print("Enter book ISBN : ");
+ String isbn = scanner.nextLine();
+
+ IssueResponse response = libraryService.issueBookToStudent(new IssueBookRequest(usn, name, isbn));
+ System.out.println();
+ System.out.println(response.getIssueReturnMessage());
+ }
+
+ private void handleListBooksByUsn() {
+ System.out.print("Enter usn : ");
+ String usn = scanner.nextLine();
+ printIssueTable(libraryService.listBooksByUsn(usn));
+ }
+
+ private void handleListBooksDueToday() {
+ printIssueTable(libraryService.listBooksDueToday());
+ }
+
+ public void printBookTable(List books, boolean includeAuthor) {
+ System.out.println();
+ if (includeAuthor) {
+ System.out.printf("%-24s%-20s%-12s%-16s%-20s%s%n",
+ "Book ISBN", "Book Title", "Category", "No of Books", "Author name", "Author mail");
+ } else {
+ System.out.printf("%-24s%-20s%-12s%s%n",
+ "Book ISBN", "Book Title", "Category", "No of Books");
+ }
+
+ for (BookResponse book : books) {
+ if (includeAuthor) {
+ System.out.printf("%-24s%-20s%-12s%-16d%-20s%s%n",
+ book.getIsbn(),
+ book.getTitle(),
+ book.getCategory(),
+ book.getQuantity(),
+ book.getAuthorName(),
+ book.getAuthorEmail());
+ } else {
+ System.out.printf("%-24s%-20s%-12s%d%n",
+ book.getIsbn(),
+ book.getTitle(),
+ book.getCategory(),
+ book.getQuantity());
+ }
+ }
+ }
+
+ public void printIssueTable(List issues) {
+ System.out.println();
+ System.out.printf("%-20s%-16s%s%n", "Book Title", "Student Name", "Return date");
+
+ for (IssueResponse issue : issues) {
+ System.out.printf("%-20s%-16s%s%n",
+ issue.getBookTitle(),
+ issue.getStudentName(),
+ issue.getReturnDate());
+ }
+ }
+
+ private int readPositiveInteger() {
+ while (true) {
+ try {
+ if (!scanner.hasNextInt()) {
+ scanner.next();
+ System.out.print("Invalid number. Enter number of books : ");
+ continue;
+ }
+ int value = scanner.nextInt();
+ scanner.nextLine();
+ if (value <= 0) {
+ System.out.print("Number must be greater than zero. Enter number of books : ");
+ continue;
+ }
+ return value;
+ } catch (Exception exception) {
+ scanner.nextLine();
+ System.out.print("Invalid number. Enter number of books : ");
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/ironhack/dto/request/AddBookRequest.java b/src/main/java/com/ironhack/dto/request/AddBookRequest.java
new file mode 100644
index 00000000..3ba78e03
--- /dev/null
+++ b/src/main/java/com/ironhack/dto/request/AddBookRequest.java
@@ -0,0 +1,72 @@
+package com.ironhack.dto.request;
+
+public class AddBookRequest {
+
+ private String isbn;
+ private String title;
+ private String category;
+ private String authorName;
+ private String authorEmail;
+ private Integer quantity;
+
+ public AddBookRequest() {
+ }
+
+ public AddBookRequest(String isbn, String title, String category, String authorName,
+ String authorEmail, Integer quantity) {
+ this.isbn = isbn;
+ this.title = title;
+ this.category = category;
+ this.authorName = authorName;
+ this.authorEmail = authorEmail;
+ this.quantity = quantity;
+ }
+
+ public String getIsbn() {
+ return isbn;
+ }
+
+ public void setIsbn(String isbn) {
+ this.isbn = isbn;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public String getAuthorName() {
+ return authorName;
+ }
+
+ public void setAuthorName(String authorName) {
+ this.authorName = authorName;
+ }
+
+ public String getAuthorEmail() {
+ return authorEmail;
+ }
+
+ public void setAuthorEmail(String authorEmail) {
+ this.authorEmail = authorEmail;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/src/main/java/com/ironhack/dto/request/IssueBookRequest.java b/src/main/java/com/ironhack/dto/request/IssueBookRequest.java
new file mode 100644
index 00000000..0f003633
--- /dev/null
+++ b/src/main/java/com/ironhack/dto/request/IssueBookRequest.java
@@ -0,0 +1,41 @@
+package com.ironhack.dto.request;
+
+public class IssueBookRequest {
+
+ private String usn;
+ private String studentName;
+ private String bookIsbn;
+
+ public IssueBookRequest() {
+ }
+
+ public IssueBookRequest(String usn, String studentName, String bookIsbn) {
+ this.usn = usn;
+ this.studentName = studentName;
+ this.bookIsbn = bookIsbn;
+ }
+
+ public String getUsn() {
+ return usn;
+ }
+
+ public void setUsn(String usn) {
+ this.usn = usn;
+ }
+
+ public String getStudentName() {
+ return studentName;
+ }
+
+ public void setStudentName(String studentName) {
+ this.studentName = studentName;
+ }
+
+ public String getBookIsbn() {
+ return bookIsbn;
+ }
+
+ public void setBookIsbn(String bookIsbn) {
+ this.bookIsbn = bookIsbn;
+ }
+}
diff --git a/src/main/java/com/ironhack/dto/response/BookResponse.java b/src/main/java/com/ironhack/dto/response/BookResponse.java
new file mode 100644
index 00000000..395e62e2
--- /dev/null
+++ b/src/main/java/com/ironhack/dto/response/BookResponse.java
@@ -0,0 +1,79 @@
+package com.ironhack.dto.response;
+
+public class BookResponse {
+
+ private String isbn;
+ private String title;
+ private String category;
+ private Integer quantity;
+ private String authorName;
+ private String authorEmail;
+
+ public BookResponse() {
+ }
+
+ public BookResponse(String isbn, String title, String category, Integer quantity) {
+ this.isbn = isbn;
+ this.title = title;
+ this.category = category;
+ this.quantity = quantity;
+ }
+
+ public BookResponse(String isbn, String title, String category, Integer quantity,
+ String authorName, String authorEmail) {
+ this.isbn = isbn;
+ this.title = title;
+ this.category = category;
+ this.quantity = quantity;
+ this.authorName = authorName;
+ this.authorEmail = authorEmail;
+ }
+
+ public String getIsbn() {
+ return isbn;
+ }
+
+ public void setIsbn(String isbn) {
+ this.isbn = isbn;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public String getAuthorName() {
+ return authorName;
+ }
+
+ public void setAuthorName(String authorName) {
+ this.authorName = authorName;
+ }
+
+ public String getAuthorEmail() {
+ return authorEmail;
+ }
+
+ public void setAuthorEmail(String authorEmail) {
+ this.authorEmail = authorEmail;
+ }
+}
diff --git a/src/main/java/com/ironhack/dto/response/IssueResponse.java b/src/main/java/com/ironhack/dto/response/IssueResponse.java
new file mode 100644
index 00000000..6aaadd24
--- /dev/null
+++ b/src/main/java/com/ironhack/dto/response/IssueResponse.java
@@ -0,0 +1,54 @@
+package com.ironhack.dto.response;
+
+public class IssueResponse {
+
+ private String bookTitle;
+ private String studentName;
+ private String returnDate;
+ private String issueReturnMessage;
+
+ public IssueResponse() {
+ }
+
+ public IssueResponse(String bookTitle, String studentName, String returnDate) {
+ this.bookTitle = bookTitle;
+ this.studentName = studentName;
+ this.returnDate = returnDate;
+ }
+
+ public IssueResponse(String issueReturnMessage) {
+ this.issueReturnMessage = issueReturnMessage;
+ }
+
+ public String getBookTitle() {
+ return bookTitle;
+ }
+
+ public void setBookTitle(String bookTitle) {
+ this.bookTitle = bookTitle;
+ }
+
+ public String getStudentName() {
+ return studentName;
+ }
+
+ public void setStudentName(String studentName) {
+ this.studentName = studentName;
+ }
+
+ public String getReturnDate() {
+ return returnDate;
+ }
+
+ public void setReturnDate(String returnDate) {
+ this.returnDate = returnDate;
+ }
+
+ public String getIssueReturnMessage() {
+ return issueReturnMessage;
+ }
+
+ public void setIssueReturnMessage(String issueReturnMessage) {
+ this.issueReturnMessage = issueReturnMessage;
+ }
+}
diff --git a/src/main/java/com/ironhack/entity/AuthorEntity.java b/src/main/java/com/ironhack/entity/AuthorEntity.java
new file mode 100644
index 00000000..0719e4df
--- /dev/null
+++ b/src/main/java/com/ironhack/entity/AuthorEntity.java
@@ -0,0 +1,71 @@
+package com.ironhack.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "author")
+public class AuthorEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "author_id")
+ private Integer authorId;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String email;
+
+ @OneToOne
+ @JoinColumn(name = "book_isbn", nullable = false, unique = true)
+ private BookEntity authorBook;
+
+ public AuthorEntity() {
+ }
+
+ public AuthorEntity(String name, String email, BookEntity authorBook) {
+ this.name = name;
+ this.email = email;
+ this.authorBook = authorBook;
+ }
+
+ public Integer getAuthorId() {
+ return authorId;
+ }
+
+ public void setAuthorId(Integer authorId) {
+ this.authorId = authorId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public BookEntity getAuthorBook() {
+ return authorBook;
+ }
+
+ public void setAuthorBook(BookEntity authorBook) {
+ this.authorBook = authorBook;
+ }
+}
diff --git a/src/main/java/com/ironhack/entity/BookEntity.java b/src/main/java/com/ironhack/entity/BookEntity.java
new file mode 100644
index 00000000..09a09d29
--- /dev/null
+++ b/src/main/java/com/ironhack/entity/BookEntity.java
@@ -0,0 +1,66 @@
+package com.ironhack.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "book")
+public class BookEntity {
+
+ @Id
+ @Column(name = "isbn", nullable = false, unique = true)
+ private String isbn;
+
+ @Column(nullable = false)
+ private String title;
+
+ @Column(nullable = false)
+ private String category;
+
+ @Column(nullable = false)
+ private Integer quantity;
+
+ public BookEntity() {
+ }
+
+ public BookEntity(String isbn, String title, String category, Integer quantity) {
+ this.isbn = isbn;
+ this.title = title;
+ this.category = category;
+ this.quantity = quantity;
+ }
+
+ public String getIsbn() {
+ return isbn;
+ }
+
+ public void setIsbn(String isbn) {
+ this.isbn = isbn;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/src/main/java/com/ironhack/entity/IssueEntity.java b/src/main/java/com/ironhack/entity/IssueEntity.java
new file mode 100644
index 00000000..806343d8
--- /dev/null
+++ b/src/main/java/com/ironhack/entity/IssueEntity.java
@@ -0,0 +1,84 @@
+package com.ironhack.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "issue")
+public class IssueEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "issue_id")
+ private Integer issueId;
+
+ @Column(name = "issue_date", nullable = false)
+ private String issueDate;
+
+ @Column(name = "return_date", nullable = false)
+ private String returnDate;
+
+ @ManyToOne
+ @JoinColumn(name = "student_usn", nullable = false)
+ private StudentEntity issueStudent;
+
+ @ManyToOne
+ @JoinColumn(name = "book_isbn", nullable = false)
+ private BookEntity issueBook;
+
+ public IssueEntity() {
+ }
+
+ public IssueEntity(String issueDate, String returnDate, StudentEntity issueStudent, BookEntity issueBook) {
+ this.issueDate = issueDate;
+ this.returnDate = returnDate;
+ this.issueStudent = issueStudent;
+ this.issueBook = issueBook;
+ }
+
+ public Integer getIssueId() {
+ return issueId;
+ }
+
+ public void setIssueId(Integer issueId) {
+ this.issueId = issueId;
+ }
+
+ public String getIssueDate() {
+ return issueDate;
+ }
+
+ public void setIssueDate(String issueDate) {
+ this.issueDate = issueDate;
+ }
+
+ public String getReturnDate() {
+ return returnDate;
+ }
+
+ public void setReturnDate(String returnDate) {
+ this.returnDate = returnDate;
+ }
+
+ public StudentEntity getIssueStudent() {
+ return issueStudent;
+ }
+
+ public void setIssueStudent(StudentEntity issueStudent) {
+ this.issueStudent = issueStudent;
+ }
+
+ public BookEntity getIssueBook() {
+ return issueBook;
+ }
+
+ public void setIssueBook(BookEntity issueBook) {
+ this.issueBook = issueBook;
+ }
+}
diff --git a/src/main/java/com/ironhack/entity/StudentEntity.java b/src/main/java/com/ironhack/entity/StudentEntity.java
new file mode 100644
index 00000000..38e7d294
--- /dev/null
+++ b/src/main/java/com/ironhack/entity/StudentEntity.java
@@ -0,0 +1,42 @@
+package com.ironhack.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "student")
+public class StudentEntity {
+
+ @Id
+ @Column(name = "usn", nullable = false, unique = true)
+ private String usn;
+
+ @Column(nullable = false)
+ private String name;
+
+ public StudentEntity() {
+ }
+
+ public StudentEntity(String usn, String name) {
+ this.usn = usn;
+ this.name = name;
+ }
+
+ public String getUsn() {
+ return usn;
+ }
+
+ public void setUsn(String usn) {
+ this.usn = usn;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/com/ironhack/exception/DuplicateResourceException.java b/src/main/java/com/ironhack/exception/DuplicateResourceException.java
new file mode 100644
index 00000000..12e1fc13
--- /dev/null
+++ b/src/main/java/com/ironhack/exception/DuplicateResourceException.java
@@ -0,0 +1,8 @@
+package com.ironhack.exception;
+
+public class DuplicateResourceException extends RuntimeException {
+
+ public DuplicateResourceException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ironhack/exception/InsufficientQuantityException.java b/src/main/java/com/ironhack/exception/InsufficientQuantityException.java
new file mode 100644
index 00000000..ce3c0a92
--- /dev/null
+++ b/src/main/java/com/ironhack/exception/InsufficientQuantityException.java
@@ -0,0 +1,8 @@
+package com.ironhack.exception;
+
+public class InsufficientQuantityException extends RuntimeException {
+
+ public InsufficientQuantityException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ironhack/exception/InvalidInputException.java b/src/main/java/com/ironhack/exception/InvalidInputException.java
new file mode 100644
index 00000000..4f4c3405
--- /dev/null
+++ b/src/main/java/com/ironhack/exception/InvalidInputException.java
@@ -0,0 +1,8 @@
+package com.ironhack.exception;
+
+public class InvalidInputException extends RuntimeException {
+
+ public InvalidInputException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ironhack/exception/ResourceNotFoundException.java b/src/main/java/com/ironhack/exception/ResourceNotFoundException.java
new file mode 100644
index 00000000..734d3333
--- /dev/null
+++ b/src/main/java/com/ironhack/exception/ResourceNotFoundException.java
@@ -0,0 +1,8 @@
+package com.ironhack.exception;
+
+public class ResourceNotFoundException extends RuntimeException {
+
+ public ResourceNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ironhack/mapper/LibraryMapper.java b/src/main/java/com/ironhack/mapper/LibraryMapper.java
new file mode 100644
index 00000000..f3caf671
--- /dev/null
+++ b/src/main/java/com/ironhack/mapper/LibraryMapper.java
@@ -0,0 +1,76 @@
+package com.ironhack.mapper;
+
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.entity.AuthorEntity;
+import com.ironhack.entity.BookEntity;
+import com.ironhack.entity.IssueEntity;
+import com.ironhack.model.Author;
+import com.ironhack.model.Book;
+import com.ironhack.model.Issue;
+import com.ironhack.model.Student;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LibraryMapper {
+
+ public Book toModel(BookEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ return new Book(entity.getIsbn(), entity.getTitle(), entity.getCategory(), entity.getQuantity());
+ }
+
+ public BookEntity toEntity(Book model) {
+ if (model == null) {
+ return null;
+ }
+ return new BookEntity(model.getIsbn(), model.getTitle(), model.getCategory(), model.getQuantity());
+ }
+
+ public Author toModel(AuthorEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ Author author = new Author(entity.getName(), entity.getEmail(), toModel(entity.getAuthorBook()));
+ author.setAuthorId(entity.getAuthorId());
+ return author;
+ }
+
+ public Issue toModel(IssueEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ Student student = new Student(entity.getIssueStudent().getUsn(), entity.getIssueStudent().getName());
+ Book book = toModel(entity.getIssueBook());
+ Issue issue = new Issue(entity.getIssueDate(), entity.getReturnDate(), student, book);
+ issue.setIssueId(entity.getIssueId());
+ return issue;
+ }
+
+ public BookResponse toBookResponse(BookEntity book) {
+ return new BookResponse(book.getIsbn(), book.getTitle(), book.getCategory(), book.getQuantity());
+ }
+
+ public BookResponse toBookResponse(BookEntity book, AuthorEntity author) {
+ if (author == null) {
+ return toBookResponse(book);
+ }
+ return new BookResponse(
+ book.getIsbn(),
+ book.getTitle(),
+ book.getCategory(),
+ book.getQuantity(),
+ author.getName(),
+ author.getEmail()
+ );
+ }
+
+ public IssueResponse toIssueResponse(IssueEntity issue) {
+ return new IssueResponse(
+ issue.getIssueBook().getTitle(),
+ issue.getIssueStudent().getName(),
+ issue.getReturnDate()
+ );
+ }
+}
diff --git a/src/main/java/com/ironhack/model/Author.java b/src/main/java/com/ironhack/model/Author.java
new file mode 100644
index 00000000..0091f984
--- /dev/null
+++ b/src/main/java/com/ironhack/model/Author.java
@@ -0,0 +1,50 @@
+package com.ironhack.model;
+
+public class Author {
+
+ private Integer authorId;
+ private String name;
+ private String email;
+ private Book authorBook;
+
+ public Author() {
+ }
+
+ public Author(String name, String email, Book authorBook) {
+ this.name = name;
+ this.email = email;
+ this.authorBook = authorBook;
+ }
+
+ public Integer getAuthorId() {
+ return authorId;
+ }
+
+ public void setAuthorId(Integer authorId) {
+ this.authorId = authorId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Book getAuthorBook() {
+ return authorBook;
+ }
+
+ public void setAuthorBook(Book authorBook) {
+ this.authorBook = authorBook;
+ }
+}
diff --git a/src/main/java/com/ironhack/model/Book.java b/src/main/java/com/ironhack/model/Book.java
new file mode 100644
index 00000000..0ee3d049
--- /dev/null
+++ b/src/main/java/com/ironhack/model/Book.java
@@ -0,0 +1,51 @@
+package com.ironhack.model;
+
+public class Book {
+
+ private String isbn;
+ private String title;
+ private String category;
+ private Integer quantity;
+
+ public Book() {
+ }
+
+ public Book(String isbn, String title, String category, Integer quantity) {
+ this.isbn = isbn;
+ this.title = title;
+ this.category = category;
+ this.quantity = quantity;
+ }
+
+ public String getIsbn() {
+ return isbn;
+ }
+
+ public void setIsbn(String isbn) {
+ this.isbn = isbn;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/src/main/java/com/ironhack/model/Issue.java b/src/main/java/com/ironhack/model/Issue.java
new file mode 100644
index 00000000..78f7f332
--- /dev/null
+++ b/src/main/java/com/ironhack/model/Issue.java
@@ -0,0 +1,60 @@
+package com.ironhack.model;
+
+public class Issue {
+
+ private Integer issueId;
+ private String issueDate;
+ private String returnDate;
+ private Student issueStudent;
+ private Book issueBook;
+
+ public Issue() {
+ }
+
+ public Issue(String issueDate, String returnDate, Student issueStudent, Book issueBook) {
+ this.issueDate = issueDate;
+ this.returnDate = returnDate;
+ this.issueStudent = issueStudent;
+ this.issueBook = issueBook;
+ }
+
+ public Integer getIssueId() {
+ return issueId;
+ }
+
+ public void setIssueId(Integer issueId) {
+ this.issueId = issueId;
+ }
+
+ public String getIssueDate() {
+ return issueDate;
+ }
+
+ public void setIssueDate(String issueDate) {
+ this.issueDate = issueDate;
+ }
+
+ public String getReturnDate() {
+ return returnDate;
+ }
+
+ public void setReturnDate(String returnDate) {
+ this.returnDate = returnDate;
+ }
+
+ public Student getIssueStudent() {
+ return issueStudent;
+ }
+
+ public void setIssueStudent(Student issueStudent) {
+ this.issueStudent = issueStudent;
+ }
+
+ public Book getIssueBook() {
+ return issueBook;
+ }
+
+ public void setIssueBook(Book issueBook) {
+ this.issueBook = issueBook;
+ }
+}
diff --git a/src/main/java/com/ironhack/model/MenuOption.java b/src/main/java/com/ironhack/model/MenuOption.java
new file mode 100644
index 00000000..d785a4d8
--- /dev/null
+++ b/src/main/java/com/ironhack/model/MenuOption.java
@@ -0,0 +1,32 @@
+package com.ironhack.model;
+
+public enum MenuOption {
+ ADD_BOOK(1),
+ SEARCH_BY_TITLE(2),
+ SEARCH_BY_CATEGORY(3),
+ SEARCH_BY_AUTHOR(4),
+ LIST_ALL_BOOKS(5),
+ ISSUE_BOOK(6),
+ LIST_BOOKS_BY_USN(7),
+ LIST_BOOKS_DUE_TODAY(8),
+ EXIT(9);
+
+ private final int value;
+
+ MenuOption(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public static MenuOption fromValue(int value) {
+ for (MenuOption option : values()) {
+ if (option.value == value) {
+ return option;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/ironhack/model/Student.java b/src/main/java/com/ironhack/model/Student.java
new file mode 100644
index 00000000..207583ed
--- /dev/null
+++ b/src/main/java/com/ironhack/model/Student.java
@@ -0,0 +1,31 @@
+package com.ironhack.model;
+
+public class Student {
+
+ private String usn;
+ private String name;
+
+ public Student() {
+ }
+
+ public Student(String usn, String name) {
+ this.usn = usn;
+ this.name = name;
+ }
+
+ public String getUsn() {
+ return usn;
+ }
+
+ public void setUsn(String usn) {
+ this.usn = usn;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/com/ironhack/repository/AuthorRepository.java b/src/main/java/com/ironhack/repository/AuthorRepository.java
new file mode 100644
index 00000000..0546a885
--- /dev/null
+++ b/src/main/java/com/ironhack/repository/AuthorRepository.java
@@ -0,0 +1,11 @@
+package com.ironhack.repository;
+
+import com.ironhack.entity.AuthorEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface AuthorRepository extends JpaRepository {
+
+ List findByNameIgnoreCase(String name);
+}
diff --git a/src/main/java/com/ironhack/repository/BookRepository.java b/src/main/java/com/ironhack/repository/BookRepository.java
new file mode 100644
index 00000000..ce6404d5
--- /dev/null
+++ b/src/main/java/com/ironhack/repository/BookRepository.java
@@ -0,0 +1,13 @@
+package com.ironhack.repository;
+
+import com.ironhack.entity.BookEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface BookRepository extends JpaRepository {
+
+ List findByTitleIgnoreCase(String title);
+
+ List findByCategoryIgnoreCase(String category);
+}
diff --git a/src/main/java/com/ironhack/repository/IssueRepository.java b/src/main/java/com/ironhack/repository/IssueRepository.java
new file mode 100644
index 00000000..9a023a25
--- /dev/null
+++ b/src/main/java/com/ironhack/repository/IssueRepository.java
@@ -0,0 +1,16 @@
+package com.ironhack.repository;
+
+import com.ironhack.entity.IssueEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface IssueRepository extends JpaRepository {
+
+ List findByIssueStudentUsn(String usn);
+
+ @Query("SELECT i FROM IssueEntity i WHERE i.returnDate LIKE :datePrefix%")
+ List findByReturnDateStartingWith(@Param("datePrefix") String datePrefix);
+}
diff --git a/src/main/java/com/ironhack/repository/StudentRepository.java b/src/main/java/com/ironhack/repository/StudentRepository.java
new file mode 100644
index 00000000..69c1f672
--- /dev/null
+++ b/src/main/java/com/ironhack/repository/StudentRepository.java
@@ -0,0 +1,7 @@
+package com.ironhack.repository;
+
+import com.ironhack.entity.StudentEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StudentRepository extends JpaRepository {
+}
diff --git a/src/main/java/com/ironhack/service/LibraryService.java b/src/main/java/com/ironhack/service/LibraryService.java
new file mode 100644
index 00000000..1e66be9e
--- /dev/null
+++ b/src/main/java/com/ironhack/service/LibraryService.java
@@ -0,0 +1,231 @@
+package com.ironhack.service;
+
+import com.ironhack.dto.request.AddBookRequest;
+import com.ironhack.dto.request.IssueBookRequest;
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.entity.AuthorEntity;
+import com.ironhack.entity.BookEntity;
+import com.ironhack.entity.IssueEntity;
+import com.ironhack.entity.StudentEntity;
+import com.ironhack.exception.DuplicateResourceException;
+import com.ironhack.exception.InsufficientQuantityException;
+import com.ironhack.exception.InvalidInputException;
+import com.ironhack.exception.ResourceNotFoundException;
+import com.ironhack.mapper.LibraryMapper;
+import com.ironhack.repository.AuthorRepository;
+import com.ironhack.repository.BookRepository;
+import com.ironhack.repository.IssueRepository;
+import com.ironhack.repository.StudentRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class LibraryService {
+
+ private static final int LOAN_PERIOD_DAYS = 7;
+
+ private final BookRepository bookRepository;
+ private final AuthorRepository authorRepository;
+ private final StudentRepository studentRepository;
+ private final IssueRepository issueRepository;
+ private final LibraryMapper libraryMapper;
+
+ public LibraryService(BookRepository bookRepository,
+ AuthorRepository authorRepository,
+ StudentRepository studentRepository,
+ IssueRepository issueRepository,
+ LibraryMapper libraryMapper) {
+ this.bookRepository = bookRepository;
+ this.authorRepository = authorRepository;
+ this.studentRepository = studentRepository;
+ this.issueRepository = issueRepository;
+ this.libraryMapper = libraryMapper;
+ }
+
+ @Transactional
+ public void addBook(AddBookRequest request) {
+ validateAddBookRequest(request);
+
+ if (bookRepository.existsById(request.getIsbn())) {
+ throw new DuplicateResourceException("A book with ISBN " + request.getIsbn() + " already exists.");
+ }
+
+ BookEntity book = new BookEntity(
+ request.getIsbn().trim(),
+ request.getTitle().trim(),
+ request.getCategory().trim(),
+ request.getQuantity()
+ );
+ bookRepository.save(book);
+
+ AuthorEntity author = new AuthorEntity(
+ request.getAuthorName().trim(),
+ request.getAuthorEmail().trim(),
+ book
+ );
+ authorRepository.save(author);
+ }
+
+ public List searchBooksByTitle(String title) {
+ validateSearchInput(title, "title");
+ List books = bookRepository.findByTitleIgnoreCase(title.trim());
+ return mapBooksToResponses(books);
+ }
+
+ public List searchBooksByCategory(String category) {
+ validateSearchInput(category, "category");
+ List books = bookRepository.findByCategoryIgnoreCase(category.trim());
+ return mapBooksToResponses(books);
+ }
+
+ public List searchBooksByAuthor(String authorName) {
+ validateSearchInput(authorName, "author name");
+ List authors = authorRepository.findByNameIgnoreCase(authorName.trim());
+ if (authors.isEmpty()) {
+ throw new ResourceNotFoundException("No books found for author: " + authorName);
+ }
+
+ List responses = new ArrayList<>();
+ for (AuthorEntity author : authors) {
+ responses.add(libraryMapper.toBookResponse(author.getAuthorBook()));
+ }
+ return responses;
+ }
+
+ public List listAllBooksWithAuthors() {
+ List authors = authorRepository.findAll();
+ if (authors.isEmpty()) {
+ throw new ResourceNotFoundException("No books available in the library.");
+ }
+
+ List responses = new ArrayList<>();
+ for (AuthorEntity author : authors) {
+ responses.add(libraryMapper.toBookResponse(author.getAuthorBook(), author));
+ }
+ return responses;
+ }
+
+ @Transactional
+ public IssueResponse issueBookToStudent(IssueBookRequest request) {
+ validateIssueBookRequest(request);
+
+ BookEntity book = bookRepository.findById(request.getBookIsbn().trim())
+ .orElseThrow(() -> new ResourceNotFoundException("Book not found with ISBN: " + request.getBookIsbn()));
+
+ if (book.getQuantity() <= 0) {
+ throw new InsufficientQuantityException("No copies available for book: " + book.getTitle());
+ }
+
+ StudentEntity student = studentRepository.findById(request.getUsn().trim())
+ .orElseGet(() -> studentRepository.save(
+ new StudentEntity(request.getUsn().trim(), request.getStudentName().trim())
+ ));
+
+ if (!student.getName().equalsIgnoreCase(request.getStudentName().trim())) {
+ student.setName(request.getStudentName().trim());
+ studentRepository.save(student);
+ }
+
+ Date issueDate = new Date();
+ Date returnDate = calculateReturnDate(issueDate);
+
+ IssueEntity issue = new IssueEntity(
+ issueDate.toString(),
+ formatReturnDateForStorage(returnDate),
+ student,
+ book
+ );
+ issueRepository.save(issue);
+
+ book.setQuantity(book.getQuantity() - 1);
+ bookRepository.save(book);
+
+ return new IssueResponse("Book issued. Return date : " + returnDate);
+ }
+
+ public List listBooksByUsn(String usn) {
+ validateSearchInput(usn, "USN");
+
+ if (!studentRepository.existsById(usn.trim())) {
+ throw new ResourceNotFoundException("No student found with USN: " + usn);
+ }
+
+ List issues = issueRepository.findByIssueStudentUsn(usn.trim());
+ if (issues.isEmpty()) {
+ throw new ResourceNotFoundException("No books issued to student with USN: " + usn);
+ }
+
+ return issues.stream().map(libraryMapper::toIssueResponse).toList();
+ }
+
+ public List listBooksDueToday() {
+ String todayPrefix = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
+ List issues = issueRepository.findByReturnDateStartingWith(todayPrefix);
+
+ if (issues.isEmpty()) {
+ throw new ResourceNotFoundException("No books are due for return today.");
+ }
+
+ return issues.stream().map(libraryMapper::toIssueResponse).toList();
+ }
+
+ public Date calculateReturnDate(Date issueDate) {
+ LocalDateTime issueDateTime = issueDate.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDateTime();
+ LocalDateTime returnDateTime = issueDateTime.plusDays(LOAN_PERIOD_DAYS);
+ return Date.from(returnDateTime.atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ public String formatReturnDateForStorage(Date returnDate) {
+ LocalDateTime returnDateTime = returnDate.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDateTime();
+ return returnDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"));
+ }
+
+ private List mapBooksToResponses(List books) {
+ if (books.isEmpty()) {
+ throw new ResourceNotFoundException("No books found matching your search.");
+ }
+ return books.stream().map(libraryMapper::toBookResponse).toList();
+ }
+
+ private void validateAddBookRequest(AddBookRequest request) {
+ if (request == null) {
+ throw new InvalidInputException("Book details cannot be empty.");
+ }
+ validateSearchInput(request.getIsbn(), "ISBN");
+ validateSearchInput(request.getTitle(), "title");
+ validateSearchInput(request.getCategory(), "category");
+ validateSearchInput(request.getAuthorName(), "author name");
+ validateSearchInput(request.getAuthorEmail(), "author email");
+ if (request.getQuantity() == null || request.getQuantity() <= 0) {
+ throw new InvalidInputException("Number of books must be greater than zero.");
+ }
+ }
+
+ private void validateIssueBookRequest(IssueBookRequest request) {
+ if (request == null) {
+ throw new InvalidInputException("Issue details cannot be empty.");
+ }
+ validateSearchInput(request.getUsn(), "USN");
+ validateSearchInput(request.getStudentName(), "student name");
+ validateSearchInput(request.getBookIsbn(), "book ISBN");
+ }
+
+ private void validateSearchInput(String value, String fieldName) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new InvalidInputException("Invalid " + fieldName + ". Please enter a valid value.");
+ }
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 00000000..74141c29
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,16 @@
+spring.application.name=IronLibrary
+
+spring.main.web-application-type=none
+ironlibrary.menu.enabled=true
+
+spring.datasource.url=jdbc:h2:file:./data/ironlibrary
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=false
+
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2-console
diff --git a/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java b/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java
new file mode 100644
index 00000000..33691575
--- /dev/null
+++ b/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java
@@ -0,0 +1,101 @@
+package com.ironhack.controller;
+
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.model.MenuOption;
+import com.ironhack.service.LibraryService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class LibraryMenuControllerTest {
+
+ @Mock
+ private LibraryService libraryService;
+
+ private LibraryMenuController controller;
+ private ByteArrayOutputStream outputStream;
+
+ @BeforeEach
+ void setUp() {
+ controller = new LibraryMenuController(libraryService);
+ outputStream = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outputStream));
+ }
+
+ @Test
+ void displayMenu_shouldShowAllOptions() {
+ controller.displayMenu();
+
+ String output = outputStream.toString();
+ assertTrue(output.contains("1. Add a book"));
+ assertTrue(output.contains("7. List books by usn"));
+ assertTrue(output.contains("8. List books to be returned today"));
+ assertTrue(output.contains("9. Exit"));
+ }
+
+ @Test
+ void handleMenuOption_shouldReturnFalseForExit() {
+ assertFalse(controller.handleMenuOption(MenuOption.EXIT));
+ }
+
+ @Test
+ void printBookTable_shouldFormatBooksWithoutAuthor() {
+ List books = List.of(
+ new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4)
+ );
+
+ controller.printBookTable(books, false);
+
+ String output = outputStream.toString();
+ assertTrue(output.contains("Book ISBN"));
+ assertTrue(output.contains("The Notebook"));
+ assertTrue(output.contains("Romance"));
+ }
+
+ @Test
+ void printBookTable_shouldFormatBooksWithAuthor() {
+ List books = List.of(
+ new BookResponse(
+ "978-3-16-148410-0",
+ "The Notebook",
+ "Romance",
+ 4,
+ "Nicholas Sparks",
+ "nicholassparks@gmail.com"
+ )
+ );
+
+ controller.printBookTable(books, true);
+
+ String output = outputStream.toString();
+ assertTrue(output.contains("Author name"));
+ assertTrue(output.contains("Nicholas Sparks"));
+ assertTrue(output.contains("nicholassparks@gmail.com"));
+ }
+
+ @Test
+ void printIssueTable_shouldFormatIssuedBooks() {
+ List issues = List.of(
+ new IssueResponse("Da Vinci Code", "John Doe", "2022-08-01 16:45:40.636000")
+ );
+
+ controller.printIssueTable(issues);
+
+ String output = outputStream.toString();
+ assertTrue(output.contains("Book Title"));
+ assertTrue(output.contains("Da Vinci Code"));
+ assertTrue(output.contains("John Doe"));
+ }
+}
diff --git a/src/test/java/com/ironhack/mapper/LibraryMapperTest.java b/src/test/java/com/ironhack/mapper/LibraryMapperTest.java
new file mode 100644
index 00000000..5bafc601
--- /dev/null
+++ b/src/test/java/com/ironhack/mapper/LibraryMapperTest.java
@@ -0,0 +1,113 @@
+package com.ironhack.mapper;
+
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.entity.AuthorEntity;
+import com.ironhack.entity.BookEntity;
+import com.ironhack.entity.IssueEntity;
+import com.ironhack.entity.StudentEntity;
+import com.ironhack.model.Author;
+import com.ironhack.model.Book;
+import com.ironhack.model.Issue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class LibraryMapperTest {
+
+ private LibraryMapper libraryMapper;
+ private BookEntity bookEntity;
+ private AuthorEntity authorEntity;
+ private StudentEntity studentEntity;
+
+ @BeforeEach
+ void setUp() {
+ libraryMapper = new LibraryMapper();
+ bookEntity = new BookEntity("978-3-16-148410-0", "The Notebook", "Romance", 4);
+ authorEntity = new AuthorEntity("Nicholas Sparks", "nicholassparks@gmail.com", bookEntity);
+ authorEntity.setAuthorId(1);
+ studentEntity = new StudentEntity("09003688800", "John Doe");
+ }
+
+ @Test
+ void toModel_shouldMapBookEntityToBook() {
+ Book book = libraryMapper.toModel(bookEntity);
+
+ assertEquals("978-3-16-148410-0", book.getIsbn());
+ assertEquals("The Notebook", book.getTitle());
+ assertEquals("Romance", book.getCategory());
+ assertEquals(4, book.getQuantity());
+ }
+
+ @Test
+ void toEntity_shouldMapBookToBookEntity() {
+ Book book = new Book("978-3-17-148410-0", "Da Vinci Code", "Mystery", 5);
+
+ BookEntity entity = libraryMapper.toEntity(book);
+
+ assertEquals("978-3-17-148410-0", entity.getIsbn());
+ assertEquals("Da Vinci Code", entity.getTitle());
+ }
+
+ @Test
+ void toModel_shouldMapAuthorEntityToAuthor() {
+ Author author = libraryMapper.toModel(authorEntity);
+
+ assertEquals(1, author.getAuthorId());
+ assertEquals("Nicholas Sparks", author.getName());
+ assertEquals("The Notebook", author.getAuthorBook().getTitle());
+ }
+
+ @Test
+ void toBookResponse_shouldMapBookWithoutAuthor() {
+ BookResponse response = libraryMapper.toBookResponse(bookEntity);
+
+ assertEquals("978-3-16-148410-0", response.getIsbn());
+ assertNull(response.getAuthorName());
+ }
+
+ @Test
+ void toBookResponse_shouldMapBookWithAuthor() {
+ BookResponse response = libraryMapper.toBookResponse(bookEntity, authorEntity);
+
+ assertEquals("Nicholas Sparks", response.getAuthorName());
+ assertEquals("nicholassparks@gmail.com", response.getAuthorEmail());
+ }
+
+ @Test
+ void toIssueResponse_shouldMapIssueEntity() {
+ IssueEntity issueEntity = new IssueEntity(
+ "Mon Aug 01 19:45:40 EEST 2022",
+ "2022-08-01 16:45:40.636000",
+ studentEntity,
+ bookEntity
+ );
+
+ IssueResponse response = libraryMapper.toIssueResponse(issueEntity);
+
+ assertEquals("The Notebook", response.getBookTitle());
+ assertEquals("John Doe", response.getStudentName());
+ assertEquals("2022-08-01 16:45:40.636000", response.getReturnDate());
+ }
+
+ @Test
+ void toModel_shouldMapIssueEntityToIssue() {
+ IssueEntity issueEntity = new IssueEntity(
+ "Mon Aug 01 19:45:40 EEST 2022",
+ "2022-08-01 16:45:40.636000",
+ studentEntity,
+ bookEntity
+ );
+ issueEntity.setIssueId(10);
+
+ Issue issue = libraryMapper.toModel(issueEntity);
+
+ assertNotNull(issue);
+ assertEquals(10, issue.getIssueId());
+ assertEquals("John Doe", issue.getIssueStudent().getName());
+ assertEquals("The Notebook", issue.getIssueBook().getTitle());
+ }
+}
diff --git a/src/test/java/com/ironhack/model/MenuOptionTest.java b/src/test/java/com/ironhack/model/MenuOptionTest.java
new file mode 100644
index 00000000..04e3d95a
--- /dev/null
+++ b/src/test/java/com/ironhack/model/MenuOptionTest.java
@@ -0,0 +1,20 @@
+package com.ironhack.model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class MenuOptionTest {
+
+ @Test
+ void fromValue_shouldReturnMatchingOption() {
+ assertEquals(MenuOption.ADD_BOOK, MenuOption.fromValue(1));
+ assertEquals(MenuOption.EXIT, MenuOption.fromValue(9));
+ }
+
+ @Test
+ void fromValue_shouldReturnNullForInvalidValue() {
+ assertNull(MenuOption.fromValue(99));
+ }
+}
diff --git a/src/test/java/com/ironhack/service/LibraryServiceTest.java b/src/test/java/com/ironhack/service/LibraryServiceTest.java
new file mode 100644
index 00000000..04a35e3c
--- /dev/null
+++ b/src/test/java/com/ironhack/service/LibraryServiceTest.java
@@ -0,0 +1,263 @@
+package com.ironhack.service;
+
+import com.ironhack.dto.request.AddBookRequest;
+import com.ironhack.dto.request.IssueBookRequest;
+import com.ironhack.dto.response.BookResponse;
+import com.ironhack.dto.response.IssueResponse;
+import com.ironhack.entity.AuthorEntity;
+import com.ironhack.entity.BookEntity;
+import com.ironhack.entity.IssueEntity;
+import com.ironhack.entity.StudentEntity;
+import com.ironhack.exception.DuplicateResourceException;
+import com.ironhack.exception.InsufficientQuantityException;
+import com.ironhack.exception.InvalidInputException;
+import com.ironhack.exception.ResourceNotFoundException;
+import com.ironhack.mapper.LibraryMapper;
+import com.ironhack.repository.AuthorRepository;
+import com.ironhack.repository.BookRepository;
+import com.ironhack.repository.IssueRepository;
+import com.ironhack.repository.StudentRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class LibraryServiceTest {
+
+ @Mock
+ private BookRepository bookRepository;
+
+ @Mock
+ private AuthorRepository authorRepository;
+
+ @Mock
+ private StudentRepository studentRepository;
+
+ @Mock
+ private IssueRepository issueRepository;
+
+ @Mock
+ private LibraryMapper libraryMapper;
+
+ @InjectMocks
+ private LibraryService libraryService;
+
+ private BookEntity sampleBook;
+ private AuthorEntity sampleAuthor;
+ private StudentEntity sampleStudent;
+
+ @BeforeEach
+ void setUp() {
+ sampleBook = new BookEntity("978-3-16-148410-0", "The Notebook", "Romance", 4);
+ sampleAuthor = new AuthorEntity("Nicholas Sparks", "nicholassparks@gmail.com", sampleBook);
+ sampleStudent = new StudentEntity("09003688800", "John Doe");
+ }
+
+ @Test
+ void addBook_shouldPersistBookAndAuthor() {
+ AddBookRequest request = new AddBookRequest(
+ "978-3-16-148410-0",
+ "The Notebook",
+ "Romance",
+ "Nicholas Sparks",
+ "nicholassparks@gmail.com",
+ 4
+ );
+
+ when(bookRepository.existsById("978-3-16-148410-0")).thenReturn(false);
+
+ libraryService.addBook(request);
+
+ verify(bookRepository).save(any(BookEntity.class));
+ verify(authorRepository).save(any(AuthorEntity.class));
+ }
+
+ @Test
+ void addBook_shouldThrowWhenIsbnAlreadyExists() {
+ AddBookRequest request = new AddBookRequest(
+ "978-3-16-148410-0",
+ "The Notebook",
+ "Romance",
+ "Nicholas Sparks",
+ "nicholassparks@gmail.com",
+ 4
+ );
+
+ when(bookRepository.existsById("978-3-16-148410-0")).thenReturn(true);
+
+ assertThrows(DuplicateResourceException.class, () -> libraryService.addBook(request));
+ verify(bookRepository, never()).save(any());
+ }
+
+ @Test
+ void addBook_shouldThrowWhenQuantityIsInvalid() {
+ AddBookRequest request = new AddBookRequest(
+ "978-3-16-148410-0",
+ "The Notebook",
+ "Romance",
+ "Nicholas Sparks",
+ "nicholassparks@gmail.com",
+ 0
+ );
+
+ assertThrows(InvalidInputException.class, () -> libraryService.addBook(request));
+ }
+
+ @Test
+ void searchBooksByTitle_shouldReturnMatchingBooks() {
+ when(bookRepository.findByTitleIgnoreCase("The Notebook")).thenReturn(List.of(sampleBook));
+ when(libraryMapper.toBookResponse(sampleBook))
+ .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4));
+
+ List results = libraryService.searchBooksByTitle("The Notebook");
+
+ assertEquals(1, results.size());
+ assertEquals("The Notebook", results.get(0).getTitle());
+ }
+
+ @Test
+ void searchBooksByTitle_shouldThrowWhenNoBooksFound() {
+ when(bookRepository.findByTitleIgnoreCase("Unknown")).thenReturn(List.of());
+
+ assertThrows(ResourceNotFoundException.class, () -> libraryService.searchBooksByTitle("Unknown"));
+ }
+
+ @Test
+ void searchBooksByCategory_shouldReturnMatchingBooks() {
+ when(bookRepository.findByCategoryIgnoreCase("Romance")).thenReturn(List.of(sampleBook));
+ when(libraryMapper.toBookResponse(sampleBook))
+ .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4));
+
+ List results = libraryService.searchBooksByCategory("Romance");
+
+ assertEquals(1, results.size());
+ assertEquals("Romance", results.get(0).getCategory());
+ }
+
+ @Test
+ void searchBooksByAuthor_shouldReturnBooksForAuthor() {
+ when(authorRepository.findByNameIgnoreCase("Nicholas Sparks")).thenReturn(List.of(sampleAuthor));
+ when(libraryMapper.toBookResponse(sampleBook))
+ .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4));
+
+ List results = libraryService.searchBooksByAuthor("Nicholas Sparks");
+
+ assertEquals(1, results.size());
+ }
+
+ @Test
+ void listAllBooksWithAuthors_shouldReturnBooksWithAuthorDetails() {
+ when(authorRepository.findAll()).thenReturn(List.of(sampleAuthor));
+ when(libraryMapper.toBookResponse(sampleBook, sampleAuthor))
+ .thenReturn(new BookResponse(
+ "978-3-16-148410-0",
+ "The Notebook",
+ "Romance",
+ 4,
+ "Nicholas Sparks",
+ "nicholassparks@gmail.com"
+ ));
+
+ List results = libraryService.listAllBooksWithAuthors();
+
+ assertEquals(1, results.size());
+ assertEquals("Nicholas Sparks", results.get(0).getAuthorName());
+ }
+
+ @Test
+ void issueBookToStudent_shouldCreateIssueAndDecreaseQuantity() {
+ IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "978-3-16-148410-0");
+
+ when(bookRepository.findById("978-3-16-148410-0")).thenReturn(Optional.of(sampleBook));
+ when(studentRepository.findById("09003688800")).thenReturn(Optional.empty());
+ when(studentRepository.save(any(StudentEntity.class))).thenReturn(sampleStudent);
+
+ IssueResponse response = libraryService.issueBookToStudent(request);
+
+ assertNotNull(response.getIssueReturnMessage());
+ assertTrue(response.getIssueReturnMessage().startsWith("Book issued. Return date :"));
+ verify(issueRepository).save(any(IssueEntity.class));
+ verify(bookRepository).save(sampleBook);
+ assertEquals(3, sampleBook.getQuantity());
+ }
+
+ @Test
+ void issueBookToStudent_shouldThrowWhenBookNotFound() {
+ IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "missing-isbn");
+
+ when(bookRepository.findById("missing-isbn")).thenReturn(Optional.empty());
+
+ assertThrows(ResourceNotFoundException.class, () -> libraryService.issueBookToStudent(request));
+ }
+
+ @Test
+ void issueBookToStudent_shouldThrowWhenNoCopiesAvailable() {
+ sampleBook.setQuantity(0);
+ IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "978-3-16-148410-0");
+
+ when(bookRepository.findById("978-3-16-148410-0")).thenReturn(Optional.of(sampleBook));
+
+ assertThrows(InsufficientQuantityException.class, () -> libraryService.issueBookToStudent(request));
+ }
+
+ @Test
+ void listBooksByUsn_shouldReturnIssuedBooks() {
+ IssueEntity issue = new IssueEntity(
+ "Mon Aug 01 19:45:40 EEST 2022",
+ "2022-08-01 16:45:40.636000",
+ sampleStudent,
+ sampleBook
+ );
+
+ when(studentRepository.existsById("09003688800")).thenReturn(true);
+ when(issueRepository.findByIssueStudentUsn("09003688800")).thenReturn(List.of(issue));
+ when(libraryMapper.toIssueResponse(issue))
+ .thenReturn(new IssueResponse("Da Vinci Code", "John Doe", "2022-08-01 16:45:40.636000"));
+
+ List results = libraryService.listBooksByUsn("09003688800");
+
+ assertEquals(1, results.size());
+ assertEquals("John Doe", results.get(0).getStudentName());
+ }
+
+ @Test
+ void listBooksByUsn_shouldThrowWhenStudentNotFound() {
+ when(studentRepository.existsById("000")).thenReturn(false);
+
+ assertThrows(ResourceNotFoundException.class, () -> libraryService.listBooksByUsn("000"));
+ }
+
+ @Test
+ void calculateReturnDate_shouldBeSevenDaysAfterIssueDate() {
+ Date issueDate = new Date(122, 7, 1, 19, 45, 40);
+
+ Date returnDate = libraryService.calculateReturnDate(issueDate);
+
+ assertTrue(returnDate.after(issueDate));
+ }
+
+ @Test
+ void formatReturnDateForStorage_shouldUseExpectedPattern() {
+ Date returnDate = new Date(122, 7, 8, 16, 45, 40);
+
+ String formatted = libraryService.formatReturnDateForStorage(returnDate);
+
+ assertTrue(formatted.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}"));
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 00000000..f4760965
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,13 @@
+spring.application.name=IronLibrary-test
+
+spring.datasource.url=jdbc:h2:mem:ironlibrary_test;DB_CLOSE_DELAY=-1
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.show-sql=false
+
+spring.main.web-application-type=none
+ironlibrary.menu.enabled=false