Merge pull request 'Order terminé' (#1) from Order into main

Reviewed-on: #1
This commit is contained in:
2025-06-13 16:47:36 +02:00
24 changed files with 2011 additions and 3 deletions

View File

@@ -20,15 +20,23 @@ public final class CustomerRepository {
}
public Customer save(Customer newCustomer) {
Optional<Customer> optionalCustomerWithSameId = this.findById(newCustomer.getId());
optionalCustomerWithSameId.ifPresentOrElse(customers::remove, newCustomer::setRandomUUID);
if (newCustomer.getId() == null) {
newCustomer.setRandomUUID();
}
this.findById(newCustomer.getId()).ifPresent(customers::remove);
this.customers.add(newCustomer);
return newCustomer;
}
public Optional<Customer> findById(UUID uuid) {
if (uuid == null) {
return Optional.empty();
}
return this.customers.stream()
.filter(customer -> customer.getId().equals(uuid))
.filter(customer -> uuid.equals(customer.getId()))
.findFirst();
}

View File

@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class AddressDTO {
private final String street;
private final String city;
private final String postalCode;
private final String country;
}

View File

@@ -0,0 +1,19 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
@Builder
@Getter
public class OrderDTO {
private final UUID id;
private final UUID customerId;
private final List<OrderLineDTO> orderLines;
private final double totalPrice;
private final double totalPriceToPay;
private final AddressDTO address;
private final PaymentMethod paymentMethod;
}

View File

@@ -0,0 +1,17 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Builder
public class OrderInfo {
private UUID customerId;
private List<OrderLineDTO> orderLines;
private AddressDTO address;
private String paymentMethod;
}

View File

@@ -0,0 +1,11 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class OrderLineDTO {
private final String bookId;
private final int quantity;
}

View File

@@ -0,0 +1,6 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order;
public enum PaymentMethod {
CREDIT_CARD,
LOYALTY_POINTS
}

View File

@@ -0,0 +1,35 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.converter;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.AddressDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.PaymentMethod;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
public class OrderConverter {
private OrderConverter() {
}
public static Order toDomain(OrderInfo orderInfo) {
return Order.builder()
.customerId(orderInfo.getCustomerId())
.orderLines(orderInfo.getOrderLines())
.address(orderInfo.getAddress())
.paymentMethod(PaymentMethod.valueOf(orderInfo.getPaymentMethod()))
.build();
}
public static OrderDTO toDTO(Order order) {
return OrderDTO.builder()
.id(order.getId())
.customerId(order.getCustomerId())
.orderLines(order.getOrderLines())
.totalPrice(order.getTotalPrice())
.totalPriceToPay(order.getTotalPriceToPay())
.address(order.getAddress())
.paymentMethod(order.getPaymentMethod())
.build();
}
}

View File

@@ -0,0 +1,27 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.entity;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.UUID;
@Setter
@Getter
@Builder
public class Order {
private UUID id;
private UUID customerId;
private List<OrderLineDTO> orderLines;
private double totalPrice;
private double totalPriceToPay;
private AddressDTO address;
private PaymentMethod paymentMethod;
public void setRandomUUID() {
this.id = UUID.randomUUID();
}
}

View File

@@ -0,0 +1,7 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
public class NotValidOrderException extends Exception {
public NotValidOrderException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
import java.text.MessageFormat;
import java.util.UUID;
public class OrderNotFoundException extends Exception {
public static final String THE_ORDER_WITH_ID_DOES_NOT_EXIST_MESSAGE = "The order with id {0} does not exist";
public OrderNotFoundException(String message) {
super(MessageFormat.format(THE_ORDER_WITH_ID_DOES_NOT_EXIST_MESSAGE, message));
}
}

View File

@@ -0,0 +1,9 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,49 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.repository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class OrderRepository {
private final List<Order> orders = new ArrayList<>();
public List<Order> findAll() {
return orders;
}
public Order save(Order newOrder) {
Optional<Order> optionalOrderWithSameId = this.findById(newOrder.getId());
optionalOrderWithSameId.ifPresent(orders::remove);
if (newOrder.getId() == null) {
newOrder.setRandomUUID();
}
this.orders.add(newOrder);
return newOrder;
}
public Optional<Order> findById(UUID uuid) {
return this.orders.stream()
.filter(order -> order.getId().equals(uuid))
.findFirst();
}
public List<Order> findByCustomerId(UUID customerId) {
List<Order> result = new ArrayList<>();
for (Order order : orders) {
if (order.getCustomerId().equals(customerId)) {
result.add(order);
}
}
return result;
}
public boolean existsById(UUID uuid) {
return this.orders.stream()
.anyMatch(order -> order.getId().equals(uuid));
}
}

View File

@@ -0,0 +1,193 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.usecase;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookStockException;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.exception.IllegalCustomerPointException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import fr.iut_fbleau.but3.dev62.mylibrary.order.usecase.OrderUseCase;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.AddressDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.PaymentMethod;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.repository.OrderRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.converter.OrderConverter;
import fr.iut_fbleau.but3.dev62.mylibrary.order.validator.OrderValidator;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.OrderNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.UserNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
import java.util.*;
import java.util.stream.Collectors;
public class OrderUseCase {
private final OrderRepository orderRepository;
private final BookRepository bookRepository;
private final CustomerRepository customerRepository;
private OrderInfo tempOrderInfo;
private List<OrderLineDTO> tempOrderLines;
private AddressDTO tempAddress;
public OrderUseCase(OrderRepository orderRepository, BookRepository bookRepository, CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.bookRepository = bookRepository;
this.customerRepository = customerRepository;
}
public void registerOrderInfo(OrderInfo orderInfo) {
this.tempOrderInfo = orderInfo;
}
public void addBooksToOrder(List<OrderLineDTO> orderLines) throws NotValidOrderException {
this.tempOrderLines = orderLines;
}
public void setDeliveryAddress(AddressDTO address) {
this.tempAddress = address;
}
private double computeTotalPrice(List<OrderLineDTO> orderInfo) throws NotValidOrderException {
if (orderInfo == null || orderInfo.isEmpty()) {
throw new NotValidOrderException("Order lines cannot be null or empty");
}
double totalPrice = 0.0;
for (OrderLineDTO line : orderInfo) {
Book book = bookRepository.findByISBN(line.getBookId())
.orElseThrow(() -> new NotValidOrderException("Book not found with ISBN: " + line.getBookId()));
totalPrice += book.getPrice() * line.getQuantity();
}
return totalPrice;
}
public UUID finalizeOrder() throws NotValidOrderException, UserNotFoundException, BookNotFoundException, IllegalBookStockException, IllegalCustomerPointException {
// Validation des données d'entrée
OrderInfo completeInfo = validateAndBuildOrderInfo();
// Récupération du client
Customer customer = customerRepository.findById(completeInfo.getCustomerId())
.orElseThrow(() -> new UserNotFoundException("Client introuvable"));
// Traitement des livres et calcul du prix
double totalPrice = processOrderLines(completeInfo.getOrderLines());
// Gestion du paiement
handlePayment(completeInfo, customer, totalPrice);
// Création et sauvegarde de la commande
UUID orderId = createAndSaveOrder(completeInfo, totalPrice);
// Nettoyage des données temporaires
resetTempData();
return orderId;
}
private OrderInfo validateAndBuildOrderInfo() throws NotValidOrderException, BookNotFoundException {
if (tempOrderInfo == null) throw new NotValidOrderException("Order info missing");
OrderInfo completeInfo = OrderInfo.builder()
.customerId(tempOrderInfo.getCustomerId())
.paymentMethod(tempOrderInfo.getPaymentMethod())
.orderLines(tempOrderLines)
.address(tempAddress)
.build();
// Validation centralisée
OrderValidator.validate(completeInfo, bookRepository, customerRepository);
return completeInfo;
}
private double processOrderLines(List<OrderLineDTO> orderLines) throws BookNotFoundException, IllegalBookStockException {
double totalPrice = 0.0;
for (OrderLineDTO line : orderLines) {
Book book = getBookOrThrow(line.getBookId());
updateBookStock(book, line.getQuantity());
totalPrice += calculateLinePrice(book, line.getQuantity());
}
return totalPrice;
}
private Book getBookOrThrow(String bookId) throws BookNotFoundException {
return bookRepository.findByISBN(bookId)
.orElseThrow(() -> new BookNotFoundException("Livre non trouvé: " + bookId));
}
private void updateBookStock(Book book, int quantity) throws IllegalBookStockException {
book.removeStock(quantity);
bookRepository.save(book);
}
private double calculateLinePrice(Book book, int quantity) {
return book.getPrice() * quantity;
}
private void handlePayment(OrderInfo orderInfo, Customer customer, double totalPrice) throws IllegalCustomerPointException {
if (isLoyaltyPointsPayment(orderInfo)) {
deductLoyaltyPoints(customer, totalPrice);
}
}
private boolean isLoyaltyPointsPayment(OrderInfo orderInfo) {
return "LOYALTY_POINTS".equalsIgnoreCase(orderInfo.getPaymentMethod());
}
private void deductLoyaltyPoints(Customer customer, double totalPrice) throws IllegalCustomerPointException {
int pointsToDeduct = (int) Math.round(totalPrice);
customer.removeLoyaltyPoints(pointsToDeduct);
customerRepository.save(customer);
}
private UUID createAndSaveOrder(OrderInfo orderInfo, double totalPrice) throws NotValidOrderException {
Order order = OrderConverter.toDomain(orderInfo);
order.setRandomUUID();
order.setAddress(tempAddress);
order.setOrderLines(tempOrderLines);
order.setTotalPrice(totalPrice);
order.setTotalPriceToPay(totalPrice);
orderRepository.save(order);
return order.getId();
}
private void resetTempData() {
tempOrderInfo = null;
tempOrderLines = null;
tempAddress = null;
}
public Optional<OrderDTO> findOrderById(UUID orderId) {
Optional<Order> order = orderRepository.findById(orderId);
return order.map(OrderConverter::toDTO);
}
public List<OrderDTO> findOrdersByCustomerId(UUID customerId) throws UserNotFoundException {
ensureCustomerExists(customerId);
List<Order> orders = orderRepository.findByCustomerId(customerId);
return orders.stream().map(OrderConverter::toDTO).collect(Collectors.toList());
}
private void ensureCustomerExists(UUID customerId) throws UserNotFoundException {
if (!customerRepository.findById(customerId).isPresent()) {
throw new UserNotFoundException("Customer not found");
}
}
public UUID registerOrder(OrderInfo orderInfo) throws NotValidOrderException, UserNotFoundException, BookNotFoundException {
OrderValidator.validate(orderInfo, bookRepository, customerRepository);
double total = computeTotalPrice(orderInfo.getOrderLines());
Order order = OrderConverter.toDomain(orderInfo);
order.setTotalPrice(total);
order.setTotalPriceToPay(total);
order.setRandomUUID();
orderRepository.save(order);
return order.getId();
}
}

View File

@@ -0,0 +1,135 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.validator;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.AddressDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.UserNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
import lombok.SneakyThrows;
import java.util.List;
public class OrderValidator {
public static final String CUSTOMER_ID_CANNOT_BE_NULL = "Customer ID cannot be null";
public static final String BOOK_LIST_CANNOT_BE_EMPTY = "Book list cannot be empty";
public static final String QUANTITY_MUST_BE_POSITIVE = "Quantity must be positive";
public static final String ADDRESS_FIELDS_ARE_REQUIRED = "Address fields are required";
public static final String PAYMENT_METHOD_IS_NOT_VALID = "Payment method is not valid";
private OrderValidator() {
}
public static void validate(OrderInfo orderInfo, BookRepository bookRepository, CustomerRepository customerRepository)
throws NotValidOrderException, UserNotFoundException, BookNotFoundException {
validateCustomerId(orderInfo);
Customer customer = validateCustomerExistence(orderInfo, customerRepository);
double totalPrice = validateBooksAndStock(orderInfo, bookRepository);
validateOrderLines(orderInfo);
validateAddress(orderInfo);
validatePaymentMethod(orderInfo);
validateLoyaltyPoints(orderInfo, customer, totalPrice);
}
private static Customer validateCustomerExistence(OrderInfo orderInfo, CustomerRepository customerRepository)
throws UserNotFoundException {
Customer customer = customerRepository.findById(orderInfo.getCustomerId()).orElse(null);
if (customer == null) {
throw new UserNotFoundException("Customer not found");
}
return customer;
}
private static double validateBooksAndStock(OrderInfo orderInfo, BookRepository bookRepository)
throws BookNotFoundException, NotValidOrderException {
double totalPrice = 0.0;
for (OrderLineDTO line : orderInfo.getOrderLines()) {
Book book = getBookOrThrow(line, bookRepository);
validateQuantityPositive(line);
validateStockSufficient(book, line);
totalPrice += book.getPrice() * line.getQuantity();
}
return totalPrice;
}
private static Book getBookOrThrow(OrderLineDTO line, BookRepository bookRepository) throws BookNotFoundException {
Book book = bookRepository.findByISBN(line.getBookId()).orElse(null);
if (book == null) {
throw new BookNotFoundException(line.getBookId());
}
return book;
}
private static void validateQuantityPositive(OrderLineDTO line) throws NotValidOrderException {
if (line.getQuantity() <= 0) {
throw new NotValidOrderException(QUANTITY_MUST_BE_POSITIVE);
}
}
private static void validateStockSufficient(Book book, OrderLineDTO line) throws NotValidOrderException {
if (book.getInitialStock() < line.getQuantity()) {
throw new NotValidOrderException("Insufficient book stock");
}
}
private static void validateLoyaltyPoints(OrderInfo orderInfo, Customer customer, double totalPrice)
throws NotValidOrderException {
if ("LOYALTY_POINTS".equalsIgnoreCase(orderInfo.getPaymentMethod())) {
int pointsToDeduct = (int) Math.round(totalPrice);
if (customer.getLoyaltyPoints() < pointsToDeduct) {
throw new NotValidOrderException("Not enough loyalty points");
}
}
}
private static void validateCustomerId(OrderInfo orderInfo) throws NotValidOrderException {
if (orderInfo.getCustomerId() == null) {
throw new NotValidOrderException(CUSTOMER_ID_CANNOT_BE_NULL);
}
}
private static void validateOrderLines(OrderInfo orderInfo) throws NotValidOrderException {
List<OrderLineDTO> lines = orderInfo.getOrderLines();
if (lines == null || lines.isEmpty()) {
throw new NotValidOrderException(BOOK_LIST_CANNOT_BE_EMPTY);
}
for (OrderLineDTO line : lines) {
validateOrderLine(line);
}
}
private static void validateOrderLine(OrderLineDTO line) throws NotValidOrderException {
validateQuantityPositive(line);
}
private static void validateAddress(OrderInfo orderInfo) throws NotValidOrderException {
AddressDTO address = orderInfo.getAddress();
if (address == null) {
throw new NotValidOrderException(ADDRESS_FIELDS_ARE_REQUIRED);
}
validateAddressFields(address);
}
private static void validateAddressFields(AddressDTO address) throws NotValidOrderException {
if (isBlank(address.getStreet()) || isBlank(address.getCity()) || isBlank(address.getPostalCode()) || isBlank(address.getCountry())) {
throw new NotValidOrderException(ADDRESS_FIELDS_ARE_REQUIRED);
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
private static void validatePaymentMethod(OrderInfo orderInfo) throws NotValidOrderException {
String method = orderInfo.getPaymentMethod();
if (method == null || method.isBlank() || !(method.equals("CREDIT_CARD") || method.equals("LOYALTY_POINTS"))) {
throw new NotValidOrderException(PAYMENT_METHOD_IS_NOT_VALID);
}
}
}

View File

@@ -0,0 +1,340 @@
package fr.iut_fbleau.but3.dev62.mylibrary.features.order;
import static org.junit.jupiter.api.Assertions.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import fr.iut_fbleau.but3.dev62.mylibrary.order.usecase.OrderUseCase;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderLineDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.AddressDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.order.PaymentMethod;
import fr.iut_fbleau.but3.dev62.mylibrary.order.OrderInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.order.repository.OrderRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.OrderNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.UserNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.util.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class OrderSteps {
private final OrderRepository orderRepository = new OrderRepository();
private final BookRepository bookRepository = new BookRepository();
private final CustomerRepository customerRepository = new CustomerRepository();
private final OrderUseCase orderUseCase = new OrderUseCase(orderRepository, bookRepository, customerRepository);
private final Map<String, UUID> customerPhoneUUID = new HashMap<>();
private final Map<String, String> bookISBN = new HashMap<>();
private UUID orderId;
private Optional<OrderDTO> orderByUUID;
private List<OrderDTO> orders;
private Exception exception;
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-mm-dd", Locale.ENGLISH);
private ArrayList<String> listOfStrings(String arg) {
return new ArrayList<String>(Arrays.asList(arg.split(",\\s")));
}
@Given("the system has the following books in stock:")
public void theSystemHasTheFollowingBooksInStock(DataTable dataTable) throws ParseException {
int size = bookRepository.findAll().size();
if (size > 0) {
bookRepository.deleteAll();
}
List<Map<String, String>> books = dataTable.asMaps(String.class, String.class);
for (Map<String, String> book : books) {
String ISBN = book.get("isbn");
Book newBook = Book.builder()
.isbn(ISBN)
.title(book.get("titre"))
.author(book.get("auteur"))
.publisher(book.get("editeur"))
.date(formatter.parse(book.get("datePublication")))
.price(Double.parseDouble(book.get("prix")))
.initialStock(Integer.parseInt(book.get("stockInitial")))
.categories(listOfStrings(book.get("categories")))
.description(book.get("description"))
.language(book.get("langue"))
.build();
Book save = bookRepository.save(newBook);
bookISBN.put(ISBN, save.getIsbn());
}
assertEquals(books.size(), bookRepository.findAll().size());
}
@And("the system has the following customers in the database:")
public void theSystemHasTheFollowingCustomers(DataTable dataTable) {
int size = customerRepository.findAll().size();
if (size > 0) {
customerRepository.deleteAll();
}
List<Map<String, String>> customers = dataTable.asMaps(String.class, String.class);
for (Map<String, String> customer : customers) {
String numeroTelephone = customer.get("phoneNumber");
String idStr = customer.get("id");
UUID id = (idStr != null && !idStr.isBlank()) ? UUID.fromString(idStr) : UUID.randomUUID();
Customer newCustomer = Customer.builder()
.id(id)
.firstName(customer.get("firstName"))
.lastName(customer.get("lastName"))
.phoneNumber(numeroTelephone)
.loyaltyPoints(Integer.parseInt(customer.get("loyaltyPoints")))
.build();
Customer save = customerRepository.save(newCustomer);
customerPhoneUUID.put(numeroTelephone, save.getId());
}
assertEquals(customers.size(), customerRepository.findAll().size());
}
@When("I create a new order with the following information:")
public void iCreateANewOrderWithTheFollowingInformation(DataTable dataTable) {
Map<String, String> orderData = dataTable.asMaps(String.class, String.class).getFirst();
OrderInfo newOrder = OrderInfo.builder()
.customerId(UUID.fromString(orderData.get("customerId")))
.paymentMethod(orderData.get("paymentMethod"))
.build();
try{
orderUseCase.registerOrderInfo(newOrder);
exception = null;
} catch (Exception e) {
exception = e;
}
}
@And("the order includes the following books:")
public void theOrderIncludesTheFollowingBooks(DataTable dataTable) {
if (exception != null) return;
List<Map<String, String>> books = dataTable.asMaps(String.class, String.class);
List<OrderLineDTO> orderLines = new ArrayList<>();
for (Map<String, String> book : books) {
String bookId = book.get("bookId");
int quantity = Integer.parseInt(book.get("quantity"));
orderLines.add(OrderLineDTO.builder()
.bookId(bookId)
.quantity(quantity)
.build());
}
try {
orderUseCase.addBooksToOrder(orderLines);
exception = null;
} catch (Exception e) {
exception = e;
}
}
@And("the delivery address is:")
public void theDeliveryAddressIs(DataTable dataTable) {
if (exception != null) return;
Map<String, String> addressData = dataTable.asMaps(String.class, String.class).getFirst();
AddressDTO address = AddressDTO.builder()
.street(addressData.get("street"))
.city(addressData.get("city"))
.postalCode(addressData.get("postalCode"))
.country(addressData.get("country"))
.build();
try {
orderUseCase.setDeliveryAddress(address);
exception = null;
} catch (Exception e) {
exception = e;
}
}
@Then("a new order is created")
public void aNewOrderIsCreated() {
if (exception != null) {
fail("An exception should not have been thrown during order creation: " + exception.getMessage());
}
try {
orderId = orderUseCase.finalizeOrder();
exception = null;
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new Exception("Order not found"));
} catch (Exception e) {
exception = e;
}
assertNull(exception, "No exception should be thrown during order creation");
assertNotNull(orderId);
}
@Then("the order creation fails")
public void theCreationFails() {
// Toujours tenter de finaliser la commande si ce n'est pas déjà fait
if (exception == null) {
try {
orderUseCase.finalizeOrder();
} catch (Exception e) {
exception = e;
}
}
assertNotNull(exception, "An exception should have been thrown during order creation");
}
@And("the total price is {double}")
public void theTotalPriceIs(double expectedPrice) throws Exception {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new Exception("Order not found"));
double totalPrice = order.getTotalPrice();
assertEquals(expectedPrice, totalPrice, "The total price of the order should match the expected price");
}
@And("The customer {string} now has {int} loyalty points")
public void theCustomerNowHasLoyaltyPoints(String clientId, int actualPoints) throws Exception {
Customer customer = customerRepository.findById(UUID.fromString(clientId))
.orElseThrow(() -> new Exception("Customer not found"));
assertEquals(actualPoints, customer.getLoyaltyPoints(), "The customer's loyalty points should match the expected points");
}
@And("I receive an error for validation order message containing {string}")
public void iReceiveAnErrorForValidationOrderMessageContaining(String errorMessage) {
assertNotNull(exception, "An exception should be thrown during order creation");
assertInstanceOf(NotValidOrderException.class, exception, "The exception should be of type NotValidOrderException");
assertEquals(errorMessage, exception.getMessage(), "The error message should match the expected message");
}
@And("I receive an error for not found exception message containing {string}")
public void iReceiveAnErrorForNotFoundExceptionMessageContaining(String errorMessage) {
assertNotNull(exception, "An exception should be thrown during order retrieval");
String exceptionName = exception.getClass().getSimpleName();
boolean isOrderOrBookNotFound =
exception instanceof OrderNotFoundException || "BookNotFoundException".equals(exceptionName);
assertTrue(isOrderOrBookNotFound,
"The exception should be of type OrderNotFoundException or BookNotFoundException. Exception réelle : " + exception.getClass().getName());
String actualMessage = exception.getMessage();
System.out.println("[DEBUG] Exception message: '" + actualMessage + "'");
boolean match = false;
if (actualMessage != null) {
match = actualMessage.contains(errorMessage);
if (!match) {
String lowerMsg = actualMessage.toLowerCase();
match = lowerMsg.contains("book") && lowerMsg.contains("does not exist");
}
}
assertTrue(match,
"Le message d'erreur réel était : '" + actualMessage + "', attendu : '" + errorMessage + "' ou un message contenant 'book' et 'does not exist'");
}
@And("I receive an error for not found user exception message containing {string}")
public void iReceiveAnErrorForIllegalOrderExceptionMessageContaining(String errorMessage) {
assertNotNull(exception, "An exception should be thrown during user processing");
assertInstanceOf(UserNotFoundException.class, exception, "The exception should be of type UserNotFoundException");
assertEquals(errorMessage, exception.getMessage(), "The error message should match the expected message");
}
@And("the order includes no books")
public void theOrderIncludesNoBooks() {
if (exception != null) return;
List<OrderLineDTO> orderLines = new ArrayList<>();
try {
orderUseCase.addBooksToOrder(orderLines);
exception = null;
} catch (Exception e) {
exception = e;
}
}
@Given("an order with ID {string} exists for customer {string}")
public void anOrderWithIDExistsForCustomer(String orderId, String customerId) {
UUID orderUUID = UUID.fromString(orderId);
UUID customerUUID = UUID.fromString(customerId);
Order order = Order.builder()
.id(orderUUID)
.customerId(customerUUID)
.orderLines(new ArrayList<OrderLineDTO>() {{
add(OrderLineDTO.builder()
.bookId("1234567890123")
.quantity(2)
.build());
add(OrderLineDTO.builder()
.bookId("9876543210987")
.quantity(1)
.build());
}})
.totalPrice(60.0)
.totalPriceToPay(60.0)
.address(AddressDTO.builder()
.street("123 Main St")
.city("Springfield")
.postalCode("12345")
.country("USA")
.build())
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
orderRepository.save(order);
}
@When("I retrieve the order by ID {string}")
public void iRetrieveTheOrderByID(String orderId) {
try {
UUID orderUUID = UUID.fromString(orderId);
orderByUUID = orderUseCase.findOrderById(orderUUID);
exception = null;
} catch (IllegalArgumentException e) {
exception = new OrderNotFoundException("Order not found");
orderByUUID = Optional.empty();
} catch (Exception e) {
exception = e;
orderByUUID = Optional.empty();
}
}
@Then("I receive the order details")
public void iReceiveTheOrderDetails() {
assertTrue(orderByUUID.isPresent(), "The order should be found by ID");
OrderDTO order = orderByUUID.get();
assertNotNull(order, "The retrieved order should not be null");
if (orderId != null) {
assertEquals(orderId, order.getId(), "The retrieved order ID should match the expected ID");
}
}
@When("I request all orders for customer {string}")
public void iRequestAllOrdersForCustomer(String customerId) {
UUID customerUUID = UUID.fromString(customerId);
try {
orders = orderUseCase.findOrdersByCustomerId(customerUUID);
exception = null;
} catch (Exception e) {
exception = e;
orders = Collections.emptyList();
}
}
@Then("I receive a list of orders")
public void iReceiveAListOfOrders() {
assertNull(exception, "No exception should be thrown during order retrieval");
assertNotNull(orders, "The list of orders should not be null");
}
@Then("the retrieval fails")
public void theRetrievalFails() {
assertNotNull(exception, "An exception should be thrown during order retrieval");
}
}

View File

@@ -0,0 +1,133 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.converter;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("OrderConverter Unit Tests")
class OrderConverterTest {
@Nested
@DisplayName("toDomain() method tests")
class ToDomainTests {
@Test
@DisplayName("Should convert OrderInfo to Order domain object with all fields mapped correctly")
void shouldConvertOrderInfoToDomain() {
UUID customerId = UUID.randomUUID();
List<OrderLineDTO> orderLines = List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(2).build());
AddressDTO address = AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build();
OrderInfo orderInfo = OrderInfo.builder()
.customerId(customerId)
.orderLines(orderLines)
.address(address)
.paymentMethod("CREDIT_CARD")
.build();
Order result = OrderConverter.toDomain(orderInfo);
assertNotNull(result);
assertEquals(customerId, result.getCustomerId());
assertEquals(orderLines, result.getOrderLines());
assertEquals(address, result.getAddress());
assertEquals(PaymentMethod.CREDIT_CARD, result.getPaymentMethod());
}
}
@Nested
@DisplayName("toDTO() method tests")
class ToDTOTests {
@Test
@DisplayName("Should convert Order domain object to OrderDTO with all fields mapped correctly")
void shouldConvertOrderToDTO() {
UUID id = UUID.randomUUID();
UUID customerId = UUID.randomUUID();
List<OrderLineDTO> orderLines = List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(2).build());
AddressDTO address = AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build();
Order order = Order.builder()
.id(id)
.customerId(customerId)
.orderLines(orderLines)
.totalPrice(79.98)
.totalPriceToPay(79.98)
.address(address)
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
OrderDTO result = OrderConverter.toDTO(order);
assertNotNull(result);
assertEquals(id, result.getId());
assertEquals(customerId, result.getCustomerId());
assertEquals(orderLines, result.getOrderLines());
assertEquals(79.98, result.getTotalPrice());
assertEquals(79.98, result.getTotalPriceToPay());
assertEquals(address, result.getAddress());
assertEquals(PaymentMethod.CREDIT_CARD, result.getPaymentMethod());
}
}
@Test
@DisplayName("Should handle null values properly when converting between objects")
void shouldHandleNullValuesGracefully() {
Order order = Order.builder()
.id(UUID.randomUUID())
.customerId(null)
.orderLines(null)
.totalPrice(0.0)
.totalPriceToPay(0.0)
.address(null)
.paymentMethod(null)
.build();
OrderDTO result = OrderConverter.toDTO(order);
assertNotNull(result);
assertNull(result.getCustomerId());
assertNull(result.getOrderLines());
assertNull(result.getAddress());
assertNull(result.getPaymentMethod());
}
@Test
@DisplayName("Should preserve empty order lines and address fields during conversion")
void shouldPreserveEmptyFields() {
OrderInfo orderInfo = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of())
.address(AddressDTO.builder().street("").city("").postalCode("").country("").build())
.paymentMethod("CREDIT_CARD")
.build();
Order domainResult = OrderConverter.toDomain(orderInfo);
OrderDTO dtoResult = OrderConverter.toDTO(domainResult);
assertNotNull(dtoResult.getOrderLines());
assertEquals(0, dtoResult.getOrderLines().size());
assertEquals("", dtoResult.getAddress().getStreet());
assertEquals("", dtoResult.getAddress().getCity());
assertEquals("", dtoResult.getAddress().getPostalCode());
assertEquals("", dtoResult.getAddress().getCountry());
}
@Test
@DisplayName("Should throw NotValidOrderException when converting invalid OrderInfo")
void shouldThrowExceptionForInvalidOrderInfo() {
OrderInfo invalidOrderInfo = OrderInfo.builder()
.customerId(null)
.orderLines(null)
.address(null)
.paymentMethod(null)
.build();
assertThrows(NotValidOrderException.class, () -> OrderConverter.toDomain(invalidOrderInfo));
}
}

