diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/in/web/LibraryController.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/in/web/LibraryController.java new file mode 100644 index 000000000000..279134c8632a --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/in/web/LibraryController.java @@ -0,0 +1,50 @@ +package com.baeldung.ddd.hexagonal.app.adapters.in.web; + +import java.math.BigDecimal; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.ddd.hexagonal.app.domain.Book; +import com.baeldung.ddd.hexagonal.app.domain.Rental; +import com.baeldung.ddd.hexagonal.app.ports.in.LibraryService; + +@RestController +@RequestMapping("/library") +public class LibraryController { + + @Autowired + LibraryService libraryService; + + @PostMapping("/books/{bookId}/rent") + public ResponseEntity rentBook(@PathVariable Long bookId) { + Rental rental = libraryService.rentBook(bookId); + return ResponseEntity.status(HttpStatus.CREATED).build().ofNullable(rental); + } + + @PostMapping("/rentals/{rentalId}") + public ResponseEntity returnBook(@PathVariable Long rentalId) { + Rental rental = libraryService.returnBook(rentalId); + return ResponseEntity.ok().build().ofNullable(rental); + } + + @GetMapping("/rentals/{rentalId}/fine") + public ResponseEntity calculateFine(@PathVariable Long rentalId) { + BigDecimal fine = libraryService.calculateFine(rentalId); + return ResponseEntity.ok(fine); + } + + @GetMapping("/books") + public ResponseEntity> getAllBooks() { + libraryService.saveBook(new Book("5 Point", "Chetan B")); + List books = libraryService.getAllBooks(); + return ResponseEntity.ok(books); + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/BookRepositoryImpl.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/BookRepositoryImpl.java new file mode 100644 index 000000000000..ad6c08ded866 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/BookRepositoryImpl.java @@ -0,0 +1,31 @@ +package com.baeldung.ddd.hexagonal.app.adapters.out.persistence; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import com.baeldung.ddd.hexagonal.app.domain.Book; +import com.baeldung.ddd.hexagonal.app.ports.out.BookRepository; + +@Repository +public class BookRepositoryImpl implements BookRepository { + + @Autowired + private JpaBookRepository jpaBookRepository; + + @Override + public Book save(Book book) { + return jpaBookRepository.save(book); + } + + @Override + public Book findById(Long bookId) { + return jpaBookRepository.findById(bookId).orElse(null); + } + + @Override + public List findAllBooks() { + return jpaBookRepository.findAll(); + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaBookRepository.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaBookRepository.java new file mode 100644 index 000000000000..975a7ce5799d --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaBookRepository.java @@ -0,0 +1,8 @@ +package com.baeldung.ddd.hexagonal.app.adapters.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.baeldung.ddd.hexagonal.app.domain.Book; + +public interface JpaBookRepository extends JpaRepository { +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaRentalRepository.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaRentalRepository.java new file mode 100644 index 000000000000..a73ef59830b4 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/JpaRentalRepository.java @@ -0,0 +1,8 @@ +package com.baeldung.ddd.hexagonal.app.adapters.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.baeldung.ddd.hexagonal.app.domain.Rental; + +public interface JpaRentalRepository extends JpaRepository { +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/RentalRepositoryImpl.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/RentalRepositoryImpl.java new file mode 100644 index 000000000000..ed3cb956086a --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/adapters/out/persistence/RentalRepositoryImpl.java @@ -0,0 +1,24 @@ +package com.baeldung.ddd.hexagonal.app.adapters.out.persistence; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import com.baeldung.ddd.hexagonal.app.domain.Rental; +import com.baeldung.ddd.hexagonal.app.ports.out.RentalRepository; + +@Repository +public class RentalRepositoryImpl implements RentalRepository { + + @Autowired + private JpaRentalRepository jpaRentalRepository; + + @Override + public Rental save(Rental rental) { + return jpaRentalRepository.save(rental); + } + + @Override + public Rental findById(Long rentalId) { + return jpaRentalRepository.findById(rentalId).orElse(null); + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Book.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Book.java new file mode 100644 index 000000000000..d4f8ee87fad5 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Book.java @@ -0,0 +1,54 @@ +package com.baeldung.ddd.hexagonal.app.domain; + +import java.io.Serializable; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Book implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String title; + private String author; + + public Book() { + super(); + } + + public Book(Long id, String title, String author) { + super(); + this.id = id; + this.title = title; + this.author = author; + } + + public Book(String title, String author) { + super(); + this.title = title; + this.author = author; + } + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } + public String getAuthor() { + return author; + } + public void setAuthor(String author) { + this.author = author; + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/LibraryServiceImpl.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/LibraryServiceImpl.java new file mode 100644 index 000000000000..94ff3faf9d9a --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/LibraryServiceImpl.java @@ -0,0 +1,79 @@ +package com.baeldung.ddd.hexagonal.app.domain; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baeldung.ddd.hexagonal.app.ports.in.LibraryService; +import com.baeldung.ddd.hexagonal.app.ports.out.BookRepository; +import com.baeldung.ddd.hexagonal.app.ports.out.RentalRepository; + +@Service +public class LibraryServiceImpl implements LibraryService { + + @Autowired + BookRepository bookRepository; + + @Autowired + RentalRepository rentalRepository; + + @Override + public Rental rentBook(Long bookId) { + Book book = bookRepository.findById(bookId); + if (book == null) { + throw new RuntimeException("Book not found"); + } + + Rental rental = new Rental(); + rental.setBook(book); + rental.setRentDate(LocalDate.now()); + Rental savedRental = rentalRepository.save(rental); + return savedRental; + } + + @Override + public Rental returnBook(Long rentalId) { + Rental rental = rentalRepository.findById(rentalId); + if (rental == null) { + throw new RuntimeException("Rental not found"); + } + + rental.setReturnDate(LocalDate.now()); + rentalRepository.save(rental); + Rental updatedRental = rentalRepository.findById(rentalId); + return updatedRental; + } + + @Override + public BigDecimal calculateFine(Long rentalId) { + Rental rental = rentalRepository.findById(rentalId); + if (rental == null) { + throw new RuntimeException("Rental not found"); + } + + long daysRented = ChronoUnit.DAYS.between(rental.getRentDate(), rental.getReturnDate() != null ? rental.getReturnDate() : LocalDate.now()); + BigDecimal fine = BigDecimal.ZERO; + if (daysRented > 14) { + fine = BigDecimal.valueOf(daysRented - 14).multiply(BigDecimal.valueOf(1)); // Assuming $1 per day after 14 days + } + + rental.setFine(fine); + rentalRepository.save(rental); + + return fine; + } + + @Override + public List getAllBooks() { + return bookRepository.findAllBooks(); + } + + @Override + public Book saveBook(Book book) { + return bookRepository.save(book); + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Rental.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Rental.java new file mode 100644 index 000000000000..864649a2b248 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/domain/Rental.java @@ -0,0 +1,70 @@ +package com.baeldung.ddd.hexagonal.app.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Rental implements Serializable{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Book book; + private LocalDate rentDate; + private LocalDate returnDate; + private BigDecimal fine; + + public Rental() {} + public Rental(Long id, Book book, LocalDate rentDate, LocalDate returnDate, BigDecimal fine) { + super(); + this.id = id; + this.book = book; + this.rentDate = rentDate; + this.returnDate = returnDate; + this.fine = fine; + } + + public Rental(Book book, LocalDate rentDate, LocalDate returnDate, BigDecimal fine) { + super(); + this.book = book; + this.rentDate = rentDate; + this.returnDate = returnDate; + this.fine = fine; + } + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Book getBook() { + return book; + } + public void setBook(Book book) { + this.book = book; + } + public LocalDate getRentDate() { + return rentDate; + } + public void setRentDate(LocalDate rentDate) { + this.rentDate = rentDate; + } + public LocalDate getReturnDate() { + return returnDate; + } + public void setReturnDate(LocalDate returnDate) { + this.returnDate = returnDate; + } + public BigDecimal getFine() { + return fine; + } + public void setFine(BigDecimal fine) { + this.fine = fine; + } +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/in/LibraryService.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/in/LibraryService.java new file mode 100644 index 000000000000..63cd45f397fe --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/in/LibraryService.java @@ -0,0 +1,19 @@ +package com.baeldung.ddd.hexagonal.app.ports.in; + +import java.math.BigDecimal; +import java.util.List; + +import com.baeldung.ddd.hexagonal.app.domain.Book; +import com.baeldung.ddd.hexagonal.app.domain.Rental; + +public interface LibraryService { + Rental rentBook(Long bookId); + + Rental returnBook(Long rentalId); + + BigDecimal calculateFine(Long rentalId); + + List getAllBooks(); + + Book saveBook(Book book); +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/BookRepository.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/BookRepository.java new file mode 100644 index 000000000000..51d6750cdf80 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/BookRepository.java @@ -0,0 +1,11 @@ +package com.baeldung.ddd.hexagonal.app.ports.out; + +import java.util.List; + +import com.baeldung.ddd.hexagonal.app.domain.Book; + +public interface BookRepository { + Book save(Book book); + Book findById(Long bookId); + List findAllBooks(); +} diff --git a/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/RentalRepository.java b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/RentalRepository.java new file mode 100644 index 000000000000..eec84236c167 --- /dev/null +++ b/patterns-modules/ddd/src/main/java/com/baeldung/ddd/hexagonal/app/ports/out/RentalRepository.java @@ -0,0 +1,8 @@ +package com.baeldung.ddd.hexagonal.app.ports.out; + +import com.baeldung.ddd.hexagonal.app.domain.Rental; + +public interface RentalRepository { + Rental save(Rental rental); + Rental findById(Long rentalId); +} diff --git a/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/controller/LibraryControllerIntegrationTests.java b/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/controller/LibraryControllerIntegrationTests.java new file mode 100644 index 000000000000..20d1589c22bf --- /dev/null +++ b/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/controller/LibraryControllerIntegrationTests.java @@ -0,0 +1,92 @@ +package com.baeldung.ddd.hexagonal.app.controller; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import com.baeldung.ddd.hexagonal.app.adapters.in.web.LibraryController; +import com.baeldung.ddd.hexagonal.app.domain.Book; +import com.baeldung.ddd.hexagonal.app.domain.Rental; +import com.baeldung.ddd.hexagonal.app.ports.in.LibraryService; +import com.fasterxml.jackson.databind.ObjectMapper; + +@WebMvcTest(LibraryController.class) +public class LibraryControllerIntegrationTests { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private LibraryService libraryService; + + private Book book; + private Rental rental; + private List bookList; + + @BeforeEach + public void setUp() { + book = new Book(1L, "Test Book", "Test Author"); + rental = new Rental(1L, book, LocalDate.now(), null, BigDecimal.ZERO); + bookList = Arrays.asList(book); + } + + @Test + public void whenRentBook_thenRentalIsReturned() throws Exception { + when(libraryService.rentBook(anyLong())).thenReturn(rental); + + ResultActions resultActions = mockMvc.perform(post("/library/books/1/rent") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + String responseString = resultActions.andReturn().getResponse().getContentAsString(); + Rental responseRental = new ObjectMapper().readValue(responseString, Rental.class); + + assertNotNull(responseRental); + assertEquals(rental.getId(), responseRental.getId()); + } + + @Test + public void whenReturnBook_thenReturnRentalIsReturned() throws Exception { + when(libraryService.returnBook(anyLong())).thenReturn(rental); + + ResultActions resultActions = mockMvc.perform(post("/library/rentals/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + String responseString = resultActions.andReturn().getResponse().getContentAsString(); + Rental responseRental = new ObjectMapper().readValue(responseString, Rental.class); + + assertNotNull(responseRental); + assertEquals(rental.getId(), responseRental.getId()); + } + + @Test + public void whenCalculateFine_thenCorrectFineIsReturned() throws Exception { + BigDecimal fine = BigDecimal.valueOf(10); + when(libraryService.calculateFine(anyLong())).thenReturn(fine); + + mockMvc.perform(get("/library/rentals/1/fine") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(fine.toString())); + } +} diff --git a/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/service/LibraryServiceUnitTest.java b/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/service/LibraryServiceUnitTest.java new file mode 100644 index 000000000000..99a01e28b32a --- /dev/null +++ b/patterns-modules/ddd/src/test/java/com/baeldung/ddd/hexagonal/app/service/LibraryServiceUnitTest.java @@ -0,0 +1,82 @@ +package com.baeldung.ddd.hexagonal.app.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; + +import com.baeldung.ddd.hexagonal.app.domain.Book; +import com.baeldung.ddd.hexagonal.app.domain.LibraryServiceImpl; +import com.baeldung.ddd.hexagonal.app.domain.Rental; +import com.baeldung.ddd.hexagonal.app.ports.out.BookRepository; +import com.baeldung.ddd.hexagonal.app.ports.out.RentalRepository; + +@SpringBootTest +public class LibraryServiceUnitTest { + + @Mock + private BookRepository bookRepository; + + @Mock + private RentalRepository rentalRepository; + + @InjectMocks + private LibraryServiceImpl libraryService; + + private Book book; + private Rental rental; + + @BeforeEach + public void setUp() { + book = new Book(1L, "Test Book", "Author"); + rental = new Rental(1L, book, LocalDate.now(), null, BigDecimal.ZERO); + } + + @Test + public void whenRentBook_thenBookIsRented() { + when(bookRepository.findById(1L)).thenReturn(book); + when(rentalRepository.save(any(Rental.class))).thenReturn(rental); + + libraryService.rentBook(1L); + + verify(bookRepository, times(1)).findById(1L); + verify(rentalRepository, times(1)).save(any(Rental.class)); + } + + @Test + public void whenReturnBook_thenReturnDateIsSet() { + when(rentalRepository.findById(1L)).thenReturn(rental); + + libraryService.returnBook(1L); + + assertNotNull(rental.getReturnDate()); + verify(rentalRepository, times(2)).findById(1L); + verify(rentalRepository, times(1)).save(rental); + } + + @Test + public void whenCalculateFine_thenCorrectFineIsReturned() { + rental.setRentDate(LocalDate.now().minusDays(20)); + when(rentalRepository.findById(1L)).thenReturn(rental); + + BigDecimal fine = libraryService.calculateFine(1L); + + assertEquals(new BigDecimal("6"), fine); // Assuming $1 per day after 14 days + verify(rentalRepository, times(1)).findById(1L); + verify(rentalRepository, times(1)).save(rental); + } +}