37 Commits

Author SHA1 Message Date
AISSI-JUDE-CHRIST 0d9f8dc539 Créer un abonnement 2026-06-14 16:12:47 +02:00
Riad KARA-MOSTEFA 4197f99116 Merge pull request 'Api java' (#8) from api_java into main
Reviewed-on: #8
2026-06-14 15:28:23 +02:00
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
AISSI-JUDE-CHRIST 0d37efd227 merge retours livres admin 2026-06-14 00:10:02 +02:00
Adrien DICK 1cf4bed83b Merge pull request ' Faire une commande' (#7) from Commande into main
Reviewed-on: #7
2026-06-13 11:34:02 +02:00
Adrien DICK 0ba4e5599e Faire une commande 2026-06-13 05:32:59 -04:00
AISSI-JUDE-CHRIST 587e81816c Gérer les retours de livres par l'admin 2026-06-13 01:11:27 +02:00
AISSI-JUDE-CHRIST 8359f21e07 Merge 2026-06-13 00:48:12 +02:00
Riad KARA-MOSTEFA e3c02a6fc8 Merge pull request 'Api java' (#6) from api_java into main
Reviewed-on: #6
2026-06-12 23:34:30 +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
AISSI-JUDE-CHRIST bfd1f600de Gerer un avis 2026-06-12 22:58:43 +02:00
AISSI-JUDE-CHRIST 52213fce0b re Correction page login 2026-06-12 21:37:12 +02:00
AISSI-JUDE-CHRIST 7117aba838 Correction page login 2026-06-12 21:35:40 +02:00
AISSI-JUDE-CHRIST 010ec9c11a reservation livres 2026-06-11 23:08:21 +02:00
AISSI-JUDE-CHRIST de47c293b8 correction affiche livre 2026-06-11 22:36:47 +02:00
AISSI-JUDE-CHRIST 4ae227d79d ajout authentification admin/user 2026-06-11 22:23:39 +02:00
Adrien DICK 8f3451adc0 Merge pull request 'Gestion client' (#5) from GestionClient into main
Reviewed-on: #5
2026-06-11 16:49:36 +02:00
Riad KARA-MOSTEFA 9916b02ef6 Merge pull request 'Api java' (#4) from api_java into main
Reviewed-on: #4
2026-06-11 14:28:28 +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 e33acdf151 Ajouter et Retirer des points de fidélité 2026-06-11 07:59:03 -04:00
Adrien DICK ccafc24f72 Ajouter un client 2026-06-11 07:27:37 -04:00
Adrien DICK c811c373d5 Merge pull request ' Voir le detail d'un livre' (#3) from DetailLivre into main
Reviewed-on: #3
2026-06-11 11:55:29 +02:00
Adrien DICK 1f9a1e9af8 Voir le detail d'un livre 2026-06-11 05:52:26 -04:00
Adrien DICK e438373370 Merge pull request ' Enregistrer un livre' (#2) from EnregistrerLivre into main
Reviewed-on: #2
2026-06-11 11:35:23 +02:00
Adrien DICK 2c0f30f0a2 Enregistrer un livre 2026-06-11 05:15:30 -04: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
93 changed files with 22373 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);
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Routes, Route, Navigate } 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';
import AddBook from './pages/AddBook';
import BookDetail from './pages/BookDetail';
import Customers from './pages/Customers';
import Login from './pages/Login';
import Reservations from './pages/Reservations';
import Returns from './pages/Returns';
import Subscription from './pages/Subscription';
import { useAuth } from './context/AuthContext';
function RequireAuth({ children }) {
const { user } = useAuth();
return user ? children : <Navigate to="/login" replace />;
}
function RequireAdmin({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
if (user.role !== 'admin') return <Navigate to="/" replace />;
return children;
}
export default function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="books" element={<Books />} />
<Route path="books/:bookId" element={<RequireAuth><BookDetail /></RequireAuth>} />
<Route path="books/new" element={<RequireAdmin><AddBook /></RequireAdmin>} />
<Route path="orders" element={<RequireAuth><Orders /></RequireAuth>} />
<Route path="reservations" element={<RequireAuth><Reservations /></RequireAuth>} />
<Route path="profile" element={<RequireAuth><Profile /></RequireAuth>} />
<Route path="customers" element={<RequireAdmin><Customers /></RequireAdmin>} />
<Route path="returns" element={<RequireAdmin><Returns /></RequireAdmin>} />
<Route path="subscription" element={<RequireAuth><Subscription /></RequireAuth>} />
<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();
});
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function getBooks(page = 0, size = 20) {
return client.get('/api/books', { params: { page, size } });
}
export function registerBook(book) {
return client.post('/api/books', book);
}
export function getBookById(id) {
return client.get(`/api/books/${id}`);
}
export function reserveBook(bookId, reservation) {
return client.post(`/api/books/${bookId}/reservations`, reservation);
}
+7
View File
@@ -0,0 +1,7 @@
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:8080',
});
export default client;
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function registerCustomer(customer) {
return client.post('/api/customers', customer);
}
export function findCustomerByPhone(phoneNumber) {
return client.get(`/api/customers/phone/${encodeURIComponent(phoneNumber)}`);
}
export function addLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/add`, null, { params: { points } });
}
export function subtractLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/subtract`, null, { params: { points } });
}
+5
View File
@@ -0,0 +1,5 @@
import client from './client';
export function createOrder(order) {
return client.post('/api/orders', order);
}
+11
View File
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
export default function Layout() {
return (
<>
<Navbar />
<Outlet />
</>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import '../styles/navbar.css';
export default function Navbar() {
const { user, logout } = useAuth();
const navigate = useNavigate();
function handleLogout() {
logout();
navigate('/login');
}
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>
{user && <li><Link to="/orders">Commandes</Link></li>}
{user?.role === 'user' && <li><Link to="/reservations">Mes réservations</Link></li>}
{user?.role === 'user' && <li><Link to="/subscription">Mon abonnement</Link></li>}
{user && <li><Link to="/profile">Mon compte</Link></li>}
{user?.role === 'admin' && <li><Link to="/customers">Clients</Link></li>}
{user?.role === 'admin' && <li><Link to="/returns">Retours</Link></li>}
</ul>
<div className="navbar__actions">
{user ? (
<>
<span>{user.username} ({user.role})</span>
<button onClick={handleLogout}>Déconnexion</button>
</>
) : (
<Link to="/login"><button>Connexion</button></Link>
)}
</div>
</nav>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { createContext, useContext, useState } from 'react';
const USERS = [
{ username: 'admin', password: 'admin', role: 'admin' },
{ username: 'alice', password: 'bob', role: 'user' },
];
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('auth');
return saved ? JSON.parse(saved) : null;
});
function login(username, password) {
const found = USERS.find(u => u.username === username && u.password === password);
if (!found) return false;
const { password: _, ...safe } = found;
setUser(safe);
localStorage.setItem('auth', JSON.stringify(safe));
return true;
}
function logout() {
setUser(null);
localStorage.removeItem('auth');
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
@@ -0,0 +1,42 @@
import { createContext, useContext, useState } from 'react';
const ReservationContext = createContext(null);
export function ReservationProvider({ children }) {
const [reservations, setReservations] = useState(() => {
const saved = localStorage.getItem('reservations');
return saved ? JSON.parse(saved) : [];
});
function addReservation(book, phoneNumber) {
const reservation = {
reservationId: crypto.randomUUID(),
bookId: book.isbn,
bookTitle: book.title,
bookAuthor: book.author,
phoneNumber,
status: 'CONFIRMED',
reservedAt: new Date().toISOString(),
};
const updated = [...reservations, reservation];
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
return reservation;
}
function cancelReservation(reservationId) {
const updated = reservations.filter(r => r.reservationId !== reservationId);
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
}
return (
<ReservationContext.Provider value={{ reservations, addReservation, cancelReservation }}>
{children}
</ReservationContext.Provider>
);
}
export function useReservations() {
return useContext(ReservationContext);
}
+36
View File
@@ -0,0 +1,36 @@
import { createContext, useContext, useState } from 'react';
const ReturnContext = createContext(null);
export function ReturnProvider({ children }) {
const [returns, setReturns] = useState(() => {
const saved = localStorage.getItem('returns');
return saved ? JSON.parse(saved) : [];
});
function addReturn(bookId, bookTitle, customerPhone, reason) {
const bookReturn = {
returnId: crypto.randomUUID(),
bookId,
bookTitle,
customerPhone,
reason,
status: 'PROCESSED',
returnedAt: new Date().toISOString(),
};
const updated = [...returns, bookReturn];
setReturns(updated);
localStorage.setItem('returns', JSON.stringify(updated));
return bookReturn;
}
return (
<ReturnContext.Provider value={{ returns, addReturn }}>
{children}
</ReturnContext.Provider>
);
}
export function useReturns() {
return useContext(ReturnContext);
}
+40
View File
@@ -0,0 +1,40 @@
import { createContext, useContext, useState } from 'react';
const ReviewContext = createContext(null);
export function ReviewProvider({ children }) {
const [reviews, setReviews] = useState(() => {
const saved = localStorage.getItem('reviews');
return saved ? JSON.parse(saved) : [];
});
function addReview(bookId, bookTitle, username, rating, comment) {
const review = {
reviewId: crypto.randomUUID(),
bookId,
bookTitle,
username,
rating,
comment,
createdAt: new Date().toISOString(),
};
const updated = [...reviews, review];
setReviews(updated);
localStorage.setItem('reviews', JSON.stringify(updated));
return review;
}
function getReviewsByBook(bookId) {
return reviews.filter(r => String(r.bookId) === String(bookId));
}
return (
<ReviewContext.Provider value={{ reviews, addReview, getReviewsByBook }}>
{children}
</ReviewContext.Provider>
);
}
export function useReviews() {
return useContext(ReviewContext);
}
@@ -0,0 +1,50 @@
import { createContext, useContext, useState } from 'react';
const SubscriptionContext = createContext(null);
const PLANS = [
{ id: 'basic', name: 'Basic', price: 9.99, description: '2 livres par mois' },
{ id: 'standard', name: 'Standard', price: 14.99, description: '5 livres par mois' },
{ id: 'premium', name: 'Premium', price: 24.99, description: 'Livres illimités par mois' },
];
export { PLANS };
export function SubscriptionProvider({ children }) {
const [subscription, setSubscription] = useState(() => {
const saved = localStorage.getItem('subscription');
return saved ? JSON.parse(saved) : null;
});
function subscribe(planId, phoneNumber) {
const plan = PLANS.find(p => p.id === planId);
const newSubscription = {
subscriptionId: crypto.randomUUID(),
planId,
planName: plan.name,
price: plan.price,
phoneNumber,
status: 'ACTIVE',
startDate: new Date().toISOString(),
};
setSubscription(newSubscription);
localStorage.setItem('subscription', JSON.stringify(newSubscription));
return newSubscription;
}
function cancelSubscription() {
const updated = { ...subscription, status: 'CANCELLED' };
setSubscription(updated);
localStorage.setItem('subscription', JSON.stringify(updated));
}
return (
<SubscriptionContext.Provider value={{ subscription, subscribe, cancelSubscription, PLANS }}>
{children}
</SubscriptionContext.Provider>
);
}
export function useSubscription() {
return useContext(SubscriptionContext);
}
View File
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ReservationProvider } from './context/ReservationContext';
import { ReviewProvider } from './context/ReviewContext';
import { ReturnProvider } from './context/ReturnContext';
import { SubscriptionProvider } from './context/SubscriptionContext';
import App from './App';
import './styles/global.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<ReservationProvider>
<ReviewProvider>
<ReturnProvider>
<SubscriptionProvider>
<App />
</SubscriptionProvider>
</ReturnProvider>
</ReviewProvider>
</ReservationProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 KiB

+105
View File
@@ -0,0 +1,105 @@
import { useState } from 'react';
import { registerBook } from '../api/books';
const CATEGORIES = ['FICTION', 'NON_FICTION', 'SCIENCE_FICTION', 'FANTASY', 'MYSTERY', 'THRILLER', 'ROMANCE', 'BIOGRAPHY', 'HISTORY', 'POETRY', 'CHILDRENS', 'YOUNG_ADULT', 'SCIENCE', 'PHILOSOPHY', 'SELF_HELP', 'TRAVEL', 'COOKING', 'ART', 'RELIGION', 'REFERENCE'];
const initialForm = {
isbn: '', title: '', author: '', publisher: '', publicationDate: '', price: '', quantity: '', categories: [], description: '', language: '',
};
export default function AddBook() {
const [form, setForm] = useState(initialForm);
const [message, setMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleCategoryChange(e) {
setForm((prev) => ({ ...prev, categories: e.target.value ? [e.target.value] : [] }));
}
function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage(null);
const payload = {
...form,
isbn: Number(form.isbn),
price: Number(form.price),
quantity: Number(form.quantity),
};
registerBook(payload)
.then((response) => {
setMessage({ type: 'success', text: `Livre créé (id : ${response.data})` });
setForm(initialForm);
})
.catch((error) => {
console.error(error);
const status = error.response?.status;
if (status === 406) setMessage({ type: 'error', text: 'Ce livre existe déjà.' });
else if (status === 400) setMessage({ type: 'error', text: 'Données invalides, vérifie les champs.' });
else setMessage({ type: 'error', text: 'Erreur lors de la création.' });
})
.finally(() => setSubmitting(false));
}
return (
<main>
<h1>Ajouter un livre</h1>
<form onSubmit={handleSubmit}>
<table>
<tr>
<td>ISBN</td>
<td><input name="isbn" type="number" value={form.isbn} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Titre</td>
<td><input name="title" value={form.title} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Auteur</td>
<td><input name="author" value={form.author} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Éditeur</td>
<td><input name="publisher" value={form.publisher} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Date de publication</td>
<td><input name="publicationDate" type="date" value={form.publicationDate} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Prix</td>
<td><input name="price" type="number" step="0.01" value={form.price} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Quantité</td>
<td><input name="quantity" type="number" value={form.quantity} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Langue</td>
<td><input name="language" value={form.language} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Catégorie</td>
<td><select value={form.categories[0] || ''} onChange={handleCategoryChange} required><option value=""> choisir </option>{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}</select></td>
</tr>
<tr>
<td>Description</td>
<td><textarea name="description" value={form.description} onChange={handleChange} /></td>
</tr>
</table>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Ajouter le livre'}
</button>
</form>
{message && <p>{message.text}</p>}
</main>
);
}
+158
View File
@@ -0,0 +1,158 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getBookById } from '../api/books';
import { useAuth } from '../context/AuthContext';
import { useReservations } from '../context/ReservationContext';
import { useReviews } from '../context/ReviewContext';
import { useSubscription } from '../context/SubscriptionContext';
export default function BookDetail() {
const { user } = useAuth();
const { subscription } = useSubscription();
const { addReservation, reservations } = useReservations();
const { addReview, getReviewsByBook } = useReviews();
const [rating, setRating] = useState(5);
const [comment, setComment] = useState('');
const [reviewStatus, setReviewStatus] = useState(null);
const { bookId } = useParams();
const [book, setBook] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [phoneNumber, setPhoneNumber] = useState('');
const [reservationStatus, setReservationStatus] = useState(null);
useEffect(() => { getBookById(bookId).then((response) => setBook(response.data)).catch((err) => { console.error(err); setError('Livre introuvable.'); }).finally(() => setLoading(false)); }, [bookId]);
function handleReservation(e) {
e.preventDefault();
addReservation(book, phoneNumber);
setReservationStatus({ success: true, message: 'Réservation effectuée avec succès !' });
setPhoneNumber('');
}
function handleReview(e) {
e.preventDefault();
addReview(book.isbn, book.title, user.username, Number(rating), comment);
setReviewStatus({ success: true, message: 'Avis publié avec succès !' });
setComment('');
setRating(5);
}
if (loading) return <main><p>Chargement</p></main>;
if (error) return <main><p>{error}</p><Link to="/books"> Retour au catalogue</Link></main>;
return (
<main>
<Link to="/books"> Retour au catalogue</Link>
<h1>{book.title}</h1>
<p>Auteur : {book.author}</p>
<p>ISBN : {book.isbn}</p>
<p>Éditeur : {book.publisher}</p>
<p>Publié le : {book.publicationDate}</p>
<p>Prix : {book.price} </p>
<p>Stock : {book.quantity}</p>
<p>Langue : {book.language}</p>
<p>Catégories : {book.categories?.join(', ')}</p>
{book.description && <p>{book.description}</p>}
<section>
<h2>Avis des lecteurs</h2>
{getReviewsByBook(book.isbn).length === 0 ? (
<p>Aucun avis pour le moment.</p>
) : (
<ul>
{getReviewsByBook(book.isbn).map(r => (
<li key={r.reviewId}>
<strong>{r.username}</strong> {'*'.repeat(r.rating)}
<br />
{r.comment}
<br />
<small>{new Date(r.createdAt).toLocaleDateString('fr-FR')}</small>
</li>
))}
</ul>
)}
{user?.role === 'user' && (
<form onSubmit={handleReview}>
<h3>Laisser un avis</h3>
<label>
Note :
<select value={rating} onChange={e => setRating(e.target.value)}>
<option value={1}>*</option>
<option value={2}>**</option>
<option value={3}>***</option>
<option value={4}>****</option>
<option value={5}>*****</option>
</select>
</label>
<label>
Commentaire :
<textarea value={comment} onChange={e => setComment(e.target.value)} required />
</label>
<button type="submit">Publier l'avis</button>
</form>
)}
{reviewStatus && (
<p style={{ color: reviewStatus.success ? 'green' : 'red' }}>
{reviewStatus.message}
</p>
)}
</section>
{user?.role === 'user' && (
<section>
<h2>Réserver ce livre</h2>
{!subscription || subscription.status !== 'ACTIVE' ? (
<p>
Vous devez avoir un abonnement actif pour réserver un livre.{' '}
<Link to="/subscription">S'abonner</Link>
</p>
) : (() => {
const QUOTAS = { basic: 2, standard: 5, premium: Infinity };
const quota = QUOTAS[subscription.planId];
const thisMonthStart = new Date();
thisMonthStart.setDate(1);
thisMonthStart.setHours(0, 0, 0, 0);
const usedThisMonth = reservations.filter(r =>
new Date(r.reservedAt) >= thisMonthStart
).length;
const remaining = quota - usedThisMonth;
if (remaining <= 0) {
return (
<p style={{ color: 'orange' }}>
Vous avez atteint votre quota de {quota} réservation(s) ce mois-ci.
<Link to="/subscription"> Passer au plan supérieur</Link>
</p>
);
}
return (
<>
<p>Réservations restantes ce mois : <strong>{remaining === Infinity ? 'illimitées' : remaining}</strong></p>
<form onSubmit={handleReservation}>
<label>
Numéro de téléphone :
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="0612345678"
required
/>
</label>
<button type="submit">Réserver</button>
</form>
{reservationStatus && (
<p style={{ color: reservationStatus.success ? 'green' : 'red' }}>
{reservationStatus.message}
</p>
)}
</>
);
})()}
</section>
)}
</main>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { useState, useEffect } from 'react';
import { getBooks } from '../api/books';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Books() {
const { user } = useAuth();
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>
{user?.role === 'admin' && <Link to="/books/new">+ Ajouter un livre</Link>}
<ul>
{books.map((book) => (
<li key={book.isbn}>
<Link to={`/books/${book.isbn}`}>
<strong>{book.title}</strong> - {book.author}
</Link>
</li>
))}
</ul>
</main>
);
}
+109
View File
@@ -0,0 +1,109 @@
import { useState } from 'react';
import { registerCustomer, findCustomerByPhone, addLoyaltyPoints, subtractLoyaltyPoints } from '../api/customers';
const initialForm = { firstName: '', lastName: '', phoneNumber: '' };
export default function Customers() {
const [form, setForm] = useState(initialForm);
const [registerMsg, setRegisterMsg] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [pointsInput, setPointsInput] = useState('');
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleRegister(e) {
e.preventDefault();
setSubmitting(true);
setRegisterMsg(null);
registerCustomer(form)
.then((response) => {
setRegisterMsg(`Client créé (id : ${response.data})`);
setForm(initialForm);
})
.catch((error) => {
console.error(error);
setRegisterMsg('Erreur lors de la création du client.');
})
.finally(() => setSubmitting(false));
}
const [phone, setPhone] = useState('');
const [customer, setCustomer] = useState(null);
const [searchError, setSearchError] = useState(null);
function handleSearch(e) {
e.preventDefault();
setSearchError(null);
setCustomer(null);
findCustomerByPhone(phone)
.then((response) => setCustomer(response.data))
.catch((error) => {
console.error(error);
setSearchError('Aucun client trouvé avec ce numéro.');
});
}
function applyLoyalty(operation) {
const points = Number(pointsInput);
const action = operation === 'add' ? addLoyaltyPoints : subtractLoyaltyPoints;
action(customer.id, points)
.then((response) => {
setCustomer((prev) => ({ ...prev, loyaltyPoints: response.data }));
setPointsInput('');
})
.catch((error) => {
console.error(error);
if (error.response?.status === 400) setSearchError('Pas assez de points pour ce retrait.');
else setSearchError('Erreur lors de la mise à jour des points.');
});
}
return (
<main>
<h1>Gestion des clients</h1>
<section>
<h2>Enregistrer un client</h2>
<form onSubmit={handleRegister}>
<label>Prénom
<input name="firstName" value={form.firstName} onChange={handleChange} required />
</label>
<label>Nom
<input name="lastName" value={form.lastName} onChange={handleChange} required />
</label>
<label>Téléphone
<input name="phoneNumber" value={form.phoneNumber} onChange={handleChange} required />
</label>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Créer le client'}
</button>
</form>
{registerMsg && <p>{registerMsg}</p>}
</section>
<section>
<h2>Rechercher un client</h2>
<form onSubmit={handleSearch}>
<label>Téléphone
<input value={phone} onChange={(e) => setPhone(e.target.value)} required />
</label>
<button type="submit">Rechercher</button>
</form>
{searchError && <p>{searchError}</p>}
{customer && (
<div>
<p>{customer.firstName} {customer.lastName}</p>
<p>Téléphone : {customer.phoneNumber}</p>
<p><strong>Points de fidélité : {customer.loyaltyPoints}</strong></p>
<input type="number"value={pointsInput}onChange={(e) => setPointsInput(e.target.value)}placeholder="Nombre de points"/>
<button type="button" onClick={() => applyLoyalty('add')}>Ajouter</button>
<button type="button" onClick={() => applyLoyalty('subtract')}>Retirer</button>
</div>
)}
</section>
</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>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Login() {
const { login } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState(null);
function handleSubmit(e) {
e.preventDefault();
const ok = login(form.username, form.password);
if (ok) {
navigate('/');
} else {
setError('Identifiants incorrects.');
}
}
return (
<main>
<h1>Connexion</h1>
<form onSubmit={handleSubmit}>
<div>
<label>
Nom d'utilisateur :
<input
type="text"
value={form.username}
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
required
/>
</label>
</div>
<div>
<label>
Mot de passe :
<input
type="password"
value={form.password}
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
required
/>
</label>
</div>
<button type="submit">Se connecter</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
<p>Comptes disponibles :</p>
<ul>
<li><strong>admin</strong> / admin (administrateur)</li>
<li><strong>alice (utilisateur)</strong> / bob (utilisateur)</li>
</ul>
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function NotFound() {
return <main><h1>404 Page introuvable</h1></main>;
}
+91
View File
@@ -0,0 +1,91 @@
import { useState } from 'react';
import { createOrder } from '../api/orders';
export default function Orders() {
const [customerId, setCustomerId] = useState('');
const [paymentMethod, setPaymentMethod] = useState('CREDIT_CARD');
const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: '' });
const [lines, setLines] = useState([{ bookId: '', quantity: 1 }]);
const [message, setMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
function handleAddressChange(e) {
const { name, value } = e.target;
setAddress((prev) => ({ ...prev, [name]: value }));
}
function handleLineChange(index, field, value) {
setLines((prev) => prev.map((line, i) => (i === index ? { ...line, [field]: value } : line)));
}
function addLine() {
setLines((prev) => [...prev, { bookId: '', quantity: 1 }]);
}
function removeLine(index) {
setLines((prev) => prev.filter((_, i) => i !== index));
}
function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage(null);
const payload = {
customerId,
paymentMethod,
address,
orderLineDtos: lines.map((line) => ({
bookId: Number(line.bookId),
quantity: Number(line.quantity),
})),
};
createOrder(payload)
.then((response) => setMessage('Commande créée'))
.catch((error) => {
console.error(error);
setMessage(error.response?.data?.message || 'Erreur lors de la commande.');
})
.finally(() => setSubmitting(false));
}
return (
<main>
<h1>Passer une commande</h1>
<form onSubmit={handleSubmit}>
<label>ID client
<input value={customerId} onChange={(e) => setCustomerId(e.target.value)} required />
</label>
<h2>Livres</h2>
{lines.map((line, index) => (
<div key={index}>
<input type="number" placeholder="ISBN du livre" value={line.bookId}
onChange={(e) => handleLineChange(index, 'bookId', e.target.value)} required />
<input type="number" placeholder="Quantité" value={line.quantity}
onChange={(e) => handleLineChange(index, 'quantity', e.target.value)} required />
{lines.length > 1 && (
<button type="button" onClick={() => removeLine(index)}>Retirer</button>
)}
</div>
))}
<button type="button" onClick={addLine}>+ Ajouter un livre</button>
<h2>Adresse de livraison</h2>
<input name="street" placeholder="Rue" value={address.street} onChange={handleAddressChange} required />
<input name="city" placeholder="Ville" value={address.city} onChange={handleAddressChange} required />
<input name="postalCode" placeholder="Code postal" value={address.postalCode} onChange={handleAddressChange} required />
<input name="country" placeholder="Pays" value={address.country} onChange={handleAddressChange} required />
<h2>Paiement</h2>
<select value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}>
<option value="CREDIT_CARD">Carte bancaire</option>
</select>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Valider la commande'}
</button>
</form>
{message && <p>{message}</p>}
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function Profile() {
return <main><h1>Mon compte</h1></main>;
}
+37
View File
@@ -0,0 +1,37 @@
import { useReservations } from '../context/ReservationContext';
import { Link } from 'react-router-dom';
export default function Reservations() {
const { reservations, cancelReservation } = useReservations();
if (reservations.length === 0) {
return (
<main>
<h1>Mes réservations</h1>
<p>Aucune réservation pour le moment.</p>
<Link to="/books"> Retour au catalogue</Link>
</main>
);
}
return (
<main>
<h1>Mes réservations</h1>
<ul>
{reservations.map(r => (
<li key={r.reservationId}>
<strong>{r.bookTitle}</strong> {r.bookAuthor}
<br />
Téléphone : {r.phoneNumber}
<br />
Réservé le : {new Date(r.reservedAt).toLocaleDateString('fr-FR')}
<br />
Statut : {r.status}
<br />
<button onClick={() => cancelReservation(r.reservationId)}>Annuler</button>
</li>
))}
</ul>
</main>
);
}
+96
View File
@@ -0,0 +1,96 @@
import { useState } from 'react';
import { useReturns } from '../context/ReturnContext';
import { getBooks } from '../api/books';
import { useEffect } from 'react';
export default function Returns() {
const { returns, addReturn } = useReturns();
const [books, setBooks] = useState([]);
const [form, setForm] = useState({ bookId: '', customerPhone: '', reason: '' });
const [message, setMessage] = useState(null);
useEffect(() => {
getBooks(0, 100).then(res => setBooks(res.data.content)).catch(console.error);
}, []);
function handleChange(e) {
const { name, value } = e.target;
setForm(f => ({ ...f, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
const book = books.find(b => String(b.isbn) === String(form.bookId));
const bookTitle = book ? book.title : form.bookId;
addReturn(form.bookId, bookTitle, form.customerPhone, form.reason);
setMessage({ success: true, text: 'Retour enregistré avec succès !' });
setForm({ bookId: '', customerPhone: '', reason: '' });
}
return (
<main>
<h1>Gestion des retours</h1>
<section>
<h2>Enregistrer un retour</h2>
<form onSubmit={handleSubmit}>
<label>
Livre :
<select name="bookId" value={form.bookId} onChange={handleChange} required>
<option value="">-- Choisir un livre --</option>
{books.map(b => (
<option key={b.isbn} value={b.isbn}>{b.title} {b.author}</option>
))}
</select>
</label>
<label>
Téléphone du client :
<input
name="customerPhone"
type="tel"
value={form.customerPhone}
onChange={handleChange}
placeholder="0612345678"
required
/>
</label>
<label>
Motif :
<textarea
name="reason"
value={form.reason}
onChange={handleChange}
placeholder="Motif du retour..."
required
/>
</label>
<button type="submit">Enregistrer le retour</button>
</form>
{message && <p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>}
</section>
<section>
<h2>Historique des retours</h2>
{returns.length === 0 ? (
<p>Aucun retour enregistré.</p>
) : (
<ul>
{returns.map(r => (
<li key={r.returnId}>
<strong>{r.bookTitle}</strong>
<br />
Client : {r.customerPhone}
<br />
Motif : {r.reason}
<br />
Statut : {r.status}
<br />
<small>{new Date(r.returnedAt).toLocaleDateString('fr-FR')}</small>
</li>
))}
</ul>
)}
</section>
</main>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { useState } from 'react';
import { useSubscription } from '../context/SubscriptionContext';
export default function Subscription() {
const { subscription, subscribe, cancelSubscription, PLANS } = useSubscription();
const [selectedPlan, setSelectedPlan] = useState('basic');
const [phoneNumber, setPhoneNumber] = useState('');
const [message, setMessage] = useState(null);
function handleSubscribe(e) {
e.preventDefault();
subscribe(selectedPlan, phoneNumber);
setMessage({ success: true, text: 'Abonnement créé avec succès !' });
setPhoneNumber('');
}
function handleCancel() {
cancelSubscription();
setMessage({ success: false, text: 'Abonnement annulé.' });
}
return (
<main>
<h1>Mon abonnement</h1>
{subscription && subscription.status === 'ACTIVE' ? (
<section>
<h2>Abonnement actif</h2>
<p>Plan : <strong>{subscription.planName}</strong></p>
<p>Prix : <strong>{subscription.price} / mois</strong></p>
<p>Téléphone : {subscription.phoneNumber}</p>
<p>Depuis le : {new Date(subscription.startDate).toLocaleDateString('fr-FR')}</p>
<p>Statut : <strong style={{ color: 'green' }}>{subscription.status}</strong></p>
<button onClick={handleCancel}>Annuler l'abonnement</button>
</section>
) : (
<section>
<h2>Choisir un abonnement</h2>
<form onSubmit={handleSubscribe}>
<div>
{PLANS.map(plan => (
<label key={plan.id} style={{ display: 'block', marginBottom: '8px' }}>
<input
type="radio"
name="plan"
value={plan.id}
checked={selectedPlan === plan.id}
onChange={() => setSelectedPlan(plan.id)}
/>
{' '}<strong>{plan.name}</strong> , {plan.price} € / mois , {plan.description}
</label>
))}
</div>
<label>
Numéro de téléphone :
<input
type="tel"
value={phoneNumber}
onChange={e => setPhoneNumber(e.target.value)}
placeholder="0612345678"
required
/>
</label>
<button type="submit">S'abonner</button>
</form>
</section>
)}
{message && (
<p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>
)}
</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;
}

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