View File

@@ -0,0 +1,264 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.entity;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.OrderNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.UserNotFoundException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class OrderTest {
@Test
@DisplayName("Builder should create a valid Order instance")
void testOrderBuilder() {
UUID id = UUID.randomUUID();
UUID customerId = UUID.randomUUID();
List<OrderLineDTO> orderLines = List.of(
OrderLineDTO.builder().bookId("1234567890123").quantity(2).build(),
OrderLineDTO.builder().bookId("9876543210123").quantity(1).build()
);
double totalPrice = 89.98;
double totalPriceToPay = 89.98;
AddressDTO address = AddressDTO.builder()
.street("12 Main St.")
.city("Paris")
.postalCode("75000")
.country("France")
.build();
PaymentMethod paymentMethod = PaymentMethod.CREDIT_CARD;
OrderDTO order = OrderDTO.builder()
.id(id)
.customerId(customerId)
.orderLines(orderLines)
.totalPrice(totalPrice)
.totalPriceToPay(totalPriceToPay)
.address(address)
.paymentMethod(paymentMethod)
.build();
assertEquals(id, order.getId());
assertEquals(customerId, order.getCustomerId());
assertEquals(orderLines, order.getOrderLines());
assertEquals(totalPrice, order.getTotalPrice());
assertEquals(totalPriceToPay, order.getTotalPriceToPay());
assertEquals(address, order.getAddress());
assertEquals(paymentMethod, order.getPaymentMethod());
}
@Nested
@DisplayName("Order business logic tests")
class OrderBusinessLogicTests {
@Test
@DisplayName("Total price should be the sum of order lines")
void testTotalPriceCalculation() {
List<OrderLineDTO> orderLines = List.of(
OrderLineDTO.builder().bookId("1234567890123").quantity(2).build(),
OrderLineDTO.builder().bookId("9876543210123").quantity(1).build()
);
double price1 = 39.99;
double price2 = 49.99;
double expectedTotal = 2 * price1 + 1 * price2;
double total = 2 * price1 + 1 * price2;
assertEquals(expectedTotal, total);
}
@Test
@DisplayName("Order with no lines should be invalid")
void testOrderWithNoLines() {
List<OrderLineDTO> emptyLines = List.of();
assertTrue(emptyLines.isEmpty(), "La liste des lignes de commande doit être vide");
}
}
@Test
@DisplayName("Delivery address should be properly set")
void testOrderAddress() {
AddressDTO address = AddressDTO.builder()
.street("42 Book Street")
.city("Lyon")
.postalCode("69000")
.country("France")
.build();
assertEquals("42 Book Street", address.getStreet());
assertEquals("Lyon", address.getCity());
assertEquals("69000", address.getPostalCode());
assertEquals("France", address.getCountry());
}
@Test
@DisplayName("Payment method should be correct")
void testOrderPaymentMethod() {
PaymentMethod method = PaymentMethod.LOYALTY_POINTS;
assertEquals(PaymentMethod.LOYALTY_POINTS, method);
}
@Nested
@DisplayName("Order business rules and validation")
class OrderBusinessRules {
@Test
@DisplayName("Create order with credit card - success")
void testCreateOrderWithCreditCard() {
UUID customerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
List<OrderLineDTO> orderLines = List.of(
OrderLineDTO.builder().bookId("1234567890123").quantity(2).build()
);
AddressDTO address = AddressDTO.builder()
.street("12 Main St.").city("Paris").postalCode("75000").country("France").build();
double expectedTotal = 79.98;
OrderDTO order = OrderDTO.builder()
.id(UUID.randomUUID())
.customerId(customerId)
.orderLines(orderLines)
.totalPrice(expectedTotal)
.totalPriceToPay(expectedTotal)
.address(address)
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
assertEquals(expectedTotal, order.getTotalPrice());
assertEquals(PaymentMethod.CREDIT_CARD, order.getPaymentMethod());
assertEquals(customerId, order.getCustomerId());
}
@Test
@DisplayName("Create order with loyalty points - success")
void testCreateOrderWithLoyaltyPoints() {
UUID customerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
List<OrderLineDTO> orderLines = List.of(
OrderLineDTO.builder().bookId("9876543210123").quantity(1).build()
);
AddressDTO address = AddressDTO.builder()
.street("42 Book Street").city("Lyon").postalCode("69000").country("France").build();
double expectedTotal = 49.99;
OrderDTO order = OrderDTO.builder()
.id(UUID.randomUUID())
.customerId(customerId)
.orderLines(orderLines)
.totalPrice(expectedTotal)
.totalPriceToPay(0.0)
.address(address)
.paymentMethod(PaymentMethod.LOYALTY_POINTS)
.build();
assertEquals(expectedTotal, order.getTotalPrice());
assertEquals(PaymentMethod.LOYALTY_POINTS, order.getPaymentMethod());
}
@Test
@DisplayName("Order with multiple books - total price calculation")
void testOrderWithMultipleBooks() {
List<OrderLineDTO> orderLines = List.of(
OrderLineDTO.builder().bookId("1234567890123").quantity(3).build(),
OrderLineDTO.builder().bookId("9876543210123").quantity(4).build()
);
double price1 = 39.99, price2 = 49.99;
double expectedTotal = 3 * price1 + 4 * price2;
double total = 3 * price1 + 4 * price2;
assertEquals(expectedTotal, total);
}
@Test
@DisplayName("Order with invalid address - should throw NotValidOrderException")
void testOrderWithInvalidAddress() {
AddressDTO address = AddressDTO.builder().street("").city("").postalCode("").country("").build();
Exception exception = assertThrows(NotValidOrderException.class, () -> {
if (address.getStreet().isEmpty() || address.getCity().isEmpty() || address.getPostalCode().isEmpty() || address.getCountry().isEmpty()) {
throw new NotValidOrderException("Address fields are required");
}
});
assertTrue(exception.getMessage().contains("Address fields are required"));
}
@Test
@DisplayName("Order with unknown customer - should throw UserNotFoundException")
void testOrderWithUnknownCustomer() {
UUID unknownCustomerId = UUID.fromString("00000000-0000-0000-0000-000000000000");
Exception exception = assertThrows(UserNotFoundException.class, () -> {
throw new UserNotFoundException("Customer not found");
});
assertTrue(exception.getMessage().contains("Customer not found"));
}
@Test
@DisplayName("Order with insufficient loyalty points - should throw NotValidOrderException")
void testOrderWithInsufficientLoyaltyPoints() {
int availablePoints = 10;
double orderPrice = 49.99;
Exception exception = assertThrows(NotValidOrderException.class, () -> {
if (orderPrice > availablePoints) {
throw new NotValidOrderException("Not enough loyalty points");
}
});
assertTrue(exception.getMessage().contains("Not enough loyalty points"));
}
@Test
@DisplayName("Order with quantity greater than stock - should throw NotValidOrderException")
void testOrderWithInsufficientStock() {
int stock = 10;
int requested = 50;
Exception exception = assertThrows(NotValidOrderException.class, () -> {
if (requested > stock) {
throw new NotValidOrderException("Insufficient book stock");
}
});
assertTrue(exception.getMessage().contains("Insufficient book stock"));
}
@Test
@DisplayName("Order with invalid payment method - should throw NotValidOrderException")
void testOrderWithInvalidPaymentMethod() {
String invalidMethod = "UNKNOWN";
Exception exception = assertThrows(NotValidOrderException.class, () -> {
try {
PaymentMethod.valueOf(invalidMethod);
} catch (IllegalArgumentException e) {
throw new NotValidOrderException("Payment method is not valid");
}
});
assertTrue(exception.getMessage().contains("Payment method is not valid"));
}
@Test
@DisplayName("Order with unknown book - should throw OrderNotFoundException")
void testOrderWithUnknownBook() {
String unknownBookId = "unknownBookId";
Exception exception = assertThrows(OrderNotFoundException.class, () -> {
throw new OrderNotFoundException("Book not found");
});
assertTrue(exception.getMessage().contains("Book not found"));
}
@Test
@DisplayName("Order with empty book list - should throw NotValidOrderException")
void testOrderWithEmptyBookList() {
List<OrderLineDTO> emptyLines = List.of();
Exception exception = assertThrows(NotValidOrderException.class, () -> {
if (emptyLines.isEmpty()) {
throw new NotValidOrderException("Book list cannot be empty");
}
});
assertTrue(exception.getMessage().contains("Book list cannot be empty"));
}
@Test
@DisplayName("Order with negative quantity - should throw NotValidOrderException")
void testOrderWithNegativeQuantity() {
int quantity = -1;
Exception exception = assertThrows(NotValidOrderException.class, () -> {
if (quantity < 0) {
throw new NotValidOrderException("Quantity must be positive");
}
});
assertTrue(exception.getMessage().contains("Quantity must be positive"));
}
}
}

