15 Commits

Author SHA1 Message Date
RKaraMos_EVO 092a020059 ajout du scenario cucumber et du README 2026-06-14 15:22:36 +02:00
RKaraMos_EVO 8708686994 ajout du usecase pour les livres + tests 2026-06-14 15:22:29 +02:00
RKaraMos_EVO a595750194 ajout du repository en memoire + tests 2026-06-12 23:32:35 +02:00
RKaraMos_EVO 00e90aaa0c ajout du converter Info-entity-DTO + tests 2026-06-12 23:32:26 +02:00
RKaraMos_EVO 009ac7f5f7 ajout du validator pour les livres + tests 2026-06-12 23:32:17 +02:00
RKaraMos_EVO 9080b8e67e ajout entity Book et BookDTO + tests 2026-06-12 23:30:58 +02:00
RKaraMos_EVO 179f259c3d ajout des exceptions du module book et leurs tests 2026-06-12 23:30:45 +02:00
RKaraMos_EVO 1aa83b0911 ajout enum Category et record BookInfo 2026-06-11 14:23:15 +02:00
RKaraMos_EVO abbd3ee95d ajout du module customer fourni par le prof 2026-06-11 14:22:59 +02:00
RKaraMos_EVO efa6fc49eb ajout du pom et du gitignore pour le module 2026-06-11 14:22:13 +02:00
Adrien DICK 4008ef4a9b Merge pull request 'Remise au propre du projet' (#1) from dick into main
Reviewed-on: #1
2026-06-11 10:34:19 +02:00
Adrien DICK b99dcd8592 Get api/books pour la page catalogue 2026-06-11 04:32:20 -04:00
Adrien DICK 4e1a0b24f4 🎨 Remise au propre 2026-06-10 16:24:37 -04:00
Riad KARA-MOSTEFA 9318d1c28b Merge branch 'main' of https://grond.iut-fbleau.fr/aissi/2026-DEV-BUT3 2026-03-16 16:45:01 +01:00
Riad KARA-MOSTEFA 038c52efc0 debut projet 2026-03-16 16:43:17 +01:00
79 changed files with 21341 additions and 0 deletions
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+70
View File
@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
+17341
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
{
"name": "my-library",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.17.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Biblio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+38
View File
@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+21
View File
@@ -0,0 +1,21 @@
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import Books from './pages/Books';
import Orders from './pages/Orders';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="books" element={<Books />} />
<Route path="orders" element={<Orders />} />
<Route path="profile" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
+8
View File
@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+5
View File
@@ -0,0 +1,5 @@
import client from './client';
export function getBooks(page = 0, size = 20) {
return client.get('/api/books', { params: { page, size } });
}
+7
View File
@@ -0,0 +1,7 @@
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:8080',
});
export default client;
+11
View File
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
export default function Layout() {
return (
<>
<Navbar />
<Outlet />
</>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import '../styles/navbar.css';
export default function Navbar() {
return (
<nav className="navbar">
<Link to="/" className="navbar__logo">Biblio</Link>
<ul className="navbar__links">
<li><Link to="/">Accueil</Link></li>
<li><Link to="/books">Catalogue</Link></li>
<li><Link to="/orders">Commandes</Link></li>
<li><Link to="/profile">Mon compte</Link></li>
</ul>
<div className="navbar__actions">
<button className="btn-ghost">Connexion</button>
<button className="btn-cta">S'inscrire</button>
</div>
</nav>
);
}
View File
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/global.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 KiB

+39
View File
@@ -0,0 +1,39 @@
import { useState, useEffect } from 'react';
import { getBooks } from '../api/books';
export default function Books() {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
getBooks()
.then((response) => {
console.log(response.data);
setBooks(response.data.content);
})
.catch((err) => {
console.error(err);
setError("Impossible de charger les livres.");
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) return <main><p>Chargement</p></main>;
if (error) return <main><p>{error}</p></main>;
return (
<main>
<h1>Catalogue</h1>
<ul>
{books.map((book) => (
<li key={book.isbn}>
<strong>{book.title}</strong> - {book.author}
</li>
))}
</ul>
</main>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { Link } from 'react-router-dom';
import '../styles/home.css';
export default function Home() {
return (
<main>
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function NotFound() {
return <main><h1>404 Page introuvable</h1></main>;
}
+3
View File
@@ -0,0 +1,3 @@
export default function Orders() {
return <main><h1>Mes commandes</h1></main>;
}
+3
View File
@@ -0,0 +1,3 @@
export default function Profile() {
return <main><h1>Mon compte</h1></main>;
}
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
View File
View File
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+78
View File
@@ -0,0 +1,78 @@
# mylibrary — back-end Java (méthode BDD)
Back-end du projet **2026-DEV-BUT3**. Reproduit, en Java pur, le cœur métier
de l'API consommée par le front React `my-library/`.
## Méthode
Conformément au cours `maintenanceApplicativeCours1.pdf` (Behavior-Driven
Development), pour chaque comportement on :
1. Écrit le scénario attendu (en Gherkin / `.feature` ou en `@DisplayName` JUnit)
2. Écrit le test qui le vérifie
3. Implémente le code qui fait passer le test
## Structure (calquée sur le module `customer` fourni par le prof)
```
src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/
├─ customer/ Module fourni par l'enseignant (intact)
│ ├─ CustomerInfo.java record d'entrée
│ ├─ CustomerDTO.java DTO de sortie
│ ├─ entity/ Customer (objet métier, règles fidélité)
│ ├─ exception/ Exceptions métier
│ ├─ converter/ Mapping Info <-> entity <-> DTO
│ ├─ validator/ Règles de validation
│ ├─ repository/ Stockage en mémoire (List)
│ └─ usecase/ Cas d'usage (orchestration)
└─ book/ Module développé en miroir (notre travail)
└─ … (mêmes sous-paquets, même découpage)
src/test/java/.../mylibrary/
├─ customer/ Tests JUnit fournis par l'enseignant
├─ book/ Tests JUnit que nous avons écrits
└─ features/
├─ RunCucumberTest.java (du prof, intact)
├─ client/CustomerSteps.java (du prof, intact)
├─ book/BookSteps.java (notre travail)
└─ resources/features/
├─ client.feature (du prof, intact)
└─ book.feature (notre travail)
```
## Dépendances
Strictement les mêmes que dans le template du prof :
- JUnit 5 (jupiter-api, params, engine + platform-suite, platform-engine, platform-launcher)
- Mockito (core, junit-jupiter)
- Cucumber (cucumber-java, cucumber-junit-platform-engine)
- Lombok
## Build & test
Pré-requis : **JDK 21** + **Maven 3.9+**
```bash
mvn -f mylibrary test
```
Cette commande exécute :
- les tests unitaires JUnit (entity, validator, converter, repository, usecase, exceptions)
- les scénarios Cucumber (`client.feature` + `book.feature`) via `RunCucumberTest`
## Périmètre couvert
Conformément à la consigne (« reproduire le strict nécessaire pour démontrer
la maîtrise »), l'API se limite aux deux domaines présents dans le swagger
qui structurent l'app React :
- **Catalogue** (`book`) : enregistrer, consulter par ISBN, lister tous les livres,
refuser ISBN dupliqué, refuser un livre invalide, gérer le stock.
- **Comptes clients** (`customer`) : enregistrer, consulter par téléphone, mettre
à jour, supprimer, ajouter / retirer des points de fidélité (module fourni).
L'exposition HTTP réelle utilisée par les développeurs front est l'API du prof
(`mylibrary-0.0.1-SNAPSHOT.jar`) ; ce module sert à démontrer la maîtrise
de la **conception métier en BDD/TDD**.
+151
View File
@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.iut_fbleau.but3.dev62</groupId>
<artifactId>mylibrary</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- Your java version-->
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Main dependencies -->
<lombok.version>1.18.36</lombok.version>
<!-- Test Verisons-->
<junit.version>5.11.4</junit.version>
<junit.platform.version>1.11.4</junit.platform.version>
<cucumber.version>7.21.1</cucumber.version>
<mockito.version>5.16.0</mockito.version>
<!-- Maven build version -->
<maven.compiler.version>3.13.0</maven.compiler.version>
<maven.surefire.version>3.5.2</maven.surefire.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-bom</artifactId>
<version>${cucumber.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
<configuration>
<properties>
<!-- Work around. Surefire does not include enough
information to disambiguate between different
examples and scenarios. -->
<configurationParameters>
cucumber.junit-platform.naming-strategy=long
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,22 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class BookDTO {
private final long isbn;
private final String title;
private final String author;
private final String publisher;
private final LocalDate publicationDate;
private final double price;
private final int quantity;
private final List<Category> categories;
private final String description;
private final String language;
}
@@ -0,0 +1,19 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category;
import java.time.LocalDate;
import java.util.List;
public record BookInfo(
long isbn,
String title,
String author,
String publisher,
LocalDate publicationDate,
double price,
int quantity,
List<Category> categories,
String description,
String language
) {
}
@@ -0,0 +1,42 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.converter;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
public final class BookConverter {
private BookConverter() {
}
public static Book toDomain(BookInfo book) {
return Book.builder()
.isbn(book.isbn())
.title(book.title())
.author(book.author())
.publisher(book.publisher())
.publicationDate(book.publicationDate())
.price(book.price())
.quantity(book.quantity())
.categories(book.categories())
.description(book.description())
.language(book.language())
.build();
}
public static BookDTO toDTO(Book book) {
return BookDTO.builder()
.isbn(book.getIsbn())
.title(book.getTitle())
.author(book.getAuthor())
.publisher(book.getPublisher())
.publicationDate(book.getPublicationDate())
.price(book.getPrice())
.quantity(book.getQuantity())
.categories(book.getCategories())
.description(book.getDescription())
.language(book.getLanguage())
.build();
}
}
@@ -0,0 +1,24 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.entity;
public enum Category {
FICTION,
NON_FICTION,
SCIENCE_FICTION,
FANTASY,
MYSTERY,
THRILLER,
ROMANCE,
BIOGRAPHY,
HISTORY,
POETRY,
CHILDRENS,
YOUNG_ADULT,
SCIENCE,
PHILOSOPHY,
SELF_HELP,
TRAVEL,
COOKING,
ART,
RELIGION,
REFERENCE
}
@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.exception;
import java.text.MessageFormat;
public class BookAlreadyExistsException extends Exception {
public static final String A_BOOK_WITH_ISBN_ALREADY_EXISTS_MESSAGE = "A book with isbn {0} already exists";
public BookAlreadyExistsException(long isbn) {
// ISBN passe en String pour ne pas que MessageFormat ajoute le separateur de milliers
super(MessageFormat.format(A_BOOK_WITH_ISBN_ALREADY_EXISTS_MESSAGE, String.valueOf(isbn)));
}
}
@@ -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_MESSAGE = "The book with isbn {0} does not exist";
public BookNotFoundException(long isbn) {
// ISBN passe en String pour ne pas que MessageFormat ajoute le separateur de milliers
super(MessageFormat.format(THE_BOOK_WITH_ISBN_DOES_NOT_EXIST_MESSAGE, String.valueOf(isbn)));
}
}
@@ -0,0 +1,12 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.exception;
import java.text.MessageFormat;
public class IllegalBookQuantityException extends Exception {
public static final String CANNOT_REMOVE_STOCK = "Cannot remove {0} units from {1} units in stock";
public IllegalBookQuantityException(int needed, int actual) {
super(MessageFormat.format(CANNOT_REMOVE_STOCK, needed, actual));
}
}
@@ -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,42 @@
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 List<Book> findAll() {
return books;
}
public void deleteAll() {
books.clear();
}
public Book save(Book book) {
Optional<Book> existing = findByIsbn(book.getIsbn());
existing.ifPresent(books::remove);
books.add(book);
return book;
}
public Optional<Book> findByIsbn(long isbn) {
return books.stream()
.filter(b -> b.getIsbn() == isbn)
.findFirst();
}
public boolean existsByIsbn(long isbn) {
return books.stream().anyMatch(b -> b.getIsbn() == isbn);
}
public void delete(Book book) {
books.remove(book);
}
}
@@ -0,0 +1,46 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.usecase;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.book.converter.BookConverter;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookAlreadyExistsException;
import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException;
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;
import java.util.List;
import java.util.Optional;
public final class BookUseCase {
private final BookRepository bookRepository;
public BookUseCase(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public long registerBook(BookInfo bookInfo) throws NotValidBookException, BookAlreadyExistsException {
BookValidator.validate(bookInfo);
if (bookRepository.existsByIsbn(bookInfo.isbn())) {
throw new BookAlreadyExistsException(bookInfo.isbn());
}
Book toRegister = BookConverter.toDomain(bookInfo);
Book registered = bookRepository.save(toRegister);
return registered.getIsbn();
}
public BookDTO getBookByIsbn(long isbn) throws BookNotFoundException {
Optional<Book> optional = bookRepository.findByIsbn(isbn);
if (optional.isEmpty()) {
throw new BookNotFoundException(isbn);
}
return BookConverter.toDTO(optional.get());
}
public List<BookDTO> getAllBooks() {
return bookRepository.findAll().stream()
.map(BookConverter::toDTO)
.toList();
}
}
@@ -0,0 +1,93 @@
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 {
public static final int TITLE_MAX_LENGTH = 255;
public static final String ISBN_MUST_BE_POSITIVE = "Isbn must be a positive number";
public static final String TITLE_CANNOT_BE_BLANK = "Title cannot be blank";
public static final String TITLE_TOO_LONG = "Title cannot exceed " + TITLE_MAX_LENGTH + " characters";
public static final String AUTHOR_CANNOT_BE_BLANK = "Author cannot be blank";
public static final String PUBLISHER_CANNOT_BE_BLANK = "Publisher cannot be blank";
public static final String PUBLICATION_DATE_REQUIRED = "Publication date is required";
public static final String PRICE_MUST_BE_POSITIVE = "Price must be strictly positive";
public static final String QUANTITY_MUST_BE_POSITIVE_OR_ZERO = "Quantity must be >= 0";
public static final String CATEGORIES_REQUIRED = "At least one category is required";
public static final String LANGUAGE_CANNOT_BE_BLANK = "Language cannot be blank";
private BookValidator() {
}
public static void validate(BookInfo book) throws NotValidBookException {
validateIsbn(book);
validateTitle(book);
validateAuthor(book);
validatePublisher(book);
validatePublicationDate(book);
validatePrice(book);
validateQuantity(book);
validateCategories(book);
validateLanguage(book);
}
private static void validateIsbn(BookInfo book) throws NotValidBookException {
if (book.isbn() <= 0) {
throw new NotValidBookException(ISBN_MUST_BE_POSITIVE);
}
}
private static void validateTitle(BookInfo book) throws NotValidBookException {
if (book.title() == null || book.title().isBlank()) {
throw new NotValidBookException(TITLE_CANNOT_BE_BLANK);
}
if (book.title().length() > TITLE_MAX_LENGTH) {
throw new NotValidBookException(TITLE_TOO_LONG);
}
}
private static void validateAuthor(BookInfo book) throws NotValidBookException {
if (book.author() == null || book.author().isBlank()) {
throw new NotValidBookException(AUTHOR_CANNOT_BE_BLANK);
}
}
private static void validatePublisher(BookInfo book) throws NotValidBookException {
if (book.publisher() == null || book.publisher().isBlank()) {
throw new NotValidBookException(PUBLISHER_CANNOT_BE_BLANK);
}
}
private static void validatePublicationDate(BookInfo book) throws NotValidBookException {
if (book.publicationDate() == null) {
throw new NotValidBookException(PUBLICATION_DATE_REQUIRED);
}
}
private static void validatePrice(BookInfo book) throws NotValidBookException {
if (book.price() <= 0) {
throw new NotValidBookException(PRICE_MUST_BE_POSITIVE);
}
}
private static void validateQuantity(BookInfo book) throws NotValidBookException {
if (book.quantity() < 0) {
throw new NotValidBookException(QUANTITY_MUST_BE_POSITIVE_OR_ZERO);
}
}
private static void validateCategories(BookInfo book) throws NotValidBookException {
if (book.categories() == null || book.categories().isEmpty()) {
throw new NotValidBookException(CATEGORIES_REQUIRED);
}
}
private static void validateLanguage(BookInfo book) throws NotValidBookException {
if (book.language() == null || book.language().isBlank()) {
throw new NotValidBookException(LANGUAGE_CANNOT_BE_BLANK);
}
}
}
@@ -0,0 +1,15 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class CustomerDTO {
private final UUID id;
private final String firstName;
private final String lastName;
private final String phoneNumber;
private final int loyaltyPoints;
}
@@ -0,0 +1,4 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer;
public record CustomerInfo(String firstName, String lastName, String phoneNumber) {
}
@@ -0,0 +1,30 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.converter;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.CustomerDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.CustomerInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
public final class CustomerConverter {
private CustomerConverter(){
}
public static Customer toDomain(CustomerInfo newCustomer) {
return Customer.builder()
.firstName(newCustomer.firstName())
.lastName(newCustomer.lastName())
.phoneNumber(newCustomer.phoneNumber())
.loyaltyPoints(0)
.build();
}
public static CustomerDTO toDTO(Customer customer) {
return CustomerDTO.builder()
.id(customer.getId())
.firstName(customer.getFirstName())
.lastName(customer.getLastName())
.phoneNumber(customer.getPhoneNumber())
.loyaltyPoints(customer.getLoyaltyPoints())
.build();
}
}
@@ -0,0 +1,30 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.entity;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.exception.IllegalCustomerPointException;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class Customer {
private UUID id;
private String firstName;
private String lastName;
private String phoneNumber;
private int loyaltyPoints;
public void setRandomUUID() {
this.id = UUID.randomUUID();
}
public void addLoyaltyPoints(int loyaltyPointToAdd) {
this.loyaltyPoints += loyaltyPointToAdd;
}
public void removeLoyaltyPoints(int loyaltyPointToRemove) throws IllegalCustomerPointException {
if (loyaltyPointToRemove > this.loyaltyPoints) throw new IllegalCustomerPointException(loyaltyPointToRemove, this.loyaltyPoints);
this.loyaltyPoints -= loyaltyPointToRemove;
}
}

Some files were not shown because too many files have changed in this diff Show More