Compare commits

...

6 commits

Author SHA1 Message Date
Imugiii
f756ba7d13 Merge remote-tracking branch 'forgejo/dev' into feat/p2-nutrition-openfoodfacts
All checks were successful
CI / js-tests (push) Successful in 28s
CI / secret-scan (push) Successful in 9s
CI / php-lint (push) Successful in 22s
CI / static-tests (push) Successful in 46s
CI / secret-scan (pull_request) Successful in 8s
CI / php-lint (pull_request) Successful in 21s
CI / static-tests (pull_request) Successful in 45s
CI / js-tests (pull_request) Successful in 26s
# Conflicts:
#	docs/PROJECT_CONTEXT.md
2026-06-22 07:19:38 +00:00
8af17842b9 feat(borne): SEO demonstratif + @supports + validation chevalet temps reel (Cr 1.e/1.b.3/2.b.1) (#78)
All checks were successful
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 58s
CI / js-tests (push) Successful in 26s
2026-06-22 08:59:25 +02:00
cb990404e3 feat(api): suivi public du statut commande GET /api/orders/{number} (P4) (#77)
All checks were successful
CI / secret-scan (push) Successful in 9s
CI / php-lint (push) Successful in 20s
CI / static-tests (push) Successful in 47s
CI / js-tests (push) Successful in 26s
2026-06-22 08:53:36 +02:00
918420c497 docs(architecture): schema fonctionnel des vues (Cr 4.a.4) (#76)
All checks were successful
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 43s
CI / js-tests (push) Successful in 23s
2026-06-22 08:50:10 +02:00
09ac9e5a3f chore(devops): hooks Git versionnes + scripts deploy et restore (Cr 4.f.2 / 7.b.2) (#75)
All checks were successful
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 22s
CI / static-tests (push) Successful in 45s
CI / js-tests (push) Successful in 23s
2026-06-22 08:44:45 +02:00
a1d4332714 docs: aligner PROJECT_CONTEXT et README sur l'etat reel (#74)
Some checks failed
CI / secret-scan (push) Has been cancelled
CI / static-tests (push) Has been cancelled
CI / php-lint (push) Has been cancelled
CI / js-tests (push) Has been cancelled
2026-06-22 08:43:02 +02:00
23 changed files with 623 additions and 60 deletions

53
.githooks/commit-msg Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env bash
#
# Wakdo - hook commit-msg : valide le format Conventional Commits.
#
# Active via scripts/install-hooks.sh (git config core.hooksPath .githooks).
# Recoit en $1 le chemin du fichier contenant le message de commit.
#
# Regle (PROJECT_CONTEXT section 9) :
# <type>(<scope optionnel>): <description min 5 caracteres>
# types : feat|fix|refactor|test|docs|chore|ci|db|perf|style
# scope : minuscules, chiffres, tirets
# interdits : emoji (Mantra IA-23)
#
# Exit codes : 0 = message conforme ; 1 = format invalide ou emoji detecte.
set -euo pipefail
MSG_FILE="${1:?usage: commit-msg <fichier-message>}"
# Premiere ligne non vide (ignore les commentaires git et les lignes vides).
SUBJECT="$(grep -m1 -vE '^\s*(#|$)' "$MSG_FILE" || true)"
if [ -z "$SUBJECT" ]; then
echo "commit-msg: message vide." >&2
exit 1
fi
# Tolerance : commits techniques de git (merge/revert/fixup) non concernes.
case "$SUBJECT" in
"Merge "*|"Revert "*|"fixup! "*|"squash! "*) exit 0 ;;
esac
PATTERN='^(feat|fix|refactor|test|docs|chore|ci|db|perf|style)(\([a-z0-9-]+\))?!?: .{5,}'
if ! printf '%s' "$SUBJECT" | grep -qE "$PATTERN"; then
echo "commit-msg: format invalide." >&2
echo " attendu : <type>(<scope>): <description (>=5 car.)>" >&2
echo " types : feat fix refactor test docs chore ci db perf style" >&2
echo " recu : $SUBJECT" >&2
exit 1
fi
# Refus des emoji (Mantra IA-23). Plage des symboles/pictogrammes courants.
# grep -P (PCRE) est requis pour les classes \x{...} ; il n'est dispo que sur le
# grep GNU (cible Linux/Alpine du projet). Si -P est absent (BSD/macOS), on saute
# ce controle plutot que de bloquer a tort (le format reste verifie ; la CI fait foi).
if printf 'a' | grep -qP 'a' 2>/dev/null; then
if printf '%s' "$SUBJECT" | grep -qP '[\x{1F000}-\x{1FAFF}\x{2600}-\x{27BF}\x{2190}-\x{21FF}\x{2B00}-\x{2BFF}]'; then
echo "commit-msg: emoji detecte dans le sujet (interdit, Mantra IA-23)." >&2
exit 1
fi
fi
exit 0

41
.githooks/pre-commit Executable file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
#
# Wakdo - hook pre-commit : garde-fous locaux avant chaque commit.
#
# Active via scripts/install-hooks.sh (git config core.hooksPath .githooks).
# Defense en profondeur cote dev ; la protection de reference reste la CI Forgejo
# (secret-scan, php-lint, static-tests) et la branch protection serveur.
#
# Controles :
# 1. Refuse un commit direct sur main ou dev (PROJECT_CONTEXT regle 18.5).
# 2. Lint PHP (php -l) sur les fichiers .php indexes, si php est disponible.
#
# Exit codes : 0 = OK ; 1 = commit bloque.
set -euo pipefail
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "dev" ]; then
echo "pre-commit: commit direct sur '$BRANCH' interdit (regle 18.5)." >&2
echo " cree une branche : git checkout -b feat/ma-feature" >&2
exit 1
fi
# Lint PHP des fichiers indexes (added/copied/modified), si l'outil est present.
if command -v php >/dev/null 2>&1; then
FAILED=0
while IFS= read -r file; do
[ -n "$file" ] || continue
[ -f "$file" ] || continue
if ! php -l "$file" >/dev/null 2>&1; then
echo "pre-commit: erreur de syntaxe PHP dans $file" >&2
php -l "$file" >&2 || true
FAILED=1
fi
done < <(git diff --cached --name-only --diff-filter=ACM -- '*.php')
if [ "$FAILED" -ne 0 ]; then
exit 1
fi
fi
exit 0

View file

@ -20,7 +20,7 @@ Trois canaux de prise de commande :
- `counter` — comptoir (un equipier saisit pour le client au guichet)
- `drive` — drive-thru (equipier saisit via intercom + casque)
Quatre statuts commande : `pending` -> `preparing` -> `ready` -> `delivered` (ou `cancelled`).
Statuts commande (machine a 4 etats) : `pending_payment` -> `paid` -> `delivered`, plus `cancelled` (atteignable depuis `pending_payment` ou `paid`). La saisie du numero tient lieu de paiement : la creation passe atomiquement de `pending_payment` a `paid`. La cuisine voit la file des commandes `paid` en lecture seule ; la remise est un geste unique `paid` -> `delivered`.
Scope metier complet, regles, horaires de service et fenetre de maintenance : voir `docs/PROJECT_CONTEXT.md`.
@ -82,7 +82,7 @@ Detail et justifications : `docs/PROJECT_CONTEXT.md` section 6.
v
wakdo-db (MariaDB 11.4)
wakdo-cron (backup BDD + purge sessions + stats)
wakdo-cron (backup BDD + purge audit-log + purge throttle)
```
Reseaux, volumes, services et decoupage reseau interne / reseau proxy : voir `docs/PROJECT_CONTEXT.md` section 5.
@ -92,8 +92,8 @@ Reseaux, volumes, services et decoupage reseau interne / reseau proxy : voir `do
## Quickstart (local)
```bash
git clone git@github.com:AcadeNice/wakdo_corentin.git
cd wakdo_corentin
git clone https://git.acadenice.com/AcadeNice/corentin_wakdo.git
cd corentin_wakdo
cp .env.example .env
docker compose up -d
```
@ -125,7 +125,7 @@ Avec un `.env` adapte : `APP_ENV=prod`, `APP_DEBUG=false`, mots de passe forts,
`APP_HOST_*` / `APP_URL_*` / `CORS_ALLOWED_ORIGIN` en vrais FQDN HTTPS, et
`REVERSE_PROXY_NETWORK` = reseau Docker du Traefik de l'hote (doit exister avant le up).
*Section mise a jour au fil de l'implementation (migrations reelles, seed, CI/CD deploiement).*
*Deploiement detaille : section Deploiement plus bas et `scripts/deploy.sh`.*
---
@ -134,36 +134,34 @@ Avec un `.env` adapte : `APP_ENV=prod`, `APP_DEBUG=false`, mots de passe forts,
```
.
|-- .claude/ # Methodologie BYAN (visible jury : CLAUDE.md + rules/)
|-- .forgejo/
| `-- workflows/ # CI Forgejo Actions (ci.yml : secret-scan, php-lint, static-tests, js-tests, auto-merge)
|-- .githooks/ # pre-commit + commit-msg [a venir]
|-- .forgejo/workflows/ # CI Forgejo Actions (ci.yml : secret-scan, php-lint, static-tests, js-tests)
|-- .githooks/ # pre-commit (refus main/dev + php -l) + commit-msg (Conventional Commits)
|-- docker/ # Dockerfiles customs par service
| |-- apache/
| |-- php-fpm/
| `-- cron/
| |-- apache/ # httpd + vhosts kiosk / admin
| |-- php-fpm/ # PHP 8.3-fpm + php.ini durci
| `-- cron/ # dcron + scripts (backup, restore, purges)
|-- db/
| |-- migrations/ # DDL MariaDB versionnes [a venir]
| `-- seeds/ # Donnees de demo [a venir]
| |-- init/ # init BDD (scope du user applicatif, moindre privilege)
| |-- migrations/ # DDL MariaDB versionnes (0001_init_schema, 0002_pin_throttle, ...)
| |-- seeds/ # donnees de reference + demo (idempotents)
| `-- *.sh # runners migrate / seed
|-- docs/
| |-- PROJECT_CONTEXT.md # Source de verite projet (scope, stack, RNCP mapping)
| |-- journal/ # Retros par session et par feature (oral RNCP)
| `-- merise/ # MCD, MCT, MLD [a venir]
|-- scripts/ # backup-db, install-hooks, ... [a venir]
|-- src/ # Code applicatif [a venir]
| |-- Core/ # Router, Autoloader, DB
| |-- Controllers/
| |-- Models/
| |-- Views/
| |-- Services/
| |-- public/ # DocumentRoot Apache
| `-- bootstrap.php
| |-- PROJECT_CONTEXT.md # source de verite projet (scope, stack, mapping RNCP)
| |-- ARCHITECTURE.md # vue technique (deploiement, stack, securite)
| |-- merise/ # dictionnaire, MCD, MCT, MLD, MLT (+ diagrammes)
| |-- uml/ # use-cases, sequences, machine a etats
| `-- adr/ api/ domaines/ design/ journal/ _ref/
|-- scripts/ # deploy, install-hooks, forgejo-* (branch-protection, pr-automerge)
|-- src/
| |-- app/ # namespace App\ : Core, Controllers, Auth, Catalogue, Order, Views
| `-- public/ # DocumentRoots Apache : borne/ (kiosk) + admin/ (back-office + API)
|-- tests/
| |-- Unit/ # [a venir]
| `-- Integration/ # [a venir]
|-- .env.example
|-- .dockerignore
|-- .gitignore
|-- docker-compose.yml
| |-- Unit/ Integration/ # PHPUnit (.phar autonome, sans Composer ; integration sur vraie MariaDB)
| |-- js/ # node:test + jsdom (front borne)
| |-- e2e/ # Playwright (parcours borne + admin, lance a la main)
| `-- Support/ # doubles de test (Fake* / Spy*)
|-- .env.example .gitleaks.toml phpstan.neon phpunit.xml
|-- docker-compose.yml # standalone local ; prod = docker-compose.prod.yml (gitignore, par hote)
`-- README.md
```
@ -178,19 +176,32 @@ Avec un `.env` adapte : `APP_ENV=prod`, `APP_DEBUG=false`, mots de passe forts,
- `main` et `dev` sont proteges cote Forgejo (PR requise, force push bloque, checks requis : secret-scan / php-lint / static-tests).
- Pas d'emoji dans le code, les commits ou les specs techniques (Mantra IA-23).
*Sections detaillees (setup env de dev, lint, tests) : a completer au fil de l'implementation.*
- **Hooks Git** : `scripts/install-hooks.sh` active `pre-commit` (refus de commit direct sur `main`/`dev`, `php -l` des fichiers indexes) et `commit-msg` (format Conventional Commits, refus emoji).
- **Verification locale** : voir la section Tests ci-dessous (PHPUnit + PHPStan via le conteneur applicatif, tests JS via node, E2E Playwright).
---
## Tests
*Section a completer. Strategie globale : PHPUnit via `.phar` autonome (sans Composer), priorite Unit > Integration > E2E, voir `docs/PROJECT_CONTEXT.md` section 6 et mantras Merise Agile.*
Trois niveaux, sans dependance Composer cote PHP (priorite Unit > Integration > E2E).
- **PHP (PHPUnit `.phar`)** — unit + integration sur vraie MariaDB, via le conteneur applicatif :
```bash
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app php -d memory_limit=-1 phpstan.phar analyse
```
- **JS (node:test + jsdom)** — modules du front borne : `npm run test:js`
- **E2E (Playwright)** — parcours borne + admin, lances a la main contre une stack jetable : `tests/e2e/run.sh`
La CI Forgejo execute secret-scan, php-lint, static-tests (PHPStan niveau 6 + PHPUnit avec service MariaDB) et js-tests sur chaque PR.
---
## Deploiement
*Strategie : CI Forgejo Actions sur PR vers `dev`/`main` (secret-scan gitleaks, php-lint, static-tests PHPStan+PHPUnit, js-tests) avec auto-merge sur label + CI verte. CD : declenchement humain, redeploiement par `docker compose pull && docker compose up -d`. Voir `docs/PROJECT_CONTEXT.md` section 7 Bloc 5.*
*CI Forgejo Actions sur PR vers `dev`/`main` (secret-scan gitleaks, php-lint, static-tests PHPStan + PHPUnit, js-tests), avec auto-merge sur CI verte. Deploiement a declenchement humain via `scripts/deploy.sh` (ff-only puis `docker compose -f docker-compose.prod.yml pull && up -d` ; le one-shot `wakdo-migrate` applique migrations + seed). Un veritable CD automatique sur merge `main` reste a armer avec un secret de connexion (choix exploitant). Voir `docs/PROJECT_CONTEXT.md` section 7 Bloc 5.*
---
@ -200,7 +211,8 @@ Avec un `.env` adapte : `APP_ENV=prod`, `APP_DEBUG=false`, mots de passe forts,
|---|---|
| `docs/PROJECT_CONTEXT.md` | Source de verite projet (17 sections : scope, stack, architecture, mapping critere RNCP, planning, risques, conventions) |
| `docs/journal/` | Retrospectives par session et par feature (preparation de l'oral RNCP) |
| `docs/merise/` *(a venir)* | Modelisation Merise : dictionnaire, MCD, MCT, MLD |
| `docs/merise/` | Modelisation Merise : dictionnaire, MCD, MCT, MLD, MLT (+ diagrammes) |
| `docs/ARCHITECTURE.md` / `docs/adr/` | Vue technique + decisions d'architecture (ADR) |
| `.claude/CLAUDE.md` | Constitution du projet pour les agents Claude Code |
| `.claude/rules/` | Protocoles appliques : fact-check, merise-agile, elo-trust, hermes-dispatcher, byan-api, byan-agents |

View file

@ -0,0 +1,82 @@
#!/usr/bin/env bash
#
# Wakdo - restauration de la BDD depuis un dump produit par backup-db.sh.
#
# Operation MANUELLE (pas un job cron) : restaurer ecrase les donnees courantes.
# A lancer dans le conteneur disposant du client mysql et du reseau de la BDD, p.ex.
# docker compose run --rm -v "$PWD/var/backups:/backups" wakdo-cron \
# /scripts/restore-db.sh /backups/wakdo_YYYYMMDD_HHMMSS.sql.gz --force
#
# Variables d'env lues (memes que backup-db.sh) :
# DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
# Note : un dump complet contient des DROP/CREATE TABLE ; le compte utilise doit
# donc avoir les privileges DDL. Le user applicatif (DML seul) ne suffit pas :
# fournir un compte privilegie via DB_USER/DB_PASSWORD pour la restauration.
#
# Usage :
# restore-db.sh <fichier.sql.gz|fichier.sql> [--force]
# Sans --force, le script demande une confirmation interactive.
#
# Exit codes :
# 0 - restauration OK
# 1 - variables env manquantes / mauvais usage / fichier absent
# 2 - restauration mysql a echoue
# 3 - confirmation refusee
set -euo pipefail
DUMP_FILE="${1:-}"
FORCE="${2:-}"
log() {
echo "[restore-db $(date -Iseconds)] $*" >&2
}
if [ -z "$DUMP_FILE" ]; then
log "usage: restore-db.sh <fichier.sql.gz|fichier.sql> [--force]"
exit 1
fi
if [ ! -f "$DUMP_FILE" ]; then
log "ERROR: fichier de dump introuvable : $DUMP_FILE"
exit 1
fi
for var in DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD; do
if [ -z "${!var:-}" ]; then
log "ERROR: variable $var vide ou non definie"
exit 1
fi
done
# Garde-fou : la restauration ecrase la base cible. Confirmation requise sauf --force.
if [ "$FORCE" != "--force" ]; then
printf 'Restaurer %s dans la base "%s" sur %s:%s ? Les donnees actuelles seront ecrasees. [oui/NON] ' \
"$DUMP_FILE" "$DB_NAME" "$DB_HOST" "$DB_PORT" >&2
read -r answer
if [ "$answer" != "oui" ]; then
log "restauration annulee."
exit 3
fi
fi
# Decompression a la volee si le dump est gzippe.
reader=(cat "$DUMP_FILE")
case "$DUMP_FILE" in
*.gz) reader=(gzip -dc "$DUMP_FILE") ;;
esac
log "restauration de ${DB_NAME} depuis ${DUMP_FILE}"
if ! "${reader[@]}" | mysql \
--host="${DB_HOST}" \
--port="${DB_PORT}" \
--user="${DB_USER}" \
--password="${DB_PASSWORD}" \
--default-character-set=utf8mb4 \
"${DB_NAME}"; then
log "ERROR: la restauration mysql a echoue"
exit 2
fi
log "restauration OK : ${DB_NAME}"
exit 0

View file

@ -142,7 +142,7 @@ Client Borne (Bloc 1) API (Bloc 2) BDD
│ (admin_proxy network) │
└──────────┬────────────────────────────┬─────────────────────────┘
│ │
wakdo.acadenice.fr wakdo-admin.acadenice.fr
corentin-wakdo.stark.a3n.fr corentin-wakdo-admin.stark.a3n.fr
│ │
▼ ▼
┌──────────────────────────────────────────┐
@ -196,7 +196,7 @@ Reseaux :
| Orchestration locale | docker compose (service wakdo-migrate) | — | Cr 7.b (script) + Cr 7.c.4 (une commande) |
| CI/CD | Forgejo Actions (act_runner auto-heberge) | — | Cr 7.d |
| Versioning | Git + Forgejo auto-heberge (push-mirror GitHub) | — | Cr 4.f (collaboration) |
| Hooks Git | pre-commit + commit-msg | versionnes dans `.githooks/` | Conventional Commits |
| Hooks Git | pre-commit (refus main/dev + php -l) + commit-msg (format Conventional Commits) | versionnes dans `.githooks/`, actives via `scripts/install-hooks.sh` | Conventional Commits |
---
@ -262,15 +262,16 @@ Reseaux :
**IN scope :**
- Dockerfile custom PHP-FPM avec extensions
- `docker-compose.yml` orchestrant les 4 services (web, app, db, cron)
- `docker-compose.yml` orchestrant 5 services : 4 longs (web, app, db, cron) + 1 one-shot (`wakdo-migrate`)
- `docker compose up` lance toute la stack (service one-shot `wakdo-migrate` : migrations + seed idempotents) en une commande (Cr 7.c.4)
- Scripts Bash d'automatisation (backup, deploy, migrate)
- **Cron tab** avec au moins 3 jobs planifies dans la fenetre de maintenance (01h30-09h30) :
- Scripts Bash d'automatisation (backup, restore, deploy, migrate)
- **Cron tab** avec 3 jobs actifs planifies dans la fenetre de maintenance (01h30-09h30) :
- `0 3 * * *` — backup BDD quotidien a 03h00 (entre fin service 01h et ouverture 10h)
- `*/15 * * * *` — purge sessions expirees toutes les 15 min (leger, peut tourner en service)
- `30 4 * * *` — agregation stats commandes a 04h30 sur le **jour de service** ecoule (10h J-1 → 01h J)
- **CI Forgejo Actions** (act_runner auto-heberge) : lint PHP + PHPStan + PHPUnit + secret-scan (gitleaks) sur PR -> dev
- **CD Forgejo Actions** : deploy auto sur merge main (SSH + `docker compose pull && docker compose up -d`)
- `15 4 * * *` — purge du journal d'audit au-dela de la fenetre de retention (~12 mois)
- `45 4 * * *` — purge des compteurs de throttle expires
- Differes (templates commentes dans `docker/cron/crontab`, a activer plus tard) : purge des sessions expirees, agregation des stats sur le jour de service
- **CI Forgejo Actions** (act_runner auto-heberge) : lint PHP + PHPStan + PHPUnit + secret-scan (gitleaks) + js-tests sur PR -> dev
- **CD : deploiement scripte a declenchement humain** (`scripts/deploy.sh` : ff-only puis `docker compose -f docker-compose.prod.yml pull && up -d`). Choix solo dev sur un environnement de prod unique. Un veritable deploiement continu (job Forgejo sur push main -> SSH -> `deploy.sh`) reste a armer avec un secret de connexion (decision exploitant)
- `.env.example` documente (parametres securite : argon2id, lockout, seuils throttle, retention RGPD), secrets hors du repo
- `php.ini` durci (expose_php off, session cookies httponly/secure/samesite, upload limite)
- Healthcheck Traefik + readiness probes
@ -303,21 +304,21 @@ Reseaux :
| Cr 2.a.1-5 | JS ES6+ + DOM + animations | Modules ES6, classes, async/await, pas de jQuery |
| Cr 2.b.1-3 | Validation formulaires | Validation client temps reel (regex) + validation serveur |
| Cr 2.c.1-4 | Ajax async | `fetch()` avec gestion erreurs, pas d'exposition donnees sensibles |
| Cr 2.d.1-3 | Librairies externes | **Non applicable** (zero dep JS) — argumenter "developpement sans lib externe" |
| Cr 2.d.1-3 | Librairies externes | Choix de stack assume : **zero lib JS** (vanilla). Cr 2.d.1-3 restent du tronc commun evaluable -> a argumenter a l'oral ; ce n'est pas une dispense du referentiel |
### Bloc 2
| Critere | Libelle court | Feature Wakdo couvrant |
|---|---|---|
| Cr 3.a.1-4 | Analyse + modele donnees | Dictionnaire + MCD + cardinalites |
| Cr 3.a.2-4 | Analyse + modele donnees (3 criteres ; le referentiel ne contient PAS de Cr 3.a.1) | Dictionnaire + MCD + cardinalites |
| Cr 3.a.3 | Exploiter donnees externes d'API | Enrichissement nutritionnel depuis **OpenFoodFacts** (API tierce) importe DANS le modele (`ingredient.energy_kcal_100g`), a la demande admin (opt-in, sans egress runtime) ; + auto-consommation de l'API interne par la borne |
| Cr 3.b.1-3 | Construction BDD | MCD → MLD → DDL MariaDB, FK + typage coherent |
| Cr 3.c.1-3 | Requetes SQL optimisees | PDO prepared, index sur FK, LIMIT/tri explicites |
| Cr 3.d.1-4 | RGPD | hash mdp, droit acces/modif/suppr, info utilisation donnees |
| Cr 4.a.1-4 | Conceptualisation | Schema fonctionnel des vues + interactions |
| Cr 4.b.1-6 | Syntaxe + indentation + erreurs | PSR-12 style, try/catch cibles, logs |
| Cr 4.c.1-3 | POO + heritage + namespaces | `BaseModel` -> `Product`, `BaseController` -> `AdminController`, PSR-4 |
| Cr 4.d.1-3 | MVC | `src/Models/`, `src/Views/`, `src/Controllers/`, separation stricte |
| Cr 4.c.1-3 | POO + heritage + namespaces | heritage de controleurs (`Controller` -> `AuthenticatedController` -> `AdminController` -> ...), couche modele en Repository pattern, autoloader PSR-4 manuel |
| Cr 4.d.1-3 | MVC | `src/app/Controllers/`, `src/app/Views/`, couche modele = Repositories (`*Repository`) + `Core/Database` ; separation stricte (pas de dossier `Models/` : Repository pattern) |
| Cr 4.e.1-3 | Securite | PDO prepared (anti-SQLi), sessions regeneration, role-based middleware |
| Cr 4.f.2 | Maitrise outil collaboratif (artefact) | Commits Conventional, branches `feat/*`, PR descriptions, squash merge, hooks Git |
| Cr 4.f.1, 4.f.3, 4.f.4 | Soft skills (evalues a l'oral) | Partage de savoir-faire (4.f.1), auto-evaluation avant PR (4.f.3), compte-rendu de la participation individuelle (4.f.4) — demontres pendant la soutenance |
@ -333,11 +334,11 @@ Reseaux :
| Cr 7.b.3 | **Cron tab** | `wakdo-cron` service avec crontab : backup BDD, purge sessions, stats |
| Cr 7.c.1 | VM operationnelle | Serveur existant Acadenice |
| Cr 7.c.2 | OS conteneur installe | Docker Engine |
| Cr 7.c.3 | App conteneurisee complete | 4 services (web, app, db, cron) |
| Cr 7.c.3 | App conteneurisee complete | 5 services : 4 longs (web, app, db, cron) + 1 one-shot (wakdo-migrate) |
| Cr 7.c.4 | **Une ligne de commande** | `docker compose up` lance toute la stack + migrate + seed |
| Cr 7.d.1 | Architecture serveur | Traefik reverse + reseaux segmentes documentes |
| Cr 7.d.2 | Tests avant deploy | CI PHPUnit + PHPStan + secret-scan sur PR (Forgejo Actions) |
| Cr 7.d.3 | Integration/deploiement continus | Forgejo Actions deploy automatique sur merge main |
| Cr 7.d.3 | Integration/deploiement continus | CI complete sur PR ; deploiement scripte a declenchement humain (`scripts/deploy.sh`). Auto-CD sur merge main non arme (choix solo dev, a argumenter) |
---
@ -680,7 +681,7 @@ Ces regles tiennent lieu de garde-fous pendant toute la duree du projet. Les enf
6. **Zero requete SQL sans prepared statement** (anti-SQLi)
7. **Zero hash mdp en clair** (bcrypt ou argon2)
8. **Zero CORS `*`** (origine explicite uniquement)
9. **Zero deployment manuel** en condition normale (CI/CD)
9. **Deploiement scripte et trace** (`scripts/deploy.sh`), declenche par l'exploitant ; pas de modification manuelle ad hoc en prod
10. **Zero feature hors scope** sans mise a jour de ce document
---

View file

@ -113,12 +113,15 @@ La borne est publique (aucune session) ; cf. `mlt.md` CREATE_ORDER, declencheur
| Methode | Chemin | Permission | Op MCT | Statut |
|---|---|---|---|---|
| GET | `/api/categories` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/products` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/products/{id}` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/menus` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/menus/{id}` | (lecture publique) | READ_CATALOGUE | prevu |
| POST | `/api/orders` | (kiosk public) | CREATE_ORDER (mlt 3.3) | prevu (idempotency_key, RG-T19) |
| GET | `/api/categories` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/products` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/products/{id}` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/menus` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/menus/{id}` | (lecture publique) | READ_CATALOGUE | livre (slots de composition) |
| GET | `/api/allergens` | (lecture publique) | READ_CATALOGUE | livre (14 allergenes INCO) |
| POST | `/api/orders` | (kiosk public) | CREATE_ORDER (mlt 3.3) | livre (idempotency_key, RG-T19) |
| POST | `/api/orders/{number}/pay` | (kiosk public) | (encaissement) | livre (paid + decrement stock RG-T20) |
| GET | `/api/orders/{number}` | (lecture publique) | (suivi statut) | livre (champs non sensibles : numero, statut, total) |
### 5.3 API / pages back-office (prevu P3-P4, session + permission)
@ -132,7 +135,7 @@ Commandes (cote equipier) :
| Methode | Chemin | Permission | Op MCT | Note |
|---|---|---|---|---|
| GET | `/api/orders` | `order.read` | READ_ORDERS | filtre par `role_visible_source` (RG-T12) |
| GET | `/api/orders/{number}` | `order.read` | READ_ORDERS | |
| GET | `/api/orders/{number}` | `order.read` | READ_ORDERS | vue back-office detaillee (differe) ; le suivi public minimal est livre en 5.2 |
| POST | `/api/orders` (comptoir/drive) | `order.create` | CREATE_COUNTER_ORDER (mlt 4.1) | source auto-taggee |
| POST | `/api/orders/{id}/deliver` | `order.deliver` | DELIVER_ORDER (mlt 6.1) | |
| POST | `/api/orders/{id}/cancel` | `order.cancel` | CANCEL_ORDER (mlt 7.1) | PIN + audit_log (RG-T13/14) |

View file

@ -0,0 +1,124 @@
# Schema fonctionnel — Wakdo
> Conceptualisation de l'application (Cr 4.a.1 a 4.a.4) : enchainement des vues en
> fonction des actions et interactions utilisateur, pour les deux interfaces
> (borne kiosk Bloc 1, back-office Bloc 2). Complete les diagrammes UML
> (`docs/uml/use-cases.md`, `sequence-passer-commande.md`, `state-commande.md`) et
> le modele Merise (`docs/merise/`).
---
## 1. Vue d'ensemble
Deux interfaces, deux parcours, un meme catalogue en base :
- **Borne (kiosk)** — publique, anonyme, tactile. Le client compose une commande et
la valide ; la borne consomme l'API de lecture (catalogue) et l'API de commande
(creation + encaissement) en `fetch` Ajax.
- **Back-office** — interne, authentifie (sessions + RBAC par permission), pages
rendues serveur (MVC). Chaque action sensible repasse par un PIN equipier.
Les transitions ci-dessous decrivent quelle vue mene a quelle vue, sous quelle
action, et quel appel API ou garde de securite intervient.
---
## 2. Parcours borne (Bloc 1)
```mermaid
flowchart TD
A["Accueil (index.html)\nchoix sur place / a emporter"] -->|clic mode| B["Categories (categories.html)"]
B -->|clic categorie| C["Produits (products.html?category)\npanneau commande persistant"]
C -->|produit simple| D["Modale options (product-options.js)\ntaille / quantite"]
C -->|menu| E["Composeur menu (page-product-menu.js)\nslots GET /api/menus/{id}"]
D -->|ajouter| C
E -->|ajouter| C
C -->|voir panier| F["Panier (cart.html)\nmodifier quantite / retirer"]
F -->|valider| G["Paiement (payment.html)\nsaisie numero chevalet si sur place"]
G -->|enregistrer| H["Confirmation (confirmation.html)\nnumero + montant"]
H -->|nouvelle commande| A
C -. "GET /api/categories,/products,/menus (data.js)" .-> API[(API kiosk)]
G -. "POST /api/orders puis /pay (checkout.js)" .-> API
H -. "suivi optionnel GET /api/orders/{number}" .-> API
```
**Transitions detaillees :**
| Vue | Action | Vue suivante | API / etat |
|---|---|---|---|
| Accueil | Choisir « sur place » / « a emporter » | Categories | mode memorise (state.js / nav.js) |
| Categories | Choisir une categorie | Produits | `GET /api/categories` (chargement) |
| Produits | Cliquer un produit simple | Modale options | `GET /api/products` |
| Produits | Cliquer un menu | Composeur de menu | `GET /api/menus/{id}` (slots) |
| Modale / Composeur | Ajouter au panier | Produits (panneau mis a jour) | panier en `localStorage` |
| Produits | Voir le panier | Panier | — |
| Panier | Valider | Paiement | — |
| Paiement | Saisir le numero (chevalet, si sur place) puis enregistrer | Confirmation | `POST /api/orders` puis `POST /api/orders/{number}/pay` (idempotent) |
| Confirmation | Nouvelle commande | Accueil | panier vide |
**Transverse borne :** bascule de police adaptee aux dyslexiques (bouton `a11y.js`,
present sur chaque vue, RGAA Cr 1.c.2) ; navigation clavier + focus-trap dans les
modales ; panneau commande persistant (aside) sur Produits.
---
## 3. Parcours back-office (Bloc 2)
```mermaid
flowchart TD
L["Connexion (/login)"] -->|identifiants valides| R{"role.default_route\n(seed)"}
L -->|oubli mdp| RP["/forgot_password -> /reset_password"]
R -->|admin| DB["Tableau de bord (/admin/dashboard)"]
R -->|manager| ST["Statistiques (/admin/stats)"]
DB --> NAV["Navigation laterale\n(conditionnee aux permissions)"]
ST --> NAV
NAV --> CAT["Categories / Produits / Menus\n(+ editeur de recette)"]
NAV --> STK["Stock / Ingredients\n(reappro, inventaire, mouvements)"]
NAV --> USR["Utilisateurs / Roles (RBAC)"]
NAV --> ORD["Commandes (liste, lecture seule)"]
NAV --> PRO["Profil : PIN + mention RGPD (/admin/privacy)"]
CAT -.->|action sensible : prix/TVA, suppression| PIN["PIN equipier + audit_log\n(meme transaction)"]
STK -.->|inventaire| PIN
USR -.->|mutation compte / matrice RBAC / effacement| PIN
```
**Gardes et regles :**
| Etape | Garde | Regle Merise |
|---|---|---|
| Acces a toute page `/admin/*` | `SessionGuard::check()` : session valide (idle 4h, absolu 10h, compte actif) | RG-6 / RG-T02 |
| Acces a une fonction | `Authorizer::can(role_id, permission)` : teste une permission, pas un nom de role | RG-T03 |
| Action sensible (annulation, prix/TVA, suppression, gestion compte/RBAC, inventaire, effacement PII) | PIN equipier verifie + ecriture `audit_log` dans la meme transaction | RG-T13 / RG-T14 |
| Echec de PIN | trace `pin.failed` + throttle degressif | RG-T22 |
**Landing par role** (seed `role.default_route`) : admin -> `/admin/dashboard`,
manager -> `/admin/stats`. Les autres roles (kitchen, counter, drive) sont definis
en base ; leurs ecrans operationnels (file cuisine, saisie comptoir/drive) sont
suivis comme evolution (voir le backlog de finition).
---
## 4. Points de contact API
| Interface | Appelle | Sens |
|---|---|---|
| Borne | `GET /api/categories`, `/products`, `/products/{id}`, `/menus`, `/menus/{id}`, `/allergens` | lecture catalogue (anonyme) |
| Borne | `POST /api/orders`, `POST /api/orders/{number}/pay`, `GET /api/orders/{number}` | commande + suivi (anonyme, idempotent) |
| Back-office | pages rendues serveur sous `/admin/*` + `GET /api/me` | session + RBAC |
CORS : la borne et le back-office partagent l'origine via une passerelle `/api/*`
(meme origine) ; le middleware CORS reste en defense (origine exacte, sans joker).
---
## 5. References croisees
- Cas d'usage et acteurs : `docs/uml/use-cases.md`
- Sequence de commande : `docs/uml/sequence-passer-commande.md`
- Machine a etats de la commande : `docs/uml/state-commande.md`
- Sequence securite (annulation PIN-gated) : `docs/uml/security-sequence.md`
- Modele de donnees : `docs/merise/{dictionary,mcd,mld,mlt}.md`
- Contrat API : `docs/api/conventions.md`

61
scripts/deploy.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Wakdo - deploiement scripte (declenchement humain).
#
# Strategie CD du projet : le deploiement est volontairement DECLENCHE A LA MAIN
# (solo dev, un seul environnement de prod). Ce script fiabilise l'operation
# (Cr 7.b.2) ; il n'est PAS execute automatiquement par la CI. Un veritable
# deploiement continu (job Forgejo sur push main -> SSH -> ce script) reste a armer
# explicitement avec un secret de connexion, decision laissee a l'exploitant.
#
# A lancer SUR L'HOTE de prod, depuis la racine du depot :
# scripts/deploy.sh [BRANCHE] (defaut : main)
#
# Prerequis : docker-compose.prod.yml present (gitignore, propre a l'hote) et un
# .env de prod renseigne. Le service one-shot wakdo-migrate applique migrations +
# seed (idempotents) avant que l'app ne serve.
#
# Exit codes : 0 = OK ; 1 = prerequis manquant / confirmation refusee.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
BRANCH="${1:-main}"
REMOTE="${GIT_REMOTE:-origin}"
COMPOSE_FILE="docker-compose.prod.yml"
if [ ! -f "$COMPOSE_FILE" ]; then
echo "deploy: $COMPOSE_FILE introuvable (fichier de prod, propre a l'hote)." >&2
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "deploy: docker introuvable sur l'hote." >&2
exit 1
fi
echo "Deploiement Wakdo : branche '$BRANCH' via $COMPOSE_FILE"
printf 'Confirmer le deploiement en production ? [oui/NON] '
read -r answer
if [ "$answer" != "oui" ]; then
echo "deploy: annule."
exit 1
fi
echo "[1/4] mise a jour du code (fast-forward only, remote: $REMOTE)"
git fetch --prune "$REMOTE" "$BRANCH"
git checkout "$BRANCH"
git merge --ff-only "$REMOTE/$BRANCH"
echo "[2/4] recuperation des images"
docker compose -f "$COMPOSE_FILE" pull
echo "[3/4] demarrage de la stack (migrate + seed idempotents puis app)"
docker compose -f "$COMPOSE_FILE" up -d
echo "[4/4] etat des services"
docker compose -f "$COMPOSE_FILE" ps
echo "Deploiement termine."

25
scripts/install-hooks.sh Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
#
# Wakdo - installe les hooks Git versionnes (.githooks).
#
# Pointe core.hooksPath vers .githooks (versionne) au lieu de .git/hooks (local,
# non versionne). A lancer une fois apres le clone :
# scripts/install-hooks.sh
#
# Les hooks (pre-commit, commit-msg) sont alors actifs pour ce clone.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOKS_DIR="$ROOT/.githooks"
if [ ! -d "$HOOKS_DIR" ]; then
echo "install-hooks: $HOOKS_DIR introuvable." >&2
exit 1
fi
chmod +x "$HOOKS_DIR"/* 2>/dev/null || true
git -C "$ROOT" config core.hooksPath .githooks
echo "Hooks Git installes (core.hooksPath = .githooks)."
echo "Actifs : $(cd "$HOOKS_DIR" && ls | tr '\n' ' ')"

View file

@ -57,6 +57,25 @@ class OrderController extends Controller
return $this->json(['data' => $this->present($order)]);
}
/**
* Lecture publique du statut d'une commande par son numero (suivi borne apres
* encaissement). Anonyme, lecture seule ; 404 si le numero est inconnu.
*
* @param array<string, string> $params
*/
public function show(array $params = []): Response
{
$order = $this->orders()->findByNumber((string) ($params['number'] ?? ''));
if ($order === null) {
return $this->json(
['data' => null, 'error' => ['code' => 'ORDER_NOT_FOUND', 'message' => $this->messageFor('ORDER_NOT_FOUND')]],
404,
);
}
return $this->json(['data' => $this->present($order)]);
}
/**
* Fabrique le repository de commande sur l'acces BDD courant. Hook de test
* (sous-classe -> double) : redefinir db() suffit a injecter une base factice.

View file

@ -59,6 +59,35 @@ class OrderRepository
];
}
/**
* Recherche une commande par son numero (prefixe canal K/C/D + id). Lecture
* publique du statut cote borne (suivi apres encaissement). Renvoie null si le
* numero est inconnu. Lecture seule : ne sert que des champs non sensibles
* (la commande kiosk est anonyme, pas de PII).
*
* @return array{id:int, order_number:string, total_ttc_cents:int, status:string}|null
*/
public function findByNumber(string $number): ?array
{
if ($number === '') {
return null;
}
$row = $this->db->fetch(
'SELECT id, order_number, total_ttc_cents, status FROM customer_order WHERE order_number = :n',
['n' => $number],
);
if ($row === null) {
return null;
}
return [
'id' => (int) $row['id'],
'order_number' => (string) $row['order_number'],
'total_ttc_cents' => (int) $row['total_ttc_cents'],
'status' => (string) $row['status'],
];
}
/**
* Cree une commande borne en pending_payment. Idempotent sur idempotency_key.
*

View file

@ -88,6 +88,8 @@ try {
// un seul segment (numero K+id), pas de collision avec un sous-chemin.
$router->add('POST', '/api/orders', [OrderController::class, 'create']);
$router->add('POST', '/api/orders/{number}/pay', [OrderController::class, 'pay']);
// Suivi public du statut d'une commande par son numero (lecture seule, anonyme).
$router->add('GET', '/api/orders/{number}', [OrderController::class, 'show']);
// Lecture catalogue borne (P4, docs/api/conventions.md section 5.2). API publique
// kiosk, ANONYME : la borne consulte sans session. Lecture seule ; ne sert que le

View file

@ -2218,3 +2218,17 @@ html.dys-font {
outline: 3px solid var(--color-brand-yellow-dk);
outline-offset: 2px;
}
/* ============================================================
13. COMPATIBILITE NAVIGATEURS (Cr 1.b.3)
============================================================ */
/* 'gap' sur conteneurs flex : non reconnu par d'anciens navigateurs (ex. Safari
anterieur a 14.1). La detection @supports applique un repli par marges sur les
enfants directs si la propriete n'est pas supportee (alternative documentee). */
@supports not (gap: 1rem) {
.welcome__choices > *,
.composer-footer__row > * {
margin: var(--space-2);
}
}

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Wakdo">
<rect width="32" height="32" rx="7" fill="#FFC72C"/>
<path d="M6 9 L11 23 L16 13 L21 23 L26 9" fill="none" stroke="#DA020E" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

View file

@ -100,6 +100,29 @@ function openChevalet(onValidate, onDismiss) {
const input = overlay.querySelector('#chevalet-input');
const errBox = overlay.querySelector('#chevalet-error');
const okBtn = overlay.querySelector('#chevalet-ok');
// Validation en TEMPS REEL (Cr 2.b.1) : controle pendant la frappe, pas seulement
// au clic. On filtre les caracteres non numeriques, on borne a 4 chiffres, on
// reflete l'etat via aria-invalid + l'activation du bouton, et on masque l'erreur
// des que la saisie redevient valide.
const isValidTag = (v) => /^[0-9]{1,4}$/.test(v);
const syncValidity = () => {
const cleaned = (input.value || '').replace(/[^0-9]/g, '').slice(0, 4);
if (cleaned !== input.value) {
input.value = cleaned;
}
const ok = isValidTag(cleaned);
input.setAttribute('aria-invalid', ok ? 'false' : 'true');
if (okBtn) {
okBtn.disabled = !ok;
}
if (ok) {
errBox.hidden = true;
}
};
input.addEventListener('input', syncValidity);
syncValidity();
const teardown = () => {
document.removeEventListener('keydown', esc);
@ -123,10 +146,13 @@ function openChevalet(onValidate, onDismiss) {
});
overlay.querySelector('#chevalet-cancel').addEventListener('click', dismiss);
overlay.querySelector('#chevalet-ok').addEventListener('click', () => {
okBtn.addEventListener('click', () => {
// Garde finale au clic (defense en profondeur) : le bouton est deja desactive
// tant que la saisie est invalide (validation temps reel ci-dessus).
const tag = (input.value || '').trim();
if (!/^[0-9]{1,4}$/.test(tag)) {
if (!isValidTag(tag)) {
errBox.hidden = false;
input.setAttribute('aria-invalid', 'true');
input.focus();
return;
}

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Votre panier">
<title>Wakdo - Panier</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/cart.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="cart-page">

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Choisissez une categorie de produits">
<title>Wakdo - Categories</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/categories.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="categories-page">

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Commande confirmee">
<title>Wakdo - Confirmation</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/confirmation.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="confirmation-page">

View file

@ -10,7 +10,23 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Borne de commande Wakdo - choisissez votre mode de consommation">
<title>Wakdo - Bienvenue</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
<!--
Donnees structurees schema.org (JSON-LD) : decrivent l'etablissement pour
les moteurs / agents. Demonstratif (la borne porte noindex), Cr 1.e.3.
-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FastFoodRestaurant",
"name": "Wakdo",
"servesCuisine": "Restauration rapide",
"openingHours": "Mo-Su 10:00-01:00",
"paymentAccepted": "Sur place, A emporter, Drive"
}
</script>
</head>
<body>

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Choisissez votre mode de paiement">
<title>Wakdo - Paiement</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/payment.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="payment-page">

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Detail du produit">
<title>Wakdo - Produit</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/product.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="product-page">

View file

@ -6,6 +6,8 @@
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Produits de la categorie selectionnee">
<title>Wakdo - Produits</title>
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/products.html">
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="products-page">

View file

@ -132,4 +132,43 @@ final class OrderControllerTest extends TestCase
self::assertIsArray($data);
self::assertSame('INVALID_TRANSITION', $data['error']['code'] ?? null);
}
public function testShowReturnsOrderStatus(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = ['id' => 100, 'order_number' => 'K100', 'total_ttc_cents' => 890, 'status' => 'paid'];
$response = $this->controller($db, '', '/api/orders/K100')->show(['number' => 'K100']);
self::assertSame(200, $response->status());
$data = json_decode($response->body(), true);
self::assertIsArray($data);
self::assertSame('K100', $data['data']['order_number'] ?? null);
self::assertSame('paid', $data['data']['status'] ?? null);
self::assertSame(890, $data['data']['total_ttc_cents'] ?? null);
}
public function testShowUnknownReturns404(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = null;
$response = $this->controller($db, '', '/api/orders/K404')->show(['number' => 'K404']);
self::assertSame(404, $response->status());
$data = json_decode($response->body(), true);
self::assertIsArray($data);
self::assertSame('ORDER_NOT_FOUND', $data['error']['code'] ?? null);
}
public function testShowEmptyNumberReturns404(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = ['id' => 1, 'order_number' => 'K1', 'total_ttc_cents' => 100, 'status' => 'paid'];
// Numero vide : court-circuite avant toute lecture BDD (findByNumber renvoie null).
$response = $this->controller($db, '', '/api/orders/')->show(['number' => '']);
self::assertSame(404, $response->status());
}
}