View File

@@ -0,0 +1,57 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class NotValidOrderExceptionTest {
@Test
@DisplayName("Exception should be created with the provided message")
void testExceptionCreation() {
String errorMessage = "Order data is not valid";
NotValidOrderException exception = new NotValidOrderException(errorMessage);
assertEquals(errorMessage, exception.getMessage());
}
@ParameterizedTest
@ValueSource(strings = {
"Address fields are required",
"Book list cannot be empty",
"Quantity must be positive",
"Not enough loyalty points",
"Insufficient book stock",
"Payment method is not valid"
})
@DisplayName("Exception should handle different validation messages")
void testExceptionWithDifferentMessages(String errorMessage) {
NotValidOrderException exception = new NotValidOrderException(errorMessage);
assertEquals(errorMessage, exception.getMessage());
}
@Test
@DisplayName("Exception should be properly thrown and caught")
void testExceptionCanBeThrownAndCaught() {
String errorMessage = "Required field is missing";
Exception exception = assertThrows(NotValidOrderException.class, () -> {
throw new NotValidOrderException(errorMessage);
});
assertEquals(errorMessage, exception.getMessage());
}
@Test
@DisplayName("Exception should be catchable as a general Exception")
void testExceptionInheritance() {
String errorMessage = "Invalid order data";
try {
throw new NotValidOrderException(errorMessage);
} catch (Exception e) {
assertEquals(NotValidOrderException.class, e.getClass());
assertEquals(errorMessage, e.getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class OrderNotFoundExceptionTest {
@Test
@DisplayName("Exception message should contain the UUID provided")
void testExceptionMessageContainsUUID() {
UUID uuid = UUID.randomUUID();
OrderNotFoundException exception = new OrderNotFoundException(uuid.toString());
String expectedMessage = String.format("The order with id %s does not exist", uuid);
assertEquals(expectedMessage, exception.getMessage());
}
@Test
@DisplayName("Exception should use the correct constant message format")
void testExceptionUsesConstantMessageFormat() {
UUID uuid = UUID.randomUUID();
OrderNotFoundException exception = new OrderNotFoundException(uuid.toString());
String expectedFormatWithPlaceholder = "The order with id {0} does not exist";
assertEquals(OrderNotFoundException.THE_ORDER_WITH_ID_DOES_NOT_EXIST_MESSAGE, expectedFormatWithPlaceholder);
assertTrue(exception.getMessage().contains(uuid.toString()));
}
@Test
@DisplayName("Exception should be properly thrown and caught")
void testExceptionCanBeThrownAndCaught() {
UUID uuid = UUID.randomUUID();
try {
throw new OrderNotFoundException(uuid.toString());
} catch (OrderNotFoundException e) {
String expectedMessage = String.format("The order with id %s does not exist", uuid);
assertEquals(expectedMessage, e.getMessage());
}
}
}

View File

@@ -0,0 +1,36 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.exception;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class UserNotFoundExceptionTest {
@Test
@DisplayName("Exception message should contain the UUID provided")
void testExceptionMessageContainsUUID() {
String expectedMessage = "Customer not found";
UserNotFoundException exception = new UserNotFoundException(expectedMessage);
assertEquals(expectedMessage, exception.getMessage());
}
@Test
@DisplayName("Exception should use the message as-is")
void testExceptionUsesMessageAsIs() {
String expectedMessage = "Customer not found";
UserNotFoundException exception = new UserNotFoundException(expectedMessage);
assertEquals(expectedMessage, exception.getMessage());
}
@Test
@DisplayName("Exception should be properly thrown and caught with custom message")
void testExceptionCanBeThrownAndCaught() {
String expectedMessage = "Customer not found";
try {
throw new UserNotFoundException(expectedMessage);
} catch (UserNotFoundException e) {
assertEquals(expectedMessage, e.getMessage());
}
}
}

View File

@@ -0,0 +1,104 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.repository;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class OrderRepositoryTest {
private OrderRepository repository;
private Order order1;
private Order order2;
private UUID customerId1;
private UUID customerId2;
@BeforeEach
void setUp() {
repository = new OrderRepository();
customerId1 = UUID.fromString("11111111-1111-1111-1111-111111111111");
customerId2 = UUID.fromString("22222222-2222-2222-2222-222222222222");
order1 = Order.builder()
.id(UUID.randomUUID())
.customerId(customerId1)
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(2).build()))
.totalPrice(79.98)
.totalPriceToPay(79.98)
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
order2 = Order.builder()
.id(UUID.randomUUID())
.customerId(customerId2)
.orderLines(List.of(OrderLineDTO.builder().bookId("9876543210123").quantity(1).build()))
.totalPrice(49.99)
.totalPriceToPay(0.0)
.address(AddressDTO.builder().street("42 Book Street").city("Lyon").postalCode("69000").country("France").build())
.paymentMethod(PaymentMethod.LOYALTY_POINTS)
.build();
}
@Test
@DisplayName("New repository should be empty")
void testNewRepositoryIsEmpty() {
List<Order> orders = repository.findAll();
assertTrue(orders.isEmpty());
assertEquals(0, orders.size());
}
@Nested
@DisplayName("Save operations")
class SaveOperations {
@Test
@DisplayName("Save should add a new order")
void testSaveNewOrder() {
Order savedOrder = repository.save(order1);
Optional<Order> foundOrder = repository.findById(order1.getId());
assertTrue(foundOrder.isPresent());
assertEquals(order1.getId(), savedOrder.getId());
assertEquals(order1.getCustomerId(), savedOrder.getCustomerId());
}
@Test
@DisplayName("Save multiple orders should add all of them")
void testSaveMultipleOrders() {
repository.save(order1);
repository.save(order2);
List<Order> orders = repository.findAll();
assertEquals(2, orders.size());
assertTrue(orders.contains(order1));
assertTrue(orders.contains(order2));
}
}
@Nested
@DisplayName("Find operations")
class FindOperations {
@BeforeEach
void setUpOrders() {
repository.save(order1);
repository.save(order2);
}
@Test
@DisplayName("FindById should return order with matching ID")
void testFindById() {
Optional<Order> foundOrder = repository.findById(order1.getId());
assertTrue(foundOrder.isPresent());
assertEquals(order1.getCustomerId(), foundOrder.get().getCustomerId());
}
@Test
@DisplayName("FindByCustomerId should return all orders for a customer")
void testFindByCustomerId() {
List<Order> orders = repository.findByCustomerId(customerId1);
assertFalse(orders.isEmpty());
assertTrue(orders.stream().allMatch(o -> o.getCustomerId().equals(customerId1)));
}
}
}

View File

@@ -0,0 +1,145 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.usecase;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.entity.Order;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.OrderNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.UserNotFoundException;
import fr.iut_fbleau.but3.dev62.mylibrary.order.repository.OrderRepository;
import org.junit.jupiter.api.BeforeEach;
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;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderUseCaseTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderUseCase orderUseCase;
private UUID orderId;
private UUID customerId;
private OrderInfo validOrderInfo;
private OrderDTO validOrderDTO;
private Order order;
@BeforeEach
void setUp() {
orderId = UUID.randomUUID();
customerId = UUID.randomUUID();
validOrderInfo = OrderInfo.builder()
.customerId(customerId)
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(2).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod("CREDIT_CARD")
.build();
order = Order.builder()
.id(orderId)
.customerId(customerId)
.orderLines(validOrderInfo.getOrderLines())
.totalPrice(79.98)
.totalPriceToPay(79.98)
.address(validOrderInfo.getAddress())
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
validOrderDTO = OrderDTO.builder()
.id(orderId)
.customerId(customerId)
.orderLines(validOrderInfo.getOrderLines())
.totalPrice(79.98)
.totalPriceToPay(79.98)
.address(validOrderInfo.getAddress())
.paymentMethod(PaymentMethod.CREDIT_CARD)
.build();
}
@Nested
@DisplayName("Register order tests")
class RegisterOrderTests {
@Test
@DisplayName("Should register order when valid data is provided")
void testRegisterOrderWithValidData() throws NotValidOrderException, BookNotFoundException {
when(orderRepository.save(any(Order.class))).thenReturn(order);
UUID registeredId = orderUseCase.registerOrder(validOrderInfo);
assertNotNull(registeredId);
verify(orderRepository, times(1)).save(any(Order.class));
}
@Test
@DisplayName("Should throw exception when order data is not valid")
void testRegisterOrderWithInvalidData() {
OrderInfo invalidOrderInfo = OrderInfo.builder()
.customerId(null)
.orderLines(null)
.address(null)
.paymentMethod(null)
.build();
assertThrows(NotValidOrderException.class,
() -> orderUseCase.registerOrder(invalidOrderInfo));
verify(orderRepository, never()).save(any(Order.class));
}
}
@Nested
@DisplayName("Find order tests")
class FindOrderTests {
@Test
@DisplayName("Should return order when ID exists")
void testFindOrderById() {
when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
Optional<OrderDTO> foundOrder = orderUseCase.findOrderById(orderId);
assertTrue(foundOrder.isPresent());
assertEquals(orderId, foundOrder.get().getId());
verify(orderRepository, times(1)).findById(orderId);
}
@Test
@DisplayName("Should throw exception when order ID doesn't exist")
void testFindOrderByIdNotFound() {
UUID nonExistentId = UUID.randomUUID();
when(orderRepository.findById(nonExistentId)).thenReturn(Optional.empty());
assertThrows(OrderNotFoundException.class,
() -> orderUseCase.findOrderById(nonExistentId));
verify(orderRepository, times(1)).findById(nonExistentId);
}
}
@Nested
@DisplayName("Find orders by customer tests")
class FindOrdersByCustomerTests {
@Test
@DisplayName("Should return all orders for a customer")
void testFindOrdersByCustomerId() {
List<OrderDTO> orders = List.of(validOrderDTO);
when(orderRepository.findByCustomerId(customerId)).thenReturn(List.of(order));
List<OrderDTO> foundOrders = orderUseCase.findOrdersByCustomerId(customerId);
assertNotNull(foundOrders);
assertFalse(foundOrders.isEmpty());
assertEquals(orders, foundOrders);
verify(orderRepository, times(1)).findByCustomerId(customerId);
}
@Test
@DisplayName("Should throw exception when customer ID doesn't exist")
void testFindOrdersByUnknownCustomer() {
UUID nonExistentCustomer = UUID.randomUUID();
when(orderRepository.findByCustomerId(nonExistentCustomer)).thenThrow(new UserNotFoundException(nonExistentCustomer.toString()));
assertThrows(UserNotFoundException.class,
() -> orderUseCase.findOrdersByCustomerId(nonExistentCustomer));
verify(orderRepository, times(1)).findByCustomerId(nonExistentCustomer);
}
}
}

View File

@@ -0,0 +1,164 @@
package fr.iut_fbleau.but3.dev62.mylibrary.order.validator;
import fr.iut_fbleau.but3.dev62.mylibrary.order.*;
import fr.iut_fbleau.but3.dev62.mylibrary.order.exception.NotValidOrderException;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.repository.CustomerRepository;
import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class OrderValidatorTest {
private final CustomerRepository customerRepository = new CustomerRepository();
private final BookRepository bookRepository = new BookRepository();
@Test
@DisplayName("Should validate order with valid data")
void testValidateValidOrder() {
OrderInfo validOrder = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod("CREDIT_CARD")
.build();
assertDoesNotThrow(() -> OrderValidator.validate(validOrder, bookRepository, customerRepository));
}
@Nested
@DisplayName("Customer ID validation tests")
class CustomerIdValidationTests {
@Test
@DisplayName("Should throw exception when customerId is null")
void testValidateNullCustomerId() {
OrderInfo order = OrderInfo.builder()
.customerId(null)
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod("CREDIT_CARD")
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.CUSTOMER_ID_CANNOT_BE_NULL, exception.getMessage());
}
}
@Nested
@DisplayName("Order lines validation tests")
class OrderLinesValidationTests {
@Test
@DisplayName("Should throw exception when order lines are empty")
void testValidateEmptyOrderLines() {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of())
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod("CREDIT_CARD")
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.BOOK_LIST_CANNOT_BE_EMPTY, exception.getMessage());
}
@Test
@DisplayName("Should throw exception when order line has negative quantity")
void testValidateNegativeQuantity() {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(-1).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod("CREDIT_CARD")
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.QUANTITY_MUST_BE_POSITIVE, exception.getMessage());
}
}
@Nested
@DisplayName("Address validation tests")
class AddressValidationTests {
@Test
@DisplayName("Should throw exception when address is null")
void testValidateNullAddress() {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(null)
.paymentMethod("CREDIT_CARD")
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.ADDRESS_FIELDS_ARE_REQUIRED, exception.getMessage());
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t", "\n"})
@DisplayName("Should throw exception when address fields are blank")
void testValidateBlankAddressFields(String blank) {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(AddressDTO.builder().street(blank).city(blank).postalCode(blank).country(blank).build())
.paymentMethod("CREDIT_CARD")
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.ADDRESS_FIELDS_ARE_REQUIRED, exception.getMessage());
}
}
@Nested
@DisplayName("Payment method validation tests")
class PaymentMethodValidationTests {
@Test
@DisplayName("Should throw exception when payment method is null")
void testValidateNullPaymentMethod() {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod(null)
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.PAYMENT_METHOD_IS_NOT_VALID, exception.getMessage());
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "UNKNOWN", "BITCOIN"})
@DisplayName("Should throw exception when payment method is invalid")
void testValidateInvalidPaymentMethod(String invalidMethod) {
OrderInfo order = OrderInfo.builder()
.customerId(UUID.randomUUID())
.orderLines(List.of(OrderLineDTO.builder().bookId("1234567890123").quantity(1).build()))
.address(AddressDTO.builder().street("12 Main St.").city("Paris").postalCode("75000").country("France").build())
.paymentMethod(invalidMethod)
.build();
NotValidOrderException exception = assertThrows(
NotValidOrderException.class,
() -> OrderValidator.validate(order, bookRepository, customerRepository)
);
assertEquals(OrderValidator.PAYMENT_METHOD_IS_NOT_VALID, exception.getMessage());
}
}
}

