Compare commits

5 Commits

Author SHA1 Message Date
AISSI-JUDE-CHRIST 4815d2d0ba Suite des tests pour passer les commandes, correction des tests 2026-04-17 17:23:02 +02:00
AISSI-JUDE-CHRIST 7b4b8124a7 stock insufisant & tests à corriger 2026-04-14 14:25:47 +02:00
AISSI-JUDE-CHRIST 84b2880b1d Passer des commandes 2026-04-14 13:56:39 +02:00
aissi 44a458dfbe BookIsbn et correction BookUseCase 2026-03-25 10:36:22 +01:00
aissi 5e1031bd71 avancé 2026-03-24 17:26:18 +01:00
18 changed files with 618 additions and 0 deletions
@@ -0,0 +1,18 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book;
import java.time.LocalDate;
import java.util.List;
public record BookInfo(
String isbn,
String title,
String author,
String publisher,
List<String> categories,
String description,
String langue,
LocalDate publicationDate,
double price,
int stockInitial
) {
}
@@ -0,0 +1,5 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book;
public record BookIsbnDTO(String isbn) {
}
@@ -0,0 +1,23 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.entity;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class Book {
private String isbn;
private String title;
private String author;
private String publisher;
private List<String> categories;
private String description;
private String langue;
private LocalDate publicationDate;
private double price;
private int stock;
}
@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.exception;
import java.text.MessageFormat;
public class BookNotFoundException extends Exception {
public static final String THE_BOOK_WITH_ISBN_DOES_NOT_EXIST =
"The book with isbn {0} does not exist";
public BookNotFoundException(String isbn) {
super(MessageFormat.format(THE_BOOK_WITH_ISBN_DOES_NOT_EXIST, isbn));
}
}
@@ -0,0 +1,8 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.exception;
public class NotValidBookException extends Exception {
public NotValidBookException(String message) {
super(message);
}
}
@@ -0,0 +1,32 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.repository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public final class BookRepository {
private final List<Book> books = new ArrayList<>();
public Book save(Book newBook) {
this.findByIsbn(newBook.getIsbn()).ifPresent(books::remove);
this.books.add(newBook);
return newBook;
}
public Optional<Book> findByIsbn(String isbn) {
return this.books.stream()
.filter(book -> book.getIsbn().equals(isbn))
.findFirst();
}
public List<Book> findAll() {
return books;
}
public void deleteAll() {
books.clear();
}
}
@@ -0,0 +1,35 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.usecase;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookIsbnDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.validator.BookValidator;
public final class BookUseCase {
private final BookRepository bookRepository;
public BookUseCase(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public BookIsbnDTO registerNewBook(BookInfo newBook) throws NotValidBookException {
BookValidator.validate(newBook);
Book bookToSave = Book.builder()
.isbn(newBook.isbn())
.title(newBook.title())
.author(newBook.author())
.publisher(newBook.publisher())
.categories(newBook.categories())
.description(newBook.description())
.langue(newBook.langue())
.publicationDate(newBook.publicationDate())
.price(newBook.price())
.stock(newBook.stockInitial())
.build();
Book savedBook = bookRepository.save(bookToSave);
return new BookIsbnDTO(savedBook.getIsbn());
}
}
@@ -0,0 +1,47 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.validator;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException;
public final class BookValidator {
private BookValidator() {
}
public static void validate(BookInfo bookInfo) throws NotValidBookException {
if (bookInfo == null) {
throw new NotValidBookException("Book cannot be null");
}
if (isBlank(bookInfo.isbn())) {
throw new NotValidBookException("ISBN is required");
}
if (!bookInfo.isbn().matches("\\d{13}")) {
throw new NotValidBookException("ISBN must be 13 digits");
}
if (isBlank(bookInfo.title())) {
throw new NotValidBookException("Title is required");
}
if (isBlank(bookInfo.author())) {
throw new NotValidBookException("Author is required");
}
if (isBlank(bookInfo.publisher())) {
throw new NotValidBookException("Publisher is required");
}
if (bookInfo.categories() == null) {
throw new NotValidBookException("Categories is required");
}
if (bookInfo.categories().stream().anyMatch(BookValidator::isBlank)) {
throw new NotValidBookException("Categories cannot contain empty values");
}
if (bookInfo.price() <= 0) {
throw new NotValidBookException("Price must be positive");
}
if (bookInfo.stockInitial() < 0) {
throw new NotValidBookException("Stock must be greater or equal to 0");
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
@@ -0,0 +1,9 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
public record OrderAddressInfo(
String street,
String city,
String postalCode,
String country
) {
}
@@ -0,0 +1,10 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import java.util.UUID;
public record OrderDTO(
UUID orderId,
double totalAmount,
int earnedLoyaltyPoints
) {
}
@@ -0,0 +1,12 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import java.util.List;
import java.util.UUID;
public record OrderInfo(
UUID customerId,
List<OrderLineInfo> orderLines,
OrderAddressInfo deliveryAddress,
PaymentMode paymentMode
) {
}
@@ -0,0 +1,7 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
public record OrderLineInfo(
String bookIsbn,
int quantity
) {
}
@@ -0,0 +1,7 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
public enum PaymentMode {
CB,
PAYPAL,
POINTS_FIDELITE
}
@@ -0,0 +1,9 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
public class InsufficientStockException extends Exception {
public InsufficientStockException(String bookIsbn, int requested, int available) {
super("Insufficient stock for book " + bookIsbn + ": requested=" + requested
+ ", available=" + available);
}
}
@@ -0,0 +1,59 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.usecase;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.exception.CustomerNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.InsufficientStockException;
import java.util.UUID;
public final class OrderUseCase {
private final CustomerRepository customerRepository;
private final BookRepository bookRepository;
public OrderUseCase(CustomerRepository customerRepository, BookRepository bookRepository) {
this.customerRepository = customerRepository;
this.bookRepository = bookRepository;
}
public OrderDTO placeOrder(OrderInfo orderInfo)
throws CustomerNotFoundException, InsufficientStockException, BookNotFoundException {
Customer customer = customerRepository.findById(orderInfo.customerId())
.orElseThrow(() -> new CustomerNotFoundException(orderInfo.customerId()));
double totalAmount = 0;
for (OrderLineInfo orderLine : orderInfo.orderLines()) {
Book book = bookRepository.findByIsbn(orderLine.bookIsbn())
.orElseThrow(() -> new BookNotFoundException(orderLine.bookIsbn()));
if (orderLine.quantity() > book.getStock()) {
throw new InsufficientStockException(book.getIsbn(), orderLine.quantity(), book.getStock());
}
totalAmount += book.getPrice() * orderLine.quantity();
Book updatedBook = Book.builder()
.isbn(book.getIsbn())
.title(book.getTitle())
.author(book.getAuthor())
.publisher(book.getPublisher())
.categories(book.getCategories())
.description(book.getDescription())
.langue(book.getLangue())
.publicationDate(book.getPublicationDate())
.price(book.getPrice())
.stock(book.getStock() - orderLine.quantity())
.build();
bookRepository.save(updatedBook);
}
int earnedLoyaltyPoints = (int) totalAmount;
customer.addLoyaltyPoints(earnedLoyaltyPoints);
customerRepository.save(customer);
return new OrderDTO(UUID.randomUUID(), totalAmount, earnedLoyaltyPoints);
}
}
@@ -0,0 +1,138 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.usecase;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookIsbnDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
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;
@ExtendWith(MockitoExtension.class)
class BookUseCaseTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookUseCase bookUseCase;
@Nested
@DisplayName("Register new book tests")
class RegisterNewBookTests {
@Test
@DisplayName("Should register a new book and return ISBN")
void shouldRegisterBookAndReturnIsbn() throws NotValidBookException {
BookInfo validBookInfo = new BookInfo(
"9783161484100",
"Clean Code",
"Robert C. Martin",
"Prentice Hall",
List.of("Programming"),
"A handbook of agile software craftsmanship.",
"fr",
LocalDate.of(2008, 8, 1),
42.50,
10
);
Book savedBook = Book.builder()
.isbn("9783161484100")
.title("Clean Code")
.author("Robert C. Martin")
.publisher("Prentice Hall")
.categories(List.of("Programming"))
.description("A handbook of agile software craftsmanship.")
.langue("fr")
.publicationDate(LocalDate.of(2008, 8, 1))
.price(42.50)
.stock(10)
.build();
when(bookRepository.save(any(Book.class))).thenReturn(savedBook);
BookIsbnDTO isbn = bookUseCase.registerNewBook(validBookInfo);
assertNotNull(isbn);
assertEquals("9783161484100", isbn.isbn());
verify(bookRepository, times(1)).save(any(Book.class));
}
@Test
@DisplayName("Should throw when required field is empty")
void shouldThrowWhenRequiredFieldIsEmpty() {
BookInfo invalidBookInfo = new BookInfo(
"9783161484100",
"",
"Robert C. Martin",
"Prentice Hall",
List.of("Programming"),
"A handbook of agile software craftsmanship.",
"fr",
LocalDate.of(2008, 8, 1),
42.50,
10
);
assertThrows(NotValidBookException.class, () -> bookUseCase.registerNewBook(invalidBookInfo));
verify(bookRepository, never()).save(any(Book.class));
}
@Test
@DisplayName("Should throw when ISBN is not 13 digits")
void shouldThrowWhenIsbnIsNot13Digits() {
BookInfo invalidBookInfo = new BookInfo(
"97831614841X",
"Clean Code",
"Robert C. Martin",
"Prentice Hall",
List.of("Programming"),
"A handbook of agile software craftsmanship.",
"fr",
LocalDate.of(2008, 8, 1),
42.50,
10
);
assertThrows(NotValidBookException.class, () -> bookUseCase.registerNewBook(invalidBookInfo));
verify(bookRepository, never()).save(any(Book.class));
}
@Test
@DisplayName("Should throw when categories contain empty value")
void shouldThrowWhenCategoriesContainEmptyValue() {
BookInfo invalidBookInfo = new BookInfo(
"9783161484100",
"Clean Code",
"Robert C. Martin",
"Prentice Hall",
List.of(""),
"A handbook of agile software craftsmanship.",
"fr",
LocalDate.of(2008, 8, 1),
42.50,
10
);
assertThrows(NotValidBookException.class, () -> bookUseCase.registerNewBook(invalidBookInfo));
verify(bookRepository, never()).save(any(Book.class));
}
}
}
@@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.UUID; import java.util.UUID;
class CustomerTest { class CustomerTest {
@Test @Test
@@ -0,0 +1,185 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.usecase;
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.mockito.Mockito.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.exception.CustomerNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderAddressInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.PaymentMode;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.InsufficientStockException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
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;
@ExtendWith(MockitoExtension.class)
class OrderUseCaseTest {
@Mock
private CustomerRepository customerRepository;
@Mock
private BookRepository bookRepository;
@InjectMocks
private OrderUseCase orderUseCase;
@Test
@DisplayName("Should place order and return expected amount and loyalty points")
void shouldPlaceOrderAndReturnExpectedAmountAndLoyaltyPoints()
throws CustomerNotFoundException, InsufficientStockException, BookNotFoundException {
UUID customerId = UUID.randomUUID();
Customer customer = Customer.builder()
.id(customerId)
.firstName("Jane")
.lastName("Doe")
.phoneNumber("0612345678")
.loyaltyPoints(10)
.build();
Book cleanCode = Book.builder()
.isbn("9780132350884")
.title("Clean Code")
.author("Robert C. Martin")
.publisher("Prentice Hall")
.categories(List.of("Programming"))
.description("A handbook of agile software craftsmanship")
.langue("fr")
.price(40.0)
.stock(10)
.build();
OrderInfo orderInfo = new OrderInfo(
customerId,
List.of(new OrderLineInfo("9780132350884", 2)),
new OrderAddressInfo("1 rue de Paris", "Paris", "75000", "France"),
PaymentMode.CB
);
when(customerRepository.findById(customerId)).thenReturn(Optional.of(customer));
when(bookRepository.findByIsbn("9780132350884")).thenReturn(Optional.of(cleanCode));
when(bookRepository.save(any(Book.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> invocation.getArgument(0));
OrderDTO orderDTO = orderUseCase.placeOrder(orderInfo);
assertNotNull(orderDTO);
assertNotNull(orderDTO.orderId());
assertEquals(80.0, orderDTO.totalAmount());
assertEquals(80, orderDTO.earnedLoyaltyPoints());
assertEquals(90, customer.getLoyaltyPoints());
verify(customerRepository, times(1)).findById(customerId);
verify(bookRepository, times(1)).findByIsbn("9780132350884");
verify(bookRepository, times(1)).save(any(Book.class));
verify(customerRepository, times(1)).save(customer);
}
@Test
@DisplayName("Should throw when requested quantity is greater than available stock")
void shouldThrowWhenRequestedQuantityIsGreaterThanAvailableStock() throws BookNotFoundException {
UUID customerId = UUID.randomUUID();
Customer customer = Customer.builder()
.id(customerId)
.firstName("Jane")
.lastName("Doe")
.phoneNumber("0612345678")
.loyaltyPoints(10)
.build();
Book cleanCode = Book.builder()
.isbn("9780132350884")
.title("Clean Code")
.author("Robert C. Martin")
.publisher("Prentice Hall")
.categories(List.of("Programming"))
.description("A handbook of agile software craftsmanship")
.langue("fr")
.price(40.0)
.stock(1)
.build();
OrderInfo orderInfo = new OrderInfo(
customerId,
List.of(new OrderLineInfo("9780132350884", 2)),
new OrderAddressInfo("1 rue de Paris", "Paris", "75000", "France"),
PaymentMode.CB
);
when(customerRepository.findById(customerId)).thenReturn(Optional.of(customer));
when(bookRepository.findByIsbn("9780132350884")).thenReturn(Optional.of(cleanCode));
assertThrows(InsufficientStockException.class, () -> orderUseCase.placeOrder(orderInfo));
verify(customerRepository, times(1)).findById(customerId);
verify(bookRepository, times(1)).findByIsbn("9780132350884");
verify(bookRepository, never()).save(any(Book.class));
verify(customerRepository, never()).save(any(Customer.class));
}
@Test
@DisplayName("Should throw when customer does not exist")
void shouldThrowWhenCustomerDoesNotExist() {
UUID customerId = UUID.randomUUID();
OrderInfo orderInfo = new OrderInfo(
customerId,
List.of(new OrderLineInfo("9780132350884", 1)),
new OrderAddressInfo("1 rue de Paris", "Paris", "75000", "France"),
PaymentMode.CB
);
when(customerRepository.findById(customerId)).thenReturn(Optional.empty());
assertThrows(CustomerNotFoundException.class, () -> orderUseCase.placeOrder(orderInfo));
verify(customerRepository, times(1)).findById(customerId);
verify(bookRepository, never()).findByIsbn(any(String.class));
verify(bookRepository, never()).save(any(Book.class));
verify(customerRepository, never()).save(any(Customer.class));
}
@Test
@DisplayName("Should throw when ordered book does not exist")
void shouldThrowWhenOrderedBookDoesNotExist() {
UUID customerId = UUID.randomUUID();
Customer customer = Customer.builder()
.id(customerId)
.firstName("Jane")
.lastName("Doe")
.phoneNumber("0612345678")
.loyaltyPoints(10)
.build();
String unknownIsbn = "9999999999999";
OrderInfo orderInfo = new OrderInfo(
customerId,
List.of(new OrderLineInfo(unknownIsbn, 1)),
new OrderAddressInfo("1 rue de Paris", "Paris", "75000", "France"),
PaymentMode.CB
);
when(customerRepository.findById(customerId)).thenReturn(Optional.of(customer));
when(bookRepository.findByIsbn(unknownIsbn)).thenReturn(Optional.empty());
assertThrows(BookNotFoundException.class, () -> orderUseCase.placeOrder(orderInfo));
verify(customerRepository, times(1)).findById(customerId);
verify(bookRepository, times(1)).findByIsbn(unknownIsbn);
verify(bookRepository, never()).save(any(Book.class));
verify(customerRepository, never()).save(any(Customer.class));
}
}