View File

@@ -0,0 +1,180 @@
# language: en
Feature: Manage customer orders
Background:
Given the system has the following books in stock:
| isbn | titre | auteur | editeur | datePublication | prix | stockInitial | categories | description | langue |
| 1234567890123 | The Pragmatic Programmer | Andy Hunt | Addison-Wesley | 2025-06-10 | 39.99 | 10 | FICTION,THRILLER | A practical guide to becoming a better and more efficient software developer. | EN |
| 9876543210123 | Clean Code | Robert Martin | Prentice Hall | 2024-01-15 | 49.99 | 5 | FICTION | A handbook of best practices for writing readable, maintainable, and clean code in Java. | EN |
And the system has the following customers in the database:
| id | firstName | lastName | phoneNumber | loyaltyPoints |
| 11111111-1111-1111-1111-111111111111 | Alice | Smith | 0612345678 | 100 |
| 22222222-2222-2222-2222-222222222222 | Bob | Martin | 0698765432 | 10 |
# Create orders
Scenario: Create an order using credit card
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 2 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then a new order is created
And the total price is 79.98
And The customer "11111111-1111-1111-1111-111111111111" now has 100 loyalty points
Scenario: Create an order using loyalty points
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | LOYALTY_POINTS |
And the order includes the following books:
| bookId | quantity |
| 9876543210123 | 1 |
And the delivery address is:
| street | city | postalCode | country |
| 42 Book Street | Lyon | 69000 | France |
Then a new order is created
And the total price is 49.99
And The customer "11111111-1111-1111-1111-111111111111" now has 50 loyalty points
Scenario: Create an order with multiple books
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 3 |
| 9876543210123 | 4 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then a new order is created
And the total price is 319.93
And The customer "11111111-1111-1111-1111-111111111111" now has 100 loyalty points
Scenario: Attempt to create an order with invalid address
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 1 |
And the delivery address is:
| street | city | postalCode | country |
| | | | |
Then the order creation fails
And I receive an error for validation order message containing "Address fields are required"
Scenario: Attempt to create an order with unknown customer
When I create a new order with the following information:
| customerId | paymentMethod |
| 00000000-0000-0000-0000-000000000000 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 1 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for not found user exception message containing "Customer not found"
Scenario: Attempt to create an order with insufficient loyalty points
When I create a new order with the following information:
| customerId | paymentMethod |
| 22222222-2222-2222-2222-222222222222 | LOYALTY_POINTS |
And the order includes the following books:
| bookId | quantity |
| 9876543210123 | 1 |
And the delivery address is:
| street | city | postalCode | country |
| 42 Book Street | Lyon | 69000 | France |
Then the order creation fails
And I receive an error for validation order message containing "Not enough loyalty points"
Scenario: Attempt to order more books than available stock
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 50 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for validation order message containing "Insufficient book stock"
Scenario: Attempt to create an order with invalid payment method
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | UNKNOWN |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | 1 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for validation order message containing "Payment method is not valid"
Scenario: Attempt to create an order with unknown book
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| unknownBookId | 1 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for not found exception message containing "Book not found"
Scenario: Attempt to create an order with empty book list
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes no books
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for validation order message containing "Book list cannot be empty"
Scenario: Attempt to create an order with a negative quantity
When I create a new order with the following information:
| customerId | paymentMethod |
| 11111111-1111-1111-1111-111111111111 | CREDIT_CARD |
And the order includes the following books:
| bookId | quantity |
| 1234567890123 | -1 |
And the delivery address is:
| street | city | postalCode | country |
| 12 Main St. | Paris | 75000 | France |
Then the order creation fails
And I receive an error for validation order message containing "Quantity must be positive"
#Get orders
Scenario: Retrieve an order by ID
Given an order with ID "abcd1234-5678-90ef-1234-567890abcdef" exists for customer "11111111-1111-1111-1111-111111111111"
When I retrieve the order by ID "abcd1234-5678-90ef-1234-567890abcdef"
Then I receive the order details
Scenario: Retrieve all orders for a customer
When I request all orders for customer "11111111-1111-1111-1111-111111111111"
Then I receive a list of orders
Scenario: Attempt to retrieve an unknown order
When I retrieve the order by ID "unknown-order-id"
Then the retrieval fails
And I receive an error for not found exception message containing "Order not found"
Scenario: Attempt to retrieve orders for an unknown customer
When I request all orders for customer "00000000-0000-0000-0000-000000000000"
Then the retrieval fails
And I receive an error for not found user exception message containing "Customer not found"