Merge pull request 'release: dev -> main v0.2.0' (#93) from dev into main
127
.env.example
|
|
@ -3,17 +3,13 @@
|
|||
#
|
||||
# Usage :
|
||||
# cp .env.example .env
|
||||
# Editer .env (gitignore) avec les valeurs reelles.
|
||||
# docker compose up -d
|
||||
#
|
||||
# Audience :
|
||||
# Destine a l'auteur, au jury et aux contributeurs futurs.
|
||||
#
|
||||
# Modele de deploiement :
|
||||
# Ce projet tourne sur serveur derriere un reverse proxy Traefik. Il n'y a
|
||||
# pas de binding de ports hote : l'acces se fait uniquement via les FQDN
|
||||
# configures ci-dessous et routes par Traefik (reseau admin_proxy).
|
||||
# Les distinctions dev / staging / prod se font par FQDN distincts
|
||||
# (ex : .dev.acadenice.fr vs .acadenice.fr) et par .env dedie.
|
||||
# Ce template fonctionne EN LOCAL tel quel (valeurs dev) : la stack est joignable
|
||||
# sur http://kiosk.localhost:8080 (borne) et http://admin.localhost:8080 (admin).
|
||||
# Le deploiement derriere un reverse proxy (Traefik) se fait via l'overlay
|
||||
# docker-compose.prod.yml + les variables du bloc "Deploiement prod" en fin de
|
||||
# fichier. En prod : changer les mots de passe, APP_DEBUG=false, vrais FQDN.
|
||||
#
|
||||
|
||||
# ===================================================================
|
||||
|
|
@ -24,27 +20,30 @@ APP_ENV=dev # dev | staging | prod
|
|||
APP_DEBUG=true # true en dev, false en prod
|
||||
APP_TIMEZONE=Europe/Paris
|
||||
|
||||
# URL publique de la borne (Bloc 1), doit pointer vers le FQDN Traefik.
|
||||
# Placeholder example.com (RFC 2606) - a remplacer par le FQDN reel.
|
||||
APP_URL_KIOSK=https://kiosk.example.com
|
||||
# Port hote publie par wakdo-web (acces local). Change si 8080 est pris.
|
||||
HTTP_PORT=8080
|
||||
|
||||
# URL publique du back-office + API (Bloc 2).
|
||||
# Placeholder example.com (RFC 2606) - a remplacer par le FQDN reel.
|
||||
APP_URL_ADMIN=https://admin.example.com
|
||||
# Hostnames des deux vhosts Apache (ServerName). En local : *.localhost resout
|
||||
# vers 127.0.0.1 nativement. En prod : les vrais FQDN (voir bloc prod en bas).
|
||||
APP_HOST_KIOSK=kiosk.localhost
|
||||
APP_HOST_ADMIN=admin.localhost
|
||||
|
||||
# URLs publiques (consommees par l'app). En local = les hostnames sur HTTP_PORT.
|
||||
APP_URL_KIOSK=http://kiosk.localhost:8080
|
||||
APP_URL_ADMIN=http://admin.localhost:8080
|
||||
|
||||
# ===================================================================
|
||||
# Base de donnees (MariaDB)
|
||||
# ===================================================================
|
||||
# Valeurs ci-dessous = PLACEHOLDERS. Remplacer par des mots de passe forts.
|
||||
# Pas accessible depuis l'exterieur : le service wakdo-db est sur le reseau
|
||||
# interne uniquement, aucun port exposé a l'hote.
|
||||
# Valeurs dev ci-dessous : OK en local. EN PROD : mots de passe forts.
|
||||
# wakdo-db est sur le reseau interne, aucun port expose a l'hote.
|
||||
|
||||
DB_HOST=wakdo-db # nom du service docker-compose
|
||||
DB_PORT=3306
|
||||
DB_NAME=wakdo
|
||||
DB_USER=wakdo
|
||||
DB_PASSWORD=change_me_strong_password
|
||||
DB_ROOT_PASSWORD=change_me_root_password
|
||||
DB_PASSWORD=wakdo_dev_password
|
||||
DB_ROOT_PASSWORD=wakdo_dev_root_password
|
||||
|
||||
# ===================================================================
|
||||
# Sessions
|
||||
|
|
@ -58,13 +57,54 @@ SESSION_NAME=WAKDO_SID # nom du cookie (evite PHPSESSID)
|
|||
# Securite
|
||||
# ===================================================================
|
||||
|
||||
# Origine autorisee pour les requetes CORS de l'API.
|
||||
# Doit correspondre exactement a APP_URL_KIOSK (pas de wildcard).
|
||||
CORS_ALLOWED_ORIGIN=https://kiosk.example.com
|
||||
# Origine autorisee pour les requetes CORS de l'API. Doit correspondre
|
||||
# exactement a APP_URL_KIOSK (pas de wildcard).
|
||||
CORS_ALLOWED_ORIGIN=http://kiosk.localhost:8080
|
||||
|
||||
# Algorithme de hashage mot de passe (password_hash PHP).
|
||||
# argon2id recommande depuis PHP 7.3 pour les nouveaux projets.
|
||||
PASSWORD_ALGO=argon2id
|
||||
# Algorithme de hashage : argon2id, FIXE dans le code (App\Auth\PasswordHasher),
|
||||
# choix security-by-design non configurable. Seuls les COUTS ci-dessous sont reglables.
|
||||
|
||||
# Parametres de cout argon2id (password_hash options). Defauts alignes OWASP
|
||||
# (memoire >= 19 MiB, >= 2 iterations). Servent aussi au hash du PIN equipier.
|
||||
ARGON2_MEMORY_COST=65536 # KiB (64 MiB)
|
||||
ARGON2_TIME_COST=4 # nombre d'iterations
|
||||
ARGON2_THREADS=1 # parallelisme (1 = portable, deterministe)
|
||||
|
||||
# ===================================================================
|
||||
# Anti brute-force - throttling de connexion (security-by-design)
|
||||
# ===================================================================
|
||||
# Deux gardes : par compte (user.failed_login_attempts / lockout_until) et par IP
|
||||
# (table login_throttle). Backoff degressif, pas de lock definitif.
|
||||
|
||||
ACCOUNT_LOCKOUT_THRESHOLD=5
|
||||
ACCOUNT_LOCKOUT_BASE_SECONDS=60
|
||||
ACCOUNT_LOCKOUT_MAX_SECONDS=900 # plafond du backoff (15 min)
|
||||
|
||||
IP_THROTTLE_WINDOW_SECONDS=900 # 15 min
|
||||
IP_THROTTLE_MAX_ATTEMPTS=20 # par IP sur la fenetre
|
||||
|
||||
# PIN equipier pour actions sensibles. Chiffres, bornes min ET max (RG-T18).
|
||||
STAFF_PIN_MIN_LENGTH=4
|
||||
STAFF_PIN_MAX_LENGTH=12
|
||||
|
||||
# Throttle du PIN d'action sensible (RG-T22) - compteurs SEPARES du login,
|
||||
# dimension = utilisateur agissant. Bornes plus permissives que le login.
|
||||
PIN_THROTTLE_THRESHOLD=5
|
||||
PIN_THROTTLE_BASE_SECONDS=30
|
||||
PIN_THROTTLE_MAX_SECONDS=300
|
||||
PIN_THROTTLE_WINDOW_SECONDS=900
|
||||
|
||||
# Expiration du token de reinitialisation de mot de passe (secondes).
|
||||
PASSWORD_RESET_TTL=3600 # 1h
|
||||
|
||||
# ===================================================================
|
||||
# Retention des donnees (RGPD)
|
||||
# ===================================================================
|
||||
# Purges executees par le service cron (docker/cron/crontab).
|
||||
|
||||
AUDIT_LOG_RETENTION_DAYS=365 # journal d'audit ~12 mois
|
||||
THROTTLE_PURGE_AFTER_HOURS=24 # login_throttle : lignes sans lockout actif > 24h
|
||||
ORDER_RETENTION_DAYS=1095 # commandes (historique/stats) ~3 ans
|
||||
|
||||
# ===================================================================
|
||||
# Upload images produits
|
||||
|
|
@ -76,35 +116,18 @@ UPLOAD_ALLOWED_MIME=image/jpeg,image/png,image/webp
|
|||
# ===================================================================
|
||||
# Cron (fenetre de maintenance 01h30 - 09h30)
|
||||
# ===================================================================
|
||||
# Les jobs sont definis dans docker/cron/crontab. Ici uniquement le TZ.
|
||||
|
||||
CRON_TIMEZONE=Europe/Paris
|
||||
|
||||
# ===================================================================
|
||||
# Exposition via Traefik
|
||||
# Deploiement prod (overlay docker-compose.prod.yml) - OPTIONNEL
|
||||
# ===================================================================
|
||||
# FQDN consommes par les labels docker-compose.yml pour generer les routes
|
||||
# Traefik et les certificats TLS (Traefik gere le resolver par defaut).
|
||||
# Le Traefik de l'hote prend en charge Let's Encrypt automatiquement.
|
||||
|
||||
TRAEFIK_DOMAIN_KIOSK=kiosk.example.com
|
||||
TRAEFIK_DOMAIN_ADMIN=admin.example.com
|
||||
|
||||
# ===================================================================
|
||||
# Reseau Docker externe du reverse proxy
|
||||
# ===================================================================
|
||||
# Nom du reseau Docker externe auquel le service web doit se connecter
|
||||
# pour etre expose par le reverse proxy de l'hote.
|
||||
# A ignorer pour un usage local. Necessaire UNIQUEMENT derriere un reverse proxy
|
||||
# Traefik, avec : docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
#
|
||||
# Adapter selon votre infrastructure. Valeurs courantes :
|
||||
# traefik_proxy - convention neutre (placeholder)
|
||||
# traefik_public - convention doc Traefik
|
||||
# traefik - setups simples
|
||||
# proxy - autre convention frequente
|
||||
# En prod, surcharger aussi : APP_ENV=prod, APP_DEBUG=false, mots de passe forts,
|
||||
# et APP_HOST_*/APP_URL_*/CORS_ALLOWED_ORIGIN avec les vrais FQDN HTTPS.
|
||||
#
|
||||
# Le reseau doit exister AVANT 'make init' (cree par votre stack de
|
||||
# reverse proxy, ou manuellement : docker network create <nom>).
|
||||
# La cible 'make init' echoue proprement avec un message d'aide si le
|
||||
# reseau est introuvable.
|
||||
|
||||
REVERSE_PROXY_NETWORK=traefik_proxy
|
||||
# Nom du reseau Docker externe partage avec le Traefik de l'hote (doit exister
|
||||
# AVANT le up : cree par la stack Traefik, ou `docker network create <nom>`).
|
||||
REVERSE_PROXY_NETWORK=admin_proxy
|
||||
|
|
|
|||
172
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
name: CI
|
||||
# CI Wakdo - Forgejo Actions (runner stark-wakdo, label `docker`).
|
||||
# Strategie solo dev : PR obligatoire ; l'auto-merge NATIF Forgejo
|
||||
# (merge_when_checks_succeed, programme a l'ouverture de la PR) fusionne en squash
|
||||
# des que les checks requis passent. Pas de job de merge dans le workflow.
|
||||
#
|
||||
# Etat des jobs selon la phase projet :
|
||||
# - secret-scan : fonctionnel des maintenant (gitleaks scanne tout le depot)
|
||||
# - php-lint : fonctionnel sur les fichiers PHP presents (stubs P1, code P2+)
|
||||
# - static-tests: PHPStan + PHPUnit GARDES - s'activent quand P2 ajoute
|
||||
# composer.json / phpstan.neon / tests + phpunit.xml
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
# dev/main : porte de merge. feat|fix|ci|refactor : feedback avant la PR.
|
||||
branches: [dev, main, 'feat/**', 'fix/**', 'ci/**', 'refactor/**']
|
||||
|
||||
jobs:
|
||||
secret-scan:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install tools
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl ca-certificates tar >/dev/null
|
||||
- name: Install gitleaks
|
||||
run: |
|
||||
VER=8.21.2
|
||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" -o /tmp/gl.tgz
|
||||
tar -xzf /tmp/gl.tgz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
- name: Scan for secrets
|
||||
run: gitleaks detect --config .gitleaks.toml --redact --no-banner --verbose
|
||||
|
||||
php-lint:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install PHP CLI
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq php-cli >/dev/null
|
||||
php --version
|
||||
- name: Lint all PHP files
|
||||
run: |
|
||||
set -eu
|
||||
files=$(find . -path ./node_modules -prune -o -name '*.php' -print)
|
||||
if [ -z "$files" ]; then echo "No PHP files yet - skip"; exit 0; fi
|
||||
echo "$files" | while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
php -l "$f"
|
||||
done
|
||||
|
||||
static-tests:
|
||||
runs-on: docker
|
||||
# COMPOSER-LESS (decision 4 / 5, PROJECT_CONTEXT.md) : PHPStan et PHPUnit
|
||||
# tournent depuis leur .phar autonome telecharge ici, jamais via Composer.
|
||||
# Versions epinglees pour des CI reproductibles (pas de "latest").
|
||||
#
|
||||
# Service MariaDB ephemere : le schema (db/migrations) et le seed (db/seeds)
|
||||
# y sont appliques, puis PHPUnit tourne avec WAKDO_DB_TESTS=1 pour que les
|
||||
# tests d'integration (tests/Integration/*DbTest) s'executent REELLEMENT.
|
||||
# Sans base, ils s'auto-skippent et le SQL porteur de securite (throttle,
|
||||
# RBAC is_active, audit in-transaction, FK) n'est jamais valide en CI.
|
||||
# Identifiants ci-dessous : ephemeres, CI uniquement, jamais des secrets.
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11.4
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
MARIADB_DATABASE: wakdo_test
|
||||
MARIADB_USER: wakdo
|
||||
MARIADB_PASSWORD: wakdo
|
||||
env:
|
||||
PHPUNIT_VERSION: "11.5.2"
|
||||
PHPSTAN_VERSION: "1.12.27"
|
||||
# Connexion des tests d'integration au service `mariadb` ci-dessus
|
||||
# (Database lit ces DB_* via getenv ; cf. src/app/Core/Database.php).
|
||||
WAKDO_DB_TESTS: "1"
|
||||
DB_HOST: mariadb
|
||||
DB_PORT: "3306"
|
||||
DB_NAME: wakdo_test
|
||||
DB_USER: wakdo
|
||||
DB_PASSWORD: wakdo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: PHPStan (guarded)
|
||||
run: |
|
||||
set -eu
|
||||
if [ ! -f phpstan.neon ]; then
|
||||
echo "PHPStan skipped: no phpstan.neon yet (activates in P2)"
|
||||
exit 0
|
||||
fi
|
||||
echo "phpstan.neon detected - running PHPStan ${PHPSTAN_VERSION} via .phar"
|
||||
apt-get update -qq && apt-get install -y -qq php-cli php-xml php-mbstring curl ca-certificates >/dev/null
|
||||
# PHPUnit phar present pour que phpstan.neon (scanDirectories phar://phpunit.phar)
|
||||
# resolve les symboles PHPUnit\Framework\* utilises sous tests/.
|
||||
curl -sSL "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" -o phpunit.phar
|
||||
curl -sSL "https://github.com/phpstan/phpstan/releases/download/${PHPSTAN_VERSION}/phpstan.phar" -o phpstan.phar
|
||||
php phpstan.phar --version
|
||||
# memory_limit=-1 : l'analyse parallele depasse les 128M par defaut du php-cli.
|
||||
php -d memory_limit=-1 phpstan.phar analyse --no-progress --error-format=raw
|
||||
- name: PHPUnit (guarded, avec tests d'integration DB)
|
||||
run: |
|
||||
set -eu
|
||||
if [ ! -d tests ] || [ ! -f phpunit.xml ]; then
|
||||
echo "PHPUnit skipped: no tests/ + phpunit.xml yet (activates in P2)"
|
||||
exit 0
|
||||
fi
|
||||
echo "phpunit.xml + tests/ detected - running PHPUnit ${PHPUNIT_VERSION} via .phar"
|
||||
# php-mysql = pilote pdo_mysql requis par les *DbTest ; mariadb-client
|
||||
# pour appliquer schema + seed au service mariadb.
|
||||
apt-get update -qq && apt-get install -y -qq php-cli php-xml php-mbstring php-mysql mariadb-client curl ca-certificates >/dev/null
|
||||
# Attente active que le service MariaDB reponde (en plus du lien de service).
|
||||
echo "Attente du service MariaDB ${DB_HOST}:${DB_PORT} ..."
|
||||
ready=0
|
||||
for i in $(seq 1 30); do
|
||||
if mariadb -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" -e "SELECT 1" "${DB_NAME}" >/dev/null 2>&1; then
|
||||
echo "MariaDB pret (tentative ${i})."; ready=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[ "${ready}" = 1 ] || { echo "ERREUR: MariaDB injoignable apres 60s"; exit 1; }
|
||||
# Schema (db/migrations) puis seed (db/seeds), ordre lexicographique.
|
||||
for f in db/migrations/*.sql; do
|
||||
echo "migrate $(basename "$f")"
|
||||
mariadb -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" < "$f"
|
||||
done
|
||||
for f in db/seeds/*.sql; do
|
||||
echo "seed $(basename "$f")"
|
||||
mariadb -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" < "$f"
|
||||
done
|
||||
curl -sSL "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" -o phpunit.phar
|
||||
php phpunit.phar --version
|
||||
# --fail-on-skipped : si un *DbTest s'auto-skippe (base injoignable), la
|
||||
# CI echoue au lieu de masquer le trou derriere un vert. C'est le coeur
|
||||
# du correctif : plus aucun skip silencieux des chemins securite.
|
||||
php phpunit.phar -c phpunit.xml --fail-on-skipped
|
||||
|
||||
js-tests:
|
||||
# Tests du front borne (kiosk) : node:test + jsdom, sans navigateur.
|
||||
# GARDE : ne s'active que si package.json + tests/js/ existent.
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js 20
|
||||
run: |
|
||||
set -eu
|
||||
# Node 20 epingle via NodeSource (self-contained, comme les .phar/gitleaks)
|
||||
# plutot que l'apt bookworm (18.x, limite basse pour jsdom). Reproductible.
|
||||
apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null
|
||||
apt-get install -y -qq nodejs >/dev/null
|
||||
node --version && npm --version
|
||||
- name: Install deps + run kiosk JS tests
|
||||
run: |
|
||||
set -eu
|
||||
if [ ! -f package.json ] || [ ! -d tests/js ]; then
|
||||
echo "JS tests skipped: no package.json + tests/js/ yet"
|
||||
exit 0
|
||||
fi
|
||||
# Skip le download des browsers Playwright : ce job ne fait que node:test+jsdom.
|
||||
# (@playwright/test est en devDep pour l'E2E, mais ses browsers ne servent
|
||||
# qu'a tests/e2e via le conteneur officiel.)
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci
|
||||
npm run test:js
|
||||
|
|
@ -24,6 +24,17 @@ Remplis les sections, coche ce qui s'applique, supprime ce qui ne sert pas.
|
|||
- [ ] Docs Merise / dictionnaire a jour si le modele de donnees change
|
||||
- [ ] Tests ajoutes et passants si du code est touche (unit > integration > e2e)
|
||||
|
||||
## Checklist securite (security-by-design)
|
||||
|
||||
<!-- Cocher ce qui s'applique ; voir SECURITY.md et PROJECT_CONTEXT section 19. -->
|
||||
|
||||
- [ ] Aucun secret commite (CI gitleaks verte) ; `.env` reste gitignore
|
||||
- [ ] Entrees utilisateur validees ; requetes SQL en prepared statements (anti-injection)
|
||||
- [ ] Mots de passe / PIN en argon2id ; pas de donnee sensible en clair ni dans les logs
|
||||
- [ ] Sorties HTML echappees (anti-XSS) ; CSRF gere sur les formulaires d'etat
|
||||
- [ ] Permissions RBAC verifiees cote serveur pour toute action sensible
|
||||
- [ ] Impact RGPD evalue si nouvelles donnees personnelles (retention, droit a l'effacement)
|
||||
|
||||
## Bloc RNCP impacte
|
||||
|
||||
<!-- ex : Bloc 2 Cr 3.b (modelisation), Bloc 1 (accessibilite), Bloc 5 (infra/CI)... -->
|
||||
|
|
|
|||
53
.githooks/commit-msg
Executable 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
|
|
@ -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
|
||||
16
.gitignore
vendored
|
|
@ -5,6 +5,11 @@
|
|||
*.pem
|
||||
*.key
|
||||
|
||||
# Compose de production (propre a chaque hote : Traefik/reverse proxy, FQDN).
|
||||
# Le repo ne ship que docker-compose.yml (standalone, local). Chaque hote derriere
|
||||
# un proxy maintient son propre docker-compose.prod.yml (hors versionnement).
|
||||
docker-compose.prod.yml
|
||||
|
||||
# === BYAN — plateforme (moteur), masquee ===
|
||||
# Le code moteur des agents n'est pas part du rendu RNCP.
|
||||
# La methodologie appliquee (CLAUDE.md + rules + hooks) reste dans .claude/
|
||||
|
|
@ -28,8 +33,11 @@ vendor/
|
|||
composer.lock
|
||||
composer.phar
|
||||
|
||||
# === Tests ===
|
||||
# === Tests / Analyse statique (tooling via .phar autonome, sans Composer) ===
|
||||
.phpunit.result.cache
|
||||
.phpunit.cache/
|
||||
/phpunit.phar
|
||||
/phpstan.phar
|
||||
/tests/_output/
|
||||
/tests/_support/_generated/
|
||||
|
||||
|
|
@ -67,6 +75,12 @@ node_modules/
|
|||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# === Playwright (E2E) ===
|
||||
playwright-report/
|
||||
test-results/
|
||||
/blob-report/
|
||||
.last-run.json
|
||||
|
||||
# === Docker volumes locaux ===
|
||||
/docker-data/
|
||||
|
||||
|
|
|
|||
31
.gitleaks.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Wakdo - configuration gitleaks (secret-scan)
|
||||
#
|
||||
# Utilise par :
|
||||
# - le hook pre-commit local (defense en profondeur)
|
||||
# - le job CI Forgejo Actions (.forgejo/workflows/, lot D) sur chaque PR -> dev
|
||||
#
|
||||
# Principe : etendre le jeu de regles par defaut de gitleaks, puis ne tolerer
|
||||
# QUE les faux positifs explicitement justifies ci-dessous (placeholders de doc).
|
||||
|
||||
[extend]
|
||||
useDefault = true
|
||||
|
||||
[allowlist]
|
||||
description = "Faux positifs documentes - placeholders de configuration, jamais des secrets reels"
|
||||
|
||||
# Fichiers de template / doc : ne contiennent que des placeholders RFC 2606 / change_me.
|
||||
paths = [
|
||||
'''\.env\.example$''',
|
||||
'''\.gitleaks\.toml$''',
|
||||
'''docs/.*\.md$''',
|
||||
]
|
||||
|
||||
# Valeurs placeholder explicites tolerees ou qu'elles apparaissent.
|
||||
regexes = [
|
||||
'''change_me_strong_password''',
|
||||
'''change_me_root_password''',
|
||||
'''example\.com''',
|
||||
]
|
||||
|
||||
# Note : le vrai .env est gitignore et ne doit jamais etre commite. Ce scan est
|
||||
# une defense en profondeur, pas un substitut a l'hygiene .gitignore.
|
||||
218
Makefile
|
|
@ -1,218 +0,0 @@
|
|||
#
|
||||
# Wakdo - Makefile d'orchestration locale
|
||||
#
|
||||
# Conventions :
|
||||
# - Une cible = une action unitaire. Les cibles composites sont commentees.
|
||||
# - Chaque cible est documentee par un `## description` pour auto-help.
|
||||
# - Echec sur erreur (set -e implicite via bash recipes + pipefail).
|
||||
#
|
||||
# Documentation :
|
||||
# make help
|
||||
#
|
||||
|
||||
SHELL := /usr/bin/env bash
|
||||
.SHELLFLAGS := -eu -o pipefail -c
|
||||
|
||||
# === Configuration ===
|
||||
|
||||
# Chargement du .env s'il existe (variables Make + export pour docker compose)
|
||||
ifneq (,$(wildcard .env))
|
||||
include .env
|
||||
export
|
||||
endif
|
||||
|
||||
# Prefixe du projet compose (utilise pour nommer les containers)
|
||||
PROJECT := wakdo
|
||||
|
||||
# Nom du fichier compose (override possible : make up COMPOSE_FILE=docker-compose.prod.yml)
|
||||
COMPOSE_FILE := docker-compose.yml
|
||||
COMPOSE := docker compose -f $(COMPOSE_FILE) -p $(PROJECT)
|
||||
|
||||
# Services docker-compose
|
||||
SERVICE_WEB := wakdo-web
|
||||
SERVICE_APP := wakdo-app
|
||||
SERVICE_DB := wakdo-db
|
||||
SERVICE_CRON := wakdo-cron
|
||||
|
||||
# === Meta ===
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help
|
||||
help: ## Liste toutes les cibles disponibles avec leur description
|
||||
@echo "Wakdo - cibles Make disponibles :"
|
||||
@echo ""
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[1m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort
|
||||
@echo ""
|
||||
|
||||
# === Orchestration principale ===
|
||||
|
||||
.PHONY: init
|
||||
init: ## Build et demarre toute la stack en une commande (Cr RNCP 7.c.4)
|
||||
@test -f .env || { echo "ERREUR: .env manquant. Executer : cp .env.example .env"; exit 1; }
|
||||
@$(MAKE) --no-print-directory check-env
|
||||
@echo "[init] Verification du reseau docker '$(REVERSE_PROXY_NETWORK)'..."
|
||||
@docker network inspect $(REVERSE_PROXY_NETWORK) >/dev/null 2>&1 || { \
|
||||
echo "ERREUR: reseau docker '$(REVERSE_PROXY_NETWORK)' introuvable."; \
|
||||
echo " - Si un Traefik est installe sur l'hote, verifier le nom de son reseau ;"; \
|
||||
echo " - Adapter REVERSE_PROXY_NETWORK dans .env en consequence ;"; \
|
||||
echo " - Sinon creer le reseau manuellement :"; \
|
||||
echo " docker network create $(REVERSE_PROXY_NETWORK)"; \
|
||||
exit 1; }
|
||||
@echo "[init] Build des images..."
|
||||
@$(COMPOSE) build
|
||||
@echo "[init] Demarrage des services..."
|
||||
@$(COMPOSE) up -d
|
||||
@echo "[init] Attente de la base de donnees..."
|
||||
@$(MAKE) --no-print-directory wait-db
|
||||
@echo "[init] Execution des migrations..."
|
||||
@$(MAKE) --no-print-directory migrate
|
||||
@echo "[init] Stack operationnelle."
|
||||
@$(COMPOSE) ps
|
||||
|
||||
.PHONY: up
|
||||
up: ## Demarre les services sans rebuild
|
||||
@$(COMPOSE) up -d
|
||||
|
||||
.PHONY: down
|
||||
down: ## Arrete et supprime les containers (volumes preserves)
|
||||
@$(COMPOSE) down
|
||||
|
||||
.PHONY: stop
|
||||
stop: ## Arrete les services sans les supprimer
|
||||
@$(COMPOSE) stop
|
||||
|
||||
.PHONY: restart
|
||||
restart: ## Redemarre tous les services
|
||||
@$(COMPOSE) restart
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build les images (utilise le cache)
|
||||
@$(COMPOSE) build
|
||||
|
||||
.PHONY: rebuild
|
||||
rebuild: ## Rebuild complet sans cache puis restart
|
||||
@$(COMPOSE) build --no-cache
|
||||
@$(COMPOSE) up -d
|
||||
|
||||
# === Observabilite ===
|
||||
|
||||
.PHONY: ps
|
||||
ps: ## Affiche le statut des services
|
||||
@$(COMPOSE) ps
|
||||
|
||||
.PHONY: logs
|
||||
logs: ## Suit les logs de tous les services (Ctrl+C pour sortir)
|
||||
@$(COMPOSE) logs -f --tail=100
|
||||
|
||||
.PHONY: logs-app
|
||||
logs-app: ## Suit les logs du service applicatif PHP-FPM
|
||||
@$(COMPOSE) logs -f --tail=100 $(SERVICE_APP)
|
||||
|
||||
.PHONY: logs-web
|
||||
logs-web: ## Suit les logs du service web Apache
|
||||
@$(COMPOSE) logs -f --tail=100 $(SERVICE_WEB)
|
||||
|
||||
.PHONY: logs-db
|
||||
logs-db: ## Suit les logs de la base de donnees
|
||||
@$(COMPOSE) logs -f --tail=100 $(SERVICE_DB)
|
||||
|
||||
# === Acces shell ===
|
||||
|
||||
.PHONY: shell-app
|
||||
shell-app: ## Ouvre un shell dans le container applicatif
|
||||
@$(COMPOSE) exec $(SERVICE_APP) sh
|
||||
|
||||
.PHONY: shell-db
|
||||
shell-db: ## Ouvre le client mariadb dans le container de base de donnees
|
||||
@$(COMPOSE) exec $(SERVICE_DB) mariadb -u root -p"$${DB_ROOT_PASSWORD}"
|
||||
|
||||
.PHONY: shell-cron
|
||||
shell-cron: ## Ouvre un shell dans le container cron
|
||||
@$(COMPOSE) exec $(SERVICE_CRON) sh
|
||||
|
||||
# === Verification env ===
|
||||
|
||||
.PHONY: check-env
|
||||
check-env: ## Verifie que les variables critiques Wakdo sont definies dans .env
|
||||
@missing=""; \
|
||||
for var in DB_PASSWORD DB_ROOT_PASSWORD REVERSE_PROXY_NETWORK TRAEFIK_DOMAIN_KIOSK TRAEFIK_DOMAIN_ADMIN APP_URL_KIOSK APP_URL_ADMIN CORS_ALLOWED_ORIGIN; do \
|
||||
if [ -z "$${!var:-}" ]; then missing="$$missing $$var"; fi; \
|
||||
done; \
|
||||
if [ -n "$$missing" ]; then \
|
||||
echo "ERREUR: variables manquantes dans .env :$$missing"; \
|
||||
echo "Conseil : si vous aviez un .env pre-existant (tooling externe),"; \
|
||||
echo " merger les variables manquantes depuis .env.example au lieu"; \
|
||||
echo " d'ecraser le fichier."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# === Base de donnees ===
|
||||
|
||||
.PHONY: wait-db
|
||||
wait-db: ## Attend que la base de donnees accepte les connexions (timeout 60s)
|
||||
@echo "[wait-db] En attente de MariaDB..."
|
||||
@timeout 60 bash -c 'until $(COMPOSE) exec -T $(SERVICE_DB) healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; do sleep 2; done' \
|
||||
|| { echo "ERREUR: MariaDB ne repond pas apres 60s"; $(COMPOSE) logs --tail=50 $(SERVICE_DB); exit 1; }
|
||||
@echo "[wait-db] OK"
|
||||
|
||||
.PHONY: migrate
|
||||
migrate: ## Applique les migrations SQL en attente [a venir]
|
||||
@echo "[migrate] Pas encore implemente. Les migrations seront dans db/migrations/."
|
||||
|
||||
.PHONY: seed
|
||||
seed: ## Charge les donnees de demo [a venir]
|
||||
@echo "[seed] Pas encore implemente. Les seeds seront dans db/seeds/."
|
||||
|
||||
.PHONY: backup
|
||||
backup: ## Declenche un dump SQL horodate immediat (via le container cron)
|
||||
@mkdir -p ./var/backups
|
||||
@echo "[backup] Execution manuelle de /scripts/backup-db.sh dans wakdo-cron..."
|
||||
@$(COMPOSE) exec -T $(SERVICE_CRON) /scripts/backup-db.sh
|
||||
@echo "[backup] Dernier dump :"
|
||||
@ls -lh ./var/backups/ | tail -n 1
|
||||
|
||||
.PHONY: backup-ls
|
||||
backup-ls: ## Liste les dumps SQL presents dans ./var/backups/
|
||||
@ls -lh ./var/backups/ 2>/dev/null || echo "[backup-ls] Pas de backups (./var/backups/ vide ou inexistant)."
|
||||
|
||||
# === Tests ===
|
||||
|
||||
.PHONY: test
|
||||
test: ## Lance la suite complete de tests PHPUnit [a venir]
|
||||
@echo "[test] Pas encore implemente. PHPUnit via .phar sera configure en P2."
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## Lance uniquement les tests unitaires [a venir]
|
||||
@echo "[test-unit] Pas encore implemente."
|
||||
|
||||
.PHONY: test-integration
|
||||
test-integration: ## Lance uniquement les tests d'integration [a venir]
|
||||
@echo "[test-integration] Pas encore implemente."
|
||||
|
||||
# === Qualite code ===
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Lance php -l sur tous les fichiers src/ [a venir]
|
||||
@echo "[lint] Pas encore implemente. PHP syntax check via php -l + outil de style en P2."
|
||||
|
||||
# === Nettoyage ===
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Stop + suppression containers + volumes (DESTRUCTIF, demande confirmation)
|
||||
@read -p "Supprimer containers ET volumes (les donnees seront perdues) ? [y/N] " ans; \
|
||||
if [ "$$ans" = "y" ] || [ "$$ans" = "Y" ]; then \
|
||||
$(COMPOSE) down -v; \
|
||||
echo "[clean] Stack et volumes supprimes."; \
|
||||
else \
|
||||
echo "[clean] Annule."; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-force
|
||||
clean-force: ## Version non interactive de clean (pour CI uniquement)
|
||||
@$(COMPOSE) down -v
|
||||
|
||||
# === Hooks Git ===
|
||||
|
||||
.PHONY: install-hooks
|
||||
install-hooks: ## Installe les hooks git depuis .githooks/ [a venir]
|
||||
@echo "[install-hooks] Pas encore implemente. Voir scripts/install-hooks.sh a venir."
|
||||
165
README.md
|
|
@ -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`.
|
||||
|
||||
|
|
@ -55,9 +55,9 @@ Realisation avec l'assistance d'outils d'IA generative (Claude Code, BYAN), conf
|
|||
| Tests | PHPUnit | 11.x (`.phar` autonome, sans Composer) |
|
||||
| Front | HTML5 + CSS3 + JS ES6+ vanilla | — |
|
||||
| Conteneurisation | Docker + docker compose v2 | — |
|
||||
| Orchestration locale | Makefile | — |
|
||||
| CI/CD | GitHub Actions | — |
|
||||
| Versioning | Git + GitHub | Conventional Commits |
|
||||
| Orchestration locale | docker compose v2 (service one-shot `wakdo-migrate`) | — |
|
||||
| CI/CD | Forgejo Actions | — |
|
||||
| Versioning | Git + Forgejo (`git.acadenice.com`, miroir GitHub) | Conventional Commits |
|
||||
|
||||
Detail et justifications : `docs/PROJECT_CONTEXT.md` section 6.
|
||||
|
||||
|
|
@ -82,86 +82,50 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
## Quickstart
|
||||
|
||||
Ce projet tourne **sur serveur derriere un reverse proxy Traefik** : pas de binding de ports hote, pas d'acces `localhost`. L'acces public se fait par FQDN HTTPS (TLS gere automatiquement par Traefik). Les environnements `dev`, `staging` et `prod` se distinguent par des FQDN et des fichiers `.env` separes.
|
||||
|
||||
### Prerequis sur l'hote
|
||||
|
||||
1. Docker Engine + docker compose v2 (voir ci-dessous)
|
||||
2. Un reverse proxy Traefik deja en place, avec un reseau Docker externe dedie. Le **nom du reseau** est configurable via la variable `REVERSE_PROXY_NETWORK` du `.env` (defaut : `admin_proxy` — convention de l'auteur). A adapter a votre infrastructure.
|
||||
3. Les FQDN cibles pointent en DNS vers l'hote
|
||||
|
||||
### Sur un hote deja equipe (Docker + Traefik)
|
||||
## 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
|
||||
# Editer .env : DB_PASSWORD, DB_ROOT_PASSWORD, APP_URL_*, TRAEFIK_DOMAIN_*
|
||||
make init
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> **Attention au `.env` pre-existant.** Si un fichier `.env` existe deja a la racine (tooling externe, autre plateforme installee dans le meme repertoire), **ne pas faire** `cp .env.example .env` — cela ecraserait les variables existantes. Faire un **merge manuel** a la place : ajouter les variables manquantes du template dans le `.env` actuel. Les prefixes de variables de ce projet (`APP_`, `DB_`, `SESSION_`, `CORS_`, `UPLOAD_`, `CRON_`, `TRAEFIK_`, `REVERSE_PROXY_`) sont disjoints de ceux utilises par des outils tiers courants, donc la cohabitation est safe.
|
||||
Une seule commande lance la stack complete (Cr 7.c.4) : le service one-shot
|
||||
`wakdo-migrate` applique les migrations puis le seed (idempotents, tables de suivi
|
||||
`schema_migrations` / `seeds_applied`) avant que l'app ne serve. Ensuite :
|
||||
|
||||
Critere RNCP Cr 7.c.4 couvert : une seule commande (`make init`) orchestre build, demarrage, attente BDD, migrations et seed.
|
||||
- Borne : http://kiosk.localhost:8080
|
||||
- Admin + API : http://admin.localhost:8080
|
||||
|
||||
Services accessibles apres `make init` :
|
||||
- Borne : la valeur de `TRAEFIK_DOMAIN_KIOSK` dans `.env`
|
||||
- Admin + API : la valeur de `TRAEFIK_DOMAIN_ADMIN` dans `.env`
|
||||
`*.localhost` resout vers `127.0.0.1` nativement ; changer le port via `HTTP_PORT`
|
||||
dans `.env`. Le `.env.example` fonctionne tel quel en local (valeurs dev).
|
||||
|
||||
Liste complete des cibles : `make help`.
|
||||
Docker non installe ? Voir https://docs.docker.com/engine/install/
|
||||
|
||||
### Installation Docker sur un hote neuf (Debian / Ubuntu)
|
||||
### Deploiement prod (derriere un reverse proxy Traefik)
|
||||
|
||||
Procedure officielle detaillee : `https://docs.docker.com/engine/install/` (selectionner la distribution). Resume pour Debian stable :
|
||||
Le repo ne ship que `docker-compose.yml` (standalone). En production derriere un
|
||||
reverse proxy, chaque hote maintient son **propre `docker-compose.prod.yml`**
|
||||
(gitignore, hors repo, comme `.env`) : meme stack, mais exposee via Traefik (reseau
|
||||
externe + labels TLS) au lieu d'un port hote.
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y ca-certificates curl
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
|
||||
-o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
|
||||
https://download.docker.com/linux/debian \
|
||||
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io \
|
||||
docker-buildx-plugin docker-compose-plugin
|
||||
sudo usermod -aG docker $USER
|
||||
# Fermer et rouvrir la session pour activer le groupe docker
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Reseau externe du reverse proxy
|
||||
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).
|
||||
|
||||
Le `docker-compose.yml` attend un reseau Docker externe deja existant sur l'hote, dont le nom est donne par la variable `REVERSE_PROXY_NETWORK` (defaut : `admin_proxy`).
|
||||
|
||||
Si vous avez deja un Traefik en place, ce reseau a generalement ete cree par son propre stack. Adaptez la variable `REVERSE_PROXY_NETWORK` dans votre `.env` au nom utilise par votre proxy. Sinon, creez-le manuellement :
|
||||
|
||||
```bash
|
||||
docker network create mon_reseau_proxy
|
||||
# puis dans .env :
|
||||
# REVERSE_PROXY_NETWORK=mon_reseau_proxy
|
||||
```
|
||||
|
||||
Avant le premier `make init`, s'assurer que le reseau existe. Verification rapide :
|
||||
|
||||
```bash
|
||||
docker network inspect "$(grep ^REVERSE_PROXY_NETWORK .env | cut -d= -f2)"
|
||||
```
|
||||
|
||||
Si la commande retourne une erreur, soit adapter `REVERSE_PROXY_NETWORK` au nom du reseau utilise par votre proxy, soit creer le reseau manuellement. La cible `make init` echoue proprement avec un message d'aide si le reseau est introuvable.
|
||||
|
||||
*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`.*
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -170,37 +134,34 @@ Si la commande retourne une erreur, soit adapter `REVERSE_PROXY_NETWORK` au nom
|
|||
```
|
||||
.
|
||||
|-- .claude/ # Methodologie BYAN (visible jury : CLAUDE.md + rules/)
|
||||
|-- .github/
|
||||
| `-- workflows/ # CI/CD GitHub Actions [a venir]
|
||||
|-- .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
|
||||
|-- Makefile
|
||||
|-- 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
|
||||
```
|
||||
|
||||
|
|
@ -212,22 +173,35 @@ Si la commande retourne une erreur, soit adapter `REVERSE_PROXY_NETWORK` au nom
|
|||
|
||||
- **Commits** : Conventional Commits en anglais (`feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `db`, `perf`, `style`). Format : `type(scope): description`. Voir `docs/PROJECT_CONTEXT.md` section 9.
|
||||
- **Branches** : `feat/*`, `fix/*`, `refactor/*`, `docs/*`, `ci/*`, `db/*`, `chore/*`, `test/*` depuis `dev`. Merge vers `dev` par PR squashee. Periodiquement `dev` -> `main` par PR avec tag semver.
|
||||
- `main` et `dev` sont proteges cote GitHub (PR requise, force push bloque, resolution des conversations requise).
|
||||
- `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
|
||||
|
||||
*Section a completer. Strategie cible : CI GitHub Actions sur PR vers `dev` (lint + PHPUnit), CD automatique sur merge vers `main` via SSH + `make rebuild`, 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` (recupere `main` depuis Forgejo puis `docker compose build --pull && up -d` ; les images sont buildees localement depuis les Dockerfiles, le one-shot `wakdo-migrate` applique migrations + seed). L'automatisation visee est pull-based (un job cron cote hote detectant un nouveau `main`), a armer ensuite. Voir `docs/PROJECT_CONTEXT.md` section 7 Bloc 5.*
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -237,7 +211,8 @@ Si la commande retourne une erreur, soit adapter `REVERSE_PROXY_NETWORK` au nom
|
|||
|---|---|
|
||||
| `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 |
|
||||
|
||||
|
|
|
|||
55
SECURITY.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Politique de securite - Wakdo
|
||||
|
||||
Wakdo est un projet de fin de formation (RNCP 37805) construit en
|
||||
**security-by-design** : la menace est modelisee avant le code. Ce document
|
||||
resume la posture, le signalement de vulnerabilites et les garde-fous CI.
|
||||
|
||||
## Modele de menace
|
||||
|
||||
Le modele STRIDE complet, le registre des risques et la classification des
|
||||
donnees (4 niveaux) vivent dans `docs/PROJECT_CONTEXT.md` section 19, et le flux
|
||||
d'authentification durci dans `docs/uml/security-sequence.md`.
|
||||
|
||||
## Mesures en place (resume)
|
||||
|
||||
| Domaine | Mesure |
|
||||
|---|---|
|
||||
| Mots de passe | `password_hash` argon2id (cout configurable, defauts OWASP) |
|
||||
| Actions sensibles | PIN equipier hashe argon2id (`pin_hash`) |
|
||||
| Brute-force | double throttle : compteur par compte (`user`) + par IP (`login_throttle`), backoff degressif |
|
||||
| Sessions | cookies `HttpOnly` + `Secure` + `SameSite=Strict`, regeneration d'ID a la connexion (anti-fixation), idle 4h / absolu 10h |
|
||||
| Injection | PDO prepared statements exclusivement |
|
||||
| Upload | validation MIME + taille, stockage hors webroot |
|
||||
| En-tetes / PHP | `expose_php=Off`, `allow_url_fopen/include=Off`, `cgi.fix_pathinfo=0`, fonctions d'execution systeme desactivees |
|
||||
| RGPD | retention limitee (audit ~12 mois, throttle 24h, commandes ~3 ans), droit consultation/modif/suppression |
|
||||
| Secrets | `.env` gitignore, tenu hors de `.git/config` (credential helper lisant `.env`), secret-scan gitleaks en CI |
|
||||
|
||||
Les seuils operationnels (couts argon2, lockout, throttle, retention) sont
|
||||
documentes dans `.env.example`.
|
||||
|
||||
## Garde-fous CI (Forgejo Actions)
|
||||
|
||||
Chaque PR vers `dev` ou `main` declenche `.forgejo/workflows/ci.yml` :
|
||||
|
||||
- **secret-scan** (gitleaks) : empeche un secret d'entrer dans l'historique
|
||||
- **php-lint** : `php -l` sur tous les fichiers PHP
|
||||
- **static-tests** : PHPStan + PHPUnit (s'activent quand le code PHP arrive en P2)
|
||||
|
||||
La strategie de merge est **PR + auto-merge sur CI verte** (travail solo) : la
|
||||
PR est obligatoire (trace de gouvernance), le merge se declenche automatiquement
|
||||
une fois les checks au vert. Voir `scripts/forgejo-pr-automerge.sh` et
|
||||
`scripts/forgejo-branch-protection.sh`.
|
||||
|
||||
## Signaler une vulnerabilite
|
||||
|
||||
Projet pedagogique non destine a la production publique. Pour signaler un
|
||||
probleme de securite : ouvrir une issue sur le depot Forgejo
|
||||
(`https://git.acadenice.com/AcadeNice/corentin_wakdo`) ou contacter l'auteur.
|
||||
Merci de ne pas divulguer publiquement un detail exploitable avant correction.
|
||||
|
||||
## Perimetre
|
||||
|
||||
Couvert : authentification, autorisation (RBAC), gestion de session, validation
|
||||
d'entree, integrite des donnees de commande, hygiene des secrets.
|
||||
Hors perimetre : paiement reel (remplace par numero de commande), durcissement
|
||||
OS de l'hote, securite physique de la borne.
|
||||
38
db/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Base de donnees - migrations & seeds
|
||||
|
||||
Transcription executable du MLD (`docs/merise/mld.md`, 21 tables) vers MariaDB 11.4.
|
||||
|
||||
## Arborescence
|
||||
|
||||
```
|
||||
db/
|
||||
migrations/ migrations SQL versionnees, appliquees dans l'ordre lexicographique
|
||||
0001_init_schema.sql schema initial : 21 tables, FK, CHECK, index (InnoDB, utf8mb4)
|
||||
seeds/ donnees de demonstration (a venir : roles/permissions, allergenes, catalogue)
|
||||
migrate.sh runner de migrations (idempotent)
|
||||
```
|
||||
|
||||
## Appliquer les migrations
|
||||
|
||||
```bash
|
||||
bash db/migrate.sh # applique les migrations en attente
|
||||
bash db/migrate.sh --status # liste l'etat sans rien appliquer
|
||||
```
|
||||
|
||||
Le runner cible le conteneur `wakdo-db` et lit les identifiants dans `.env`
|
||||
(`DB_NAME`, `DB_ROOT_PASSWORD`). Il maintient une table `schema_migrations`
|
||||
(une ligne par fichier applique) : relancer ne rejoue que les nouvelles
|
||||
migrations. La cible `bash db/migrate.sh` est destinee a appeler ce script.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Une migration = un fichier `NNNN_description.sql`. Un fichier deja applique en
|
||||
commun n'est plus edite : on ajoute une nouvelle migration pour corriger.
|
||||
- Pas de `CREATE DATABASE` / `USE` dans les fichiers : la base cible est choisie
|
||||
par le runner.
|
||||
- Le schema suit le MLD v0.2 a la lettre : montants en centimes (INT UNSIGNED),
|
||||
`vat_rate` en pour-mille, `service_day` NON materialise (calcule applicatif,
|
||||
decision D6), stock signe (survente), journaux append-only (`stock_movement`,
|
||||
`audit_log`).
|
||||
- Verification : le DDL a ete applique sur une instance MariaDB 11.4 reelle
|
||||
(21 tables, 28 FK, 22 CHECK) sans erreur avant integration.
|
||||
25
db/init/10-scope-app-user.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Wakdo - durcissement du privilege du user applicatif (moindre privilege).
|
||||
#
|
||||
# L'image mariadb cree MARIADB_USER avec GRANT ALL PRIVILEGES sur la base
|
||||
# MARIADB_DATABASE. C'est trop large : le code applicatif expose (back-office)
|
||||
# n'a besoin que de DML, jamais de DDL (CREATE/ALTER/DROP), de GRANT OPTION ni
|
||||
# de DROP. Les migrations tournent separement en root (db/migrate.sh).
|
||||
#
|
||||
# Ce script s'execute UNIQUEMENT au premier demarrage sur volume vierge
|
||||
# (/docker-entrypoint-initdb.d). Pour une base deja initialisee, appliquer le
|
||||
# meme REVOKE/GRANT manuellement en root (voir db/init/README ou la PR).
|
||||
#
|
||||
# Set retenu : DML (SELECT/INSERT/UPDATE/DELETE) + ce dont mysqldump peut avoir
|
||||
# besoin (SHOW VIEW, TRIGGER, LOCK TABLES). Pas de DDL, pas de GRANT, pas de DROP.
|
||||
set -euo pipefail
|
||||
|
||||
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" <<-EOSQL
|
||||
REVOKE ALL PRIVILEGES ON \`${MARIADB_DATABASE}\`.* FROM '${MARIADB_USER}'@'%';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE, SHOW VIEW, TRIGGER, LOCK TABLES
|
||||
ON \`${MARIADB_DATABASE}\`.* TO '${MARIADB_USER}'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOSQL
|
||||
|
||||
echo "[init] privilege du user '${MARIADB_USER}' restreint au moindre privilege sur '${MARIADB_DATABASE}'."
|
||||
66
db/migrate-container.sh
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Wakdo - runner migrations + seed IN-CONTAINER (service compose one-shot wakdo-migrate).
|
||||
#
|
||||
# Applique, dans l'ordre lexicographique et de maniere IDEMPOTENTE :
|
||||
# 1. db/migrations/*.sql (suivi : table schema_migrations)
|
||||
# 2. db/seeds/*.sql (suivi : table seeds_applied)
|
||||
# Relancer ne rejoue que les fichiers en attente (tracking par nom de fichier).
|
||||
#
|
||||
# Contrairement a db/migrate.sh (hote, via `docker exec`), ce runner tourne DANS
|
||||
# un conteneur et se connecte a la base PAR LE RESEAU compose (DB_HOST). Il est
|
||||
# lance par le service `wakdo-migrate` apres que `wakdo-db` soit healthy ; les
|
||||
# services applicatifs (app/web) attendent sa COMPLETION (service_completed_successfully).
|
||||
#
|
||||
# But : `docker compose up` amene une stack COMPLETE et utilisable (schema + donnees
|
||||
# de reference, dont l'admin bootstrap) en une seule commande, sans dependance a
|
||||
# l'hote (Cr 7.c.4) -> remplace `make init`.
|
||||
#
|
||||
# Variables injectees par docker-compose : DB_HOST, DB_PORT, DB_NAME, DB_ROOT_PASSWORD.
|
||||
# Root requis : migrations = DDL, seeds = INSERT de reference.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
: "${DB_HOST:?DB_HOST manquant}"
|
||||
: "${DB_NAME:?DB_NAME manquant}"
|
||||
: "${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD manquant}"
|
||||
PORT="${DB_PORT:-3306}"
|
||||
|
||||
db() { mariadb -h "$DB_HOST" -P "$PORT" -uroot -p"$DB_ROOT_PASSWORD" "$@"; }
|
||||
|
||||
# Applique les *.sql d'un dossier non encore enregistres dans sa table de suivi.
|
||||
apply_tracked() {
|
||||
local dir="$1" table="$2"
|
||||
local f base n applied=0
|
||||
|
||||
db "$DB_NAME" -e "CREATE TABLE IF NOT EXISTS ${table} (
|
||||
filename VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
|
||||
|
||||
shopt -s nullglob
|
||||
local files=("$dir"/*.sql)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "[${table}] aucun fichier dans ${dir}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for f in "${files[@]}"; do
|
||||
base="$(basename "$f")"
|
||||
n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM ${table} WHERE filename='${base}';")"
|
||||
if [ "$n" = "0" ]; then
|
||||
echo "[${table}] application de ${base} ..."
|
||||
db "$DB_NAME" < "$f"
|
||||
db "$DB_NAME" -e "INSERT INTO ${table} (filename) VALUES ('${base}');"
|
||||
applied=$((applied + 1))
|
||||
else
|
||||
echo "[${table}] ${base} deja applique, ignore"
|
||||
fi
|
||||
done
|
||||
echo "[${table}] termine (${applied} nouveau(x))."
|
||||
}
|
||||
|
||||
echo "[migrate] cible ${DB_HOST}:${PORT}/${DB_NAME}"
|
||||
apply_tracked /db/migrations schema_migrations
|
||||
apply_tracked /db/seeds seeds_applied
|
||||
echo "[migrate] stack a jour (schema + donnees de reference)."
|
||||
70
db/migrate.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Wakdo - migration runner.
|
||||
#
|
||||
# Applique les fichiers db/migrations/*.sql dans l'ordre lexicographique,
|
||||
# de maniere idempotente : une table schema_migrations enregistre les fichiers
|
||||
# deja appliques, donc relancer ne rejoue que les nouvelles migrations.
|
||||
#
|
||||
# Cible : le service docker-compose `wakdo-db` (MariaDB). Lance depuis l'hote
|
||||
# (usage manuel / `--status`, identifiants lus dans .env). Au boot de la stack,
|
||||
# c'est le service `wakdo-migrate` (db/migrate-container.sh, via le reseau) qui
|
||||
# applique migrations + seed automatiquement.
|
||||
#
|
||||
# Usage :
|
||||
# bash db/migrate.sh # applique les migrations en attente
|
||||
# bash db/migrate.sh --status # liste l'etat sans rien appliquer
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ENV_FILE="$ROOT/.env"
|
||||
CONTAINER="${WAKDO_DB_CONTAINER:-wakdo-db}"
|
||||
MIGRATIONS_DIR="$ROOT/db/migrations"
|
||||
|
||||
[ -f "$ENV_FILE" ] || { echo "ERREUR : .env introuvable ($ENV_FILE)" >&2; exit 1; }
|
||||
DB_NAME="$(grep -E '^DB_NAME=' "$ENV_FILE" | cut -d= -f2- | tr -d '[:space:]')"
|
||||
DB_ROOT_PASSWORD="$(grep -E '^DB_ROOT_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)"
|
||||
: "${DB_NAME:?DB_NAME absent de .env}"
|
||||
: "${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD absent de .env}"
|
||||
|
||||
# Client mariadb dans le conteneur (root : les migrations sont des operations DDL).
|
||||
db() { docker exec -i "$CONTAINER" mariadb -uroot -p"$DB_ROOT_PASSWORD" "$@"; }
|
||||
|
||||
# Le conteneur doit etre en marche.
|
||||
docker exec "$CONTAINER" true 2>/dev/null || { echo "ERREUR : conteneur $CONTAINER non demarre (docker compose up -d)" >&2; exit 1; }
|
||||
|
||||
# Journal des migrations appliquees.
|
||||
db "$DB_NAME" -e "CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
filename VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
|
||||
|
||||
shopt -s nullglob
|
||||
files=("$MIGRATIONS_DIR"/*.sql)
|
||||
[ ${#files[@]} -gt 0 ] || { echo "[migrate] aucune migration dans $MIGRATIONS_DIR"; exit 0; }
|
||||
|
||||
if [ "${1:-}" = "--status" ]; then
|
||||
echo "[migrate] etat des migrations (base $DB_NAME) :"
|
||||
for f in "${files[@]}"; do
|
||||
base="$(basename "$f")"
|
||||
n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM schema_migrations WHERE filename='$base';")"
|
||||
[ "$n" = "0" ] && echo " PENDING $base" || echo " applied $base"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
applied=0
|
||||
for f in "${files[@]}"; do
|
||||
base="$(basename "$f")"
|
||||
n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM schema_migrations WHERE filename='$base';")"
|
||||
if [ "$n" = "0" ]; then
|
||||
echo "[migrate] application de $base ..."
|
||||
db "$DB_NAME" < "$f"
|
||||
db "$DB_NAME" -e "INSERT INTO schema_migrations (filename) VALUES ('$base');"
|
||||
applied=$((applied + 1))
|
||||
else
|
||||
echo "[migrate] $base deja applique, ignore"
|
||||
fi
|
||||
done
|
||||
echo "[migrate] termine ($applied nouvelle(s) migration(s) appliquee(s))."
|
||||
465
db/migrations/0001_init_schema.sql
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Initial schema (DDL)
|
||||
-- =============================================================================
|
||||
-- Purpose : Create the 21-table relational schema for the Wakdo fast-food
|
||||
-- ordering system (catalogue, ingredients/stock, orders, RBAC,
|
||||
-- security-by-design layer).
|
||||
-- Source : docs/merise/mld.md (MLD v0.2 — prod-like, 21 tables) +
|
||||
-- docs/merise/dictionary.md (data dictionary v0.2, types source of truth).
|
||||
-- Phase : P2 — generated from the validated Logical Data Model (P1 conception).
|
||||
-- Target : MariaDB 11.4 LTS, engine InnoDB, charset utf8mb4, collation
|
||||
-- utf8mb4_unicode_ci.
|
||||
--
|
||||
-- Notes derived from the MLD:
|
||||
-- - All technical PKs are INT UNSIGNED AUTO_INCREMENT.
|
||||
-- - Monetary amounts are INT UNSIGNED in cents (anti-FLOAT, dict. note 1).
|
||||
-- - vat_rate stored per-mille (55 = 5.5%, 100 = 10%).
|
||||
-- - service_day is NOT a stored/generated column (decision D6): computed in
|
||||
-- the application layer.
|
||||
-- - No CREATE DATABASE / USE here: the target DB is chosen by the runner.
|
||||
-- - No seed / INSERT data here (see db/seeds/0001_demo_data.sql).
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
SET @OLD_SQL_MODE = @@SQL_MODE;
|
||||
SET SQL_MODE = 'STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,NO_AUTO_VALUE_ON_ZERO';
|
||||
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.1 category — root table for the Catalogue sub-domain (no FK)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE category (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(60) NOT NULL,
|
||||
slug VARCHAR(60) NOT NULL,
|
||||
image_path VARCHAR(255) NULL,
|
||||
display_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_category_name (name),
|
||||
UNIQUE KEY uk_category_slug (slug)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.6 ingredient — root table for Ingredients & Stock (no FK)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE ingredient (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
unit VARCHAR(40) NOT NULL,
|
||||
stock_quantity INT NOT NULL DEFAULT 0,
|
||||
stock_capacity INT NOT NULL,
|
||||
pack_size SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
pack_label VARCHAR(80) NULL,
|
||||
low_stock_pct SMALLINT UNSIGNED NOT NULL DEFAULT 10,
|
||||
critical_stock_pct SMALLINT UNSIGNED NOT NULL DEFAULT 5,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ingredient_name (name),
|
||||
CONSTRAINT chk_ingredient_stock_capacity CHECK (stock_capacity > 0),
|
||||
CONSTRAINT chk_ingredient_pack_size CHECK (pack_size > 0),
|
||||
CONSTRAINT chk_ingredient_low_stock_pct CHECK (low_stock_pct BETWEEN 0 AND 100),
|
||||
CONSTRAINT chk_ingredient_critical_stock_pct CHECK (critical_stock_pct BETWEEN 0 AND 100),
|
||||
CONSTRAINT chk_ingredient_critical_lt_low CHECK (critical_stock_pct < low_stock_pct)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.8 allergen — reference table (INCO EU 1169/2011), no FK
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE allergen (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(30) NOT NULL,
|
||||
name VARCHAR(80) NOT NULL,
|
||||
description TEXT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_allergen_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.10 role — root table for RBAC (no FK)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE role (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
label VARCHAR(80) NOT NULL,
|
||||
description TEXT NULL,
|
||||
default_route VARCHAR(120) NULL,
|
||||
order_source ENUM('kiosk','counter','drive') NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_role_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.13 permission — reference table, catalogue frozen at 23 codes (no FK)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE permission (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(60) NOT NULL,
|
||||
label VARCHAR(120) NOT NULL,
|
||||
description TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_permission_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.21 login_throttle — per-source-IP brute-force throttle (no FK)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE login_throttle (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
failed_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
window_started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
lockout_until DATETIME NULL,
|
||||
last_attempt_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_login_throttle_ip_address (ip_address),
|
||||
KEY idx_login_throttle_lockout_until (lockout_until)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.2 product — depends on category
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE product (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
category_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
description TEXT NULL,
|
||||
price_cents INT UNSIGNED NOT NULL,
|
||||
vat_rate SMALLINT UNSIGNED NOT NULL DEFAULT 100,
|
||||
image_path VARCHAR(255) NULL,
|
||||
is_available TINYINT(1) NOT NULL DEFAULT 1,
|
||||
display_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_product_category_available_order (category_id, is_available, display_order),
|
||||
CONSTRAINT fk_product_category_id FOREIGN KEY (category_id)
|
||||
REFERENCES category (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_product_price_cents CHECK (price_cents > 0),
|
||||
CONSTRAINT chk_product_vat_rate CHECK (vat_rate IN (55, 100))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.3 menu — depends on category, product
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE menu (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
category_id INT UNSIGNED NOT NULL,
|
||||
burger_product_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
description TEXT NULL,
|
||||
price_normal_cents INT UNSIGNED NOT NULL,
|
||||
price_maxi_cents INT UNSIGNED NOT NULL,
|
||||
image_path VARCHAR(255) NULL,
|
||||
is_available TINYINT(1) NOT NULL DEFAULT 1,
|
||||
display_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_menu_category_available_order (category_id, is_available, display_order),
|
||||
CONSTRAINT fk_menu_category_id FOREIGN KEY (category_id)
|
||||
REFERENCES category (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_menu_burger_product_id FOREIGN KEY (burger_product_id)
|
||||
REFERENCES product (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_menu_price_normal_cents CHECK (price_normal_cents > 0),
|
||||
CONSTRAINT chk_menu_price_maxi_cents CHECK (price_maxi_cents > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.4 menu_slot — depends on menu (no audit fields)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE menu_slot (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
menu_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(80) NOT NULL,
|
||||
slot_type ENUM('drink','side','sauce','dessert','extra') NOT NULL,
|
||||
is_required TINYINT(1) NOT NULL DEFAULT 1,
|
||||
display_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_menu_slot_menu_order (menu_id, display_order),
|
||||
CONSTRAINT fk_menu_slot_menu_id FOREIGN KEY (menu_id)
|
||||
REFERENCES menu (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.5 menu_slot_option — pure join table, composite PK
|
||||
-- depends on menu_slot, product
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE menu_slot_option (
|
||||
menu_slot_id INT UNSIGNED NOT NULL,
|
||||
product_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (menu_slot_id, product_id),
|
||||
KEY idx_menu_slot_option_product_id (product_id),
|
||||
CONSTRAINT fk_menu_slot_option_menu_slot_id FOREIGN KEY (menu_slot_id)
|
||||
REFERENCES menu_slot (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_menu_slot_option_product_id FOREIGN KEY (product_id)
|
||||
REFERENCES product (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.7 product_ingredient — join table with attributes, composite PK
|
||||
-- depends on product, ingredient
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE product_ingredient (
|
||||
product_id INT UNSIGNED NOT NULL,
|
||||
ingredient_id INT UNSIGNED NOT NULL,
|
||||
quantity_normal SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
quantity_maxi SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
is_removable TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_addable TINYINT(1) NOT NULL DEFAULT 0,
|
||||
extra_price_cents INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (product_id, ingredient_id),
|
||||
KEY idx_product_ingredient_ingredient_id (ingredient_id),
|
||||
CONSTRAINT fk_product_ingredient_product_id FOREIGN KEY (product_id)
|
||||
REFERENCES product (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_product_ingredient_ingredient_id FOREIGN KEY (ingredient_id)
|
||||
REFERENCES ingredient (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_product_ingredient_quantity_normal CHECK (quantity_normal > 0),
|
||||
CONSTRAINT chk_product_ingredient_quantity_maxi CHECK (quantity_maxi >= quantity_normal),
|
||||
CONSTRAINT chk_product_ingredient_extra_price CHECK (extra_price_cents >= 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.9 ingredient_allergen — pure join table, composite PK
|
||||
-- depends on ingredient, allergen
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE ingredient_allergen (
|
||||
ingredient_id INT UNSIGNED NOT NULL,
|
||||
allergen_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (ingredient_id, allergen_id),
|
||||
KEY idx_ingredient_allergen_allergen_id (allergen_id),
|
||||
CONSTRAINT fk_ingredient_allergen_ingredient_id FOREIGN KEY (ingredient_id)
|
||||
REFERENCES ingredient (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ingredient_allergen_allergen_id FOREIGN KEY (allergen_id)
|
||||
REFERENCES allergen (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.11 user — depends on role
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE user (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
email VARCHAR(254) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
pin_hash VARCHAR(255) NULL,
|
||||
first_name VARCHAR(60) NOT NULL,
|
||||
last_name VARCHAR(60) NOT NULL,
|
||||
role_id INT UNSIGNED NOT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
last_login_at DATETIME NULL,
|
||||
failed_login_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_failed_login_at DATETIME NULL,
|
||||
lockout_until DATETIME NULL,
|
||||
password_reset_token_hash VARCHAR(255) NULL,
|
||||
password_reset_expires_at DATETIME NULL,
|
||||
anonymized_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_user_email (email),
|
||||
KEY idx_user_active_role (is_active, role_id),
|
||||
CONSTRAINT fk_user_role_id FOREIGN KEY (role_id)
|
||||
REFERENCES role (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.12 role_visible_source — pure join table, composite PK
|
||||
-- depends on role
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE role_visible_source (
|
||||
role_id INT UNSIGNED NOT NULL,
|
||||
source ENUM('kiosk','counter','drive') NOT NULL,
|
||||
PRIMARY KEY (role_id, source),
|
||||
CONSTRAINT fk_role_visible_source_role_id FOREIGN KEY (role_id)
|
||||
REFERENCES role (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.14 role_permission — pure join table, composite PK
|
||||
-- depends on role, permission
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE role_permission (
|
||||
role_id INT UNSIGNED NOT NULL,
|
||||
permission_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
KEY idx_role_permission_permission_id (permission_id),
|
||||
CONSTRAINT fk_role_permission_role_id FOREIGN KEY (role_id)
|
||||
REFERENCES role (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_role_permission_permission_id FOREIGN KEY (permission_id)
|
||||
REFERENCES permission (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.15 customer_order — depends on user (acting_user_id)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE customer_order (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_number VARCHAR(20) NOT NULL,
|
||||
idempotency_key VARCHAR(36) NULL,
|
||||
source ENUM('kiosk','counter','drive') NOT NULL,
|
||||
acting_user_id INT UNSIGNED NULL,
|
||||
service_mode ENUM('dine_in','takeaway','drive') NOT NULL,
|
||||
status ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment',
|
||||
total_ht_cents INT UNSIGNED NOT NULL,
|
||||
total_vat_cents INT UNSIGNED NOT NULL,
|
||||
total_ttc_cents INT UNSIGNED NOT NULL,
|
||||
paid_at DATETIME NULL,
|
||||
delivered_at DATETIME NULL,
|
||||
cancelled_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customer_order_order_number (order_number),
|
||||
UNIQUE KEY uk_customer_order_idempotency_key (idempotency_key),
|
||||
KEY idx_customer_order_status_created (status, created_at),
|
||||
KEY idx_customer_order_source_created (source, created_at),
|
||||
KEY idx_customer_order_created (created_at),
|
||||
CONSTRAINT fk_customer_order_acting_user_id FOREIGN KEY (acting_user_id)
|
||||
REFERENCES user (id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_customer_order_total_ht CHECK (total_ht_cents >= 0),
|
||||
CONSTRAINT chk_customer_order_total_vat CHECK (total_vat_cents >= 0),
|
||||
CONSTRAINT chk_customer_order_total_ttc CHECK (total_ttc_cents > 0),
|
||||
CONSTRAINT chk_customer_order_total_coherent CHECK (total_ttc_cents = total_ht_cents + total_vat_cents),
|
||||
CONSTRAINT chk_customer_order_drive_mode CHECK (source <> 'drive' OR service_mode = 'drive')
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.16 order_item — depends on customer_order, product, menu
|
||||
-- polymorphic line (product XOR menu)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE order_item (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_id INT UNSIGNED NOT NULL,
|
||||
item_type ENUM('product','menu') NOT NULL,
|
||||
product_id INT UNSIGNED NULL,
|
||||
menu_id INT UNSIGNED NULL,
|
||||
format ENUM('normal','maxi') NOT NULL DEFAULT 'normal',
|
||||
label_snapshot VARCHAR(120) NOT NULL,
|
||||
unit_price_cents_snapshot INT UNSIGNED NOT NULL,
|
||||
vat_rate_snapshot SMALLINT UNSIGNED NOT NULL,
|
||||
quantity SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order_item_order_id (order_id),
|
||||
KEY idx_order_item_product_id (product_id),
|
||||
KEY idx_order_item_menu_id (menu_id),
|
||||
CONSTRAINT fk_order_item_order_id FOREIGN KEY (order_id)
|
||||
REFERENCES customer_order (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_order_item_product_id FOREIGN KEY (product_id)
|
||||
REFERENCES product (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_order_item_menu_id FOREIGN KEY (menu_id)
|
||||
REFERENCES menu (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_order_item_unit_price CHECK (unit_price_cents_snapshot > 0),
|
||||
CONSTRAINT chk_order_item_vat_rate CHECK (vat_rate_snapshot IN (55, 100)),
|
||||
CONSTRAINT chk_order_item_quantity CHECK (quantity > 0),
|
||||
CONSTRAINT chk_order_item_polymorphism CHECK (
|
||||
(item_type = 'product' AND product_id IS NOT NULL AND menu_id IS NULL)
|
||||
OR (item_type = 'menu' AND menu_id IS NOT NULL AND product_id IS NULL)
|
||||
)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.17 order_item_selection — depends on order_item, menu_slot, product
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE order_item_selection (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_item_id INT UNSIGNED NOT NULL,
|
||||
menu_slot_id INT UNSIGNED NOT NULL,
|
||||
product_id INT UNSIGNED NOT NULL,
|
||||
label_snapshot VARCHAR(120) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order_item_selection_order_item_id (order_item_id),
|
||||
KEY idx_order_item_selection_menu_slot_id (menu_slot_id),
|
||||
KEY idx_order_item_selection_product_id (product_id),
|
||||
CONSTRAINT fk_order_item_selection_order_item_id FOREIGN KEY (order_item_id)
|
||||
REFERENCES order_item (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_order_item_selection_menu_slot_id FOREIGN KEY (menu_slot_id)
|
||||
REFERENCES menu_slot (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_order_item_selection_product_id FOREIGN KEY (product_id)
|
||||
REFERENCES product (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.18 order_item_modifier — depends on order_item, ingredient
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE order_item_modifier (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_item_id INT UNSIGNED NOT NULL,
|
||||
ingredient_id INT UNSIGNED NOT NULL,
|
||||
action ENUM('remove','add') NOT NULL,
|
||||
extra_price_cents INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order_item_modifier_order_item_id (order_item_id),
|
||||
KEY idx_order_item_modifier_ingredient_id (ingredient_id),
|
||||
CONSTRAINT fk_order_item_modifier_order_item_id FOREIGN KEY (order_item_id)
|
||||
REFERENCES order_item (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_order_item_modifier_ingredient_id FOREIGN KEY (ingredient_id)
|
||||
REFERENCES ingredient (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_order_item_modifier_extra_price CHECK (extra_price_cents >= 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.19 stock_movement — append-only audit log
|
||||
-- depends on ingredient, customer_order, user
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE stock_movement (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
ingredient_id INT UNSIGNED NOT NULL,
|
||||
movement_type ENUM('sale','cancellation','restock','inventory_correction') NOT NULL,
|
||||
delta INT NOT NULL,
|
||||
order_id INT UNSIGNED NULL,
|
||||
user_id INT UNSIGNED NULL,
|
||||
note VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_stock_movement_ingredient_created (ingredient_id, created_at),
|
||||
KEY idx_stock_movement_type_created (movement_type, created_at),
|
||||
KEY idx_stock_movement_order_id (order_id),
|
||||
KEY idx_stock_movement_user_id (user_id),
|
||||
CONSTRAINT fk_stock_movement_ingredient_id FOREIGN KEY (ingredient_id)
|
||||
REFERENCES ingredient (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_stock_movement_order_id FOREIGN KEY (order_id)
|
||||
REFERENCES customer_order (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_stock_movement_user_id FOREIGN KEY (user_id)
|
||||
REFERENCES user (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4.20 audit_log — append-only sensitive-action log
|
||||
-- depends on user, role
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE audit_log (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
actor_user_id INT UNSIGNED NULL,
|
||||
actor_role_id INT UNSIGNED NULL,
|
||||
action_code VARCHAR(60) NOT NULL,
|
||||
entity_type VARCHAR(40) NULL,
|
||||
entity_id INT UNSIGNED NULL,
|
||||
summary VARCHAR(255) NULL,
|
||||
details JSON NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_audit_log_actor_created (actor_user_id, created_at),
|
||||
KEY idx_audit_log_entity (entity_type, entity_id),
|
||||
KEY idx_audit_log_action_created (action_code, created_at),
|
||||
KEY idx_audit_log_actor_role_id (actor_role_id),
|
||||
CONSTRAINT fk_audit_log_actor_user_id FOREIGN KEY (actor_user_id)
|
||||
REFERENCES user (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_audit_log_actor_role_id FOREIGN KEY (actor_role_id)
|
||||
REFERENCES role (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- =============================================================================
|
||||
-- Restore session settings
|
||||
-- =============================================================================
|
||||
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
|
||||
SET SQL_MODE = @OLD_SQL_MODE;
|
||||
39
db/migrations/0002_pin_throttle.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
-- db/migrations/0002_pin_throttle.sql
|
||||
-- =============================================================================
|
||||
-- Wakdo - Migration 0002 : pin_throttle (entite 22, RG-T22)
|
||||
-- =============================================================================
|
||||
-- Purpose : Throttle des tentatives de PIN d'action sensible, par UTILISATEUR
|
||||
-- AGISSANT (identite de session authentifiee, GuardResult->userId).
|
||||
-- STRICTEMENT SEPARE des compteurs de connexion
|
||||
-- (user.failed_login_attempts / user.lockout_until / login_throttle)
|
||||
-- pour qu'un echec de PIN ne verrouille jamais la CONNEXION d'un
|
||||
-- compte (pas d'escalade DoS sur la surface plus sensible). Sibling de
|
||||
-- login_throttle (4.21) : meme forme, dimension differente (l'acteur,
|
||||
-- pas l'IP). Le runner db/migrate.sh applique *.sql dans l'ordre
|
||||
-- lexicographique via la table schema_migrations.
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE pin_throttle (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
actor_user_id INT UNSIGNED NOT NULL,
|
||||
failed_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
window_started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
lockout_until DATETIME NULL,
|
||||
last_attempt_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pin_throttle_actor_user_id (actor_user_id),
|
||||
KEY idx_pin_throttle_lockout_until (lockout_until),
|
||||
CONSTRAINT fk_pin_throttle_actor_user_id FOREIGN KEY (actor_user_id)
|
||||
REFERENCES user (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Note : pas de seed. La cle est l'acteur (un user back-office authentifie), donc
|
||||
-- la FK ON DELETE CASCADE est sure (contrairement a login_throttle, dont la cle
|
||||
-- est une IP arbitraire et qui n'a pas de FK). La purge cron des lignes sans
|
||||
-- verrou actif au-dela de THROTTLE_PURGE_AFTER_HOURS s'aligne sur login_throttle :
|
||||
-- DELETE FROM pin_throttle
|
||||
-- WHERE (lockout_until IS NULL OR lockout_until < NOW())
|
||||
-- AND last_attempt_at < NOW() - INTERVAL <THROTTLE_PURGE_AFTER_HOURS> HOUR;
|
||||
17
db/migrations/0003_order_service_tag.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- db/migrations/0003_order_service_tag.sql
|
||||
-- =============================================================================
|
||||
-- Wakdo - Migration 0003 : service_tag (numero de chevalet) sur customer_order
|
||||
-- =============================================================================
|
||||
-- Purpose : numero de chevalet pour le service EN SALLE (mode dine_in / sur place).
|
||||
-- Saisi a la borne quand le client choisit "sur place" ; permet au
|
||||
-- service d'apporter la commande a la bonne table (B4). NULL pour
|
||||
-- takeaway / drive. Colonne additive nullable (aucune donnee existante
|
||||
-- a retro-remplir). Le runner applique *.sql dans l'ordre lexicographique
|
||||
-- via schema_migrations.
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE customer_order
|
||||
ADD COLUMN service_tag VARCHAR(20) NULL AFTER service_mode;
|
||||
25
db/migrations/0005_ingredient_nutrition.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- db/migrations/0005_ingredient_nutrition.sql
|
||||
-- =============================================================================
|
||||
-- Wakdo - Migration 0005 : enrichissement nutritionnel depuis une API EXTERNE
|
||||
-- =============================================================================
|
||||
-- Purpose : ajoute a `ingredient` des colonnes nullables pour stocker des donnees
|
||||
-- nutritionnelles importees depuis une API TIERCE (OpenFoodFacts), a la
|
||||
-- demande d'un manager/admin (action explicite, pas au runtime borne).
|
||||
-- Demontre l'exploitation, DANS LE MODELE de donnees, d'informations
|
||||
-- externes provenant d'une API (Cr 3.a.3). Egress maitrise et opt-in :
|
||||
-- aucun appel automatique ; la passerelle (App\Catalogue\
|
||||
-- OpenFoodFactsGateway) est invoquee seulement par IngredientController::enrich.
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE ingredient
|
||||
ADD COLUMN energy_kcal_100g SMALLINT UNSIGNED NULL AFTER pack_label,
|
||||
ADD COLUMN nutrition_source VARCHAR(120) NULL AFTER energy_kcal_100g,
|
||||
ADD COLUMN nutrition_fetched_at DATETIME NULL AFTER nutrition_source;
|
||||
|
||||
-- energy_kcal_100g : apport energetique pour 100 g (SMALLINT UNSIGNED suffit ; les
|
||||
-- valeurs reelles restent < 1000). nutrition_source : provenance ("OpenFoodFacts").
|
||||
-- nutrition_fetched_at : horodatage de l'import, pour tracer la fraicheur. Toutes
|
||||
-- nullables : un ingredient non enrichi reste valide (donnee optionnelle).
|
||||
32
db/migrations/0006_product_maxi_variant.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- db/migrations/0006_product_maxi_variant.sql
|
||||
-- =============================================================================
|
||||
-- Wakdo - Migration 0006 : variante Maxi d'un produit (accompagnement de menu)
|
||||
-- =============================================================================
|
||||
-- Purpose : ajoute a `product` une auto-reference nullable vers la variante
|
||||
-- servie quand un menu est commande au format Maxi. L'accompagnement
|
||||
-- de menu (slot_type='side') propose la version standard (ex. Moyenne
|
||||
-- Frite, Potatoes) ; au format Maxi, le serveur substitue la variante
|
||||
-- Grande (Grande Frite / Grande Potatoes) sans choix supplementaire.
|
||||
-- Approche data-driven : la regle vit dans la donnee, pas dans le code,
|
||||
-- et le decrement de stock (consumption()) frappe alors le bon produit.
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE product
|
||||
ADD COLUMN maxi_variant_product_id INT UNSIGNED NULL AFTER price_cents,
|
||||
ADD CONSTRAINT fk_product_maxi_variant_product_id FOREIGN KEY (maxi_variant_product_id)
|
||||
REFERENCES product (id) ON DELETE SET NULL;
|
||||
|
||||
-- maxi_variant_product_id : produit servi a la place de celui-ci quand le menu est
|
||||
-- au format Maxi (ex. Moyenne Frite -> Grande Frite). Place AFTER price_cents :
|
||||
-- regroupe avec les attributs de commercialisation du produit. Nullable : la
|
||||
-- plupart des produits n'ont pas de variante Maxi (un produit sans variante reste
|
||||
-- valide et n'est jamais substitue).
|
||||
--
|
||||
-- ON DELETE SET NULL (et non RESTRICT) : si la variante Grande est supprimee du
|
||||
-- catalogue, le produit de base reste vendable, il perd seulement sa substitution
|
||||
-- Maxi (degradation gracieuse). RESTRICT bloquerait la suppression d'une Grande
|
||||
-- referencee, ce qui n'est pas souhaitable : la reference est un confort metier,
|
||||
-- pas une integrite forte de commande (les commandes figent deja leurs snapshots).
|
||||
53
db/migrations/0007_product_size_variant.sql
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
-- db/migrations/0007_product_size_variant.sql
|
||||
-- =============================================================================
|
||||
-- Wakdo - Migration 0007 : variante de TAILLE d'un produit (boisson 30/50 cl)
|
||||
-- =============================================================================
|
||||
-- Purpose : ajoute a `product` la dimension TAILLE des boissons fontaine (la
|
||||
-- maquette borne propose 30 cl / 50 cl), modelisee comme des LIGNES
|
||||
-- produit distinctes (meme approche que Moyenne/Grande Frite). Le
|
||||
-- domaine commande facture deja par product_id : le flux de commande
|
||||
-- reste inchange, la borne resout juste la taille choisie en product_id.
|
||||
--
|
||||
-- Grouping DEDIE, distinct de maxi_variant_product_id (migration 0006) :
|
||||
-- ce dernier pilote la substitution Maxi de l'accompagnement de menu
|
||||
-- (resolveSelections) ; le reutiliser ferait basculer en 50 cl une
|
||||
-- boisson 30 cl glissee dans un menu Maxi (effet de bord non voulu).
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Idempotence : meme garde information_schema que 0006 (re-jouable sans erreur).
|
||||
-- On verifie l'absence de la colonne `size_cl` avant l'ALTER ; les deux colonnes
|
||||
-- sont ajoutees ensemble, l'existence de l'une suffit donc a court-circuiter.
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'product' AND column_name = 'size_cl'
|
||||
);
|
||||
|
||||
SET @ddl := IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE product
|
||||
ADD COLUMN size_cl SMALLINT UNSIGNED NULL AFTER price_cents,
|
||||
ADD COLUMN base_product_id INT UNSIGNED NULL AFTER size_cl,
|
||||
ADD CONSTRAINT fk_product_base_product_id FOREIGN KEY (base_product_id)
|
||||
REFERENCES product (id) ON DELETE CASCADE',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- size_cl : volume en centilitres. NULL = le produit n'a pas de dimension taille
|
||||
-- (bouteilles, produits non-boissons). La ligne de base (30) ET la variante (50)
|
||||
-- portent toutes deux leur volume, pour que le picker affiche un libelle humain.
|
||||
--
|
||||
-- base_product_id : auto-reference vers la ligne de base. NULL = produit de base
|
||||
-- ou autonome (visible dans le catalogue) ; NON NULL = variante de taille du
|
||||
-- produit reference (masquee de la grille catalogue, atteinte via le picker).
|
||||
--
|
||||
-- ON DELETE CASCADE (et non SET NULL comme 0006) : une variante de taille n'a
|
||||
-- AUCUN sens sans sa base (une "Coca Cola 50cl" orpheline n'est pas commercialisable),
|
||||
-- alors que la substitution Maxi de 0006 est un confort optionnel survivant a la
|
||||
-- perte de sa cible. Supprimer la base emporte donc ses variantes de taille. Les
|
||||
-- commandes passees ne sont pas affectees (elles figent leurs snapshots, RG-T05).
|
||||
69
db/seed.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Wakdo - seed runner.
|
||||
#
|
||||
# Applique les fichiers db/seeds/*.sql dans l'ordre lexicographique, de maniere
|
||||
# idempotente : une table seed_history enregistre les fichiers deja charges.
|
||||
# Les seeds doivent etre joues APRES les migrations (les tables doivent exister).
|
||||
#
|
||||
# Cible : le service docker-compose `wakdo-db`. Identifiants lus dans .env.
|
||||
#
|
||||
# Usage :
|
||||
# bash db/seed.sh # charge les seeds en attente
|
||||
# bash db/seed.sh --status # liste l'etat sans rien charger
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ENV_FILE="$ROOT/.env"
|
||||
CONTAINER="${WAKDO_DB_CONTAINER:-wakdo-db}"
|
||||
SEEDS_DIR="$ROOT/db/seeds"
|
||||
|
||||
[ -f "$ENV_FILE" ] || { echo "ERREUR : .env introuvable ($ENV_FILE)" >&2; exit 1; }
|
||||
DB_NAME="$(grep -E '^DB_NAME=' "$ENV_FILE" | cut -d= -f2- | tr -d '[:space:]')"
|
||||
DB_ROOT_PASSWORD="$(grep -E '^DB_ROOT_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)"
|
||||
: "${DB_NAME:?DB_NAME absent de .env}"
|
||||
: "${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD absent de .env}"
|
||||
|
||||
db() { docker exec -i "$CONTAINER" mariadb -uroot -p"$DB_ROOT_PASSWORD" "$@"; }
|
||||
|
||||
docker exec "$CONTAINER" true 2>/dev/null || { echo "ERREUR : conteneur $CONTAINER non demarre (docker compose up -d)" >&2; exit 1; }
|
||||
|
||||
if [ ! -d "$SEEDS_DIR" ]; then
|
||||
echo "[seed] aucun repertoire db/seeds/ - rien a charger"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
db "$DB_NAME" -e "CREATE TABLE IF NOT EXISTS seed_history (
|
||||
filename VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
|
||||
|
||||
shopt -s nullglob
|
||||
files=("$SEEDS_DIR"/*.sql)
|
||||
[ ${#files[@]} -gt 0 ] || { echo "[seed] aucun fichier seed dans $SEEDS_DIR"; exit 0; }
|
||||
|
||||
if [ "${1:-}" = "--status" ]; then
|
||||
echo "[seed] etat des seeds (base $DB_NAME) :"
|
||||
for f in "${files[@]}"; do
|
||||
base="$(basename "$f")"
|
||||
n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seed_history WHERE filename='$base';")"
|
||||
[ "$n" = "0" ] && echo " PENDING $base" || echo " loaded $base"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
loaded=0
|
||||
for f in "${files[@]}"; do
|
||||
base="$(basename "$f")"
|
||||
n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seed_history WHERE filename='$base';")"
|
||||
if [ "$n" = "0" ]; then
|
||||
echo "[seed] chargement de $base ..."
|
||||
db "$DB_NAME" < "$f"
|
||||
db "$DB_NAME" -e "INSERT INTO seed_history (filename) VALUES ('$base');"
|
||||
loaded=$((loaded + 1))
|
||||
else
|
||||
echo "[seed] $base deja charge, ignore"
|
||||
fi
|
||||
done
|
||||
echo "[seed] termine ($loaded nouveau(x) seed(s) charge(s))."
|
||||
190
db/seeds/0001_rbac_and_reference.sql
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0001 : RBAC + reference data + admin user
|
||||
-- =============================================================================
|
||||
-- Purpose : Seed the foundational rows the back-office cannot boot without:
|
||||
-- the 5 RBAC roles, the frozen catalogue of 23 permissions, the
|
||||
-- default role/permission matrix, per-role visible order sources,
|
||||
-- the 14 EU INCO allergens, and a single bootstrap admin user.
|
||||
-- Source : docs/merise/dictionary.md (3.8 allergen, 3.15 role, 3.16
|
||||
-- role_visible_source, 3.17 permission catalogue + default grants,
|
||||
-- 3.18 role_permission), docs/merise/mct.md (operations 1-28),
|
||||
-- docs/PROJECT_CONTEXT.md section 7 (role responsibilities) and
|
||||
-- decision D5 (admin gets order.create / order.deliver ; manager
|
||||
-- does NOT get order.cancel).
|
||||
-- Phase : P2 — demo/reference seed, applied AFTER db/migrations/0001_init_schema.sql.
|
||||
-- Target : MariaDB 11.4 LTS. Fed by db/seed.sh into the already-selected DB.
|
||||
--
|
||||
-- Notes:
|
||||
-- - Statements are ordered so every FK resolves: role and permission first,
|
||||
-- then role_permission / role_visible_source, then user (FK -> role).
|
||||
-- - role_permission rows use subqueries on role.code and permission.code so
|
||||
-- no surrogate ids are hardcoded (robust to AUTO_INCREMENT gaps).
|
||||
-- - admin/manager get no role_visible_source rows: they have a global view of
|
||||
-- all sources (the absence of rows means "no source filter applied").
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. role (5) — dictionary.md 3.15
|
||||
-- order_source: counter/drive auto-tag their own source; admin/manager NULL
|
||||
-- (they may create on behalf of any channel); kitchen NULL (read-only on
|
||||
-- orders, never creates one).
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO role (code, label, description, default_route, order_source, is_active) VALUES
|
||||
('admin', 'Administrator', 'Full back-office access: complete catalogue CRUD (incl. deletes), user/role/permission (RBAC) management, stock, stats, order create/deliver/cancel.', '/admin/dashboard', NULL, 1),
|
||||
('manager', 'Manager', 'Catalogue create/update, ingredient and stock management (restock + inventory), statistics. No user/RBAC administration, no order cancellation.', '/admin/stats', NULL, 1),
|
||||
('kitchen', 'Kitchen Staff', 'Read-only kitchen display (KDS) of paid orders sorted by paid_at ascending, plus inventory counting. Performs no order status transition.', '/kitchen/display', NULL, 1),
|
||||
('counter', 'Counter Staff', 'Takes orders at the counter, delivers them to the customer, can cancel. Inventory counting. source auto-tagged as counter.', '/counter/orders', 'counter', 1),
|
||||
('drive', 'Drive Staff', 'Takes orders at the drive-thru (intercom + headset), delivers them, can cancel. Inventory counting. source auto-tagged as drive.', '/drive/orders', 'drive', 1);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. permission (23) — frozen catalogue, dictionary.md 3.17.
|
||||
-- code format <resource>.<action>. The catalogue is fixed at the seed and
|
||||
-- never created through the UI (only assigned to roles via MANAGE_RBAC).
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO permission (code, label, description) VALUES
|
||||
('product.create', 'Create product', 'Create a new catalogue product.'),
|
||||
('product.read', 'Read products', 'View products in the back-office and on order screens.'),
|
||||
('product.update', 'Update product', 'Edit an existing product (name, price, VAT, availability, etc.).'),
|
||||
('product.delete', 'Delete product', 'Permanently delete a product when no FK references block it.'),
|
||||
('menu.create', 'Create menu', 'Create a new menu with its slot configuration.'),
|
||||
('menu.read', 'Read menus', 'View menus, slots and slot options.'),
|
||||
('menu.update', 'Update menu', 'Edit an existing menu and its slot configuration.'),
|
||||
('menu.delete', 'Delete menu', 'Permanently delete a menu when no historical order references it.'),
|
||||
('category.manage', 'Manage categories', 'Create, update or deactivate product/menu categories.'),
|
||||
('ingredient.manage', 'Manage ingredients', 'Manage ingredients, product composition and allergen mapping.'),
|
||||
('stock.read', 'Read stock', 'View ingredient stock levels and movement history.'),
|
||||
('stock.count', 'Count stock', 'Record a physical inventory count (inventory correction).'),
|
||||
('stock.manage', 'Manage stock', 'Record restocks (pack deliveries) and manage stock parameters.'),
|
||||
('order.read', 'Read orders', 'View orders and the preparation display.'),
|
||||
('order.create', 'Create order', 'Create an order at the counter or drive-thru.'),
|
||||
('order.deliver', 'Deliver order', 'Mark a paid order as delivered (single-gesture handover).'),
|
||||
('order.cancel', 'Cancel order', 'Cancel a pending or paid order (restocks ingredients if paid).'),
|
||||
('user.create', 'Create user', 'Create a new back-office user.'),
|
||||
('user.read', 'Read users', 'View the list and details of back-office users.'),
|
||||
('user.update', 'Update user', 'Edit a back-office user (incl. password reset, RGPD anonymisation).'),
|
||||
('user.deactivate', 'Deactivate user', 'Deactivate a back-office user without deleting the row.'),
|
||||
('role.manage', 'Manage roles and RBAC', 'Manage roles, role/permission assignments and visible sources.'),
|
||||
('stats.read', 'Read statistics', 'Access the statistics / KPI dashboard.');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. role_permission — default matrix, dictionary.md 3.17 grants + PROJECT_CONTEXT
|
||||
-- section 7 + decision D5. Subqueries on role.code / permission.code avoid
|
||||
-- hardcoded ids.
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- admin: ALL 23 permissions (cross join the admin role with the whole catalogue).
|
||||
INSERT INTO role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM role r
|
||||
CROSS JOIN permission p
|
||||
WHERE r.code = 'admin';
|
||||
|
||||
-- manager: catalogue create/update + category/ingredient + full stock + stats.
|
||||
-- NO order.* (incl. no order.cancel per D5), NO user/role admin.
|
||||
INSERT INTO role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM role r
|
||||
JOIN permission p ON p.code IN (
|
||||
'product.create', 'product.read', 'product.update',
|
||||
'menu.create', 'menu.read', 'menu.update',
|
||||
'category.manage', 'ingredient.manage',
|
||||
'stock.read', 'stock.count', 'stock.manage',
|
||||
'user.read',
|
||||
'stats.read'
|
||||
)
|
||||
WHERE r.code = 'manager';
|
||||
|
||||
-- kitchen: read-only orders + read-only catalogue + inventory (read + count).
|
||||
INSERT INTO role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM role r
|
||||
JOIN permission p ON p.code IN (
|
||||
'product.read', 'menu.read',
|
||||
'stock.read', 'stock.count',
|
||||
'order.read'
|
||||
)
|
||||
WHERE r.code = 'kitchen';
|
||||
|
||||
-- counter: read catalogue + full order lifecycle (read/create/deliver/cancel)
|
||||
-- + inventory (read + count).
|
||||
INSERT INTO role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM role r
|
||||
JOIN permission p ON p.code IN (
|
||||
'product.read', 'menu.read',
|
||||
'stock.read', 'stock.count',
|
||||
'order.read', 'order.create', 'order.deliver', 'order.cancel'
|
||||
)
|
||||
WHERE r.code = 'counter';
|
||||
|
||||
-- drive: identical grant set to counter (read catalogue + full order lifecycle
|
||||
-- + inventory). The source differs (auto-tagged drive), not the rights.
|
||||
INSERT INTO role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM role r
|
||||
JOIN permission p ON p.code IN (
|
||||
'product.read', 'menu.read',
|
||||
'stock.read', 'stock.count',
|
||||
'order.read', 'order.create', 'order.deliver', 'order.cancel'
|
||||
)
|
||||
WHERE r.code = 'drive';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. role_visible_source — dictionary.md 3.16.
|
||||
-- kitchen sees all 3 sources; counter sees kiosk+counter; drive sees drive.
|
||||
-- admin/manager: no rows -> global view (no source filter).
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO role_visible_source (role_id, source)
|
||||
SELECT r.id, s.source
|
||||
FROM role r
|
||||
JOIN (
|
||||
SELECT 'kitchen' AS role_code, 'kiosk' AS source UNION ALL
|
||||
SELECT 'kitchen', 'counter' UNION ALL
|
||||
SELECT 'kitchen', 'drive' UNION ALL
|
||||
SELECT 'counter', 'kiosk' UNION ALL
|
||||
SELECT 'counter', 'counter' UNION ALL
|
||||
SELECT 'drive', 'drive'
|
||||
) s ON s.role_code = r.code;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. allergen (14) — EU INCO Regulation (EU) No 1169/2011, Annex II.
|
||||
-- dictionary.md 3.8. code = machine code (en), name = French display label.
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO allergen (code, name, description) VALUES
|
||||
('gluten', 'Gluten', 'Cereales contenant du gluten (ble, seigle, orge, avoine, epeautre, kamut) et produits a base de ces cereales.'),
|
||||
('crustaceans', 'Crustaces', 'Crustaces et produits a base de crustaces.'),
|
||||
('eggs', 'Oeufs', 'Oeufs et produits a base d''oeufs.'),
|
||||
('fish', 'Poisson', 'Poissons et produits a base de poissons.'),
|
||||
('peanuts', 'Arachides', 'Arachides et produits a base d''arachides.'),
|
||||
('soybeans', 'Soja', 'Soja et produits a base de soja.'),
|
||||
('milk', 'Lait', 'Lait et produits a base de lait (y compris le lactose).'),
|
||||
('nuts', 'Fruits a coque', 'Fruits a coque : amandes, noisettes, noix, noix de cajou, de pecan, du Bresil, pistaches, noix de Macadamia.'),
|
||||
('celery', 'Celeri', 'Celeri et produits a base de celeri.'),
|
||||
('mustard', 'Moutarde', 'Moutarde et produits a base de moutarde.'),
|
||||
('sesame', 'Graines de sesame', 'Graines de sesame et produits a base de graines de sesame.'),
|
||||
('sulphites', 'Anhydride sulfureux et sulfites', 'Anhydride sulfureux et sulfites en concentration superieure a 10 mg/kg ou 10 mg/l (exprimes en SO2).'),
|
||||
('lupin', 'Lupin', 'Lupin et produits a base de lupin.'),
|
||||
('molluscs', 'Mollusques', 'Mollusques et produits a base de mollusques.');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6. user (1) — bootstrap administrator. dictionary.md 3.14.
|
||||
-- role_id resolved from role.code = 'admin'. pin_hash NULL (no PIN set yet).
|
||||
--
|
||||
-- DEV password: WakdoAdmin2026! (argon2id hash below, generated via
|
||||
-- `docker exec wakdo-app php -r 'echo password_hash("WakdoAdmin2026!",
|
||||
-- PASSWORD_ARGON2ID);'`). MUST be changed in production — this is a known
|
||||
-- demo credential and must never reach a real deployment as-is.
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO user (email, password_hash, pin_hash, first_name, last_name, role_id, is_active)
|
||||
SELECT
|
||||
'admin@wakdo.local',
|
||||
'$argon2id$v=19$m=65536,t=4,p=1$V3dVMi55cDVBYVZPMU1TRw$8iMoNyfC12t7V2CU+YgqwvEb3xNywm7PUSIoNMgRdvc',
|
||||
NULL,
|
||||
'Wakdo',
|
||||
'Admin',
|
||||
r.id,
|
||||
1
|
||||
FROM role r
|
||||
WHERE r.code = 'admin';
|
||||
195
db/seeds/0002_catalogue.sql
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0002 : Catalogue (reference / demo data)
|
||||
-- =============================================================================
|
||||
-- Purpose : Populate the Catalogue sub-domain (category, product, menu,
|
||||
-- menu_slot, menu_slot_option) from the school JSON sources.
|
||||
-- Sources : docs/merise/_sources/categories.json (9 categories)
|
||||
-- docs/merise/_sources/produits.json (menus + 53 products)
|
||||
-- src/public/borne/data/produits.json (cents prices + clean paths,
|
||||
-- used for cross-check)
|
||||
-- Phase : P2 — demo seed, assumes a fresh schema (0001_init_schema.sql).
|
||||
--
|
||||
-- Conventions:
|
||||
-- - Monetary amounts are INT in CENTS (euros float x 100, rounded).
|
||||
-- - vat_rate is per-mille: 100 = 10% (default), 55 = 5.5% for products in
|
||||
-- resealable containers (bottled water, bottled juices) — dictionary note 9.
|
||||
-- - image_path is a relative path under the public root, normalised to
|
||||
-- assets/images/produits/<category>/<file>.png (dictionary note 8).
|
||||
-- - Menus go to the `menu` table (NOT `product`); every other category goes
|
||||
-- to `product`. The "burgers" category items are the anchor products that
|
||||
-- menus reference via burger_product_id.
|
||||
-- - price_maxi_cents = price_normal_cents + 150 (Maxi format, +1.50 EUR).
|
||||
-- - Foreign keys are resolved by subquery on natural keys (slug / name)
|
||||
-- rather than hardcoded ids.
|
||||
-- - Insertion order respects FK dependencies:
|
||||
-- category -> product -> menu -> menu_slot -> menu_slot_option.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. category (9) — root table, source order = display_order
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO category (name, slug, image_path, display_order, is_active) VALUES
|
||||
('menus', 'menus', 'assets/images/categories/menus.png', 1, 1),
|
||||
('boissons', 'boissons', 'assets/images/categories/boissons.png', 2, 1),
|
||||
('burgers', 'burgers', 'assets/images/categories/burgers.png', 3, 1),
|
||||
('frites', 'frites', 'assets/images/categories/frites.png', 4, 1),
|
||||
('encas', 'encas', 'assets/images/categories/encas.png', 5, 1),
|
||||
('wraps', 'wraps', 'assets/images/categories/wraps.png', 6, 1),
|
||||
('salades', 'salades', 'assets/images/categories/salades.png', 7, 1),
|
||||
('desserts', 'desserts', 'assets/images/categories/desserts.png', 8, 1),
|
||||
('sauces', 'sauces', 'assets/images/categories/sauces.png', 9, 1);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. product — every non-menu item (53 rows)
|
||||
-- category_id resolved via subquery on category.slug.
|
||||
-- display_order follows source order within each category.
|
||||
-- vat_rate defaults to 100; 55 only for resealable-container drinks
|
||||
-- (Eau, Jus d'Orange, Jus de Pommes Bio) per dictionary note 9.
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- 2.a burgers (anchor products for menus)
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Le 280', 680, 100, 'assets/images/produits/burgers/280.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Big Tasty', 860, 100, 'assets/images/produits/burgers/big-tasty-1-viande.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Big Tasty Bacon', 890, 100, 'assets/images/produits/burgers/big-tasty-bacon-1-viande.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Big Mac', 600, 100, 'assets/images/produits/burgers/bigmac.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'CBO', 890, 100, 'assets/images/produits/burgers/cbo.png', 1, 5),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'MC Chicken', 730, 100, 'assets/images/produits/burgers/mcchicken.png', 1, 6),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'MC Crispy', 530, 100, 'assets/images/produits/burgers/mccrispy.png', 1, 7),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'MC Fish', 485, 100, 'assets/images/produits/burgers/mcfish.png', 1, 8),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Royal Bacon', 510, 100, 'assets/images/produits/burgers/royalbacon.png', 1, 9),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Royal Cheese', 440, 100, 'assets/images/produits/burgers/royalcheese.png', 1, 10),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Royal Deluxe', 540, 100, 'assets/images/produits/burgers/royaldeluxe.png', 1, 11),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Signature BBQ Beef 2 viandes', 1140, 100, 'assets/images/produits/burgers/signature-bbq-beef-2-viandes.png', 1, 12),
|
||||
((SELECT id FROM category WHERE slug='burgers'), 'Signature Beef BBQ', 1030, 100, 'assets/images/produits/burgers/signature-beef-bbq-burger-1-viande.png', 1, 13);
|
||||
|
||||
-- 2.b boissons (Eau + the two bottled juices are resealable-container = vat_rate 55)
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Coca Cola', 190, 100, 'assets/images/produits/boissons/coca-cola.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Coca Sans Sucres', 190, 100, 'assets/images/produits/boissons/coca-sans-sucres.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Eau', 100, 55, 'assets/images/produits/boissons/eau.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Fanta Orange', 190, 100, 'assets/images/produits/boissons/fanta.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Ice Tea Peche', 190, 100, 'assets/images/produits/boissons/ice-tea-peche.png', 1, 5),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Ice Tea Citron', 190, 100, 'assets/images/produits/boissons/the-vert-citron-sans-sucres.png', 1, 6),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Jus d''Orange', 210, 55, 'assets/images/produits/boissons/jus-orange.png', 1, 7),
|
||||
((SELECT id FROM category WHERE slug='boissons'), 'Jus de Pommes Bio', 230, 55, 'assets/images/produits/boissons/jus-pomme-bio.png', 1, 8);
|
||||
|
||||
-- 2.c frites
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='frites'), 'Petite Frite', 145, 100, 'assets/images/produits/frites/petite-frite.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='frites'), 'Moyenne Frite', 275, 100, 'assets/images/produits/frites/moyenne-frite.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='frites'), 'Grande Frite', 350, 100, 'assets/images/produits/frites/grande-frite.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='frites'), 'Potatoes', 215, 100, 'assets/images/produits/frites/potatoes.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='frites'), 'Grande Potatoes', 340, 100, 'assets/images/produits/frites/grande-potatoes.png', 1, 5);
|
||||
|
||||
-- 2.d encas
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='encas'), 'Cheeseburger', 260, 100, 'assets/images/produits/encas/cheeseburger.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='encas'), 'Croc MCdo', 320, 100, 'assets/images/produits/encas/croc-mc-do.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='encas'), 'Nuggets x4', 420, 100, 'assets/images/produits/encas/nuggets-4.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='encas'), 'Nuggets x20', 1300, 100, 'assets/images/produits/encas/nuggets-20.png', 1, 4);
|
||||
|
||||
-- 2.e wraps
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='wraps'), 'MC Wrap Chevre', 310, 100, 'assets/images/produits/wraps/mcwrap-chevre.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='wraps'), 'MC Wrap Poulet Bacon', 330, 100, 'assets/images/produits/wraps/mcwrap-poulet-bacon.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='wraps'), 'Ptit Wrap Chevre', 260, 100, 'assets/images/produits/wraps/ptit-wrap-chevre.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='wraps'), 'Ptit Wrap Ranch', 260, 100, 'assets/images/produits/wraps/ptit-wrap-ranch.png', 1, 4);
|
||||
|
||||
-- 2.f salades
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='salades'), 'Petite Salade', 330, 100, 'assets/images/produits/salades/petite-salade.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='salades'), 'Cesar Classic', 880, 100, 'assets/images/produits/salades/salade-classic-caesar.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='salades'), 'Italienne Mozza', 880, 100, 'assets/images/produits/salades/salade-italian-mozza.png', 1, 3);
|
||||
|
||||
-- 2.g desserts
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Brownie', 260, 100, 'assets/images/produits/desserts/brownies.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Cheesecake chocolat M&M''S', 310, 100, 'assets/images/produits/desserts/cheesecake-choconuts-m&m-s.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Cheesecake Fraise', 310, 100, 'assets/images/produits/desserts/cheesecake-fraise.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Cookie', 320, 100, 'assets/images/produits/desserts/cookie.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Donut', 260, 100, 'assets/images/produits/desserts/doghnut.png', 1, 5),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Macarons', 270, 100, 'assets/images/produits/desserts/macarons.png', 1, 6),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'MC Fleury', 440, 100, 'assets/images/produits/desserts/mcfleury.png', 1, 7),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Muffin', 360, 100, 'assets/images/produits/desserts/muffin.png', 1, 8),
|
||||
((SELECT id FROM category WHERE slug='desserts'), 'Sunday', 100, 100, 'assets/images/produits/desserts/sunday.png', 1, 9);
|
||||
|
||||
-- 2.h sauces
|
||||
INSERT INTO product (category_id, name, price_cents, vat_rate, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Classic Barbecue', 70, 100, 'assets/images/produits/sauces/classic-barbecue.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Classic Moutarde', 70, 100, 'assets/images/produits/sauces/classic-moutarde.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Creamy Deluxe', 70, 100, 'assets/images/produits/sauces/cremy-deluxe.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Ketchup', 70, 100, 'assets/images/produits/sauces/ketchup.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Chinoise', 70, 100, 'assets/images/produits/sauces/sauce-chinoise.png', 1, 5),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Curry', 70, 100, 'assets/images/produits/sauces/sauce-curry.png', 1, 6),
|
||||
((SELECT id FROM category WHERE slug='sauces'), 'Pommes Frites', 70, 100, 'assets/images/produits/sauces/sauce-pommes-frite.png', 1, 7);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. menu (13) — the "menus" category items.
|
||||
-- category_id = the menus category.
|
||||
-- burger_product_id resolved by matching the anchor burger name
|
||||
-- ("Menu Le 280" -> product "Le 280", etc.).
|
||||
-- price_normal_cents from source; price_maxi_cents = normal + 150.
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO menu (category_id, burger_product_id, name, price_normal_cents, price_maxi_cents, image_path, is_available, display_order) VALUES
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Le 280'), 'Menu Le 280', 880, 1030, 'assets/images/produits/burgers/280.png', 1, 1),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Big Tasty'), 'Menu Big Tasty', 1060, 1210, 'assets/images/produits/burgers/big-tasty-1-viande.png', 1, 2),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Big Tasty Bacon'), 'Menu Big Tasty Bacon', 1090, 1240, 'assets/images/produits/burgers/big-tasty-bacon-1-viande.png', 1, 3),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Big Mac'), 'Menu Big Mac', 800, 950, 'assets/images/produits/burgers/bigmac.png', 1, 4),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='CBO'), 'Menu CBO', 1090, 1240, 'assets/images/produits/burgers/cbo.png', 1, 5),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='MC Chicken'), 'Menu MC Chicken', 930, 1080, 'assets/images/produits/burgers/mcchicken.png', 1, 6),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='MC Crispy'), 'Menu MC Crispy', 720, 870, 'assets/images/produits/burgers/mccrispy.png', 1, 7),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='MC Fish'), 'Menu MC Fish', 720, 870, 'assets/images/produits/burgers/mcfish.png', 1, 8),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Royal Bacon'), 'Menu Royal Bacon', 705, 855, 'assets/images/produits/burgers/royalbacon.png', 1, 9),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Royal Cheese'), 'Menu Royal Cheese', 640, 790, 'assets/images/produits/burgers/royalcheese.png', 1, 10),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Royal Deluxe'), 'Menu Royal Deluxe', 740, 890, 'assets/images/produits/burgers/royaldeluxe.png', 1, 11),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), 'Menu Signature BBQ Beef 2 viandes', 1350, 1500, 'assets/images/produits/burgers/signature-bbq-beef-2-viandes.png', 1, 12),
|
||||
((SELECT id FROM category WHERE slug='menus'), (SELECT id FROM product WHERE name='Signature Beef BBQ'), 'Menu Signature Beef BBQ', 1190, 1340, 'assets/images/produits/burgers/signature-beef-bbq-burger-1-viande.png', 1, 13);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. menu_slot — three standard slots per menu:
|
||||
-- drink (required), side (required), sauce (optional).
|
||||
-- One INSERT per slot_type, fanning out over all 13 menus via SELECT.
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO menu_slot (menu_id, name, slot_type, is_required, display_order)
|
||||
SELECT m.id, 'Boisson', 'drink', 1, 1
|
||||
FROM menu m
|
||||
JOIN category c ON c.id = m.category_id AND c.slug = 'menus';
|
||||
|
||||
INSERT INTO menu_slot (menu_id, name, slot_type, is_required, display_order)
|
||||
SELECT m.id, 'Accompagnement', 'side', 1, 2
|
||||
FROM menu m
|
||||
JOIN category c ON c.id = m.category_id AND c.slug = 'menus';
|
||||
|
||||
INSERT INTO menu_slot (menu_id, name, slot_type, is_required, display_order)
|
||||
SELECT m.id, 'Sauce', 'sauce', 0, 3
|
||||
FROM menu m
|
||||
JOIN category c ON c.id = m.category_id AND c.slug = 'menus';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. menu_slot_option — eligible products per slot:
|
||||
-- drink slot -> all products in category 'boissons'
|
||||
-- side slot -> all products in category 'frites'
|
||||
-- sauce slot -> all products in category 'sauces'
|
||||
-- Composite PK (menu_slot_id, product_id) is naturally satisfied: each
|
||||
-- (slot, product) pair is unique because slots are unique per menu.
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO menu_slot_option (menu_slot_id, product_id)
|
||||
SELECT ms.id, p.id
|
||||
FROM menu_slot ms
|
||||
JOIN product p ON p.category_id = (SELECT id FROM category WHERE slug='boissons')
|
||||
WHERE ms.slot_type = 'drink';
|
||||
|
||||
INSERT INTO menu_slot_option (menu_slot_id, product_id)
|
||||
SELECT ms.id, p.id
|
||||
FROM menu_slot ms
|
||||
JOIN product p ON p.category_id = (SELECT id FROM category WHERE slug='frites')
|
||||
WHERE ms.slot_type = 'side';
|
||||
|
||||
INSERT INTO menu_slot_option (menu_slot_id, product_id)
|
||||
SELECT ms.id, p.id
|
||||
FROM menu_slot ms
|
||||
JOIN product p ON p.category_id = (SELECT id FROM category WHERE slug='sauces')
|
||||
WHERE ms.slot_type = 'sauce';
|
||||
238
db/seeds/0003_ingredients_recipes.sql
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0003 : Ingredients + recettes (product_ingredient)
|
||||
-- =============================================================================
|
||||
-- Active le decrement de stock (RG-T20) au paiement et la dispo calculee
|
||||
-- (RG-T21). Catalogue d'ingredients ferme + une recette par produit (53).
|
||||
-- FK resolus par sous-requete sur le nom (convention seed 0002). INSERT IGNORE
|
||||
-- => idempotent (UNIQUE name sur ingredient ; PK (product_id,ingredient_id)).
|
||||
-- Quantites : burger/wrap/salade normal==maxi ; frites/boissons maxi>=normal.
|
||||
-- Lignes quantity_normal=0 (extras hors base) ecartees : CHECK quantity_normal>0.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 1. ingredient (catalogue, stock plein)
|
||||
INSERT IGNORE INTO ingredient (name, unit, stock_quantity, stock_capacity) VALUES
|
||||
('Pain burger', 'piece', 300, 300),
|
||||
('Pain sesame', 'piece', 300, 300),
|
||||
('Pain signature', 'piece', 250, 250),
|
||||
('Tortilla', 'piece', 200, 200),
|
||||
('Steak hache', 'piece', 400, 400),
|
||||
('Filet de poulet pane', 'piece', 300, 300),
|
||||
('Galette de poisson', 'piece', 150, 150),
|
||||
('Tranche de bacon', 'piece', 500, 500),
|
||||
('Nugget de poulet', 'piece', 1500, 1500),
|
||||
('Jambon', 'tranche', 200, 200),
|
||||
('Cheddar', 'tranche', 600, 600),
|
||||
('Fromage de chevre', 'portion', 150, 150),
|
||||
('Mozzarella', 'portion', 150, 150),
|
||||
('Emmental', 'tranche', 250, 250),
|
||||
('Salade', 'portion', 600, 600),
|
||||
('Tomate', 'rondelle', 400, 400),
|
||||
('Oignon', 'portion', 600, 600),
|
||||
('Cornichon', 'rondelle', 800, 800),
|
||||
('Roquette', 'portion', 200, 200),
|
||||
('Sauce Big Mac', 'dose', 1000, 1000),
|
||||
('Sauce ranch', 'dose', 1000, 1000),
|
||||
('Sauce barbecue', 'dose', 1000, 1000),
|
||||
('Sauce deluxe', 'dose', 1000, 1000),
|
||||
('Pomme de terre frite', 'portion', 3000, 3000),
|
||||
('Galette de pomme de terre', 'portion', 1000, 1000),
|
||||
('Dose Coca', 'dose', 1500, 1500),
|
||||
('Dose Coca Zero', 'dose', 1000, 1000),
|
||||
('Dose Eau', 'dose', 800, 800),
|
||||
('Dose Fanta', 'dose', 1000, 1000),
|
||||
('Dose Ice Tea Peche', 'dose', 1000, 1000),
|
||||
('Dose Ice Tea Citron', 'dose', 800, 800),
|
||||
('Dose Jus d Orange', 'dose', 400, 400),
|
||||
('Dose Jus de Pomme', 'dose', 400, 400),
|
||||
('Gobelet', 'piece', 3000, 3000),
|
||||
('Brownie', 'piece', 150, 150),
|
||||
('Cheesecake', 'piece', 150, 150),
|
||||
('Cookie', 'piece', 150, 150),
|
||||
('Donut', 'piece', 150, 150),
|
||||
('Macaron', 'piece', 200, 200),
|
||||
('Glace McFleury', 'piece', 200, 200),
|
||||
('Muffin', 'piece', 150, 150),
|
||||
('Glace sundae', 'piece', 200, 200),
|
||||
('Topping chocolat', 'dose', 300, 300),
|
||||
('Dosette Barbecue', 'dosette', 1500, 1500),
|
||||
('Dosette Moutarde', 'dosette', 1500, 1500),
|
||||
('Dosette Deluxe', 'dosette', 1500, 1500),
|
||||
('Dosette Ketchup', 'dosette', 2000, 2000),
|
||||
('Dosette Chinoise', 'dosette', 1000, 1000),
|
||||
('Dosette Curry', 'dosette', 1500, 1500),
|
||||
('Dosette Pommes Frites', 'dosette', 1500, 1500);
|
||||
|
||||
-- 2. product_ingredient (recettes ; FK par nom)
|
||||
INSERT IGNORE INTO product_ingredient (product_id, ingredient_id, quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents) VALUES
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Pain signature'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Cheddar'), 2, 2, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Le 280'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Emmental'), 2, 2, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 2, 2, 1, 1, 80),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Emmental'), 2, 2, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Tasty Bacon'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Steak hache'), 2, 2, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Cornichon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Big Mac'), (SELECT id FROM ingredient WHERE name='Sauce Big Mac'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Pain signature'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 2, 2, 1, 1, 80),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Emmental'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='CBO'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Chicken'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Chicken'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Chicken'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Chicken'), (SELECT id FROM ingredient WHERE name='Sauce ranch'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Crispy'), (SELECT id FROM ingredient WHERE name='Pain signature'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Crispy'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Crispy'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Crispy'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Crispy'), (SELECT id FROM ingredient WHERE name='Sauce deluxe'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Fish'), (SELECT id FROM ingredient WHERE name='Pain burger'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Fish'), (SELECT id FROM ingredient WHERE name='Galette de poisson'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Fish'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='MC Fish'), (SELECT id FROM ingredient WHERE name='Sauce deluxe'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 2, 2, 1, 1, 80),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Bacon'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Cheddar'), 2, 2, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Cornichon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Cheese'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Pain sesame'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Cornichon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Royal Deluxe'), (SELECT id FROM ingredient WHERE name='Sauce deluxe'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Pain signature'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Steak hache'), 2, 2, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Cheddar'), 2, 2, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 2, 2, 1, 1, 80),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature BBQ Beef 2 viandes'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Pain signature'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Signature Beef BBQ'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Coca Cola'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Coca Cola'), (SELECT id FROM ingredient WHERE name='Dose Coca'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Coca Sans Sucres'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Coca Sans Sucres'), (SELECT id FROM ingredient WHERE name='Dose Coca Zero'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Eau'), (SELECT id FROM ingredient WHERE name='Dose Eau'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Fanta Orange'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Fanta Orange'), (SELECT id FROM ingredient WHERE name='Dose Fanta'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ice Tea Peche'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ice Tea Peche'), (SELECT id FROM ingredient WHERE name='Dose Ice Tea Peche'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ice Tea Citron'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ice Tea Citron'), (SELECT id FROM ingredient WHERE name='Dose Ice Tea Citron'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Jus d''Orange'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Jus d''Orange'), (SELECT id FROM ingredient WHERE name='Dose Jus d Orange'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Jus de Pommes Bio'), (SELECT id FROM ingredient WHERE name='Gobelet'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Jus de Pommes Bio'), (SELECT id FROM ingredient WHERE name='Dose Jus de Pomme'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Petite Frite'), (SELECT id FROM ingredient WHERE name='Pomme de terre frite'), 1, 2, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Moyenne Frite'), (SELECT id FROM ingredient WHERE name='Pomme de terre frite'), 2, 3, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Grande Frite'), (SELECT id FROM ingredient WHERE name='Pomme de terre frite'), 3, 4, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Potatoes'), (SELECT id FROM ingredient WHERE name='Galette de pomme de terre'), 2, 3, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Grande Potatoes'), (SELECT id FROM ingredient WHERE name='Galette de pomme de terre'), 3, 4, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Pain burger'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Steak hache'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 1, 60),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Cornichon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheeseburger'), (SELECT id FROM ingredient WHERE name='Sauce Big Mac'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Croc MCdo'), (SELECT id FROM ingredient WHERE name='Pain burger'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Croc MCdo'), (SELECT id FROM ingredient WHERE name='Jambon'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Croc MCdo'), (SELECT id FROM ingredient WHERE name='Emmental'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Nuggets x4'), (SELECT id FROM ingredient WHERE name='Nugget de poulet'), 4, 4, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Nuggets x20'), (SELECT id FROM ingredient WHERE name='Nugget de poulet'), 20, 20, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Tortilla'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Fromage de chevre'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Sauce ranch'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Tortilla'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Wrap Poulet Bacon'), (SELECT id FROM ingredient WHERE name='Sauce barbecue'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Tortilla'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Fromage de chevre'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Chevre'), (SELECT id FROM ingredient WHERE name='Sauce ranch'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Tortilla'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Cheddar'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Oignon'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ptit Wrap Ranch'), (SELECT id FROM ingredient WHERE name='Sauce ranch'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Petite Salade'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Petite Salade'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Petite Salade'), (SELECT id FROM ingredient WHERE name='Roquette'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Filet de poulet pane'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Emmental'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Sauce ranch'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cesar Classic'), (SELECT id FROM ingredient WHERE name='Tranche de bacon'), 1, 1, 1, 1, 80),
|
||||
((SELECT id FROM product WHERE name='Italienne Mozza'), (SELECT id FROM ingredient WHERE name='Salade'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Italienne Mozza'), (SELECT id FROM ingredient WHERE name='Mozzarella'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Italienne Mozza'), (SELECT id FROM ingredient WHERE name='Tomate'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Italienne Mozza'), (SELECT id FROM ingredient WHERE name='Roquette'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Italienne Mozza'), (SELECT id FROM ingredient WHERE name='Sauce deluxe'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Brownie'), (SELECT id FROM ingredient WHERE name='Brownie'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheesecake chocolat M&M''S'), (SELECT id FROM ingredient WHERE name='Cheesecake'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheesecake chocolat M&M''S'), (SELECT id FROM ingredient WHERE name='Topping chocolat'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cheesecake Fraise'), (SELECT id FROM ingredient WHERE name='Cheesecake'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Cookie'), (SELECT id FROM ingredient WHERE name='Cookie'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Donut'), (SELECT id FROM ingredient WHERE name='Donut'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Macarons'), (SELECT id FROM ingredient WHERE name='Macaron'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Fleury'), (SELECT id FROM ingredient WHERE name='Glace McFleury'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='MC Fleury'), (SELECT id FROM ingredient WHERE name='Topping chocolat'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Muffin'), (SELECT id FROM ingredient WHERE name='Muffin'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Sunday'), (SELECT id FROM ingredient WHERE name='Glace sundae'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Sunday'), (SELECT id FROM ingredient WHERE name='Topping chocolat'), 1, 1, 1, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Classic Barbecue'), (SELECT id FROM ingredient WHERE name='Dosette Barbecue'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Classic Moutarde'), (SELECT id FROM ingredient WHERE name='Dosette Moutarde'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Creamy Deluxe'), (SELECT id FROM ingredient WHERE name='Dosette Deluxe'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Ketchup'), (SELECT id FROM ingredient WHERE name='Dosette Ketchup'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Chinoise'), (SELECT id FROM ingredient WHERE name='Dosette Chinoise'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Curry'), (SELECT id FROM ingredient WHERE name='Dosette Curry'), 1, 1, 0, 0, 0),
|
||||
((SELECT id FROM product WHERE name='Pommes Frites'), (SELECT id FROM ingredient WHERE name='Dosette Pommes Frites'), 1, 1, 0, 0, 0);
|
||||
|
||||
61
db/seeds/0004_menu_side_maxi.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0004 : accompagnement de menu = variante Maxi automatique
|
||||
-- =============================================================================
|
||||
-- Purpose : cabler la regle metier "accompagnement Maxi" sur les donnees seedees
|
||||
-- par 0002_catalogue.sql, sans toucher au code :
|
||||
-- 1. lier chaque accompagnement standard a sa variante Grande
|
||||
-- (Moyenne Frite -> Grande Frite, Potatoes -> Grande Potatoes) ;
|
||||
-- 2. restreindre les options du slot 'side' des menus aux deux seuls
|
||||
-- choix conformes a la maquette (ecran 4) : Moyenne Frite + Potatoes.
|
||||
-- Phase : P4 — depend du schema 0006 (product.maxi_variant_product_id) et du
|
||||
-- catalogue 0002 (produits frites + menu_slot 'side').
|
||||
--
|
||||
-- Etat initial (0002_catalogue.sql, section 5) : le slot 'side' recoit TOUS les
|
||||
-- produits de la categorie 'frites', soit les 5 : Petite Frite, Moyenne Frite,
|
||||
-- Grande Frite, Potatoes, Grande Potatoes. Ce seed retire Petite Frite, Grande
|
||||
-- Frite et Grande Potatoes des options de menu (elles restent a la carte dans la
|
||||
-- categorie frites) : le DELETE n'est donc PAS un no-op sur une base 0002.
|
||||
--
|
||||
-- Conventions:
|
||||
-- - Aucun id en dur : toutes les references sont resolues par sous-requete sur
|
||||
-- le nom du produit / le type de slot (memes noms que 0002_catalogue.sql).
|
||||
-- - IDEMPOTENT : UPDATE convergent (repositionne la meme valeur) et DELETE par
|
||||
-- appartenance (re-supprimer des options deja absentes ne fait rien) ; rejouer
|
||||
-- ce seed laisse la base dans le meme etat.
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Lier chaque accompagnement standard a sa variante Grande.
|
||||
-- Le SELECT cible la table `product`, que l'UPDATE modifie aussi : MariaDB/
|
||||
-- MySQL interdit de lire et d'ecrire la meme table dans une seule requete
|
||||
-- sans niveau de derivation. La sous-requete est donc enveloppee dans une
|
||||
-- table derivee (SELECT ... FROM (SELECT ...) x) qui materialise l'id avant
|
||||
-- l'UPDATE, contournant l'erreur "can't specify target table for update".
|
||||
-- -----------------------------------------------------------------------------
|
||||
UPDATE product
|
||||
SET maxi_variant_product_id = (
|
||||
SELECT id FROM (SELECT id FROM product WHERE name = 'Grande Frite') x
|
||||
)
|
||||
WHERE name = 'Moyenne Frite';
|
||||
|
||||
UPDATE product
|
||||
SET maxi_variant_product_id = (
|
||||
SELECT id FROM (SELECT id FROM product WHERE name = 'Grande Potatoes') x
|
||||
)
|
||||
WHERE name = 'Potatoes';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Restreindre les options du slot 'side' des menus aux deux choix de la
|
||||
-- maquette. On supprime des slots 'side' toute option qui n'est ni Moyenne
|
||||
-- Frite ni Potatoes (Petite Frite, Grande Frite, Grande Potatoes). Les autres
|
||||
-- slots (drink, sauce) et les produits a la carte ne sont pas touches.
|
||||
-- Idempotent : sur une base deja restreinte, ces lignes n'existent plus, le
|
||||
-- DELETE affecte 0 ligne.
|
||||
-- -----------------------------------------------------------------------------
|
||||
DELETE FROM menu_slot_option
|
||||
WHERE menu_slot_id IN (SELECT id FROM menu_slot WHERE slot_type = 'side')
|
||||
AND product_id IN (
|
||||
SELECT id FROM product WHERE name IN ('Petite Frite', 'Grande Frite', 'Grande Potatoes')
|
||||
);
|
||||
86
db/seeds/0005_drink_sizes.sql
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0005 : tailles a la carte des boissons fontaine (30 / 50 cl)
|
||||
-- =============================================================================
|
||||
-- Purpose : cabler la dimension TAILLE (schema 0007) sur les boissons fontaine
|
||||
-- seedees par 0002_catalogue.sql, sans toucher au code :
|
||||
-- 1. la ligne existante de chaque soda devient la BASE 30 cl ;
|
||||
-- 2. une ligne VARIANTE 50 cl est inseree par soda (base_product_id ->
|
||||
-- la base, prix = base + 50c par defaut, +50 cl) ;
|
||||
-- 3. la recette (product_ingredient) de la base est dupliquee sur la
|
||||
-- variante, pour que le decrement de stock (consumption) frappe
|
||||
-- aussi la 50 cl.
|
||||
--
|
||||
-- Perimetre : seules les boissons fontaine ont deux tailles (Coca Cola, Coca Sans
|
||||
-- Sucres, Fanta Orange, Ice Tea Peche, Ice Tea Citron). Les boissons en bouteille
|
||||
-- (Eau, Jus d'Orange, Jus de Pommes Bio) restent mono-taille (size_cl laisse NULL,
|
||||
-- aucune variante).
|
||||
--
|
||||
-- Phase : R4 — depend du schema 0007 (product.size_cl + base_product_id) et du
|
||||
-- catalogue 0002 (lignes boissons) ; la duplication de recette depend de
|
||||
-- 0003 (product_ingredient des sodas).
|
||||
--
|
||||
-- Conventions:
|
||||
-- - Aucun id en dur : toutes les references sont resolues par sous-requete sur
|
||||
-- le nom du produit (memes noms que 0002_catalogue.sql).
|
||||
-- - IDEMPOTENT : UPDATE convergent (repositionne la meme valeur) ; INSERT gardes
|
||||
-- par WHERE NOT EXISTS (re-jouer n'insere pas de doublon). La sous-requete qui
|
||||
-- lit `product` dans un INSERT INTO product est enveloppee en table derivee
|
||||
-- pour contourner l'erreur MariaDB 1093 (technique de 0004_menu_side_maxi.sql).
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Marquer chaque soda fontaine comme BASE 30 cl. UPDATE convergent (rejouer
|
||||
-- repose 30) -> idempotent. Le nom de base reste propre ("Coca Cola") : la
|
||||
-- tuile catalogue garde le nom court, le picker affiche "30 cl" / "50 cl".
|
||||
-- -----------------------------------------------------------------------------
|
||||
UPDATE product
|
||||
SET size_cl = 30
|
||||
WHERE base_product_id IS NULL
|
||||
AND name IN ('Coca Cola', 'Coca Sans Sucres', 'Fanta Orange', 'Ice Tea Peche', 'Ice Tea Citron');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Inserer la VARIANTE 50 cl de chaque soda. category_id / vat_rate / image
|
||||
-- copies de la base ; price = base + 50c (defaut sensible, a confirmer) ;
|
||||
-- base_product_id -> id de la base ; size_cl = 50 ; is_available = 1.
|
||||
-- L'INSERT lit ET ecrit `product` : la sous-requete est enveloppee en table
|
||||
-- derivee (b) pour contourner l'erreur 1093. WHERE NOT EXISTS garde le doublon
|
||||
-- a la re-execution (une variante 50 cl de cette base existe deja -> 0 ligne).
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO product (category_id, name, price_cents, size_cl, base_product_id, vat_rate, image_path, is_available, display_order)
|
||||
SELECT b.category_id, b.name_50, b.price_cents + 50, 50, b.id, b.vat_rate, b.image_path, 1, b.display_order
|
||||
FROM (
|
||||
SELECT id, category_id, CONCAT(name, ' 50cl') AS name_50, price_cents, vat_rate, image_path, display_order
|
||||
FROM product
|
||||
WHERE base_product_id IS NULL
|
||||
AND name IN ('Coca Cola', 'Coca Sans Sucres', 'Fanta Orange', 'Ice Tea Peche', 'Ice Tea Citron')
|
||||
) b
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM (SELECT base_product_id FROM product WHERE base_product_id IS NOT NULL) v
|
||||
WHERE v.base_product_id = b.id
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. Dupliquer la recette de chaque base 30 cl sur sa variante 50 cl, pour que
|
||||
-- le decrement de stock frappe aussi la 50 cl. Memes ingredients / quantites
|
||||
-- que la base (simplification assumee : R4 vise le flux de commande, pas une
|
||||
-- consommation volumetrique exacte). Une base sans recette (ex. theorique) ne
|
||||
-- produit aucune ligne pour sa variante.
|
||||
-- PK composite (product_id, ingredient_id) : WHERE NOT EXISTS garde la
|
||||
-- re-execution (les lignes de la variante existent deja -> 0 ligne inseree).
|
||||
-- -----------------------------------------------------------------------------
|
||||
INSERT INTO product_ingredient (product_id, ingredient_id, quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents)
|
||||
SELECT v.id, src.ingredient_id, src.quantity_normal, src.quantity_maxi, src.is_removable, src.is_addable, src.extra_price_cents
|
||||
FROM product v
|
||||
JOIN (
|
||||
SELECT pi.product_id AS base_id, pi.ingredient_id, pi.quantity_normal, pi.quantity_maxi,
|
||||
pi.is_removable, pi.is_addable, pi.extra_price_cents
|
||||
FROM product_ingredient pi
|
||||
) src ON src.base_id = v.base_product_id
|
||||
WHERE v.base_product_id IS NOT NULL
|
||||
AND v.size_cl = 50
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM (SELECT product_id, ingredient_id FROM product_ingredient) e
|
||||
WHERE e.product_id = v.id AND e.ingredient_id = src.ingredient_id
|
||||
);
|
||||
|
|
@ -1,88 +1,19 @@
|
|||
#
|
||||
# Wakdo - orchestration des 4 services de la stack (Bloc 5 DevOps, Cr 7.c.3 / 7.c.4).
|
||||
#
|
||||
# Services :
|
||||
# wakdo-web : Apache httpd, reverse proxy FastCGI -> wakdo-app, expose 80 via Traefik
|
||||
# wakdo-app : PHP-FPM 8.3, execute le code PHP back-office + API
|
||||
# wakdo-db : MariaDB 11.4, persistance des donnees metier
|
||||
# wakdo-cron : Alpine + dcron, backups nocturnes et taches planifiees
|
||||
#
|
||||
# Reseaux :
|
||||
# default (nom = wakdo_internal) : bridge interne pour l'inter-service,
|
||||
# non expose a l'hote. app/db/cron ne sont PAS joignables publiquement.
|
||||
# <REVERSE_PROXY_NETWORK> : reseau externe (ex. traefik_proxy) partage
|
||||
# avec le Traefik de l'hote. Seul wakdo-web y est attache.
|
||||
#
|
||||
# Volumes :
|
||||
# wakdo_db_data : named volume pour /var/lib/mysql (persistance BDD)
|
||||
# wakdo_uploads : named volume pour les uploads produits back-office
|
||||
# ./var/backups : bind-mount lecture-ecriture pour les dumps SQL
|
||||
#
|
||||
# Variables d'env consommees depuis .env (chargees par make via `include .env`
|
||||
# et transmises ici par docker compose qui fait l'expansion automatique).
|
||||
#
|
||||
# Persistance : un `make down` preserve les named volumes. Seul `make clean`
|
||||
# (interactif) ou `docker compose down -v` supprime les donnees.
|
||||
#
|
||||
|
||||
name: wakdo
|
||||
|
||||
networks:
|
||||
# Reseau interne : isolement des services applicatifs.
|
||||
# wakdo-app, wakdo-db et wakdo-cron n'y sont accessibles que via les autres
|
||||
# conteneurs de la stack. Aucun port hote expose.
|
||||
wakdo_internal:
|
||||
driver: bridge
|
||||
internal: false
|
||||
# internal: false (par defaut) car wakdo-app et wakdo-cron ont besoin
|
||||
# de sortir sur internet (pour telecharger des packages au build et pour
|
||||
# de potentiels appels API externes futurs). L'isolation vient du fait
|
||||
# qu'aucun port hote n'est binde ici.
|
||||
#
|
||||
# Subnet explicite (RFC 1918) : l'auto-allocateur Docker du daemon hote
|
||||
# est sature (15 /16 + 15 /20 deja alloues par d'autres stacks), il ne
|
||||
# peut plus creer de reseau bridge sans subnet explicite. 192.168.148.0/24
|
||||
# est dans le gap libre 192.168.144-159 (256 IP, largement suffisant pour
|
||||
# 4 services), aucune collision avec les /24 acquagest voisins (150/154/
|
||||
# 155/157). Choix defendable : right-sizing + isolation des fluctuations
|
||||
# d'allocation auto sur cet hote mutualise.
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 192.168.148.0/24
|
||||
|
||||
# Reseau du reverse proxy (Traefik) pre-existant sur l'hote.
|
||||
# Son nom est configurable via REVERSE_PROXY_NETWORK dans .env pour
|
||||
# supporter differents setups (traefik_proxy, traefik_public, proxy, ...).
|
||||
reverse_proxy:
|
||||
name: ${REVERSE_PROXY_NETWORK}
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
wakdo_db_data:
|
||||
# Named volume MariaDB. Permissions gerees par Docker (UID mysql=999
|
||||
# dans le conteneur), zero souci cote hote. Survit a `docker compose down`.
|
||||
# Pour remise a zero : `make clean` (interactif, confirme) ou
|
||||
# `docker compose down -v` (destructif direct).
|
||||
|
||||
wakdo_uploads:
|
||||
# Images produits uploadees par les equipiers depuis le back-office.
|
||||
# Named volume pour les memes raisons que wakdo_db_data : permissions
|
||||
# propres (www-data UID 82 en Alpine) et pas de pollution du repo git
|
||||
# par des binaires.
|
||||
|
||||
services:
|
||||
|
||||
# =======================================================================
|
||||
# wakdo-db : MariaDB 11.4 LTS
|
||||
# =======================================================================
|
||||
wakdo-db:
|
||||
image: mariadb:11.4
|
||||
container_name: wakdo-db
|
||||
restart: unless-stopped
|
||||
|
||||
# Variables d'env d'initialisation (ne s'appliquent qu'au premier demarrage
|
||||
# sur volume vide - voir docs/notes/docker-volumes-vs-bind-mounts.md).
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
MARIADB_DATABASE: ${DB_NAME}
|
||||
|
|
@ -90,18 +21,11 @@ services:
|
|||
MARIADB_PASSWORD: ${DB_PASSWORD}
|
||||
MARIADB_AUTO_UPGRADE: "1"
|
||||
TZ: ${APP_TIMEZONE:-Europe/Paris}
|
||||
|
||||
volumes:
|
||||
- wakdo_db_data:/var/lib/mysql
|
||||
|
||||
- ./db/init:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- wakdo_internal
|
||||
|
||||
# Pas de ports exposes a l'hote : seuls wakdo-app et wakdo-cron peuvent
|
||||
# joindre wakdo-db:3306 via le reseau interne.
|
||||
|
||||
# Healthcheck officiel fourni par l'image mariadb : le script bundled
|
||||
# healthcheck.sh teste la connexion et l'init innodb.
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
|
|
@ -109,16 +33,30 @@ services:
|
|||
retries: 6
|
||||
start_period: 30s
|
||||
|
||||
# =======================================================================
|
||||
# wakdo-app : PHP-FPM 8.3 (execute le code back-office + API)
|
||||
# =======================================================================
|
||||
wakdo-migrate:
|
||||
image: mariadb:11.4
|
||||
container_name: wakdo-migrate
|
||||
restart: "no"
|
||||
environment:
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- ./db:/db:ro
|
||||
networks:
|
||||
- wakdo_internal
|
||||
depends_on:
|
||||
wakdo-db:
|
||||
condition: service_healthy
|
||||
entrypoint: ["bash", "/db/migrate-container.sh"]
|
||||
|
||||
wakdo-app:
|
||||
build:
|
||||
context: ./docker/php-fpm
|
||||
dockerfile: Dockerfile
|
||||
container_name: wakdo-app
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
APP_ENV: ${APP_ENV}
|
||||
APP_DEBUG: ${APP_DEBUG}
|
||||
|
|
@ -134,131 +72,78 @@ services:
|
|||
SESSION_LIFETIME_ABSOLUTE: ${SESSION_LIFETIME_ABSOLUTE}
|
||||
SESSION_NAME: ${SESSION_NAME}
|
||||
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN}
|
||||
PASSWORD_ALGO: ${PASSWORD_ALGO}
|
||||
ARGON2_MEMORY_COST: ${ARGON2_MEMORY_COST}
|
||||
ARGON2_TIME_COST: ${ARGON2_TIME_COST}
|
||||
ARGON2_THREADS: ${ARGON2_THREADS}
|
||||
ACCOUNT_LOCKOUT_THRESHOLD: ${ACCOUNT_LOCKOUT_THRESHOLD}
|
||||
ACCOUNT_LOCKOUT_BASE_SECONDS: ${ACCOUNT_LOCKOUT_BASE_SECONDS}
|
||||
ACCOUNT_LOCKOUT_MAX_SECONDS: ${ACCOUNT_LOCKOUT_MAX_SECONDS}
|
||||
IP_THROTTLE_WINDOW_SECONDS: ${IP_THROTTLE_WINDOW_SECONDS}
|
||||
IP_THROTTLE_MAX_ATTEMPTS: ${IP_THROTTLE_MAX_ATTEMPTS}
|
||||
STAFF_PIN_MIN_LENGTH: ${STAFF_PIN_MIN_LENGTH}
|
||||
STAFF_PIN_MAX_LENGTH: ${STAFF_PIN_MAX_LENGTH}
|
||||
PIN_THROTTLE_THRESHOLD: ${PIN_THROTTLE_THRESHOLD}
|
||||
PIN_THROTTLE_BASE_SECONDS: ${PIN_THROTTLE_BASE_SECONDS}
|
||||
PIN_THROTTLE_MAX_SECONDS: ${PIN_THROTTLE_MAX_SECONDS}
|
||||
PIN_THROTTLE_WINDOW_SECONDS: ${PIN_THROTTLE_WINDOW_SECONDS}
|
||||
PASSWORD_RESET_TTL: ${PASSWORD_RESET_TTL}
|
||||
UPLOAD_MAX_SIZE_MB: ${UPLOAD_MAX_SIZE_MB}
|
||||
UPLOAD_ALLOWED_MIME: ${UPLOAD_ALLOWED_MIME}
|
||||
|
||||
volumes:
|
||||
# Bind-mount du code source pour le hot-reload en dev.
|
||||
# En prod, cette ligne est remplacee par un COPY dans l'image
|
||||
# via docker-compose.prod.yml (override a venir en P7).
|
||||
- ./src:/var/www/html
|
||||
|
||||
# Named volume pour les uploads, plus specifique que le bind-mount
|
||||
# parent : les uploads ne vont pas dans ./src, ils restent dans le
|
||||
# volume Docker et survivent aux `make down`.
|
||||
- wakdo_uploads:/var/www/html/public/uploads
|
||||
|
||||
networks:
|
||||
- wakdo_internal
|
||||
|
||||
depends_on:
|
||||
wakdo-migrate:
|
||||
condition: service_completed_successfully
|
||||
wakdo-db:
|
||||
condition: service_healthy
|
||||
|
||||
# Healthcheck defini dans le Dockerfile (php -r exit 0).
|
||||
|
||||
# =======================================================================
|
||||
# wakdo-web : Apache httpd (reverse FastCGI vers wakdo-app, expose via Traefik)
|
||||
# =======================================================================
|
||||
wakdo-web:
|
||||
build:
|
||||
context: ./docker/apache
|
||||
dockerfile: Dockerfile
|
||||
container_name: wakdo-web
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
# Noms de domaine injectes dans les vhosts (ServerName ${TRAEFIK_DOMAIN_*}).
|
||||
TRAEFIK_DOMAIN_KIOSK: ${TRAEFIK_DOMAIN_KIOSK}
|
||||
TRAEFIK_DOMAIN_ADMIN: ${TRAEFIK_DOMAIN_ADMIN}
|
||||
|
||||
APP_HOST_KIOSK: ${APP_HOST_KIOSK}
|
||||
APP_HOST_ADMIN: ${APP_HOST_ADMIN}
|
||||
ports:
|
||||
- "${HTTP_PORT:-8080}:80"
|
||||
volumes:
|
||||
# Meme bind-mount que wakdo-app : Apache sert les fichiers statiques
|
||||
# (HTML, CSS, JS, images) depuis ce dossier. PHP ne passe pas par
|
||||
# ce chemin ici, il passe par le proxy FastCGI vers wakdo-app.
|
||||
- ./src:/var/www/html
|
||||
- wakdo_uploads:/var/www/html/public/uploads
|
||||
|
||||
networks:
|
||||
- wakdo_internal
|
||||
- reverse_proxy
|
||||
|
||||
depends_on:
|
||||
wakdo-migrate:
|
||||
condition: service_completed_successfully
|
||||
wakdo-app:
|
||||
condition: service_started
|
||||
wakdo-db:
|
||||
condition: service_healthy
|
||||
|
||||
# === Labels Traefik : deux routers (kiosk + admin) sur le meme conteneur ===
|
||||
# Le Traefik de l'hote decouvre ces labels automatiquement (provider docker).
|
||||
# On ne configure PAS le certresolver ici : le Traefik hote le gere via
|
||||
# sa propre config (acme.json, resolver par defaut).
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=${REVERSE_PROXY_NETWORK}"
|
||||
|
||||
# --- Router kiosk (borne client) ---
|
||||
- "traefik.http.routers.wakdo-kiosk.rule=Host(`${TRAEFIK_DOMAIN_KIOSK}`)"
|
||||
- "traefik.http.routers.wakdo-kiosk.entrypoints=websecure"
|
||||
- "traefik.http.routers.wakdo-kiosk.tls=true"
|
||||
- "traefik.http.routers.wakdo-kiosk.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.wakdo-kiosk.service=wakdo-kiosk-svc"
|
||||
- "traefik.http.services.wakdo-kiosk-svc.loadbalancer.server.port=80"
|
||||
|
||||
# --- Router admin (back-office + API) ---
|
||||
- "traefik.http.routers.wakdo-admin.rule=Host(`${TRAEFIK_DOMAIN_ADMIN}`)"
|
||||
- "traefik.http.routers.wakdo-admin.entrypoints=websecure"
|
||||
- "traefik.http.routers.wakdo-admin.tls=true"
|
||||
- "traefik.http.routers.wakdo-admin.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.wakdo-admin.service=wakdo-admin-svc"
|
||||
- "traefik.http.services.wakdo-admin-svc.loadbalancer.server.port=80"
|
||||
|
||||
# --- Middleware : redirection HTTP -> HTTPS ---
|
||||
# Applique aux 2 hosts via un router "catch-all" sur entrypoints=web.
|
||||
- "traefik.http.routers.wakdo-kiosk-http.rule=Host(`${TRAEFIK_DOMAIN_KIOSK}`)"
|
||||
- "traefik.http.routers.wakdo-kiosk-http.entrypoints=web"
|
||||
- "traefik.http.routers.wakdo-kiosk-http.middlewares=wakdo-to-https"
|
||||
- "traefik.http.routers.wakdo-admin-http.rule=Host(`${TRAEFIK_DOMAIN_ADMIN}`)"
|
||||
- "traefik.http.routers.wakdo-admin-http.entrypoints=web"
|
||||
- "traefik.http.routers.wakdo-admin-http.middlewares=wakdo-to-https"
|
||||
- "traefik.http.middlewares.wakdo-to-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.wakdo-to-https.redirectscheme.permanent=true"
|
||||
|
||||
# =======================================================================
|
||||
# wakdo-cron : taches planifiees (backup, purge, agregations)
|
||||
# =======================================================================
|
||||
wakdo-cron:
|
||||
build:
|
||||
context: ./docker/cron
|
||||
dockerfile: Dockerfile
|
||||
container_name: wakdo-cron
|
||||
restart: unless-stopped
|
||||
# init: true -> Docker injecte tini comme PID 1. dcron exige un init
|
||||
# parent pour pouvoir setpgid() sur ses jobs (sinon "Operation not
|
||||
# permitted" en boucle car un PID 1 sans init ne peut pas changer les
|
||||
# groupes de processus). Cf. busybox-utils issue tracker.
|
||||
init: true
|
||||
|
||||
environment:
|
||||
# Credentials BDD pour mysqldump (lecture seule via USER applicatif,
|
||||
# PAS le root password). Le user applicatif doit avoir SELECT +
|
||||
# LOCK TABLES + SHOW VIEW sur la BDD (migrations P2).
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
AUDIT_LOG_RETENTION_DAYS: ${AUDIT_LOG_RETENTION_DAYS:-365}
|
||||
THROTTLE_PURGE_AFTER_HOURS: ${THROTTLE_PURGE_AFTER_HOURS:-24}
|
||||
TZ: ${CRON_TIMEZONE:-Europe/Paris}
|
||||
|
||||
volumes:
|
||||
# Bind-mount vers l'hote pour les dumps : inspectables par ls, scp-able
|
||||
# hors docker. Le dossier ./var/backups est gitignore.
|
||||
- ./var/backups:/backups
|
||||
|
||||
networks:
|
||||
- wakdo_internal
|
||||
|
||||
depends_on:
|
||||
wakdo-db:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
# Wakdo - vhosts applicatifs
|
||||
#
|
||||
# Un seul conteneur Apache derriere Traefik sert **les deux** FQDN :
|
||||
# - TRAEFIK_DOMAIN_KIOSK -> /var/www/html/public/borne (borne client, Bloc 1)
|
||||
# - TRAEFIK_DOMAIN_ADMIN -> /var/www/html/public/admin (back-office + API, Bloc 2)
|
||||
# - APP_HOST_KIOSK -> /var/www/html/public/borne (borne client, Bloc 1)
|
||||
# - APP_HOST_ADMIN -> /var/www/html/public/admin (back-office + API, Bloc 2)
|
||||
#
|
||||
# Comme Traefik termine TLS en amont et communique en HTTP clair avec Apache
|
||||
# sur le reseau docker, les vhosts ecoutent sur :80 et font confiance aux
|
||||
|
|
@ -39,8 +39,8 @@
|
|||
|
||||
# === Borne client (Bloc 1 - front vanilla HTML/CSS/JS) ===
|
||||
<VirtualHost *:80>
|
||||
# Hostname injecte par la var d'env TRAEFIK_DOMAIN_KIOSK au runtime.
|
||||
ServerName ${TRAEFIK_DOMAIN_KIOSK}
|
||||
# Hostname injecte par la var d'env APP_HOST_KIOSK au runtime.
|
||||
ServerName ${APP_HOST_KIOSK}
|
||||
|
||||
DocumentRoot "/var/www/html/public/borne"
|
||||
|
||||
|
|
@ -55,7 +55,10 @@
|
|||
|
||||
# SPA-like fallback : toute URL non-fichier -> index.html
|
||||
# (pour permettre de bookmarker un chemin profond dans la borne).
|
||||
# Exclusion /api/ : ces requetes sont relayees a l'API (cf. <Location /api>
|
||||
# plus bas) et ne doivent JAMAIS retomber sur index.html.
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} !^/api/
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^ index.html [L]
|
||||
|
|
@ -66,6 +69,28 @@
|
|||
Require all denied
|
||||
</Directory>
|
||||
|
||||
# === API en MEME origine (P4 - passerelle same-origin) ===
|
||||
# La borne consomme l'API publique (/api/*) sur SA PROPRE origine : ce vhost
|
||||
# relaie ces requetes au front controller admin via PHP-FPM. data.js garde
|
||||
# donc ses URLs relatives (/api/categories...) -> aucune requete cross-origin
|
||||
# cote borne -> CORS inutile pour ce parcours (le middleware reste en place
|
||||
# cote API comme defense en profondeur). SEUL /api est relaye : le back-office
|
||||
# (/login, /admin/*) n'est PAS joignable depuis l'origine borne.
|
||||
#
|
||||
# Le chemin apres host:port dans l'URL fcgi EST le SCRIPT_FILENAME envoye a
|
||||
# FPM : on le force sur le front controller admin (un .php REEL). Sans ca, FPM
|
||||
# recevrait un chemin sous le docroot borne sans extension .php et rejetterait
|
||||
# (security.limit_extensions par defaut = .php -> reponse "Access denied").
|
||||
# ProxyPassMatch intercepte des la phase translate -> le faux chemin
|
||||
# /.../borne/api/... n'est jamais calcule. REQUEST_URI (=/api/categories) et la
|
||||
# query string sont preserves -> le Router (qui lit REQUEST_URI) route correctement.
|
||||
ProxyPassMatch "^/api(/.*)?$" "fcgi://wakdo-app:9000/var/www/html/public/admin/index.php"
|
||||
# mod_proxy_fcgi derive un SCRIPT_FILENAME corrompu (prefixe proxy: + chemin
|
||||
# original colle apres index.php) -> FPM rejette (extension != .php). On force
|
||||
# la valeur sur le front controller admin (un .php REEL) ; REQUEST_URI reste
|
||||
# intact, donc le Router route correctement.
|
||||
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/var/www/html/public/admin/index.php"
|
||||
|
||||
# Compression text/html, css, js, json (Cr 1.e.8 temps de chargement).
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript \
|
||||
|
|
@ -89,7 +114,7 @@
|
|||
|
||||
# === Back-office + API REST (Bloc 2 - PHP from scratch + MVC) ===
|
||||
<VirtualHost *:80>
|
||||
ServerName ${TRAEFIK_DOMAIN_ADMIN}
|
||||
ServerName ${APP_HOST_ADMIN}
|
||||
|
||||
DocumentRoot "/var/www/html/public/admin"
|
||||
|
||||
|
|
@ -128,7 +153,7 @@
|
|||
</DirectoryMatch>
|
||||
|
||||
# CORS : l'API admin sous /api/* doit accepter les requetes venant
|
||||
# de la borne kiosk (TRAEFIK_DOMAIN_KIOSK). Wildcard interdit.
|
||||
# de la borne kiosk (APP_HOST_KIOSK). Wildcard interdit.
|
||||
# La vraie valeur vient de CORS_ALLOWED_ORIGIN dans .env, lue cote PHP.
|
||||
# Ici on pose juste les headers de prealable OPTIONS.
|
||||
<Location /api>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@
|
|||
# 03h00 : dump BDD complet, compresse et rotate (garde 14 derniers).
|
||||
0 3 * * * /scripts/backup-db.sh 2>&1
|
||||
|
||||
# 04h15 : purge de retention du journal d'audit (mlt.md 13.4, AUDIT_LOG_RETENTION_DAYS).
|
||||
15 4 * * * /scripts/purge-audit-log.sh 2>&1
|
||||
|
||||
# 04h45 : purge des compteurs de throttle sans verrou actif (mlt.md 13.5, THROTTLE_PURGE_AFTER_HOURS).
|
||||
45 4 * * * /scripts/purge-throttle.sh 2>&1
|
||||
|
||||
# Toutes les 15 min pendant la fenetre de maintenance : purge des sessions
|
||||
# PHP expirees cote BDD (pas les sessions systeme qui sont en /tmp du conteneur
|
||||
# wakdo-app, donc ephemeres par nature). A activer quand la table sessions
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@
|
|||
# - DB_USER (on utilise le user applicatif, pas root)
|
||||
# - DB_PASSWORD
|
||||
#
|
||||
# Le USER applicatif doit avoir SELECT + LOCK TABLES + SHOW VIEW sur wakdo.
|
||||
# (GRANT donnes dans les migrations a venir en P2.)
|
||||
# Le USER applicatif a un privilege restreint (moindre privilege) : DML
|
||||
# (SELECT/INSERT/UPDATE/DELETE) + SHOW VIEW, TRIGGER, LOCK TABLES sur wakdo,
|
||||
# sans DDL ni GRANT OPTION. mysqldump --single-transaction (ci-dessous) n'exige
|
||||
# que SELECT (+ SHOW VIEW/TRIGGER pour ces objets). Privileges poses par
|
||||
# db/init/10-scope-app-user.sh (volume vierge) ou manuellement (base existante).
|
||||
#
|
||||
# Exit codes :
|
||||
# 0 - backup OK
|
||||
|
|
|
|||
34
docker/cron/scripts/purge-audit-log.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Wakdo - purge de retention du journal d'audit (mlt.md 13.4).
|
||||
#
|
||||
# Supprime les lignes audit_log plus anciennes que AUDIT_LOG_RETENTION_DAYS
|
||||
# (interet legitime / tracabilite fiscale, configurable). L'imputabilite recente
|
||||
# est preservee. C'est l'unique exception documentee a l'append-only de audit_log
|
||||
# (RG-T14) : une purge de retention planifiee, jamais une mutation applicative.
|
||||
#
|
||||
# Variables d'env (injectees par docker-compose depuis .env) :
|
||||
# DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
|
||||
# AUDIT_LOG_RETENTION_DAYS (defaut 365)
|
||||
#
|
||||
# Exit codes : 0 OK | 1 env manquant/invalide | 2 requete SQL echouee
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo "[purge-audit-log $(date -Iseconds)] $*" >&2; }
|
||||
|
||||
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
|
||||
|
||||
DAYS="${AUDIT_LOG_RETENTION_DAYS:-365}"
|
||||
case "$DAYS" in
|
||||
''|*[!0-9]*) log "ERROR: AUDIT_LOG_RETENTION_DAYS non entier ('$DAYS')"; exit 1 ;;
|
||||
esac
|
||||
|
||||
if ! n="$(mariadb --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password="$DB_PASSWORD" \
|
||||
--default-character-set=utf8mb4 -N -B "$DB_NAME" \
|
||||
-e "DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL ${DAYS} DAY; SELECT ROW_COUNT();")"; then
|
||||
log "ERROR: purge audit_log a echoue"
|
||||
exit 2
|
||||
fi
|
||||
log "audit_log: ${n} ligne(s) purgee(s) (> ${DAYS} jours)"
|
||||
40
docker/cron/scripts/purge-throttle.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Wakdo - purge des compteurs de throttle sans verrou actif (mlt.md 13.5).
|
||||
#
|
||||
# Borne la croissance de login_throttle (per-IP, RG-8) et pin_throttle
|
||||
# (per-acteur, RG-T22) : supprime les lignes dont le verrou n'est plus actif
|
||||
# ET dont la derniere tentative est plus ancienne que THROTTLE_PURGE_AFTER_HOURS.
|
||||
# Les lignes servant encore un verrou actif sont conservees.
|
||||
#
|
||||
# Variables d'env (injectees par docker-compose depuis .env) :
|
||||
# DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
|
||||
# THROTTLE_PURGE_AFTER_HOURS (defaut 24)
|
||||
#
|
||||
# Exit codes : 0 OK | 1 env manquant/invalide | 2 requete SQL echouee
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo "[purge-throttle $(date -Iseconds)] $*" >&2; }
|
||||
|
||||
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
|
||||
|
||||
HOURS="${THROTTLE_PURGE_AFTER_HOURS:-24}"
|
||||
case "$HOURS" in
|
||||
''|*[!0-9]*) log "ERROR: THROTTLE_PURGE_AFTER_HOURS non entier ('$HOURS')"; exit 1 ;;
|
||||
esac
|
||||
|
||||
db() {
|
||||
mariadb --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password="$DB_PASSWORD" \
|
||||
--default-character-set=utf8mb4 -N -B "$DB_NAME" -e "$1"
|
||||
}
|
||||
|
||||
# login_throttle et pin_throttle partagent le meme predicat (mlt.md 13.5).
|
||||
for table in login_throttle pin_throttle; do
|
||||
if ! n="$(db "DELETE FROM ${table} WHERE (lockout_until IS NULL OR lockout_until < NOW()) AND last_attempt_at < NOW() - INTERVAL ${HOURS} HOUR; SELECT ROW_COUNT();")"; then
|
||||
log "ERROR: purge ${table} a echoue"
|
||||
exit 2
|
||||
fi
|
||||
log "${table}: ${n} ligne(s) purgee(s) (sans verrou actif, > ${HOURS}h)"
|
||||
done
|
||||
82
docker/cron/scripts/restore-db.sh
Normal 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
|
||||
|
|
@ -41,8 +41,31 @@ session.cookie_secure = 1
|
|||
; Persistance inter-container non necessaire : chaque session est liee a une
|
||||
; instance unique du service wakdo-app (pas de scale horizontal pour ce projet).
|
||||
|
||||
; --- Expose_php = Off : ne pas leak la version PHP dans l'entete HTTP ---
|
||||
; session.gc_maxlifetime : filet de securite cote serveur (l'idle reel est
|
||||
; pilote par l'appli via SESSION_LIFETIME_IDLE). 4h.
|
||||
session.gc_maxlifetime = 14400
|
||||
; IDs de session longs et a forte entropie (anti-prediction/fixation).
|
||||
session.sid_length = 48
|
||||
session.sid_bits_per_character = 6
|
||||
; Pas de cache navigateur sur les pages avec session (anti-fuite via cache).
|
||||
session.cache_limiter = nocache
|
||||
|
||||
; --- Durcissement general (security-by-design, cf. PROJECT_CONTEXT 19) ---
|
||||
; Expose_php = Off : ne pas leak la version PHP dans l'entete HTTP.
|
||||
expose_php = Off
|
||||
; Anti RFI/SSRF : interdire l'ouverture d'URL distantes et leur inclusion.
|
||||
allow_url_fopen = Off
|
||||
allow_url_include = Off
|
||||
; FPM : ne pas deviner le script a partir du PATH_INFO (anti exploitation
|
||||
; d'upload mal route vers l'interpreteur). Le routage passe par le front controller.
|
||||
cgi.fix_pathinfo = 0
|
||||
; Interdire le chargement dynamique d'extensions au runtime.
|
||||
enable_dl = Off
|
||||
; Ne pas inclure les arguments dans les stack traces (anti-fuite de secrets).
|
||||
zend.exception_ignore_args = On
|
||||
; Desactiver les fonctions d'execution systeme : l'appli n'en a aucun usage
|
||||
; legitime (anti-RCE en cas d'injection). Les scripts d'ops vivent cote cron/host.
|
||||
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
|
||||
|
||||
; --- OPcache (perf + stabilite) ---
|
||||
[opcache]
|
||||
|
|
|
|||
270
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Architecture — Wakdo
|
||||
|
||||
Vue d'ensemble technique du projet (borne de commande fast-food, certification RNCP 37805).
|
||||
Point d'entree pour comprendre la stack, le decoupage et les choix de conception.
|
||||
|
||||
- Scope metier, planning, mapping RNCP : `docs/PROJECT_CONTEXT.md`.
|
||||
- Modelisation detaillee (entites, operations, regles) : `docs/merise/` (dictionary, mcd, mct, mlt).
|
||||
- Decisions tracees : `docs/adr/` et `docs/journal/`.
|
||||
|
||||
**Auteur : BYAN** (formalisation ; arbitrage et validation par l'auteur du projet).
|
||||
|
||||
---
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
|
||||
Wakdo simule une borne de commande tactile de restauration rapide, avec back-office
|
||||
d'administration, workflow cuisine et API REST interne. Deux surfaces applicatives :
|
||||
|
||||
- **Borne (kiosk)** — front statique (HTML/CSS/JS vanilla ES6) servi par Apache,
|
||||
consommant des donnees (JSON statique en P5, API DB-backed au swap P4).
|
||||
- **Back-office + API** — application PHP rendue serveur (MVC maison) + endpoints
|
||||
`/api/*`, derriere authentification et RBAC.
|
||||
|
||||
Trois canaux de commande (`source`) : `kiosk`, `counter`, `drive`. Le cycle de vie
|
||||
d'une commande et la machine a etats sont decrits dans `docs/merise/` (domaine
|
||||
commande = phase **P4**, schema en base mais workflow applicatif a venir).
|
||||
|
||||
---
|
||||
|
||||
## 2. Stack technique
|
||||
|
||||
| Couche | Techno | Note |
|
||||
|---|---|---|
|
||||
| Langage back | PHP 8.3 | from scratch, sans framework |
|
||||
| Autoloader | PSR-4 manuel (`spl_autoload_register`) | namespace `App\` -> `src/app/` |
|
||||
| Base de donnees | MariaDB 11.4 | PDO, requetes preparees uniquement |
|
||||
| Serveur web | Apache httpd 2.4 (Alpine) | reverse FastCGI -> PHP-FPM |
|
||||
| Serveur app | PHP-FPM 8.3 (Alpine) | execute le code back-office + API |
|
||||
| Front borne | HTML5 + CSS3 + JS ES6 (modules) | vanilla, sans build |
|
||||
| Conteneurisation | Docker + docker compose v2 | `docker compose up` = stack complete |
|
||||
| Tests PHP | PHPUnit 11 (`.phar`, sans Composer) | unit + integration DB |
|
||||
| Tests front | node:test + jsdom | harnais kiosk (`tests/js/`) |
|
||||
| Analyse statique | PHPStan niveau 6 (`.phar`) | |
|
||||
| CI/CD | Forgejo Actions | secret-scan, lint, tests ; merge natif sur CI verte |
|
||||
| Versioning | Git + Forgejo (`git.acadenice.com`, miroir GitHub) | Conventional Commits |
|
||||
|
||||
Justifications (composer-less, from-scratch, etc.) : `docs/PROJECT_CONTEXT.md` section 6.
|
||||
|
||||
---
|
||||
|
||||
## 3. Topologie de deploiement
|
||||
|
||||
Cinq services Docker. Deux modes, par fichier compose :
|
||||
|
||||
- **`docker-compose.yml`** (versionne) — standalone : tourne en local sans configuration.
|
||||
`wakdo-web` publie un port hote (`${HTTP_PORT:-8080}`), reseau interne seul.
|
||||
- **`docker-compose.prod.yml`** (gitignore, propre a chaque hote) — meme stack exposee
|
||||
via un reverse proxy Traefik (reseau externe + labels TLS), sans port hote.
|
||||
|
||||
```
|
||||
[ docker compose up -d ]
|
||||
|
|
||||
wakdo-db (MariaDB 11.4, healthcheck)
|
||||
| service_healthy
|
||||
v
|
||||
wakdo-migrate (one-shot : migrations + seed idempotents, puis sort)
|
||||
| service_completed_successfully
|
||||
+---------------+----------------+
|
||||
v v
|
||||
wakdo-app (PHP-FPM 8.3) wakdo-web (Apache)
|
||||
^ FastCGI :9000 <-----------/ publie ${HTTP_PORT}:80 (mode local)
|
||||
| ou labels Traefik (mode prod)
|
||||
|
|
||||
wakdo-db <-- PDO
|
||||
|
||||
wakdo-cron (dcron) : backup BDD + purges retention (RGPD)
|
||||
```
|
||||
|
||||
- **Reseau** : `wakdo_internal` (bridge) isole les services ; aucun port hote en mode
|
||||
prod (acces par le proxy). En mode local, seul `wakdo-web` publie un port.
|
||||
- **Volumes** : `wakdo_db_data` (persistance MariaDB), `wakdo_uploads` (images produits) ;
|
||||
bind-mount `./var/backups` pour les dumps.
|
||||
- **`wakdo-cron`** utilise `init: true` (tini comme PID 1 : dcron a besoin d'un init
|
||||
parent pour `setpgid` sur ses jobs).
|
||||
- Choix d'un **subnet RFC 1918 explicite** sur `wakdo_internal` cote prod : l'hote
|
||||
mutualise a un allocateur Docker sature ; le subnet evite l'echec d'allocation auto.
|
||||
|
||||
Detail reseaux/volumes : `docs/PROJECT_CONTEXT.md` section 5.
|
||||
|
||||
---
|
||||
|
||||
## 4. Demarrage : une commande (Cr 7.c.4)
|
||||
|
||||
`docker compose up -d` amene une stack complete et utilisable :
|
||||
|
||||
1. `wakdo-db` demarre, devient *healthy* (script `healthcheck.sh` de l'image).
|
||||
2. `wakdo-migrate` (service one-shot) applique, par le reseau et de maniere
|
||||
**idempotente** :
|
||||
- `db/migrations/*.sql` — suivi dans la table `schema_migrations` ;
|
||||
- `db/seeds/*.sql` — suivi dans la table `seeds_applied`.
|
||||
Relancer ne rejoue que les fichiers en attente. Le runner : `db/migrate-container.sh`.
|
||||
3. `wakdo-app` et `wakdo-web` attendent la **completion** de `wakdo-migrate`
|
||||
(`depends_on: service_completed_successfully`) avant de servir.
|
||||
|
||||
Le schema (DDL) et les donnees de reference (roles, permissions, catalogue, admin
|
||||
bootstrap) sont donc en place sans etape manuelle. `db/migrate.sh` (hote, via
|
||||
`docker exec`) reste disponible pour l'usage manuel / `--status`.
|
||||
|
||||
> Migration de mecanisme : sur une base **deja seedee** avant l'introduction du suivi
|
||||
> (`seeds_applied` absente), back-filler la table avant le premier `up` (sinon re-seed
|
||||
> -> conflits d'unicite). Volume vierge : aucun souci. Cf.
|
||||
> `docs/journal/2026-06-17--makefile-to-compose-migrate.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Structure du code
|
||||
|
||||
Namespace `App\` -> `src/app/` (PSR-4 manuel). Front controller du vhost admin :
|
||||
`src/public/admin/index.php` (Apache reecrit tout vers ce fichier ; le routeur voit
|
||||
le `REQUEST_URI` intact).
|
||||
|
||||
```
|
||||
src/app/
|
||||
Core/ Autoloader, Config, Database (PDO), Request, Response, Router
|
||||
Auth/ AuthService, SessionManager, SessionGuard, Authorizer, PinVerifier,
|
||||
PinThrottle, ThrottlePolicy, PasswordHasher, Csrf, PasswordResetService,
|
||||
UserRepository, RoleRepository, UserDirectory, Mailer/LogMailer
|
||||
Catalogue/ Category / Product / Menu / Ingredient / Stats Repository
|
||||
Controllers/ Admin (base), Authenticated (base), Auth, PasswordReset, Profile, Me,
|
||||
Dashboard, Stats, Category, Product, Menu, Ingredient, User, Role,
|
||||
Health, Home
|
||||
Views/ admin/* (pages back-office rendues serveur), auth/* (login/reset)
|
||||
src/public/
|
||||
admin/ front controller + assets (CSS/JS) du back-office
|
||||
borne/ front kiosk statique (index, categories, products, product, cart,
|
||||
payment, confirmation) + assets JS modules + data JSON
|
||||
```
|
||||
|
||||
Conventions transverses : controleurs non-`final` (seam de test : sous-classe injectant
|
||||
des doubles via `db()` / `sessionManager()`) ; repository sur `DatabaseInterface` ;
|
||||
chaque mutation passe par CSRF + validation serveur + allowlist (voir section 7).
|
||||
|
||||
---
|
||||
|
||||
## 6. Flux d'une requete back-office
|
||||
|
||||
```
|
||||
Navigateur --(HTTPS via Traefik | HTTP local)--> wakdo-web (Apache)
|
||||
| vhost par ServerName (APP_HOST_KIOSK -> public/borne, APP_HOST_ADMIN -> public/admin)
|
||||
| PHP -> FastCGI :9000
|
||||
v
|
||||
wakdo-app (PHP-FPM) : src/public/admin/index.php
|
||||
| Router (methode + chemin) -> [Controller, action]
|
||||
v
|
||||
Controller (extends AdminController)
|
||||
| guard(permission) -> SessionGuard (RG-6/RG-T02 : session valide ?)
|
||||
| + Authorizer::can(role, permission) (RG-T03, recharge DB)
|
||||
| (mutation) Csrf::validate + validation serveur (RG-T18) + allowlist (RG-T16)
|
||||
| (action sensible) PinVerifier + throttle, audit_log dans la meme transaction
|
||||
v
|
||||
Repository -> PDO (prepared) -> MariaDB
|
||||
|
|
||||
v
|
||||
Vue rendue dans admin/layout (sorties echappees, RG-T15) | ou JSON pour /api/*
|
||||
```
|
||||
|
||||
La borne (kiosk) est servie en statique par Apache ; ses pages consomment les donnees
|
||||
via `fetch` (JSON statique en P5 ; bascule sur `/api/*` DB-backed au swap P4).
|
||||
|
||||
---
|
||||
|
||||
## 7. Securite (security-by-design)
|
||||
|
||||
Couche transverse, regles `RG-T*` definies dans `docs/merise/mlt.md`. Synthese :
|
||||
|
||||
- **Authentification** : mot de passe hache **argon2id** (cout configurable, defauts
|
||||
OWASP) ; sessions PHP avec regeneration d'ID au login, idle 4h + absolu 10h ; cookie
|
||||
nomme `WAKDO_SID`.
|
||||
- **RBAC** : `Authorizer::can(role_id, permission_code)` teste une **permission** (pas
|
||||
un nom de role), rechargee depuis la base a chaque verification. 5 roles seedes, 23
|
||||
permissions figees, matrice `role_permission` editable (back-office, voir domaine 10).
|
||||
- **PIN d'action sensible (RG-T13)** : les operations sensibles (annulation, prix/TVA,
|
||||
suppressions, inventaire, gestion utilisateur, RBAC, effacement PII) exigent une
|
||||
re-autorisation par PIN equipier (argon2id). L'`acting_user_id` resolu par le PIN est
|
||||
ecrit dans `audit_log` (RG-T14) dans la **meme transaction** que l'effet (RG-T08). Les
|
||||
operations de stock tracent via `stock_movement.user_id` (pas de double-journal).
|
||||
- **Throttling** (backoff degressif, pas de verrou definitif) :
|
||||
- login par compte (`user.failed_login_attempts` / `lockout_until`) + par IP
|
||||
(`login_throttle`, RG-8/9) ;
|
||||
- PIN d'action sensible (`pin_throttle`, RG-T22) — compteur **separe** du login, par
|
||||
utilisateur agissant.
|
||||
- **Entrees / sorties** : validation serveur bornee (RG-T18) ; allowlist d'affectation
|
||||
de masse (RG-T16, empeche d'injecter `role_id`/`price_cents`/`is_active`...) ; toutes
|
||||
les sorties HTML echappees (RG-T15) ; front borne CSP-safe (pas de script inline cote
|
||||
code projet).
|
||||
- **Conventions HTTP** : conflit d'etat (unicite, FK RESTRICT) -> **409** ; validation
|
||||
qui echoue -> **422** ; CSRF/permission -> **403**.
|
||||
- **RGPD** : anonymisation (mlt 10.5) qui conserve la ligne (tombstone) pour preserver
|
||||
les FK et la trace d'audit, en vidant la PII ; purges de retention par `wakdo-cron`
|
||||
(audit_log, throttle, sessions, commandes).
|
||||
- **Isolation** : pas de port hote en mode prod (acces par le proxy) ; user applicatif
|
||||
MariaDB en moindre privilege (DDL reserve au runner migrate root ; cf.
|
||||
`db/init/10-scope-app-user.sh`).
|
||||
|
||||
Threat model STRIDE + classification des donnees : `docs/PROJECT_CONTEXT.md` section 19.
|
||||
|
||||
---
|
||||
|
||||
## 8. Modele de donnees
|
||||
|
||||
22 tables (DDL `db/migrations/`), regroupees par domaine :
|
||||
|
||||
- **Catalogue** : `category`, `product`, `menu`, `menu_slot`, `menu_slot_option`,
|
||||
`ingredient`, `product_ingredient`, `allergen`, `ingredient_allergen`, `stock_movement`.
|
||||
- **RBAC / comptes** : `user`, `role`, `permission`, `role_permission`,
|
||||
`role_visible_source`.
|
||||
- **Commande (P4, schema pret)** : `customer_order`, `order_item`,
|
||||
`order_item_selection`, `order_item_modifier`.
|
||||
- **Transverses** : `audit_log` (journal immuable), `login_throttle`, `pin_throttle`.
|
||||
|
||||
Quelques derivations **calculees, non stockees** :
|
||||
|
||||
- **Stock en pourcentage** (mcd 5.3) : `stock_pct = round(stock_quantity / stock_capacity
|
||||
* 100)` ; 3 bandes (normal / alerte / critique) selon `low_stock_pct` /
|
||||
`critical_stock_pct`. `stock_quantity` est signe (survente assumee).
|
||||
- **Disponibilite produit (RG-T21)** : un produit est commandable si `is_available = 1`
|
||||
ET chaque ingredient non retirable de sa composition est au-dessus de la bande
|
||||
critique. Pas de cascade ni de colonne stockee.
|
||||
- **`service_day`** : journee de service (coupure a 10:00) pour les agregations stats,
|
||||
expression SQL non materialisee.
|
||||
|
||||
MCD / MLD / dictionnaire : `docs/merise/`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Tests & qualite
|
||||
|
||||
- **PHPUnit** (`.phar`, sans Composer) : tests *unit* (controleurs via double
|
||||
`FakeDatabase`, logique pure) + *integration* contre une vraie MariaDB (auto-skip si
|
||||
`WAKDO_DB_TESTS != 1`). Lancement :
|
||||
`docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml`.
|
||||
- **Front borne** : `node --test` + jsdom (`tests/js/`).
|
||||
- **PHPStan niveau 6** (`.phar`).
|
||||
- **CI Forgejo Actions** (`.forgejo/workflows/ci.yml`) : `secret-scan` (gitleaks),
|
||||
`php-lint`, `static-tests` (PHPStan + PHPUnit avec service MariaDB ephemere migre +
|
||||
seede), `js-tests` (Node 20). Fusion par auto-merge NATIF Forgejo (squash,
|
||||
`merge_when_checks_succeed`) des que les checks requis sont verts — pas de job de merge.
|
||||
- **Branch protection** : `dev` et `main` proteges (PR requise, force-push bloque,
|
||||
checks requis).
|
||||
|
||||
Pyramide visee : Unit > Integration > E2E. Les tests E2E navigateur (Playwright) sont
|
||||
une initiative a venir.
|
||||
|
||||
---
|
||||
|
||||
## 10. Methodologie & tracabilite
|
||||
|
||||
Projet developpe avec l'appui de **BYAN** (agents IA custom, Merise Agile + 64 mantras)
|
||||
et d'outils d'IA generative, conformement a l'autorisation du centre de formation.
|
||||
|
||||
- Decisions d'architecture, scope et design : prises par l'auteur.
|
||||
- Code, tests, doc : co-rediges et valides par l'auteur avant commit.
|
||||
- **Pas de trailer `Co-Authored-By`** sur les commits : la transparence vit dans le
|
||||
README et `docs/PROJECT_CONTEXT.md` section 17, pas dans les metadonnees git.
|
||||
- Tracabilite : `docs/journal/` (retros par session et par feature).
|
||||
|
||||
---
|
||||
|
||||
*Document vivant — mis a jour au fil de l'implementation. Source de verite scope/RNCP :
|
||||
`docs/PROJECT_CONTEXT.md`.*
|
||||
144
docs/DEVELOPER.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Guide developpeur — Wakdo
|
||||
|
||||
Comment lancer, tester et contribuer. Pour l'architecture (stack, services, modele,
|
||||
securite), voir `docs/ARCHITECTURE.md`. Pour le scope et le mapping RNCP,
|
||||
`docs/PROJECT_CONTEXT.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequis
|
||||
|
||||
- Docker Engine + docker compose v2 (https://docs.docker.com/engine/install/).
|
||||
- Node 20+ (uniquement pour les tests front borne ; pas requis pour faire tourner l'app).
|
||||
|
||||
Aucune installation de PHP / Composer / PHPUnit sur l'hote : tout passe par les
|
||||
conteneurs et des `.phar` autonomes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Lancer en local
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
- Borne : http://kiosk.localhost:8080
|
||||
- Admin + API : http://admin.localhost:8080
|
||||
|
||||
`*.localhost` resout vers `127.0.0.1`. Changer le port via `HTTP_PORT` dans `.env`.
|
||||
Le `.env.example` fonctionne tel quel en local (valeurs dev). Au boot, le service
|
||||
`wakdo-migrate` applique migrations + seed (admin bootstrap inclus) avant que l'app
|
||||
ne serve.
|
||||
|
||||
Commandes utiles :
|
||||
|
||||
```bash
|
||||
docker compose ps # etat des services
|
||||
docker compose logs -f wakdo-app # logs PHP-FPM
|
||||
docker compose down # arret (volumes preserves)
|
||||
docker compose down -v # arret + suppression des donnees
|
||||
```
|
||||
|
||||
Deploiement derriere un reverse proxy : voir le `README.md` (section prod) +
|
||||
`docker-compose.prod.yml` (gitignore, propre a l'hote).
|
||||
|
||||
---
|
||||
|
||||
## 3. Base de donnees : migrations & seed
|
||||
|
||||
- `db/migrations/*.sql` (DDL) et `db/seeds/*.sql` (donnees de reference) sont appliques
|
||||
de maniere **idempotente** par `wakdo-migrate` (suivi `schema_migrations` /
|
||||
`seeds_applied`). Relancer `docker compose up` ne rejoue que les fichiers en attente.
|
||||
- **Ajouter une migration** : creer `db/migrations/000N_description.sql` (ordre
|
||||
lexicographique). Appliquee au prochain `docker compose up`, ou a la main :
|
||||
|
||||
```bash
|
||||
bash db/migrate.sh # applique les migrations en attente (hote)
|
||||
bash db/migrate.sh --status # liste l'etat sans rien appliquer
|
||||
```
|
||||
|
||||
- Idem pour un seed : `db/seeds/000N_description.sql`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tests & analyse statique
|
||||
|
||||
Les tests PHP tournent dans l'image applicative (PHPUnit `.phar`). La stack doit etre
|
||||
demarree pour les tests d'integration (ils ciblent le service `wakdo-db`).
|
||||
|
||||
```bash
|
||||
# Tests unitaires (sans base)
|
||||
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app \
|
||||
php phpunit.phar -c phpunit.xml --testsuite unit
|
||||
|
||||
# Tests d'integration (vraie MariaDB ; auto-skip si WAKDO_DB_TESTS != 1)
|
||||
docker run --rm --network wakdo_wakdo_internal --env-file .env -e WAKDO_DB_TESTS=1 \
|
||||
-v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml
|
||||
|
||||
# Analyse statique PHPStan niveau 6
|
||||
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app \
|
||||
php -d memory_limit=512M phpstan.phar analyse -c phpstan.neon --no-progress
|
||||
```
|
||||
|
||||
Tests front borne (Node + jsdom) :
|
||||
|
||||
```bash
|
||||
npm install # une fois (devDependency jsdom)
|
||||
npm run test:js # node --test tests/js/
|
||||
```
|
||||
|
||||
> Le nom de reseau `wakdo_wakdo_internal` et l'image `wakdo-wakdo-app` derivent du
|
||||
> nom de projet compose (`name: wakdo`). Les `.phar` (phpunit, phpstan) sont
|
||||
> gitignores ; les retelecharger si absents (voir `docs/journal/`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Conventions de code
|
||||
|
||||
- **PSR-4 manuel** : namespace `App\` -> `src/app/`. Pas de framework.
|
||||
- **Controleurs** : non-`final` (seam de test ; les tests sous-classent et injectent
|
||||
des doubles via `db()` / `sessionManager()`). Heritent de `AdminController`
|
||||
(back-office) ou `AuthenticatedController`.
|
||||
- **Acces donnees** : un repository par entite, dependant de `DatabaseInterface`
|
||||
(PDO en prod, `FakeDatabase` en test). Requetes preparees uniquement.
|
||||
- **Mutations** : CSRF (`Csrf::validate`) + validation serveur bornee (RG-T18) +
|
||||
allowlist de colonnes (RG-T16). Sorties HTML echappees (RG-T15).
|
||||
- **Actions sensibles** : PIN equipier (`PinVerifier`) + `audit_log` dans la meme
|
||||
transaction ; throttle PIN (`PinThrottle`). Voir `docs/ARCHITECTURE.md` section 7.
|
||||
- **Statuts HTTP** : conflit -> 409 ; validation -> 422 ; CSRF/permission -> 403.
|
||||
- **Pas d'emoji** dans le code, les commits, les specs (Mantra IA-23).
|
||||
|
||||
Detail par entite : `docs/merise/` et `docs/domaines/` (a venir).
|
||||
|
||||
---
|
||||
|
||||
## 6. Git & CI
|
||||
|
||||
- **Conventional Commits** (anglais) : `type(scope): description` — types `feat`, `fix`,
|
||||
`docs`, `refactor`, `test`, `chore`, `ci`, `db`, `perf`, `style`.
|
||||
- **Branches** depuis `dev` : `feat/*`, `fix/*`, `docs/*`, `chore/*`, `ci/*`, `db/*`,
|
||||
`refactor/*`, `test/*`. Merge vers `dev` par **PR squashee**. Periodiquement
|
||||
`dev -> main` avec tag semver.
|
||||
- **Auto-merge** : l'ouverture de la PR programme la fusion squash automatique des que
|
||||
les checks requis passent (auto-merge NATIF Forgejo `merge_when_checks_succeed`, sans
|
||||
label ni job CI). Script : `scripts/forgejo-pr-automerge.sh`.
|
||||
- **Pas de trailer `Co-Authored-By`** : la transparence sur l'usage de l'IA vit dans le
|
||||
`README.md` et `docs/PROJECT_CONTEXT.md` section 17.
|
||||
|
||||
---
|
||||
|
||||
## 7. Ou trouver quoi
|
||||
|
||||
| Besoin | Emplacement |
|
||||
|---|---|
|
||||
| Architecture, stack, securite, modele | `docs/ARCHITECTURE.md` |
|
||||
| Scope metier, planning, mapping RNCP | `docs/PROJECT_CONTEXT.md` |
|
||||
| Modelisation Merise (dictionnaire, MCD/MCT/MLT, regles RG-T*) | `docs/merise/` |
|
||||
| Decisions d'architecture (le pourquoi) | `docs/adr/` |
|
||||
| Retros par session / feature | `docs/journal/` |
|
||||
| Methodologie agents | `.claude/CLAUDE.md` + `.claude/rules/` |
|
||||
|
||||
---
|
||||
|
||||
*Document vivant — mis a jour au fil de l'implementation.*
|
||||
|
|
@ -26,12 +26,19 @@ Wakdo est une **borne de commande tactile** pour un restaurant de restauration r
|
|||
|
||||
### Acteurs
|
||||
|
||||
| Acteur | Role | Interface |
|
||||
| Acteur | Role RBAC | Interface |
|
||||
|---|---|---|
|
||||
| **Client** | Passe sa commande sur la borne | Borne tactile (Bloc 1) |
|
||||
| **Accueil** | Saisit commandes au **comptoir** (client au guichet) ou au **drive** (client en voiture via intercom + casque equipier), remet les commandes livrees aux clients | Back-office (Bloc 2) |
|
||||
| **Preparation** | Voit les commandes a preparer triees par heure croissante, les declare "preparees" | Back-office (Bloc 2) |
|
||||
| **Administration** | CRUD sur donnees (produits, menus, prix, images) + gestion utilisateurs + stats | Back-office (Bloc 2) |
|
||||
| **Client** | (non authentifie) | Borne tactile (Bloc 1, canal `kiosk`) |
|
||||
| **Counter** | `counter` | Back-office : saisit les commandes au **comptoir**, les remet au client, peut annuler |
|
||||
| **Drive** | `drive` | Back-office : saisit les commandes au **drive** (intercom + casque), les remet, peut annuler |
|
||||
| **Kitchen** | `kitchen` | Back-office : voit la file des commandes `paid` triees par `paid_at` croissant, en **lecture seule** (KDS visuel, aucune transition) |
|
||||
| **Manager** | `manager` | Back-office : catalogue (create/update), stock/reappro, statistiques |
|
||||
| **Administration** | `admin` | Back-office : catalogue complet (+ suppressions), gestion utilisateurs, roles et permissions (RBAC), stats |
|
||||
|
||||
> Modele v0.2 : 5 roles RBAC (`admin`, `manager`, `kitchen`, `counter`, `drive`)
|
||||
> + Customer non authentifie. RBAC permission-driven (le code teste une
|
||||
> permission, pas un nom de role) ; catalogue de 23 permissions fige au seed.
|
||||
> Voir `docs/merise/dictionary.md` 3.15-3.18 et `docs/uml/use-cases.md`.
|
||||
|
||||
### Processus metier cle
|
||||
|
||||
|
|
@ -46,21 +53,21 @@ Client Borne (Bloc 1) API (Bloc 2) BDD
|
|||
│ │─POST /api/orders─────▶│───INSERT──────────▶│
|
||||
│ │◀──────────201─────────│ │
|
||||
│─recupere au comptoir │ │ │
|
||||
Preparation voit commande pending
|
||||
→ declare "preparee"
|
||||
Accueil voit commande prete
|
||||
→ declare "livree"
|
||||
Kitchen voit la file des commandes paid (lecture seule, KDS)
|
||||
Counter / Drive remettent au client
|
||||
→ declarent "livree" (geste unique paid -> delivered)
|
||||
```
|
||||
|
||||
### Regles metier (MCT - a modeliser en Merise)
|
||||
|
||||
- Un **menu** = burger + accompagnement (frites OU salade) + boisson + sauce
|
||||
- Les **accompagnements** et **boissons** ont **2 tailles** (normale / grande)
|
||||
- **Grande taille** = +0,50 € sur le prix de base
|
||||
- Une **commande** a un **numero** saisi par le client (remplace le paiement dans le cadre de l'exam)
|
||||
- Statuts commande : `pending` -> `preparing` -> `ready` -> `delivered` (ou `cancelled`)
|
||||
- Un **menu** = burger fixe + slots a choix (boisson, accompagnement, sauce). Modele relationnel `menu_slot` + `menu_slot_option` (voir `dictionary.md` 3.4-3.5)
|
||||
- Format **Normal / Maxi** au niveau du menu (deux prix : `price_normal_cents`, `price_maxi_cents`) ; le Maxi agrandit accompagnement + boisson uniquement
|
||||
- **Personnalisation des ingredients** (retirer = gratuit, ajouter = supplement) sur les sandwichs composes, via le configurateur (`ingredient`, `product_ingredient`, `order_item_modifier`)
|
||||
- **TVA portee par le produit** (`vat_rate` : 10% defaut, 5,5% contenant conservable), calculee ligne par ligne et snapshotee sur `order_item` (fact-check BOFiP, voir `dictionary.md` note 9)
|
||||
- Une **commande** a un **numero** saisi par le client, prefixe par canal `K`/`C`/`D` (remplace le paiement dans le cadre de l'exam)
|
||||
- Statuts commande (machine a **4 etats**) : `pending_payment` -> `paid` -> `delivered` (+ `cancelled`). La transition `pending_payment -> paid` est **atomique** a la creation (saisie du numero = substitut de paiement). `cancelled` est atteignable depuis `pending_payment` et `paid` (pas depuis `delivered`). Plus de `preparing` / `ready` : la cuisine est en lecture seule, la remise est un geste unique
|
||||
- **Source commande** (trace sur chaque commande) : `kiosk` (borne autonome) | `counter` (comptoir) | `drive` (drive-thru)
|
||||
- La preparation voit les commandes triees par **heure de livraison croissante** (tous canaux confondus)
|
||||
- Le canal de prepa (`kitchen`/`counter`/`drive`) voit la file des commandes `paid` triee par `paid_at` **croissant**, filtree par `role_visible_source` (kitchen voit tout ; counter voit kiosk+counter ; drive voit drive)
|
||||
- **Horaires service** : 10h00 → 01h00 du matin (service continu 15h, pas de fermeture intermediaire)
|
||||
- **Pas de notion de "session de service" a modeliser** : les equipiers se relaient, chacun se connecte a sa prise de poste et se deconnecte a la fin. Pas de "shift" a tracer dans la BDD (hors scope RNCP)
|
||||
- **Fenetre de maintenance systeme** : 01h30 → 09h30 (crons lourds, backups, agregations) — evite toute interference avec le service actif
|
||||
|
|
@ -106,13 +113,13 @@ Client Borne (Bloc 1) API (Bloc 2) BDD
|
|||
|
||||
**Un seul codebase, deux FQDN d'exposition publique.** Le front Bloc 1 et le back Bloc 2 coexistent dans la meme arborescence. Une bascule mode JSON-seuls (Bloc 1 isole) vs mode API-connecte doit rester possible via configuration.
|
||||
|
||||
**Pourquoi pas strategie A (deux rendus isoles)** : le Bloc 5 DevOps impose une conteneurisation **unique** qui lance la stack complete avec `make init` en une commande (Cr 7.c.4). Deux codebases isolees seraient incoherentes avec cette exigence.
|
||||
**Pourquoi pas strategie A (deux rendus isoles)** : le Bloc 5 DevOps impose une conteneurisation **unique** qui lance la stack complete avec `docker compose up` en une commande (Cr 7.c.4). Deux codebases isolees seraient incoherentes avec cette exigence.
|
||||
|
||||
### Compatibilite evaluation par bloc
|
||||
|
||||
- **Jury Bloc 1** : voit le front seul ; le front peut tomber en fallback sur JSON statiques fournis (`src/public/borne/data/*.json`) si l'API est indisponible.
|
||||
- **Jury Bloc 2** : voit le back-office + teste l'API via curl/Postman de maniere autonome, sans dependre du front.
|
||||
- **Jury Bloc 5** : lance `make init` ou `docker compose up`, verifie la CI/CD, les crons, l'archi, les scripts.
|
||||
- **Jury Bloc 5** : lance `docker compose up` ou `docker compose up`, verifie la CI/CD, les crons, l'archi, les scripts.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -135,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
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────────────┐
|
||||
|
|
@ -186,10 +193,10 @@ Reseaux :
|
|||
| Reverse proxy | Traefik (deja en place) | existant | `admin_proxy` network |
|
||||
| TLS | Let's Encrypt via Traefik | auto | `acme.json` existant |
|
||||
| Conteneurisation | Docker + docker compose | v2 | Cr 7.c |
|
||||
| Orchestration locale | Makefile | — | Cr 7.b (script) + Cr 7.c.4 (une commande) |
|
||||
| CI/CD | GitHub Actions | — | Cr 7.d |
|
||||
| Versioning | Git + GitHub | — | Cr 4.f (collaboration) |
|
||||
| Hooks Git | pre-commit + commit-msg | versionnes dans `.githooks/` | Conventional Commits |
|
||||
| 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 (refus main/dev + php -l) + commit-msg (format Conventional Commits) | versionnes dans `.githooks/`, actives via `scripts/install-hooks.sh` | Conventional Commits |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -220,10 +227,11 @@ Reseaux :
|
|||
|
||||
**IN scope — Back-office :**
|
||||
- Authentification sessions securisees (hash bcrypt/argon2, protection CSRF, fixation session) — duree de session adaptee a un poste complet d'equipier (idle timeout 4h, absolute timeout 10h)
|
||||
- 3 roles RBAC : `admin`, `preparation`, `accueil`
|
||||
- **Admin** : CRUD categories, produits (nom, description, prix, image, dispo), menus (composition + options), utilisateurs
|
||||
- **Preparation** : liste commandes a preparer triees par heure livraison croissante, bouton "declarer preparee"
|
||||
- **Accueil** : saisir commande manuellement (comptoir ou drive-thru via casque/intercom), bouton "declarer livree" ; champ `source` enregistre sur chaque commande (`counter` ou `drive`)
|
||||
- 5 roles RBAC seed : `admin`, `manager`, `kitchen`, `counter`, `drive` (RBAC permission-driven, 23 permissions figees au seed ; roles personnalises possibles)
|
||||
- **Admin** : CRUD complet catalogue (+ suppressions), gestion utilisateurs, roles et permissions (RBAC), stats
|
||||
- **Manager** : catalogue (create/update), stock (reappro + inventaire), statistiques ; utilisateurs en **lecture seule** (`user.read`, pas de creation/modification/desactivation), pas d'acces RBAC
|
||||
- **Kitchen** : file des commandes `paid` triee par `paid_at` croissant, en **lecture seule** (KDS visuel) ; inventaire
|
||||
- **Counter** / **Drive** : saisir une commande (comptoir / drive-thru via casque/intercom), bouton "declarer livree" (geste unique `paid -> delivered`), annuler ; `source` auto-tague depuis `role.order_source` ; inventaire
|
||||
- Upload images produits (validation type MIME + taille + stockage dans volume `wakdo_uploads`)
|
||||
- Historique commandes par statut
|
||||
- Stats de base (commandes du jour, CA jour, produits top)
|
||||
|
|
@ -254,16 +262,18 @@ Reseaux :
|
|||
|
||||
**IN scope :**
|
||||
- Dockerfile custom PHP-FPM avec extensions
|
||||
- `docker-compose.yml` orchestrant les 4 services (web, app, db, cron)
|
||||
- `Makefile` avec cible `make init` qui lance tout 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) :
|
||||
- `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, 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 GitHub Actions** : lint PHP + PHPUnit sur PR -> dev
|
||||
- **CD GitHub Actions** : deploy auto sur merge main (SSH + pull + `make rebuild`)
|
||||
- `.env.example` documente, secrets hors du repo
|
||||
- `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` : recupere `main` depuis Forgejo puis `docker compose build --pull && up -d` -- les images wakdo sont buildees localement depuis les Dockerfiles, pas de registre). Choix solo dev sur un environnement de prod unique. L'automatisation visee est **pull-based** : un job cron cote hote qui detecte un nouveau `main` et lance `deploy.sh` (a armer ensuite, reutilise le meme script)
|
||||
- `.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
|
||||
- Logs centralises (stdout des conteneurs)
|
||||
- Documentation deploiement + architecture (schemas dans `docs/`)
|
||||
|
|
@ -294,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.3 | Exploiter donnees externes d'API | API interne consommee par le front (auto-consommation) |
|
||||
| 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 |
|
||||
|
|
@ -319,16 +329,16 @@ Reseaux :
|
|||
| Critere | Libelle court | Feature Wakdo couvrant |
|
||||
|---|---|---|
|
||||
| Cr 7.a.1-3 | Analyse infra + securite | Audit code + proposition automatisation documentee |
|
||||
| Cr 7.b.1 | Langage de script | Bash (deploy, backup) + Makefile |
|
||||
| Cr 7.b.2 | Automatisation fiabilisee | Makefile avec exit codes, retries, logs |
|
||||
| Cr 7.b.1 | Langage de script | Bash (db/*.sh migrate/seed, scripts/forgejo-*.sh, entrypoints, backup) |
|
||||
| Cr 7.b.2 | Automatisation fiabilisee | Scripts Bash (set -euo pipefail, exit codes, logs) + service compose wakdo-migrate idempotent |
|
||||
| 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.4 | **Une ligne de commande** | `make init` lance toute la stack + migrate + seed |
|
||||
| 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 + lint sur PR |
|
||||
| Cr 7.d.3 | Integration/deploiement continus | GitHub Actions deploy automatique sur merge main |
|
||||
| Cr 7.d.2 | Tests avant deploy | CI PHPUnit + PHPStan + secret-scan sur PR (Forgejo Actions) |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -344,13 +354,13 @@ main ← production (tag vX.Y.Z sur chaque release)
|
|||
fix/* ← corrections
|
||||
refactor/* ← refactos
|
||||
docs/* ← doc seulement
|
||||
ci/* ← GitHub Actions
|
||||
ci/* ← Forgejo Actions
|
||||
db/* ← migrations / schema BDD
|
||||
chore/* ← tooling, config
|
||||
test/* ← ajout de tests
|
||||
```
|
||||
|
||||
Les branches `main` et `dev` sont **protegees** cote GitHub. Pas de commit direct autorise. Hook pre-commit local les bloque egalement.
|
||||
Les branches `main` et `dev` sont **protegees** cote Forgejo (push direct interdit, force-push bloque, PR obligatoire via l'API `branch_protections`). Hook pre-commit local les bloque egalement.
|
||||
|
||||
**Flow :**
|
||||
1. `git checkout -b feat/menu-composition` (depuis `dev`)
|
||||
|
|
@ -430,11 +440,12 @@ Les branches `main` et `dev` sont **protegees** cote GitHub. Pas de commit direc
|
|||
| 8 | 2 FQDN | Separation claire borne publique / admin+API interne, defensible jury |
|
||||
| 9 | API sous `/api` sur le FQDN admin | Simplicite d'exploitation, CORS explicite gere |
|
||||
| 10 | Service cron dedie | Cr 7.b.3 explicite + realiste prod |
|
||||
| 11 | Makefile avec `make init` | Cr 7.c.4 + demonstration DevOps |
|
||||
| 11 | Orchestration `docker compose up` (service wakdo-migrate) | Cr 7.c.4 + demonstration DevOps |
|
||||
| 12 | Conventional Commits + hooks | Cr 4.f.x + discipline de versioning |
|
||||
| 13 | Branches feat/* -> dev -> main | Pipeline propre pour jury, GitHub PR trace |
|
||||
| 14 | CI/CD GitHub Actions | Cr 7.d explicite dans referentiel |
|
||||
| 13 | Branches feat/* -> dev -> main | Pipeline propre pour jury, PR tracee (Forgejo, mirror GitHub) |
|
||||
| 14 | CI/CD Forgejo Actions (act_runner auto-heberge) | Cr 7.d explicite ; forge + CI maitrisees de bout en bout (argument Bloc 5) |
|
||||
| 15 | RGPD implemente minimal | Cr 3.d.1-4 evaluees meme projet ecole |
|
||||
| 16 | Security-by-design (threat model STRIDE + classification donnees) | Audit Cr 7.a ; stock en %, throttle brute-force, retention RGPD documentes en amont du code |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -442,18 +453,18 @@ Les branches `main` et `dev` sont **protegees** cote GitHub. Pas de commit direc
|
|||
|
||||
| Phase | Scope | Budget (h) | Deadline intermediaire |
|
||||
|---|---|---|---|
|
||||
| **P0 - Setup** | PC, arborescence, Docker, hooks, CI squelette, init Git/GitHub | 20 | Semaine 1 |
|
||||
| **P1 - Conception Merise** | Dictionnaire, MCD, MCT, MLD, schemas fonctionnels, DDL | 30 | Semaine 3 |
|
||||
| **P0 - Setup** | PC, arborescence, Docker, hooks, CI squelette, migration Forgejo + act_runner | 22 | Semaine 1 |
|
||||
| **P1 - Conception Merise + Security-by-design** | Dictionnaire, MCD, MCT, MLD, schemas fonctionnels, DDL, threat model STRIDE + classification donnees + sequence securite | 38 | Semaine 3 |
|
||||
| **P2 - Back squelette** | POO base (Core, Router, Autoloader, DB), auth + roles | 30 | Semaine 6 |
|
||||
| **P3 - Back CRUD admin** | Produits, menus, utilisateurs, views | 40 | Semaine 10 |
|
||||
| **P4 - API REST** | Endpoints + CORS + tests | 20 | Semaine 12 |
|
||||
| **P5 - Front borne** | Integration maquette, Ajax, accessibilite, responsive | 60 | Semaine 16 |
|
||||
| **P6 - Tests + finition** | PHPUnit, tests E2E borne, corrections | 25 | Semaine 18 |
|
||||
| **P7 - DevOps finalisation** | CI/CD deploy auto, crons, docs argumentation | 20 | Semaine 19 |
|
||||
| **P7 - DevOps finalisation** | Forgejo Actions CI/CD (PHPUnit + PHPStan + secret-scan + deploy auto), crons, SECURITY.md, docs argumentation | 22 | Semaine 19 |
|
||||
| **P8 - Prep soutenance** | README pour jury, schemas finaux, repetitions, modifs en direct | 15 | Semaine 20 |
|
||||
| **TOTAL** | | **260** | **Semaine 20 = fin aout 2026** |
|
||||
| **TOTAL** | | **272** | **Semaine 20 = fin aout 2026** |
|
||||
|
||||
Buffer : ~20 h pour imprevus. Cible effective : ~240 h sur 20 semaines = **12 h/semaine**.
|
||||
Buffer : ~8 h pour imprevus. Cible effective : ~264 h sur 20 semaines = **~13 h/semaine**.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -480,8 +491,8 @@ Buffer : ~20 h pour imprevus. Cible effective : ~240 h sur 20 semaines = **12 h/
|
|||
**Bloc 5 :**
|
||||
- `docker-compose.yml` commente
|
||||
- Dockerfiles customs commentes
|
||||
- `Makefile` avec `make help`
|
||||
- `.github/workflows/` avec CI + CD
|
||||
- Orchestration via `docker compose` (service one-shot `wakdo-migrate` : migrate + seed)
|
||||
- `.forgejo/workflows/` avec CI (PHPUnit + PHPStan + secret-scan) + CD
|
||||
- Crontab documente
|
||||
- Script de backup/restore teste
|
||||
- Architecture serveur decrite (`docs/architecture/deployment.md`)
|
||||
|
|
@ -490,7 +501,7 @@ Buffer : ~20 h pour imprevus. Cible effective : ~240 h sur 20 semaines = **12 h/
|
|||
|
||||
- **README.md** synthetique (quick start + liens docs)
|
||||
- **Presentation** (slides ou live) argumentant les choix
|
||||
- **Demo** live : borne + back-office + API (Postman/curl) + `make init`
|
||||
- **Demo** live : borne + back-office + API (Postman/curl) + `docker compose up`
|
||||
- **Capacite modification en direct** (Cr 4.a.1) : code structure pour permettre modifs sans casser
|
||||
|
||||
---
|
||||
|
|
@ -501,7 +512,7 @@ Buffer : ~20 h pour imprevus. Cible effective : ~240 h sur 20 semaines = **12 h/
|
|||
|---|---|---|---|
|
||||
| Sous-estimation temps front (accessibilite RGAA stricte) | Haute | Moyen | 60 h budgetees + tests W3C/axe-core pendant le dev, pas a la fin |
|
||||
| Complexite MCT (statuts commande) mal modelisee | Moyenne | Fort | Valider MCT avec un pair ou prof avant d'implementer Bloc 2 |
|
||||
| Dockerfile PHP extensions manquantes decouvert tard | Moyenne | Faible | Tester `make up` + un vrai appel BDD des P0 |
|
||||
| Dockerfile PHP extensions manquantes decouvert tard | Moyenne | Faible | Tester `docker compose up -d` + un vrai appel BDD des P0 |
|
||||
| Conflit reseau Docker `wakdo_internal` existant | Faible | Faible | Verifie au setup, fallback nom `wakdo_backend` |
|
||||
| CORS mal configure bloque la borne | Moyenne | Moyen | Test immediat apres setup 2 FQDN |
|
||||
| Performance borne sur ecran tactile reel | Faible | Fort | Optimiser images + lazy loading + tests sur device tactile si possible |
|
||||
|
|
@ -604,7 +615,7 @@ L'auteur peut recourir ponctuellement a d'autres outils IA (completion IDE, assi
|
|||
- **Choix du scope fonctionnel** : defini par l'auteur a partir du brief RNCP. L'IA n'ajoute ni ne retire de fonctionnalite sans instruction explicite.
|
||||
- **Modelisation Merise** (MCD, MCT, MLD) : formalisation produite par l'IA a partir du dictionnaire de donnees et des user stories ; arbitrage, validation et corrections par l'auteur. Chaque cardinalite, chaque relation et chaque transition de statut est validee par l'auteur avant integration. Le livrable final reflete ses decisions.
|
||||
- **Validation des livrables** : reservee au jury. L'IA n'emet pas de jugement final sur la conformite RNCP.
|
||||
- **Deploiements** : declenchement humain uniquement, y compris sur `make init` local. Aucune action sur environnement serveur sans instruction explicite.
|
||||
- **Deploiements** : declenchement humain uniquement, y compris sur `docker compose up` local. Aucune action sur environnement serveur sans instruction explicite.
|
||||
- **Commit en son nom** : aucun trailer `Co-Authored-By: Claude...` n'est appose sur les commits. Voir section 17.7.
|
||||
- **Decisions de securite critiques** : tous les choix de type hash mdp, CORS, RBAC, politique sessions sont valides par l'auteur meme si l'IA en propose la mise en oeuvre.
|
||||
|
||||
|
|
@ -670,9 +681,143 @@ 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
|
||||
|
||||
---
|
||||
|
||||
*Document vivant — version 1.1 — 2026-04-24 (ajout section 17 transparence IA). A mettre a jour a chaque decision structurante.*
|
||||
## 19. Security threat model and data classification
|
||||
|
||||
Cette section formalise la couche **security-by-design** ajoutee au modele Merise v0.2
|
||||
(voir `docs/merise/dictionary.md` note 13, `docs/merise/mlt.md` section 2 pour les regles
|
||||
transverses RG-T13 a RG-T21). Elle se lit a deux niveaux : un **registre des risques** de
|
||||
synthese pour une lecture gestion, suivi d'une **analyse STRIDE par element** pour la
|
||||
profondeur technique, puis une **matrice de classification des donnees** en 4 niveaux. Tous
|
||||
les claims securite sont rattaches a un mecanisme concret (une regle RG-T, une colonne, une
|
||||
entite) plutot qu'enonces comme des absolus.
|
||||
|
||||
### 19.1 Perimetre et frontieres de confiance
|
||||
|
||||
Le systeme expose cinq frontieres de confiance (trust boundaries), correspondant aux points
|
||||
d'entree analyses ci-dessous :
|
||||
|
||||
- **E1 — Borne kiosk (public anonyme)** : `POST /api/orders`, consultation du catalogue.
|
||||
Aucune authentification ; la borne est anonyme par conception (les commandes kiosk ont
|
||||
`customer_order.acting_user_id = NULL`). Surface la plus exposee, donc traitee sans
|
||||
hypothese de confiance sur l'entree.
|
||||
- **E2 — Back-office admin (staff authentifie, poste partage + PIN par equipier)** : CRUD
|
||||
catalogue/menus/ingredients, RBAC, gestion utilisateurs, stock, annulation de commande,
|
||||
stats. Session partagee par poste pour le flux courant ; un PIN par equipier
|
||||
(`user.pin_hash`) re-autorise l'ensemble sensible (RG-T13).
|
||||
- **E3 — Surface d'authentification** : login (`AUTHENTICATE_USER`, op 25, `mlt.md` 12.1) et
|
||||
reinitialisation de mot de passe (`RESET_PASSWORD`, op 28, `mlt.md` 12.3).
|
||||
- **E4 — Couche donnees / BDD** : acces PDO, requetes preparees (RG-T06), allowlists
|
||||
(RG-T16/RG-T17), integrite transactionnelle (RG-T08/RG-T11), snapshots immuables (RG-T05).
|
||||
- **E5 — Stock / inventaire** : decrement de vente, reappro, comptage d'inventaire, avec un
|
||||
journal append-only `stock_movement` et attribution de l'acteur ; la correction d'inventaire
|
||||
est PIN-gated (RG-T13, `mlt.md` 9.2) car elle peut masquer de la demarque (shrinkage).
|
||||
|
||||
Hors perimetre de cette section : la securite reseau/infra (Traefik, segmentation Docker,
|
||||
TLS), couverte en section 5 ; le durcissement CORS, couvert en section 5.
|
||||
|
||||
### 19.2 Registre des risques (risk register)
|
||||
|
||||
Synthese gestion. Likelihood et residual risk sont evalues a dire d'expert pour ce projet
|
||||
fictif (`[REASONING]`, non quantifies par benchmark) ; chaque mitigation cite une regle RG-T
|
||||
et/ou une entite reelle du modele.
|
||||
|
||||
| # | Actif | Menace | Impact | Likelihood | Mitigation (regle / entite) | Risque residuel |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | Recette (cash) sur commande payee | Un equipier annule une commande `paid` pour detourner l'encaissement (fraude interne) | Fort | Moyenne | `CANCEL_ORDER` (`mlt.md` 7.1) PIN-gated (RG-T13) + ecriture `audit_log` dans la meme transaction (RG-T14, RG-T11) ; acteur capture via `audit_log.actor_user_id` | Faible — l'annulation reste possible mais devient nominative et tracee ; dissuasion plus que blocage |
|
||||
| R2 | `product.price_cents` / `vat_rate` / `role_id` | Falsification via un champ de formulaire injecte (mass-assignment) | Fort | Moyenne | Allowlist de colonnes par operation (RG-T16) sur `UPDATE_PRODUCT` (`mlt.md` 8.2) et `UPDATE_USER` (10.2) ; seules les colonnes autorisees sont bindees | Faible — les champs hors allowlist sont ignores ; un changement de prix reste audite (RG-T14) |
|
||||
| R3 | Comptes back-office (`user.password_hash`) | Brute-force sur le login staff | Moyen | Haute | Backoff degressif par compte (`user.failed_login_attempts` / `lockout_until`) + par IP (`login_throttle`, entite 21) ; gate avant verification (`mlt.md` 12.1 PRE-3, RG-8) | Faible — ralentissement sans lock indefini ; un service de 15h n'est pas bloque par une saisie maladroite |
|
||||
| R4 | Vues kiosk et admin (texte stocke) | XSS stocke via `product.name` / `ingredient.name` / `user.first_name` | Moyen | Moyenne | Echappement au rendu (RG-T15) : `htmlspecialchars(..., ENT_QUOTES)` cote admin, injection via `textContent` (pas `innerHTML`) cote kiosk vanilla-JS | Faible — l'echappement reduit le risque d'execution de script injecte |
|
||||
| R5 | `ingredient.stock_quantity` | Survente (oversell) sous concurrence multi-borne | Moyen | Moyenne | Decrement atomique auto-verrouillant (RG-T20) sans read-gate + disponibilite calculee (RG-T21) ; `stock_quantity` signe, la magnitude de survente est remontee aux managers | Moyen accepte — le systeme ne bloque pas une commande sur le stock ; la survente est mesuree, pas empechee (decision metier) |
|
||||
| R6 | Commande payee | Double-charge sur retry reseau de `POST /api/orders` | Moyen | Moyenne | Idempotence (RG-T19) : `customer_order.idempotency_key` UNIQUE ; un retry renvoie la commande existante au lieu d'en creer une seconde | Faible — la cle UNIQUE deduplique les rejeux ; depend d'une cle client correctement generee |
|
||||
| R7 | PII utilisateur (`user.email`/`first_name`/`last_name`) | Demande d'effacement RGPD non honoree, ou rupture de l'integrite referentielle a la suppression | Fort (conformite) | Faible | Anonymisation (`ERASE_USER_PII`, `mlt.md` 10.5) : la ligne est conservee, PII remplacees par un placeholder `anon-<id>@wakdo.invalid`, credentials invalides, `anonymized_at` pose ; `audit_log` retient sa propre fenetre | Faible — effacement et tracabilite coexistent ; les FK (`audit_log.actor_user_id`, `customer_order.acting_user_id`, `stock_movement.user_id`) restent valides |
|
||||
| R8 | Matrice RBAC (`role_permission`) | Elevation de privilege via modification de role non controlee | Fort | Faible | `MANAGE_RBAC` (`mlt.md` 10.4) PIN-gated (RG-T13) + `audit_log` du diff de permissions (RG-T14, RG-6) ; `role_id` derriere l'allowlist (RG-T16) | Faible — tout gain/perte de capacite est nominatif et trace |
|
||||
| R9 | `stock_movement` (demarque) | Correction d'inventaire masquant une demarque | Moyen | Moyenne | `INVENTORY_COUNT` (`mlt.md` 9.2) PIN-gated (RG-T13) ; le `user_id` capture par PIN est ecrit dans `stock_movement.user_id` (append-only) | Faible — la correction devient attribuable a une personne meme sur poste partage |
|
||||
|
||||
### 19.3 Analyse STRIDE par element
|
||||
|
||||
Un bloc par categorie STRIDE, mappe aux controles reels du modele (verifies contre
|
||||
`mlt.md` section 2).
|
||||
|
||||
**Spoofing (usurpation d'identite).** L'authentification back-office repose sur argon2id
|
||||
(`user.password_hash`, `mlt.md` 12.1 RG-2) avec regeneration de session a la connexion
|
||||
(`session_regenerate(true)`, RG-3) pour contrer la fixation. Le login est enumeration-safe :
|
||||
meme erreur generique que l'email existe ou non, avec un `password_verify` leurre pour garder
|
||||
le timing comparable (RG-2). Sur un poste partage, un PIN par equipier (`user.pin_hash`,
|
||||
RG-T13) re-authentifie l'acteur reel pour les actions sensibles. La reinitialisation de mot de
|
||||
passe (`RESET_PASSWORD`, `mlt.md` 12.3) stocke le token hashe (`password_reset_token_hash`),
|
||||
n'envoie le token brut qu'une seule fois, l'expire a 1h et le rend a usage unique (RG-2/RG-3) ;
|
||||
la phase requete renvoie une reponse neutre identique que le compte existe ou non (RG-1,
|
||||
enumeration-safe). La borne kiosk est anonyme
|
||||
par conception, donc hors perimetre d'usurpation (pas de compte a usurper).
|
||||
|
||||
**Tampering (alteration).** L'allowlist de mass-assignment (RG-T16) limite les colonnes
|
||||
bindees aux champs autorises par operation, protegeant `price_cents`, `vat_rate`, `role_id`,
|
||||
`is_active`, `status`. La validation cote serveur (RG-T18) re-verifie type, plage, longueur,
|
||||
appartenance ENUM et existence des FK independamment du client. Les requetes preparees PDO
|
||||
(RG-T06) traitent les valeurs hors de la chaine SQL, ce qui ferme l'injection SQL par valeur.
|
||||
Les identifiants SQL dynamiques (colonne et direction d'un `ORDER BY`/`GROUP BY`) sont resolus
|
||||
contre une allowlist fixe avant construction de la requete (RG-T17), car un identifiant ne
|
||||
peut pas etre bind comme une valeur.
|
||||
Les snapshots de commande (`order_item.label_snapshot`, `unit_price_cents_snapshot`,
|
||||
`vat_rate_snapshot`) sont immuables apres INSERT (RG-T05), preservant l'integrite historique
|
||||
des commandes placees. La re-validation serveur des modifiers (`mlt.md` 3.3 RG-9) rejette un
|
||||
`POST` forge ajoutant un ingredient non-`is_addable`.
|
||||
|
||||
**Repudiation (deni d'action).** Le journal `audit_log` (entite 20, RG-T14) enregistre les
|
||||
actions sensibles non-stock avec `actor_user_id` (capture par PIN, RG-T13), `actor_role_id`
|
||||
(denormalise pour survivre a l'anonymisation), `action_code`, `entity_type`/`entity_id` et un
|
||||
`summary` non-personnel ; pas d'UPDATE/DELETE applicatif. L'attribution des commandes
|
||||
comptoir/drive passe par `customer_order.acting_user_id` (`mlt.md` 4.1 RG-5) et celle du stock
|
||||
par `stock_movement.user_id` (`mlt.md` 9.1/9.2). Les actions stock ne sont pas doublement
|
||||
journalisees : `stock_movement` (append-only) fournit deja la piste.
|
||||
|
||||
**Information disclosure (divulgation).** La matrice de classification (19.4) borne ce qui
|
||||
sort des logs et des reponses API. Les erreurs d'auth sont generiques (RG-2, pas de
|
||||
distinction email inconnu / mot de passe faux). L'`audit_log` stocke des **noms de champs**
|
||||
modifies, pas les valeurs PII (`audit_log.details`, RG-T14). L'attribution de stock
|
||||
(`stock_movement.user_id`) n'est visible que pour manager/admin ; le staff de ligne voit les
|
||||
deltas sans l'identite de l'acteur (`mlt.md` 9.3 RG-4). Les credentials (`password_hash`,
|
||||
`pin_hash`, `password_reset_token_hash`) sont tenus hors logs et hors reponses API.
|
||||
|
||||
**Denial of service.** Le throttling de login est degressif (backoff exponentiel plafonne)
|
||||
plutot qu'un lock indefini, dans les deux dimensions compte (`user.lockout_until`) et IP
|
||||
(`login_throttle.lockout_until`, `mlt.md` 12.1 RG-8) : une saisie maladroite ne bloque pas une
|
||||
cuisine en plein service de 15h continu. L'idempotence (RG-T19) absorbe les doubles-soumissions
|
||||
de retry reseau sur le kiosk anonyme. Le decrement de stock atomique (RG-T20) evite tout
|
||||
contentieux de verrou (pas de `SELECT ... FOR UPDATE`, pas d'ordre de deadlock).
|
||||
|
||||
**Elevation of privilege.** Le RBAC est permission-driven : le code teste une permission, pas
|
||||
un nom de role (catalogue de 23 permissions fige au seed, `dictionary.md` 3.17). Les
|
||||
changements de role passent par `MANAGE_RBAC` (`mlt.md` 10.4) PIN-gated (RG-T13) et audites
|
||||
avec le diff de permissions (RG-T14, RG-6). `role_id` est derriere l'allowlist de
|
||||
mass-assignment (RG-T16) sur `UPDATE_USER` (10.2). Les permissions sont rechargees depuis la
|
||||
BDD a chaque verification (`mlt.md` 10.4 RG-3), donc un changement de droits prend effet sans
|
||||
re-login force.
|
||||
|
||||
### 19.4 Matrice de classification des donnees (4 niveaux)
|
||||
|
||||
Les 21 entites du modele (`dictionary.md` 3.1-3.21) sont reparties en quatre niveaux. La
|
||||
classification suit l'entite ; quelques colonnes sont surclassees explicitement (credentials,
|
||||
PII).
|
||||
|
||||
| Niveau | Definition | Entites / colonnes | Regle de manipulation |
|
||||
|---|---|---|---|
|
||||
| **RESTRICTED** (secrets / credentials) | Secrets d'authentification ; tenus hors de toute exposition | Colonnes de `user` (14) : `password_hash`, `pin_hash`, `password_reset_token_hash` | Hors logs et hors reponses API ; argon2id ; invalides a l'anonymisation (`mlt.md` 10.5 RG-1) ; exclus de `audit_log.details` qui ne retient que des noms de champs (RG-T14) |
|
||||
| **CONFIDENTIAL** (PII, RGPD) | Donnees a caractere personnel d'un staff identifiable | Colonnes de `user` (14) : `email`, `first_name`, `last_name` | Sujet a l'anonymisation a l'effacement (`ERASE_USER_PII`, op 27) ; `audit_log` stocke les noms de champs, pas les valeurs ; echappement au rendu (RG-T15) |
|
||||
| **INTERNAL** (sensible metier) | Donnees d'exploitation, non publiques, a acces restreint par RBAC | `customer_order` (10), `order_item` (11), `order_item_selection` (12), `order_item_modifier` (13), `stock_movement` (19), `audit_log` (20), `login_throttle` (21, contient l'IP source), `role` (15), `permission` (17), `role_permission` (18), `role_visible_source` (16) ; sorties de stats (`READ_STATS`, op 24) | Acces filtre par permission (RG-T03) ; attribution stock visible manager/admin seulement (`mlt.md` 9.3 RG-4) ; integrite par snapshots (RG-T05) et transactions (RG-T08/RG-T11) |
|
||||
| **PUBLIC** (catalogue, face kiosk) | Donnees servies a la borne anonyme | `category` (1), `product` (2), `menu` (3), `menu_slot` (4), `menu_slot_option` (5), `ingredient` (6, nom + dispo calculee), `product_ingredient` (7), `allergen` (8), `ingredient_allergen` (9) | Lecture publique via `LOAD_CATALOGUE` (op 1) ; ecriture reservee admin/manager (RG-T03) ; texte echappe au rendu (RG-T15) ; disponibilite calculee (RG-T21) |
|
||||
|
||||
**Couverture** : 21/21 entites classifiees (9 PUBLIC, 11 INTERNAL incluant les deux entites
|
||||
security-by-design `audit_log` et `login_throttle`, plus `user` dont les colonnes sont
|
||||
reparties entre RESTRICTED, CONFIDENTIAL et — pour `is_active`, `role_id`, `last_login_at`,
|
||||
les compteurs de throttle — INTERNAL). L'entite `user` (14) est la seule a porter trois
|
||||
niveaux simultanement, d'ou son traitement par colonne.
|
||||
|
||||
---
|
||||
|
||||
*Document vivant — version 1.3 — 2026-06-15 (drift GitHub -> Forgejo Actions corrige, CI securite PHPStan/secret-scan, planning rechiffre pour la couche security-by-design). A mettre a jour a chaque decision structurante.*
|
||||
|
|
|
|||
104
docs/TESTING.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Strategie de test — Wakdo
|
||||
|
||||
> Comment le projet est teste, comment lancer chaque niveau, ce qui tourne en CI,
|
||||
> comment mesurer la couverture, et pourquoi les tests E2E ne tournent pas en CI.
|
||||
> Priorite : Unit > Integration > E2E. Cote PHP, aucune dependance Composer
|
||||
> (PHPUnit en `.phar` autonome).
|
||||
|
||||
---
|
||||
|
||||
## 1. Niveaux de test
|
||||
|
||||
| Niveau | Outil | Perimetre | Ou |
|
||||
|---|---|---|---|
|
||||
| Unitaire PHP | PHPUnit (`.phar`) | logique (Auth, RBAC, PIN, throttle, calcul commande, controleurs via doubles) | CI + local |
|
||||
| Integration PHP | PHPUnit + vraie MariaDB | requetes SQL preparees, contraintes, RBAC `is_active`, audit, FK | CI + local |
|
||||
| Analyse statique | PHPStan niveau 6 | typage, erreurs potentielles sur `src/` + `tests/` | CI + local |
|
||||
| Unitaire JS | `node:test` + jsdom | modules du front borne (panier, composeur, checkout, allergenes, a11y, validation) | CI + local |
|
||||
| E2E | Playwright | parcours borne + admin de bout en bout | **local / manuel** (voir section 5) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Lancer les tests PHP (sans Composer)
|
||||
|
||||
Le binaire `php` n'est pas requis sur l'hote : on passe par l'image applicative.
|
||||
|
||||
```bash
|
||||
# Unitaire + integration (la vraie base est utilisee si WAKDO_DB_TESTS=1) :
|
||||
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml
|
||||
|
||||
# Integration DB explicite (vraie MariaDB du reseau interne) :
|
||||
docker run --rm --network wakdo_wakdo_internal --env-file .env -e WAKDO_DB_TESTS=1 \
|
||||
-v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml
|
||||
|
||||
# Analyse statique :
|
||||
docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app \
|
||||
php -d memory_limit=-1 phpstan.phar analyse --no-progress
|
||||
```
|
||||
|
||||
Les `*.phar` (phpunit 11.5.2, phpstan 1.12.27) sont gitignores ; les retelecharger si absents.
|
||||
Les tests d'integration s'auto-skippent hors `WAKDO_DB_TESTS=1` ; la CI force `--fail-on-skipped`
|
||||
pour qu'aucun test de securite (throttle, RBAC, audit, FK) ne soit silencieusement saute.
|
||||
|
||||
---
|
||||
|
||||
## 3. Lancer les tests JS
|
||||
|
||||
```bash
|
||||
npm run test:js # node --test tests/js/ (jsdom en devDependency)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Couverture de code
|
||||
|
||||
Le pilote de couverture (pcov ou Xdebug) n'est **pas** embarque dans l'image de
|
||||
production (surcout au runtime, sans interet en prod). La couverture se mesure en
|
||||
dev/CI, avec un PHP equipe d'un pilote de couverture :
|
||||
|
||||
```bash
|
||||
# avec pcov ou xdebug actif dans le PHP utilise :
|
||||
php -d pcov.enabled=1 phpunit.phar -c phpunit.xml --coverage-text --coverage-html var/coverage
|
||||
```
|
||||
|
||||
`phpunit.xml` declare deja la source a mesurer (`<source><include><directory>src`).
|
||||
A ce stade, la couverture est produite **a la demande** : aucun seuil n'est impose
|
||||
en CI (pas de gate de pourcentage). L'ajout d'un pilote de couverture a l'etape CI
|
||||
`static-tests` et d'un seuil minimal est une evolution identifiee (decision exploitant :
|
||||
elle suppose un PHP de CI equipe de pcov).
|
||||
|
||||
---
|
||||
|
||||
## 5. E2E (Playwright) — execution manuelle, hors CI
|
||||
|
||||
Les parcours E2E (borne : accueil -> commande -> chevalet -> confirmation ; admin :
|
||||
login -> dashboard -> logout) se lancent **a la main**, contre une stack jetable :
|
||||
|
||||
```bash
|
||||
tests/e2e/run.sh
|
||||
```
|
||||
|
||||
Le script monte une stack isolee (`docker-compose.yml` + `tests/e2e/docker-compose.e2e.yml`),
|
||||
attend migrate + healthcheck, puis lance Playwright dans le conteneur officiel.
|
||||
|
||||
**Pourquoi pas en CI ?** Decision assumee : le runner Forgejo de production execute les
|
||||
jobs sans acces au socket Docker (pas de docker-in-docker), et les jobs sont repartis sur
|
||||
plusieurs runners. Monter une stack Docker complete + Playwright dans ce contexte n'est pas
|
||||
fiable. Les E2E restent donc un filet **manuel** (lance avant une livraison sensible), tandis
|
||||
que la CI couvre l'unitaire, l'integration DB, l'analyse statique, le lint et le scan de secrets.
|
||||
A l'oral, c'est la position a defendre : E2E reels et reproductibles, mais declenches a la main.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ce que la CI execute (Forgejo Actions, sur PR)
|
||||
|
||||
`.forgejo/workflows/ci.yml`, sur `pull_request` vers `dev`/`main` :
|
||||
|
||||
| Job | Verifie |
|
||||
|---|---|
|
||||
| `secret-scan` | gitleaks (aucun secret dans le diff/historique) |
|
||||
| `php-lint` | `php -l` sur tous les fichiers `.php` |
|
||||
| `static-tests` | PHPStan niveau 6 + PHPUnit (unit + integration sur service MariaDB, `--fail-on-skipped`) |
|
||||
| `js-tests` | `node --test tests/js/` (jsdom) |
|
||||
|
||||
L'auto-merge ne se declenche que lorsque ces checks requis sont verts (branch protection).
|
||||
23
docs/adr/0001-php-from-scratch-sans-composer.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# ADR-0001 — PHP from scratch, sans framework ni Composer
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-04-23
|
||||
|
||||
## Contexte
|
||||
Certification RNCP (Titre Developpeur Web, option DevOps). L'objectif pedagogique est
|
||||
de demontrer la maitrise des fondamentaux (routage, PDO, sessions, securite) plutot que
|
||||
la configuration d'un framework. Options : Symfony/Laravel ; micro-framework (Slim) ;
|
||||
from scratch.
|
||||
|
||||
## Decision
|
||||
Application PHP 8.3 ecrite **from scratch** : routeur, autoloader PSR-4 manuel
|
||||
(`spl_autoload_register`), couche `Database` sur PDO, le tout **sans Composer**. Les
|
||||
outils de dev (PHPUnit, PHPStan) sont utilises via leurs **`.phar` autonomes**.
|
||||
|
||||
## Consequences
|
||||
- (+) Chaque mecanisme (routage, auth, RBAC, requetes preparees) est explicite et
|
||||
defendable a l'oral ; pas de magie de framework.
|
||||
- (+) Surface de dependances minimale (moins de supply-chain a auditer).
|
||||
- (-) Du code d'infrastructure a ecrire et tester soi-meme (Core, Auth).
|
||||
- CI sans Composer : les `.phar` (phpunit, phpstan) sont epingles/telecharges.
|
||||
Voir `docs/PROJECT_CONTEXT.md` section 6.
|
||||
22
docs/adr/0002-back-office-mvc-rendu-serveur.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# ADR-0002 — Back-office en MVC rendu serveur (pas de SPA)
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-15
|
||||
|
||||
## Contexte
|
||||
Le back-office (login, CRUD catalogue, stock, users, RBAC, stats) doit etre construit.
|
||||
Options : SPA JS consommant une API JSON ; pages rendues serveur (MVC PHP) ; hybride.
|
||||
La borne client, elle, est deja un front statique distinct (Bloc 1).
|
||||
|
||||
## Decision
|
||||
Le back-office est en **MVC rendu serveur** : formulaires POST + redirections, vues PHP
|
||||
injectees dans un layout commun. L'API REST (`/api/*`) reste interne, consommee par la
|
||||
borne. Login = vue PHP, pas un endpoint JSON.
|
||||
|
||||
## Consequences
|
||||
- (+) CSRF, sessions, garde de permission et echappement de sortie se branchent
|
||||
naturellement sur chaque page ; demontre le MVC sans build front.
|
||||
- (+) Pas de duplication d'etat client/serveur pour l'admin.
|
||||
- (-) Interactions riches (matrice RBAC, editeur recette) gerees en JS vanilla cible,
|
||||
CSP-safe (champs caches / cases scalaires), sans framework front.
|
||||
- Controleurs non-`final` (seam de test) ; vues sous `src/app/Views/admin`.
|
||||
25
docs/adr/0003-stock-pourcentage-dispo-calculee.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# ADR-0003 — Stock en pourcentage + disponibilite produit calculee (RG-T21)
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-12
|
||||
|
||||
## Contexte
|
||||
Modeliser le stock des ingredients et la commandabilite des produits. Un stock en
|
||||
quantites absolues seules rend les seuils d'alerte arbitraires d'un ingredient a
|
||||
l'autre ; et marquer la disponibilite produit "en dur" exige une cascade a maintenir
|
||||
a chaque mouvement de stock.
|
||||
|
||||
## Decision
|
||||
Stock ancre sur une **`stock_capacity`** (reference 100%, `CHECK > 0`) ; `stock_pct` et
|
||||
les 3 bandes (normal / alerte / critique) sont **calcules**, pas stockes. La
|
||||
**disponibilite produit (RG-T21)** est derivee : commandable si `is_available = 1` ET
|
||||
chaque ingredient non retirable est au-dessus de la bande critique. Aucune colonne
|
||||
stockee, aucune cascade.
|
||||
|
||||
## Consequences
|
||||
- (+) Seuils homogenes (en %) ; un reappro au-dessus du critique rend le produit
|
||||
commandable de lui-meme, sans ecriture.
|
||||
- (+) `stock_quantity` signe (survente assumee, remontee manager) : le systeme ne bloque
|
||||
pas une commande sur une lecture de stock.
|
||||
- (-) Le calcul de dispo se fait a la lecture (jointure composition) ; borne par requete.
|
||||
- Source unique de la derivation : `IngredientRepository::stockBand`. Voir `docs/merise/`.
|
||||
25
docs/adr/0004-pin-action-sensible-audit.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# ADR-0004 — PIN d'action sensible (equipier) + audit dans la meme transaction
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-15
|
||||
|
||||
## Contexte
|
||||
Les postes back-office sont partages (session ouverte au comptoir). Pour les operations
|
||||
sensibles (annulation, changement prix/TVA, suppressions, inventaire, gestion
|
||||
utilisateur, RBAC, effacement PII), il faut imputer l'acte a une personne, pas a la
|
||||
session partagee.
|
||||
|
||||
## Decision
|
||||
Modele **identifiant equipier + PIN** : l'operation sensible exige email + PIN, verifies
|
||||
contre `user.pin_hash` (argon2id). Le `user_id` ainsi resolu est l'**acteur** ecrit dans
|
||||
`audit_log` (RG-T14), dans la **meme transaction** que l'effet (RG-T08). Le set sensible
|
||||
est defini par RG-T13. Les operations de stock tracent via `stock_movement.user_id`
|
||||
(pas de double-journal).
|
||||
|
||||
## Consequences
|
||||
- (+) Imputabilite reelle sur poste partage ; trace immuable et atomique (pas d'effet
|
||||
sans audit, ni l'inverse).
|
||||
- (+) Le PIN n'identifie pas la session : un manager peut autoriser sur le poste d'un
|
||||
autre sans relog.
|
||||
- (-) Surface d'attaque PIN (4 chiffres) -> necessite un throttle dedie (voir ADR-0005).
|
||||
- Brique : `App\Auth\PinVerifier`. Regle : `docs/merise/mlt.md` RG-T13/RG-T14.
|
||||
23
docs/adr/0005-throttle-pin-separe-du-login.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# ADR-0005 — Throttle du PIN separe des compteurs de connexion (RG-T22)
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-15
|
||||
|
||||
## Contexte
|
||||
Le PIN d'action sensible (ADR-0004) est court (4 chiffres) : il faut limiter le
|
||||
brute-force. Question : reutiliser les compteurs de login (`user.lockout_until` /
|
||||
`login_throttle`) ou un compteur dedie ? Et sur quelle dimension compter ?
|
||||
|
||||
## Decision
|
||||
Table **`pin_throttle`** dediee, **separee** des compteurs de connexion. La dimension
|
||||
est l'**utilisateur agissant** (la session authentifiee qui soumet le PIN), pas l'email
|
||||
cible (contournable par rotation) ni l'IP (collateral sur poste partage). Backoff
|
||||
degressif, bornes propres plus permissives que le login. Verrou evalue AVANT la
|
||||
verification ; sous verrou actif, pas de nouvelle ligne `pin.failed` (anti-amplification).
|
||||
|
||||
## Consequences
|
||||
- (+) Spammer le PIN d'une victime ne verrouille pas sa CONNEXION (pas d'escalade DoS
|
||||
sur une surface plus sensible).
|
||||
- (+) Detection : un pic de `pin.failed` reste alertable.
|
||||
- (-) Un compteur de plus a purger (cron, comme `login_throttle`).
|
||||
- Brique : `App\Auth\PinThrottle`. Regle : RG-T22. Cf. ADR-0004.
|
||||
25
docs/adr/0006-http-409-conflit-422-validation.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# ADR-0006 — HTTP 409 (conflit) vs 422 (validation)
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-17
|
||||
|
||||
## Contexte
|
||||
Les controleurs renvoyaient 422 a la fois pour une validation qui echoue ET pour un
|
||||
conflit d'etat (unicite, suppression bloquee par FK RESTRICT). Le contrat documente
|
||||
(`byan-api.md`) attendait 409 pour les conflits. Derive a corriger.
|
||||
|
||||
## Decision
|
||||
Convention harmonisee sur tous les controleurs :
|
||||
- **422** : requete bien formee mais **semantiquement invalide** (validation serveur,
|
||||
RG-T18) ;
|
||||
- **409** : **conflit d'etat** (violation d'unicite SQLSTATE 23000, hard-delete bloque
|
||||
par FK RESTRICT) ;
|
||||
- **403** : CSRF invalide ou permission manquante ; **404** : ressource introuvable.
|
||||
|
||||
## Consequences
|
||||
- (+) Statuts semantiquement justes (RFC 9110), testables, coherents entre Category /
|
||||
Product / Menu / Ingredient / User / Role.
|
||||
- (+) Aligne le code sur le contrat d'API documente.
|
||||
- (-) Pages rendues serveur : un 200-avec-erreurs "marcherait" visuellement, mais le
|
||||
statut correct est verrouille par les tests (un oubli = test rouge).
|
||||
- Remediation : PR #33 (Category/Product/Menu) ; les controleurs suivants naissent en 409.
|
||||
24
docs/adr/0007-rgpd-anonymisation-tombstone.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# ADR-0007 — Effacement RGPD par anonymisation (tombstone), pas DELETE
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-17
|
||||
|
||||
## Contexte
|
||||
Le droit a l'effacement (RGPD, Cr 3.d) s'applique aux comptes back-office. Un `DELETE`
|
||||
dur casserait l'integrite referentielle (FK entrantes depuis `stock_movement.user_id`,
|
||||
`customer_order.acting_user_id`, `audit_log.actor_user_id`) et effacerait la trace
|
||||
d'imputabilite des actes passes.
|
||||
|
||||
## Decision
|
||||
**Anonymisation** (mlt 10.5), pas suppression : en une transaction, vider la PII de la
|
||||
ligne `user` (email -> `anon-<id>@wakdo.invalid` RFC 2606, prenom/nom vides, hash vide,
|
||||
PIN/reset NULL), poser `anonymized_at`, `is_active = 0`. La ligne **persiste** comme
|
||||
tombstone. Idempotent (clause `anonymized_at IS NULL`). Trace : `audit_log`
|
||||
`user.erase_pii`.
|
||||
|
||||
## Consequences
|
||||
- (+) FK preservees ; les actes passes restent imputables a un principal anonymise
|
||||
(qui-en-tant-qu-id), sans PII.
|
||||
- (+) Email unique conserve, non identifiant.
|
||||
- (-) La ligne reste en base (tombstone) : a documenter dans le registre de traitement.
|
||||
- Garde-fou : interdit d'anonymiser le dernier admin actif / soi-meme (anti-lockout).
|
||||
26
docs/adr/0008-makefile-vers-compose-migrate.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# ADR-0008 — Du Makefile a `docker compose up` (service wakdo-migrate)
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-17
|
||||
|
||||
## Contexte
|
||||
Le critere Cr 7.c.4 demande de lancer la stack complete en une commande. C'etait
|
||||
`make init`. Mais le Makefile portait surtout des cibles mortes/trompeuses
|
||||
(`test`/`lint` annoncaient "pas implemente" alors que les tests tournent) ; sa seule
|
||||
cible porteuse, `init`, existait parce que `docker compose up` seul n'applique pas les
|
||||
migrations. Le critere parle d'un **resultat**, pas de `make`.
|
||||
|
||||
## Decision
|
||||
Migration + seed deplaces **dans la stack** : un service one-shot **`wakdo-migrate`**
|
||||
(image mariadb, `db/migrate-container.sh` par le reseau) applique
|
||||
`db/migrations/*.sql` (suivi `schema_migrations`) puis `db/seeds/*.sql` (suivi
|
||||
`seeds_applied`), idempotents. `wakdo-app`/`wakdo-web` `depends_on:
|
||||
service_completed_successfully`. **Makefile supprime.** `docker compose up` devient
|
||||
l'unique commande.
|
||||
|
||||
## Consequences
|
||||
- (+) Commande universelle, sans dependance a l'outil `make` sur l'hote.
|
||||
- (+) Comportement = doc (l'ancien `make init` ne seedait meme pas).
|
||||
- (-) Migrations/seed evalues a chaque `up` (cout negligeable, suivi -> re-run sans effet).
|
||||
- (-) Base **deja seedee** avant le suivi : back-filler `seeds_applied` avant le 1er up.
|
||||
- `db/migrate.sh` (hote) conserve pour l'usage manuel. Detail : journal 2026-06-17.
|
||||
29
docs/adr/0009-compose-standalone-et-prod-gitignore.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# ADR-0009 — docker-compose.yml standalone + docker-compose.prod.yml gitignore
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-17
|
||||
|
||||
## Contexte
|
||||
Le `docker-compose.yml` versionne supposait un reverse proxy Traefik (reseau externe,
|
||||
labels, aucun port hote) : `docker compose up` echouait pour quiconque sans Traefik
|
||||
(jury, contributeur, tests E2E). Options envisagees : overlay `-f` (base + prod fusionnes
|
||||
via `!reset`) ; un seul fichier parametre ; deux fichiers complets independants.
|
||||
|
||||
## Decision
|
||||
Deux fichiers **complets et independants** (pas d'overlay) :
|
||||
- **`docker-compose.yml`** (versionne) : standalone, `wakdo-web` publie
|
||||
`${HTTP_PORT:-8080}:80`, reseau interne seul, sans Traefik. `docker compose up` tourne
|
||||
partout, facon app open-source self-hostable.
|
||||
- **`docker-compose.prod.yml`** : **gitignore**, propre a chaque hote derriere un proxy
|
||||
(meme stack + reseau externe + labels Traefik, sans port). `docker compose -f
|
||||
docker-compose.prod.yml up -d`.
|
||||
|
||||
Renommage `TRAEFIK_DOMAIN_*` -> `APP_HOST_*` (ce sont des `ServerName` de vhosts, pas du
|
||||
Traefik). `.env.example` local-first.
|
||||
|
||||
## Consequences
|
||||
- (+) `docker compose up` marche en local sans configuration ; le repo ne porte aucune
|
||||
hypothese d'infra.
|
||||
- (+) Le critere Cr 7.c.4 tient avec un fichier que tout le monde peut lancer.
|
||||
- (-) Duplication entre les deux fichiers (assumee : clarte > DRY pour l'infra).
|
||||
- (-) Le serveur maintient son propre fichier prod (comme `.env`).
|
||||
28
docs/adr/0010-cookie-secure-conditionnel-https.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# ADR-0010 — Cookie de session Secure conditionnel au HTTPS
|
||||
|
||||
- Statut : Accepte
|
||||
- Date : 2026-06-17
|
||||
|
||||
## Contexte
|
||||
Le cookie de session du back-office etait pose avec `secure => true` en dur
|
||||
(security-by-design). Or un cookie `Secure` n'est emis/renvoye par le navigateur que
|
||||
sur HTTPS : en HTTP (dev, stack standalone locale, E2E sans TLS) la session ne tenait
|
||||
pas d'une requete a l'autre, donc le login admin echouait ("Session expiree" au POST,
|
||||
le jeton CSRF ne pouvant matcher une session perdue). Revele par le parcours E2E admin.
|
||||
En prod le souci n'apparait pas : Traefik termine le TLS.
|
||||
|
||||
## Decision
|
||||
`secure` devient **conditionnel au schema** : vrai si la requete est HTTPS, faux sinon.
|
||||
Detection (`SessionManager::cookieSecure()`) : `X-Forwarded-Proto: https` (pose par
|
||||
Traefik en prod) en priorite, sinon la variable serveur `HTTPS`, sinon le port 443.
|
||||
Applique aux deux points (pose du cookie + expiration au logout).
|
||||
|
||||
## Consequences
|
||||
- (+) Le back-office est utilisable en **HTTP local** (dev, standalone, E2E) ; prod
|
||||
**inchange** (derriere Traefik -> `X-Forwarded-Proto=https` -> `Secure` reste pose).
|
||||
- (+) Comportement standard (les frameworks derivent `Secure` du schema).
|
||||
- Confiance en `X-Forwarded-Proto` : sure ici car l'app n'est joignable que par le
|
||||
reverse proxy sur le reseau interne (aucun acces client direct).
|
||||
- (-) Un deploiement en **HTTP nu** (sans proxy TLS) n'aurait pas `Secure` — mais servir
|
||||
l'authentification en HTTP nu est de toute facon a proscrire (independant de ce flag).
|
||||
- `httponly` et `SameSite=Strict` restent inconditionnels. Revele par [E2E admin](../domaines/auth.md).
|
||||
38
docs/adr/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Registre des decisions d'architecture (ADR)
|
||||
|
||||
Une fiche courte par decision structurante : **contexte**, **decision**, **consequences**.
|
||||
Format inspire des Architecture Decision Records (M. Nygard). Les ADR sont immuables :
|
||||
une decision revisee donne une nouvelle fiche qui *supersede* l'ancienne (statut mis a jour).
|
||||
|
||||
**Auteur : BYAN** (formalisation ; arbitrage et validation par l'auteur).
|
||||
|
||||
| # | Decision | Statut |
|
||||
|---|---|---|
|
||||
| [0001](0001-php-from-scratch-sans-composer.md) | PHP from scratch, sans framework ni Composer | Accepte |
|
||||
| [0002](0002-back-office-mvc-rendu-serveur.md) | Back-office en MVC rendu serveur (pas de SPA) | Accepte |
|
||||
| [0003](0003-stock-pourcentage-dispo-calculee.md) | Stock en pourcentage + disponibilite produit calculee (RG-T21) | Accepte |
|
||||
| [0004](0004-pin-action-sensible-audit.md) | PIN d'action sensible (equipier) + audit dans la meme transaction | Accepte |
|
||||
| [0005](0005-throttle-pin-separe-du-login.md) | Throttle du PIN separe des compteurs de connexion (RG-T22) | Accepte |
|
||||
| [0006](0006-http-409-conflit-422-validation.md) | HTTP 409 (conflit) vs 422 (validation) | Accepte |
|
||||
| [0007](0007-rgpd-anonymisation-tombstone.md) | Effacement RGPD par anonymisation (tombstone), pas DELETE | Accepte |
|
||||
| [0008](0008-makefile-vers-compose-migrate.md) | Du Makefile a `docker compose up` (service wakdo-migrate) | Accepte |
|
||||
| [0009](0009-compose-standalone-et-prod-gitignore.md) | docker-compose.yml standalone + docker-compose.prod.yml gitignore | Accepte |
|
||||
| [0010](0010-cookie-secure-conditionnel-https.md) | Cookie de session Secure conditionnel au HTTPS | Accepte |
|
||||
|
||||
## Modele de fiche
|
||||
|
||||
```
|
||||
# ADR-NNNN — Titre
|
||||
|
||||
- Statut : Propose | Accepte | Supersede par ADR-XXXX
|
||||
- Date : AAAA-MM-JJ
|
||||
|
||||
## Contexte
|
||||
Le probleme, les contraintes, les options envisagees.
|
||||
|
||||
## Decision
|
||||
Le choix retenu, en une ou deux phrases nettes.
|
||||
|
||||
## Consequences
|
||||
Ce que ca implique (positif et negatif), et les regles/fichiers concernes.
|
||||
```
|
||||
343
docs/api/conventions.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# API Wakdo - conventions de nommage, structure et listing
|
||||
|
||||
**Statut** : v0.2 - convention de casse arbitree (snake_case, voir section 4)
|
||||
**Perimetre** : back-office admin (rendu serveur) + API REST sous `/api/*`
|
||||
**Auteur methodologie** : BYAN
|
||||
**A lire avec** : `docs/PROJECT_CONTEXT.md`, `docs/merise/dictionary.md` (source de verite des
|
||||
noms de champs), `docs/merise/mct.md` + `mlt.md` (operations metier), `db/seeds/0001_rbac_and_reference.sql`
|
||||
(catalogue des 23 permissions). NB : `docs/api/byan-api.md` documente l'API de la plateforme BYAN,
|
||||
distincte de l'API Wakdo decrite ici.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objet
|
||||
|
||||
Fixer les conventions de nommage, la structure des points d'entree HTTP de Wakdo, et tenir le
|
||||
listing des endpoints (en service et prevus). Objectif : que chaque endpoint ajoute suive le meme
|
||||
moule. Les choix sont des conventions de projet (coherence, lisibilite), pas des regles universelles ;
|
||||
une convention peut evoluer, auquel cas ce document est mis a jour en premier.
|
||||
|
||||
---
|
||||
|
||||
## 2. Par quoi passe une requete
|
||||
|
||||
Deux hotes distincts, un seul conteneur web (Apache), routes par le Traefik de l'hote :
|
||||
|
||||
```
|
||||
Client (borne / navigateur back-office)
|
||||
-> Traefik (TLS, ajoute X-Forwarded-For, route par Host)
|
||||
-> wakdo-web (Apache, vhost selon le Host)
|
||||
- vhost kiosk : DocumentRoot src/public/borne (statique + futur appel /api)
|
||||
- vhost admin : DocumentRoot src/public/admin
|
||||
- fichier existant (assets/ : css, js, images) : servi tel quel
|
||||
- sinon RewriteRule -> index.php (front controller)
|
||||
-> wakdo-app (PHP-FPM, via proxy FastCGI sur *.php)
|
||||
front controller -> Router -> Controller -> Response
|
||||
-> wakdo-db (MariaDB, requetes preparees PDO uniquement)
|
||||
```
|
||||
|
||||
Consequence de nommage : le DocumentRoot du vhost admin est `src/public/admin`, donc le
|
||||
`REQUEST_URI` arrive **sans prefixe** `/admin`. Le Router voit `/login`, `/api/health`, etc.
|
||||
On n'ajoute pas de segment `/admin` dans les chemins de routes.
|
||||
|
||||
Code de reference : routes dans `src/public/admin/index.php`, controleurs dans
|
||||
`src/app/Controllers/`, enveloppe de reponse dans `src/app/Core/Response.php`, resolution
|
||||
(404 / 405) dans `src/app/Core/Router.php`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Deux familles d'endpoints
|
||||
|
||||
| Famille | Prefixe | Rendu | Authentification | Exemple |
|
||||
|---|---|---|---|---|
|
||||
| Pages back-office | aucun | HTML (vue serveur + `layout.php`) | session admin | `/login`, `/forgot_password` |
|
||||
| API REST | `/api/` | JSON (enveloppe section 7) | selon la ressource (section 10) | `/api/health`, `/api/categories` (prevu) |
|
||||
|
||||
La borne (kiosk) consommera l'API REST `/api/*` (P4). En attendant, elle lit un repli JSON
|
||||
statique sous `src/public/borne/data/` (voir section 8.3).
|
||||
|
||||
---
|
||||
|
||||
## 4. Nommage des chemins (URL)
|
||||
|
||||
Deux decisions, dont une sourcee et une de coherence :
|
||||
|
||||
- **Minuscules** sur tout le chemin. Sourced : RFC 3986 §6.2.2.1 - seuls le scheme et l'hote sont
|
||||
insensibles a la casse, le path est sensible a la casse ; le minuscule evite les bugs de casse.
|
||||
- **Separateur de mots : `_` (snake_case)**. Aucun standard n'impose `-` ou `_` dans un segment
|
||||
(les deux sont des caracteres `unreserved`, RFC 3986 §2.3). On retient `_` pour n'avoir **qu'une
|
||||
seule convention de casse** sur tout le projet : colonnes DB, champs JSON (section 8) et chemins
|
||||
d'URL partagent le snake_case. Cela calque les noms de tables (`order_item` -> `/api/order_items`)
|
||||
et reduit la charge a memoriser (Rasoir d'Ockham, mantra #37).
|
||||
|
||||
Autres regles :
|
||||
|
||||
- **Noms de ressources au pluriel** pour les collections : `/api/categories`, `/api/products`,
|
||||
`/api/orders`.
|
||||
- **Identifiant en segment** pour une ressource unitaire : `/api/orders/{number}`,
|
||||
`/api/products/{id}`. Parametre dynamique : `{nom}` (groupe nomme cote Router).
|
||||
- **Sous-ressource** par imbrication : `/api/orders/{id}/items` (prevu).
|
||||
- **Action non-CRUD** par sous-chemin verbe : `POST /api/orders/{id}/cancel`
|
||||
(cf. `docs/uml/security-sequence.md`).
|
||||
- Pas de barre oblique finale signifiante : `Request::normalizePath` aligne `/api/health/` et
|
||||
`/api/health`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Listing des endpoints
|
||||
|
||||
### 5.1 En service (P2)
|
||||
|
||||
| Methode | Chemin | Auth | Rendu | Role |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/` | (session en P3) | HTML | accueil back-office (squelette) |
|
||||
| GET | `/api/health` | public | JSON (plat) | sonde de sante (DB reelle) |
|
||||
| GET | `/login` | public | HTML | formulaire de connexion |
|
||||
| POST | `/login` | public + CSRF | 302 / HTML | authentification (mlt 12.1) |
|
||||
| POST | `/logout` | session + CSRF | 302 | deconnexion (mlt 12.2) |
|
||||
| GET | `/forgot_password` | public | HTML | demande de reinitialisation |
|
||||
| POST | `/forgot_password` | public + CSRF | HTML (neutre) | envoi du lien (mlt 12.3) |
|
||||
| GET | `/reset_password` | public (token en query) | HTML | formulaire nouveau mot de passe |
|
||||
| POST | `/reset_password` | public + CSRF | 302 / HTML | confirmation (mlt 12.3) |
|
||||
| GET | `/api/me` | session | JSON | identite + permissions du compte courant (RG-6/RG-T02/RG-T03) |
|
||||
|
||||
`/api/me` est le premier consommateur reel de `SessionGuard` (RG-6 idle/absolu + RG-T02
|
||||
is_active) et d'`Authorizer` (RG-T03, permissions rechargees depuis la base). Reponse :
|
||||
`{ "data": { "user_id", "role_id", "role_code", "permissions": [...] } }` ; `401 AUTH_REQUIRED`
|
||||
si la session est absente, expiree ou le compte desactive. Les autorisations par operation
|
||||
(et le PIN des actions sensibles, RG-T13) se cablent quand les operations existent (P3).
|
||||
|
||||
### 5.2 API kiosk - lecture catalogue + commande (prevu P4, public)
|
||||
|
||||
La borne est publique (aucune session) ; cf. `mlt.md` CREATE_ORDER, declencheur kiosk.
|
||||
|
||||
| Methode | Chemin | Permission | Op MCT | Statut |
|
||||
|---|---|---|---|---|
|
||||
| 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)
|
||||
|
||||
Provisoire : le choix entre endpoints JSON `/api/*` et pages rendues serveur pour les ecritures
|
||||
admin est tranche phase par phase (P3 CRUD). Les colonnes Permission renvoient au catalogue fige
|
||||
des 23 permissions (`db/seeds/0001_rbac_and_reference.sql`) ; l'imputabilite et le PIN suivent
|
||||
`mlt.md` RG-T13/RG-T14.
|
||||
|
||||
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 | 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) |
|
||||
|
||||
Catalogue (produits, menus, categories) :
|
||||
|
||||
| Methode | Chemin | Permission | Op MCT |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/products` | `product.create` | CREATE_PRODUCT (mlt 8.1) |
|
||||
| PUT | `/api/products/{id}` | `product.update` | UPDATE_PRODUCT (mlt 8.2) - PIN sur prix/TVA |
|
||||
| DELETE | `/api/products/{id}` | `product.delete` | DELETE_PRODUCT (mlt 8.3) - PIN |
|
||||
| POST | `/api/menus` | `menu.create` | CREATE_MENU |
|
||||
| PUT | `/api/menus/{id}` | `menu.update` | UPDATE_MENU |
|
||||
| DELETE | `/api/menus/{id}` | `menu.delete` | DELETE_MENU - PIN |
|
||||
| POST/PUT/DELETE | `/api/categories[/{id}]` | `category.manage` | MANAGE_CATEGORY |
|
||||
|
||||
Stock et ingredients :
|
||||
|
||||
| Methode | Chemin | Permission | Op MCT |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/ingredients` | `ingredient.manage` | READ_INGREDIENTS |
|
||||
| GET | `/api/stock` | `stock.read` | READ_STOCK |
|
||||
| POST | `/api/stock/restock` | `stock.manage` | RESTOCK (mlt 9.1) |
|
||||
| POST | `/api/stock/count` | `stock.count` | INVENTORY_COUNT (mlt 9.2) - PIN |
|
||||
|
||||
Utilisateurs et RBAC :
|
||||
|
||||
| Methode | Chemin | Permission | Op MCT |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/users` | `user.read` | READ_USERS |
|
||||
| POST | `/api/users` | `user.create` | CREATE_USER (mlt 10.1) - PIN |
|
||||
| PUT | `/api/users/{id}` | `user.update` | UPDATE_USER (mlt 10.2) - PIN |
|
||||
| POST | `/api/users/{id}/deactivate` | `user.deactivate` | DEACTIVATE_USER (mlt 10.3) - PIN |
|
||||
| GET/PUT | `/api/roles[/{id}/permissions]` | `role.manage` | MANAGE_RBAC (mlt 10.4) - PIN |
|
||||
|
||||
Statistiques :
|
||||
|
||||
| Methode | Chemin | Permission | Op MCT |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/stats` | `stats.read` | READ_STATS (mlt 11.x) |
|
||||
|
||||
> Les chemins exacts en 5.2/5.3 sont une projection a partir des operations MCT et des permissions
|
||||
> seedees ; ils sont confirmes au moment d'ecrire chaque endpoint. Seule la section 5.1 est en service.
|
||||
|
||||
---
|
||||
|
||||
## 6. Methodes HTTP
|
||||
|
||||
| Methode | Usage |
|
||||
|---|---|
|
||||
| GET | lecture, sans effet de bord |
|
||||
| POST | creation, ou action de formulaire back-office (login, logout, reset) |
|
||||
| PUT | mise a jour d'une ressource (prevu, CRUD admin P3) |
|
||||
| DELETE | suppression d'une ressource (prevu) |
|
||||
|
||||
Le Router fait une correspondance exacte de la methode : methode connue sur chemin connu mais non
|
||||
enregistree -> `405` ; chemin inconnu -> `404` (`Router::dispatch`). Une requete `HEAD` sur une
|
||||
route `GET` renvoie aujourd'hui `405` (correspondance exacte) ; un assouplissement reste possible
|
||||
si un besoin apparait.
|
||||
|
||||
---
|
||||
|
||||
## 7. Enveloppe de reponse JSON
|
||||
|
||||
L'API enveloppe ses reponses pour qu'un client distingue donnees et erreur de maniere uniforme.
|
||||
|
||||
Succes - ressource unitaire :
|
||||
|
||||
```json
|
||||
{ "data": { "id": 3, "name": "Big Mac", "price_cents": 590 } }
|
||||
```
|
||||
|
||||
Succes - collection (`total` optionnel pour la pagination future) :
|
||||
|
||||
```json
|
||||
{ "data": [ { "id": 1 }, { "id": 2 } ], "total": 2 }
|
||||
```
|
||||
|
||||
Erreur :
|
||||
|
||||
```json
|
||||
{ "data": null, "error": { "code": "NOT_FOUND", "message": "Resource not found" } }
|
||||
```
|
||||
|
||||
Exception documentee : `GET /api/health` renvoie un objet de diagnostic plat (`status`, `app_env`,
|
||||
`php_version`, `db`, `categories`), hors enveloppe, car il sert le monitoring et non un client
|
||||
applicatif.
|
||||
|
||||
Type de contenu : `application/json; charset=utf-8` (`Response::json`). Les pages back-office
|
||||
renvoient `text/html; charset=utf-8`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Normalisation des noms de champs
|
||||
|
||||
### 8.1 Regle generale : snake_case aligne sur le dictionnaire
|
||||
|
||||
Les champs JSON reprennent les noms du dictionnaire (`docs/merise/dictionary.md`), source de verite,
|
||||
ce qui evite une couche de traduction entre base, code et contrat HTTP.
|
||||
|
||||
| Categorie | Convention | Exemple |
|
||||
|---|---|---|
|
||||
| Champ simple | snake_case, anglais | `display_order`, `image_path` |
|
||||
| Montant monetaire | entier en centimes, suffixe `_cents` | `price_cents`, `total_ttc_cents` |
|
||||
| Taux de TVA | entier pour mille | `vat_rate` (55 = 5,5 % ; 100 = 10 %) |
|
||||
| Booleen | prefixe `is_` | `is_available`, `is_active` |
|
||||
| Horodatage | suffixe `_at`, ISO 8601 en sortie API | `created_at`, `paid_at` |
|
||||
| Cle etrangere | suffixe `_id` | `category_id`, `role_id` |
|
||||
| Valeur d'enumeration | minuscules snake_case | `pending_payment`, `dine_in`, `kiosk` |
|
||||
| Identifiant | `id` (entier) ou `order_number` (chaine metier) | `id`, `order_number` |
|
||||
|
||||
Les horodatages sont stockes en `DATETIME` ; leur exposition API se fait en ISO 8601 (a cadrer
|
||||
au moment d'ecrire les endpoints de lecture P4).
|
||||
|
||||
### 8.2 Codes d'erreur
|
||||
|
||||
SCREAMING_SNAKE_CASE, stables (un client peut s'y fier) ; le `message` reste lisible (non garanti
|
||||
stable).
|
||||
|
||||
| Code | HTTP | Sens |
|
||||
|---|---|---|
|
||||
| `NOT_FOUND` | 404 | ressource introuvable |
|
||||
| `METHOD_NOT_ALLOWED` | 405 | methode non autorisee sur ce chemin |
|
||||
| `VALIDATION_ERROR` | 422 | entree invalide (champ, longueur, enum) |
|
||||
| `CONFLICT` | 409 | conflit d'etat (ex. transition de commande concurrente) ; suppression dure bloquee par une reference (FK RESTRICT) ; unicite slug/name deja prise (remontee par la base). La validation simple en amont (champ/format/bornes) reste `VALIDATION_ERROR` 422 |
|
||||
| `AUTH_REQUIRED` | 401 | authentification requise (prevu, API admin) |
|
||||
| `FORBIDDEN` | 403 | permission insuffisante, ou jeton CSRF invalide cote formulaire |
|
||||
| `RATE_LIMITED` | 429 | throttling (prevu) |
|
||||
| `INTERNAL_ERROR` | 500 | erreur interne, message generique (pas de divulgation) |
|
||||
|
||||
Codes specifiques nommes par le MLT, en surcharge du socle : `CANNOT_CANCEL_IN_STATE` (422) et
|
||||
`INVALID_TRANSITION` (409) pour l'annulation (`mlt.md` 7.1, `security-sequence.md`). Meme format
|
||||
d'enveloppe.
|
||||
|
||||
### 8.3 Divergence connue : repli JSON de la borne
|
||||
|
||||
Le repli statique de la borne (`src/public/borne/data/categories.json`, `produits.json`) provient
|
||||
des sources de l'ecole et porte un nommage different et heterogene (`title`/`nom`, `prix`, `image`,
|
||||
`type`). Ce contrat est fige par le brief ecole et consomme tel quel par le JS de la borne via
|
||||
`data.js`.
|
||||
|
||||
La convention canonique reste celle de 8.1. Le rapprochement se fait en un point unique : la couche
|
||||
`data.js` (bascule prevue en P4). Quand l'API exposera `/api/categories` et `/api/products`, elle
|
||||
servira la forme canonique ; `data.js` mappera vers ce que la borne attend.
|
||||
|
||||
| Repli borne | Canonique API / dictionnaire |
|
||||
|---|---|
|
||||
| `title` (categorie) | `name` |
|
||||
| `nom` (produit) | `name` |
|
||||
| `prix` | `price_cents` |
|
||||
| `image` | `image_path` |
|
||||
| `type` | `item_type` (`product` / `menu`) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Authentification et sessions
|
||||
|
||||
- **Cookie de session** : `WAKDO_SID` (`SESSION_NAME`), attributs `secure`, `HttpOnly`,
|
||||
`SameSite=Strict`. Bornes de validite appliquees cote application (idle 4h, absolue 10h),
|
||||
pas par la duree du cookie.
|
||||
- **Formulaires back-office** : jeton CSRF synchroniseur en champ cache `_csrf`, verifie sur chaque
|
||||
POST (`/login`, `/logout`, `/forgot_password`, `/reset_password`). Jeton invalide -> `403`.
|
||||
- **API REST** : endpoints kiosk de lecture catalogue et creation de commande publics (pas de
|
||||
session ; `mlt.md` CREATE_ORDER). Endpoints d'administration sous `/api` (P3/P4) : session admin +
|
||||
verification de permission via `role_permission` ; actions sensibles avec re-autorisation PIN
|
||||
(`mlt.md` RG-T13).
|
||||
|
||||
Le schema `ApiKey` / `Bearer` de l'API plateforme BYAN (`docs/api/byan-api.md`) ne s'applique pas
|
||||
ici.
|
||||
|
||||
---
|
||||
|
||||
## 10. CORS
|
||||
|
||||
La borne consomme `/api/*` en **meme origine** : le vhost kiosk (`docker/apache/vhost.conf`)
|
||||
relaie `/api/*` au front controller admin via PHP-FPM (`ProxyPassMatch` + `ProxyFCGISetEnvIf`
|
||||
qui force `SCRIPT_FILENAME` sur `public/admin/index.php`). `data.js` garde donc des URLs
|
||||
relatives et le navigateur n'emet pas de requete cross-origin pour ce parcours.
|
||||
|
||||
Le middleware `App\Core\Cors` reste en place comme defense en profondeur : il lit
|
||||
`CORS_ALLOWED_ORIGIN` (valeur exacte, sans joker, = `APP_URL_KIOSK`) et autorise un eventuel
|
||||
consommateur cross-origin de l'API. Il n'est pas sur le chemin de la borne.
|
||||
|
||||
---
|
||||
|
||||
## 11. Versionnement
|
||||
|
||||
Demarrage sans segment de version (`/api/...`), ce qui correspond a une v1 implicite. En cas de
|
||||
changement de contrat non retrocompatible, l'option retenue est un prefixe explicite `/api/v2/...`
|
||||
introduit a ce moment-la, en gardant `/api/...` pour la v1 tant que des clients en dependent.
|
||||
|
||||
---
|
||||
|
||||
## 12. Ou est defini quoi (recap code)
|
||||
|
||||
| Element | Fichier |
|
||||
|---|---|
|
||||
| Declaration des routes | `src/public/admin/index.php` |
|
||||
| Resolution / 404 / 405 | `src/app/Core/Router.php` |
|
||||
| Enveloppe `data` / `error` / contenu JSON | `src/app/Core/Response.php` |
|
||||
| Lecture de la requete (chemin, query, corps, IP) | `src/app/Core/Request.php` |
|
||||
| Controleurs | `src/app/Controllers/` |
|
||||
| Acces base (requetes preparees, transaction) | `src/app/Core/Database.php` |
|
||||
| Noms de champs (source de verite) | `docs/merise/dictionary.md` |
|
||||
| Operations metier et permissions | `docs/merise/mct.md`, `mlt.md`, `db/seeds/0001_rbac_and_reference.sql` |
|
||||
102
docs/architecture/forgejo-actions-runner.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Forgejo Actions - runner (act_runner)
|
||||
|
||||
Prerequis d'infrastructure pour la CI/CD Wakdo. Les workflows vivent dans
|
||||
`.forgejo/workflows/` (lot D) ; ils ne s'executent que si un `act_runner` est
|
||||
enregistre et en ligne sur le serveur.
|
||||
|
||||
## Pourquoi un runner separe de la stack app
|
||||
|
||||
La stack `docker-compose.yml` de Wakdo = runtime applicatif (web, app, db, cron).
|
||||
Le runner CI est du **tooling** : il se rattache au depot Forgejo, pas a l'app.
|
||||
On le fait tourner comme service dedie sur l'hote stark (meme lecon que
|
||||
"gh dans Docker = mauvaise idee", cf. journal session 6). Cela evite que la CI
|
||||
puisse impacter le runtime, et garde un cycle de vie independant.
|
||||
|
||||
## 1. Obtenir le token de registration (action manuelle, niveau admin)
|
||||
|
||||
Le token vient de l'instance Forgejo, pas du repo. Dans l'UI Forgejo :
|
||||
|
||||
- niveau **repo** : `Settings > Actions > Runners > Create new runner`
|
||||
- ou niveau **org/instance** : `Site Administration > Actions > Runners`
|
||||
|
||||
Recuperer le `REGISTRATION_TOKEN` affiche. Il est a usage unique pour
|
||||
l'enregistrement (pas a versionner).
|
||||
|
||||
## 2. Enregistrer le runner (sur stark)
|
||||
|
||||
Setup reel en place (image `simplyforma/forgejo-runner` deja presente sur
|
||||
l'hote, data dir sous `$HOME` car `/srv` non inscriptible par `corentin`).
|
||||
Le conteneur tourne sous l'uid de l'hote (`--user`) pour pouvoir ecrire
|
||||
`.runner` dans le volume monte.
|
||||
|
||||
```bash
|
||||
DATA=/home/corentin/forgejo-runner-wakdo
|
||||
mkdir -p "$DATA"
|
||||
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-v "$DATA":/data --workdir /data \
|
||||
--entrypoint forgejo-runner \
|
||||
simplyforma/forgejo-runner:12.10.2 \
|
||||
register --no-interactive \
|
||||
--instance https://git.acadenice.com \
|
||||
--token "<REGISTRATION_TOKEN>" \
|
||||
--name stark-wakdo \
|
||||
--labels 'docker:docker://node:20-bookworm,php-ci:docker://php:8.3-cli'
|
||||
```
|
||||
|
||||
L'enregistrement ecrit `$DATA/.runner` (contient le secret du runner - ne pas
|
||||
versionner, ne pas sortir de l'hote). Runner enregistre le 2026-06-15
|
||||
(uuid `e4a3dbef-...`, labels `docker` + `php-ci`).
|
||||
|
||||
## 3. Lancer le runner en service
|
||||
|
||||
```bash
|
||||
DATA=/home/corentin/forgejo-runner-wakdo
|
||||
DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
|
||||
docker run -d --restart=always \
|
||||
--name forgejo-runner-wakdo \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--group-add "$DOCKER_GID" \
|
||||
-e HOME=/data \
|
||||
-v "$DATA":/data --workdir /data \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--entrypoint forgejo-runner \
|
||||
simplyforma/forgejo-runner:12.10.2 \
|
||||
daemon
|
||||
```
|
||||
|
||||
Notes :
|
||||
- `--group-add $DOCKER_GID` : acces au socket Docker pour executer les jobs
|
||||
dans des conteneurs (sans tourner en root).
|
||||
- `-e HOME=/data` : evite l'erreur `mkdir /.cache: permission denied` (le cache
|
||||
server interne ecrit sous `$HOME`).
|
||||
- Verifier `docker logs forgejo-runner-wakdo` : `declared successfully` +
|
||||
`[poller] launched`, et `Settings > Actions > Runners` doit montrer `stark-wakdo` **Idle**.
|
||||
- Prerequis cote depot : **Actions activees** (`Settings > Actions` du depot).
|
||||
|
||||
## 4. Labels et usage en workflow
|
||||
|
||||
Les jobs ciblent un label via `runs-on`. Pour la CI PHP de Wakdo :
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: docker # image par defaut node:20-bookworm
|
||||
# les etapes installent/php via le conteneur ou une action setup-php
|
||||
```
|
||||
|
||||
## Securite du runner
|
||||
|
||||
- Le `.runner` (secret) reste sur l'hote, hors du repo.
|
||||
- Le socket Docker monte donne un acces privilegie : le runner ne doit executer
|
||||
que des workflows du depot Wakdo (runner dedie au repo, pas partage).
|
||||
- Roter le secret = re-enregistrer avec un nouveau token et supprimer l'ancien
|
||||
runner dans l'UI.
|
||||
|
||||
## Lien avec les autres lots
|
||||
|
||||
- **Lot C** : ce document + prerequis infra.
|
||||
- **Lot D** : `.forgejo/workflows/ci.yml` (PHPUnit + PHPStan + secret-scan gitleaks)
|
||||
et auto-merge des PR sur CI verte (strategie solo dev validee).
|
||||
124
docs/architecture/functional-schema.md
Normal 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`
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
## Fichiers
|
||||
|
||||
- `maquette-borne.pdf` : maquette ecrans complete fournie avec le brief ecole
|
||||
- `screens/` : les 10 ecrans de la maquette exportes en PNG (un par ecran)
|
||||
- `maquette-vs-build.md` : decomposition ecran par ecran + tracabilite maquette vs kiosk construit (ecarts structurants)
|
||||
|
||||
## Source en ligne
|
||||
|
||||
|
|
|
|||
121
docs/design/maquette-vs-build.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Maquette borne vs kiosk construit — decomposition et tracabilite
|
||||
|
||||
> Auteur : BYAN. Note de tracabilite maquette -> code (appui oral RNCP Bloc 1 :
|
||||
> "comment etes-vous passe de la maquette au code ?").
|
||||
> Source : `docs/design/maquette-borne.pdf` (export Figma de l'ecole, 10 ecrans,
|
||||
> format 1440x1024). Ecrans exportes un a un dans `docs/design/screens/`.
|
||||
|
||||
## 1. Lecture d'ensemble
|
||||
|
||||
La maquette decrit un **parcours de type McDonald's** (Big Mac, Best Of, McCafe,
|
||||
arches M, Coca) : c'est la base de reference a rebrander en Wakdo.
|
||||
|
||||
Point central : les **10 "ecrans" ne sont pas 10 pages**. Ce sont en realite
|
||||
~4 ecrans de base plus un systeme de modales qui s'ouvrent par-dessus l'ecran de
|
||||
commande :
|
||||
|
||||
```
|
||||
Accueil
|
||||
-> Ecran de commande UNIQUE
|
||||
(bandeau categories en haut + grille produits + panneau commande persistant a droite)
|
||||
sur lequel s'ouvrent les modales de composition (taille menu, accompagnement, boisson, format/quantite)
|
||||
-> Chevalet (sur place : saisie du numero)
|
||||
-> Remerciement
|
||||
```
|
||||
|
||||
Le kiosk construit, lui, eclate cet ecran unique en **pages distinctes** et n'a
|
||||
pas de panneau de commande persistant. C'est l'origine du sentiment "ca ne
|
||||
correspond pas".
|
||||
|
||||
## 2. Decomposition ecran par ecran
|
||||
|
||||
### Ecran 1 — Accueil
|
||||

|
||||
- "Bonjour," + "Souhaitez-vous consommer votre menu sur place ou preferez-vous l'emporter ?"
|
||||
- Deux grandes cartes : **Sur Place** (icone table) / **A Emporter** (icone sac).
|
||||
- Fond : arches + Big Mac + Coca.
|
||||
|
||||
### Ecran 2 — Ecran de commande (pivot)
|
||||

|
||||
- **Bandeau categories horizontal** (Menus actif, Sandwiches, Wraps, Frites, Boissons Froides, Encas, Desserts...) avec fleches rouges ◀ ▶ pour faire defiler.
|
||||
- Titre de section "Nos menus" + sous-titre + **grille de produits** (image, nom, prix).
|
||||
- **Panneau de commande persistant a droite** : numero de commande (72), "Sur place : 326", lignes de commande avec options en puces et icone corbeille, "TOTAL (ttc) 36,50 EUR", boutons "Abandon" / "Payer", logo W en haut.
|
||||
- Cet ecran est le coeur de la maquette : tout le reste (sauf accueil/chevalet/remerciement) se joue ici ou en modale par-dessus.
|
||||
|
||||
### Ecran 3 — Modale "Une grosse faim ?" (composition menu, etape 1)
|
||||

|
||||
- Choix de la taille : **Menu Maxi Best Of** / **Menu Best Of**.
|
||||
- Bouton "Etape Suivante". Premiere etape d'un assistant en modale.
|
||||
|
||||
### Ecran 4 — Modale "Choisissez votre accompagnement" (etape 2)
|
||||

|
||||
- **Frites** / **Potatoes**.
|
||||
- Boutons "Retour" + "Etape Suivante".
|
||||
|
||||
### Ecran 5 — Modale "Choisissez votre boisson" (etape 3)
|
||||

|
||||
- Carrousel de boissons (Eau, Coca, Coca Zero, Jus de pomme BIO, The...) avec ◀ ▶.
|
||||
- Bouton "Ajouter le menu a ma commande" (fin de l'assistant).
|
||||
|
||||
### Ecran 6 — Ecran de commande, categorie Boissons Froides (a la carte)
|
||||

|
||||
- Meme ecran pivot, categorie "Boissons Froides" active.
|
||||
- Grille de 8 boissons avec prix unitaires (Eau 1 EUR, Coca 1.90 EUR, Fanta 1.90 EUR, Jus de pomme BIO 2.30 EUR...).
|
||||
|
||||
### Ecran 7 — Selection d'un produit (etat)
|
||||

|
||||
- Meme grille, "Coca Cola" entoure en jaune : etat visuel de selection.
|
||||
|
||||
### Ecran 8 — Modale "Une petite soif ?" (option produit a la carte)
|
||||

|
||||
- Taille **30Cl / 50Cl** (+0.50 EUR pour le 50Cl).
|
||||
- **Stepper de quantite** (- 1 +).
|
||||
- Boutons "Annuler" / "Ajouter a ma commande".
|
||||
|
||||
### Ecran 9 — Chevalet (sur place)
|
||||

|
||||
- "Pour etre servis a table," + "Recuperez un chevalet et indiquez ici le numero inscrit dessus".
|
||||
- Grands chiffres `2 6 1` + bouton "Enregistrer le numero".
|
||||
|
||||
### Ecran 10 — Remerciement
|
||||

|
||||
- "Toute l'equipe vous remercie, Et vous souhaite un bon appetit dans nos restaurants, A bientot !"
|
||||
- Bouton "Nouvelle commande".
|
||||
|
||||
## 3. Maquette -> kiosk construit (mapping)
|
||||
|
||||
| Maquette | Kiosk construit | Verdict |
|
||||
|----------|-----------------|---------|
|
||||
| 1. Accueil sur place / a emporter | `index.html` | conforme |
|
||||
| 2 + 6. Ecran de commande unique (bandeau + grille + **panneau persistant**) | eclate en `categories.html` -> `products.html` -> `cart.html` | divergence structurante : multi-pages, et **pas de panneau de commande persistant** |
|
||||
| (pas de page categories separee) | `categories.html` plein ecran "Que souhaitez-vous commander ?" | ecran **ajoute** (la maquette met les categories en bandeau) |
|
||||
| 3-5. Composeur menu = **assistant modal en etapes** | `page-product-menu.js` = composition **libre** | divergence (le refactor "consommer les slots /api/menus" est deja en file P4) |
|
||||
| 8. Modale d'option produit (taille + quantite) | `product.html` (page) | divergence : page au lieu de modale |
|
||||
| 9. Ecran **chevalet** dedie (saisie numero) | numero gere par l'API (chunk 1a), affiche en confirmation | manquant cote ecran |
|
||||
| (aucun ecran de paiement) | `payment.html` "Carte bancaire / Especes" | ecran **ajoute** par le build |
|
||||
| 10. Remerciement | `confirmation.html` | conforme |
|
||||
|
||||
## 4. Ecarts structurants (le fond du sujet)
|
||||
|
||||
1. **Paradigme inverse.** Maquette = **mono-ecran** (un plan de commande avec
|
||||
categories en bandeau et un panneau recapitulatif persistant a droite, modales
|
||||
par-dessus). Build = **multi-pages** classiques (categories -> produits ->
|
||||
produit -> panier). C'est l'ecart structurant principal.
|
||||
2. **Panneau de commande lateral absent.** La piece centrale de la maquette
|
||||
(numero de commande, lignes editables avec corbeille, TOTAL ttc, Abandon /
|
||||
Payer, visible en permanence) n'est pas presente dans le build.
|
||||
3. **Composition de menu.** Maquette = assistant modal en etapes ; build =
|
||||
composition libre cote client (`page-product-menu.js`).
|
||||
|
||||
## 5. Rebrand McDonald's -> Wakdo
|
||||
|
||||
Le visuel de la maquette est du McDonald's litteral (Big Mac, Best Of, McCafe,
|
||||
arches, "Tous a l'eau by M"). Le rebrand vers Wakdo (logo W, catalogue propre)
|
||||
est attendu et legitime : le branding McDo n'est pas livrable. Le sujet de cette
|
||||
note n'est donc pas le rebrand mais la **structure** des ecrans.
|
||||
|
||||
## 6. Suite
|
||||
|
||||
Re-alignement du kiosk sur la maquette (panneau persistant + bandeau categories +
|
||||
composeur en modale) = chantier UI conduit via un cycle FD dedie. Backlog des
|
||||
divergences = section 3 ci-dessus.
|
||||
BIN
docs/design/screens/01-accueil.png
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
docs/design/screens/02-commande-menus.png
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
docs/design/screens/03-modale-taille-menu.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
docs/design/screens/04-modale-accompagnement.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
docs/design/screens/05-modale-boisson.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
docs/design/screens/06-commande-boissons.png
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
docs/design/screens/07-boissons-selection.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
docs/design/screens/08-modale-format-quantite.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
docs/design/screens/09-chevalet.png
Normal file
|
After Width: | Height: | Size: 910 KiB |
BIN
docs/design/screens/10-remerciement.png
Normal file
|
After Width: | Height: | Size: 916 KiB |
18
docs/domaines/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Documentation par domaine
|
||||
|
||||
Une fiche par domaine fonctionnel livre : **perimetre**, **ce qui est livre** (code +
|
||||
routes), **regles metier** (RG-T* de `docs/merise/mlt.md`), **decisions** (renvoi
|
||||
`docs/adr/`), **tables**. Vue d'ensemble : `docs/ARCHITECTURE.md`.
|
||||
|
||||
**Auteur : BYAN** (formalisation ; arbitrage et validation par l'auteur).
|
||||
|
||||
| Domaine | Fiche | Statut |
|
||||
|---|---|---|
|
||||
| Authentification & sessions | [auth.md](auth.md) | Livre (P2) |
|
||||
| Catalogue (categories, produits, menus) | [catalogue.md](catalogue.md) | Livre (P3) |
|
||||
| Stock & recettes (ingredients) | [stock-recettes.md](stock-recettes.md) | Livre (P3) |
|
||||
| Comptes utilisateurs | [users.md](users.md) | Livre (P3) |
|
||||
| RBAC (roles & permissions) | [rbac.md](rbac.md) | Livre (P3) |
|
||||
| Statistiques | [stats.md](stats.md) | Livre (P3, KPIs vente differes P4) |
|
||||
| Borne (kiosk) | [borne.md](borne.md) | Front P5 (API au swap P4) |
|
||||
| Commande | — | P4 (schema pret, workflow a venir) |
|
||||
29
docs/domaines/auth.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Domaine — Authentification & sessions
|
||||
|
||||
## Perimetre
|
||||
Connexion back-office, deconnexion, reinitialisation de mot de passe, garde de session,
|
||||
PIN d'action sensible. Pas d'auth cote borne (front public).
|
||||
|
||||
## Ce qui est livre
|
||||
- `App\Auth\AuthService` (login 12.1 / logout 12.2), `PasswordResetService` (12.3).
|
||||
- `SessionManager` (seul a toucher `$_SESSION`/cookie, mode test memoire), `SessionGuard`
|
||||
(RG-6/RG-T02 : idle 4h, absolu 10h, `is_active`), `Csrf` (jeton synchroniseur).
|
||||
- `PasswordHasher` (argon2id + leurre de timing), `PinVerifier`, `PinThrottle`,
|
||||
`ThrottlePolicy` (backoff degressif).
|
||||
- Controleurs `AuthController`, `PasswordResetController`, `ProfileController` (set-PIN
|
||||
self-service), `MeController` (`/api/me`).
|
||||
|
||||
## Regles metier
|
||||
- RG-6 / RG-T02 : session valide (idle + absolu + compte actif) sinon 302 `/login`.
|
||||
- RG-8 / RG-9 : throttle login par compte + par IP (`login_throttle`), backoff degressif.
|
||||
- RG-T13 : PIN d'action sensible (voir [users](users.md), [rbac](rbac.md), stock).
|
||||
- Anti-enumeration : reponses neutres (reset, login) ; leurre de timing argon2id.
|
||||
|
||||
## Decisions
|
||||
[ADR-0001](../adr/0001-php-from-scratch-sans-composer.md) (from scratch),
|
||||
[ADR-0004](../adr/0004-pin-action-sensible-audit.md) (PIN),
|
||||
[ADR-0005](../adr/0005-throttle-pin-separe-du-login.md) (throttle PIN).
|
||||
|
||||
## Tables
|
||||
`user`, `login_throttle`, `pin_throttle`, `audit_log` (login + pin.failed). Detail :
|
||||
`docs/merise/mlt.md` section 12 + 22.
|
||||
31
docs/domaines/borne.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Domaine — Borne (kiosk)
|
||||
|
||||
## Perimetre
|
||||
Front client tactile (Bloc 1) : parcours welcome -> categories -> produit -> panier ->
|
||||
confirmation. HTML/CSS/JS vanilla, servi en statique par Apache.
|
||||
|
||||
## Ce qui est livre
|
||||
- Pages : `index`, `categories`, `products`, `product`, `cart`, `payment`,
|
||||
`confirmation` (`src/public/borne/`).
|
||||
- JS modules ES6 (`assets/js/`) : `data.js` (chargement, point de swap P4), `state.js`
|
||||
(panier), `page-*.js`, `nav.js`, et `allergens.js` (modale generale 14 INCO sur carte
|
||||
et fiche).
|
||||
- Donnees : JSON statiques (`data/`) en P5 ; basculent sur `/api/*` DB-backed au swap P4.
|
||||
|
||||
## Regles metier / conventions
|
||||
- Allergenes : info **generale** (les 14 INCO, reglement UE 1169/2011), pas un calcul
|
||||
par produit (mapping `ingredient_allergen` differe).
|
||||
- CSP-safe pour le code projet : pas de script inline ajoute (donnees via `data-*`,
|
||||
`addEventListener`). Source allergenes = liste fixe `data/allergens.json`, se branchera
|
||||
sur `/api/allergens` au swap P4.
|
||||
|
||||
## Tests
|
||||
Harnais front `node:test` + jsdom (`tests/js/allergens.test.js`) : 14 INCO, bouton "i",
|
||||
ouverture/fermeture (bouton/overlay/Echap), idempotence. Job CI `js-tests` (Node 20).
|
||||
|
||||
## Decisions
|
||||
Swap point P5 -> API au P4 (cf. `data.js` + journaux). Modele = app self-hostable
|
||||
([ADR-0009](../adr/0009-compose-standalone-et-prod-gitignore.md)).
|
||||
|
||||
## Tables (au swap P4)
|
||||
`category`, `product`, `menu` + `allergen` (lecture). Aujourd'hui : JSON statiques.
|
||||
29
docs/domaines/catalogue.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Domaine — Catalogue (categories, produits, menus)
|
||||
|
||||
## Perimetre
|
||||
CRUD des categories, produits et menus composes (borne de base + slots). Base du
|
||||
catalogue consomme par la borne.
|
||||
|
||||
## Ce qui est livre
|
||||
- Repositories : `CategoryRepository`, `ProductRepository`, `MenuRepository`.
|
||||
- Controleurs : `CategoryController` (`category.manage`), `ProductController`
|
||||
(`product.read/create/update/delete`), `MenuController` (`menu.read/create/update/delete`).
|
||||
- Menus composes : burger de base + `menu_slot` / `menu_slot_option`, editeur slots en
|
||||
JS vanilla CSP-safe (champ cache `slots_json`), reecriture delete-and-reinsert en tx.
|
||||
|
||||
## Regles metier
|
||||
- RG-T16 (allowlist colonnes), RG-T18 (validation serveur bornee : prix > 0, TVA dans
|
||||
{55,100}, etc.), RG-T15 (sorties echappees).
|
||||
- Produit : PIN equipier + audit UNIQUEMENT si prix ou TVA change (mlt 8.2 RG-4) ;
|
||||
suppression = PIN + audit (8.3). Menu : suppression = PIN + audit (8.6).
|
||||
- Pas de suppression dure si reference (FK RESTRICT depuis order_item / menu / selection)
|
||||
-> 409, alternative = desactivation (`is_available`).
|
||||
|
||||
## Decisions
|
||||
[ADR-0002](../adr/0002-back-office-mvc-rendu-serveur.md) (MVC serveur),
|
||||
[ADR-0006](../adr/0006-http-409-conflit-422-validation.md) (409/422),
|
||||
[ADR-0004](../adr/0004-pin-action-sensible-audit.md) (PIN + audit).
|
||||
|
||||
## Tables
|
||||
`category`, `product`, `menu`, `menu_slot`, `menu_slot_option`. Detail :
|
||||
`docs/merise/mlt.md` section 8.
|
||||
30
docs/domaines/rbac.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Domaine — RBAC (roles & permissions)
|
||||
|
||||
## Perimetre
|
||||
Gestion des roles et de la matrice role/permission (mlt 10.4 MANAGE_RBAC), permission
|
||||
`role.manage`. Catalogue de permissions fige au seed (lecture seule).
|
||||
|
||||
## Ce qui est livre
|
||||
- `RoleRepository` (App\Auth) : roles (CRUD, code immuable), permissions (lecture),
|
||||
matrice (`permissionIdsFor`/`permissionCodesFor`, `setPermissions` tx +
|
||||
`replacePermissions` raw), `role_visible_source` (`setVisibleSources` / raw).
|
||||
- `RoleController` (`role.manage`) : index, create/store (role custom RG-4), edit/update
|
||||
(champs role + matrice + sources visibles en UNE transaction). Vues `admin/roles/{index,form}`.
|
||||
- Matrice soumise en champs **scalaires** (`perm_<id>`, `source_<enum>`) : `Request::formBody`
|
||||
ne garde que les scalaires (pas de `name[]`, pas de JS).
|
||||
|
||||
## Regles metier
|
||||
- RG-6 (mlt 10.4) : PIN equipier + `audit_log` (`role.manage`) dans une transaction ;
|
||||
`details` JSON = **diff** des codes de permission (ajoutes/retires), calcule avant la
|
||||
reecriture delete-and-reinsert.
|
||||
- `Authorizer::can` recharge les permissions a chaque verification (effet immediat).
|
||||
- Garde-fous anti-lockout : le role `admin` conserve `role.manage` ET reste actif ;
|
||||
`code` immuable apres creation ; `order_source` borne a l'ENUM ; code dupli -> 409.
|
||||
|
||||
## Decisions
|
||||
[ADR-0004](../adr/0004-pin-action-sensible-audit.md) (PIN + audit),
|
||||
[ADR-0006](../adr/0006-http-409-conflit-422-validation.md) (409).
|
||||
|
||||
## Tables
|
||||
`role`, `permission`, `role_permission`, `role_visible_source`, `audit_log`. Detail :
|
||||
`docs/merise/mlt.md` section 10.4.
|
||||
26
docs/domaines/stats.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Domaine — Statistiques
|
||||
|
||||
## Perimetre
|
||||
Tableau de bord de pilotage (mlt domaine 11), permission `stats.read`. Landing par
|
||||
defaut du role manager.
|
||||
|
||||
## Ce qui est livre
|
||||
- `StatsRepository` : `counts()` (compteurs catalogue : produits/menus/categories/
|
||||
ingredients, total + actifs/disponibles), `stockHealth()` (repartition des ingredients
|
||||
actifs par bande RG-T21 + liste d'alerte triee du plus critique).
|
||||
- `StatsController` (`stats.read`) -> `/admin/stats` + vue `admin/stats/index` (cartes
|
||||
KPI + table d'alerte stock) + lien nav "Pilotage".
|
||||
|
||||
## Regles metier / perimetre
|
||||
- KPIs sur les **donnees disponibles** en P3 : sante catalogue + stock. **Ferme le 404**
|
||||
du landing manager (`role.default_route = /admin/stats`).
|
||||
- KPIs de **vente** (CA, volumes, `service_day`) = **P4** : ils dependent du domaine
|
||||
commande (encore en schema seul).
|
||||
- Sante stock = reutilise `IngredientRepository::stockBand` (source unique RG-T21).
|
||||
|
||||
## Decisions
|
||||
[ADR-0003](../adr/0003-stock-pourcentage-dispo-calculee.md) (bandes RG-T21).
|
||||
|
||||
## Tables
|
||||
Lecture seule : `product`, `menu`, `category`, `ingredient` (compteurs + bandes).
|
||||
KPIs vente (P4) : `customer_order`, `order_item`. Detail : `docs/merise/mlt.md` section 11.
|
||||
31
docs/domaines/stock-recettes.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Domaine — Stock & recettes (ingredients)
|
||||
|
||||
## Perimetre
|
||||
Gestion des ingredients, du stock (reappro + inventaire), des mouvements de stock, et de
|
||||
la composition des produits (recettes). Sous-tend la disponibilite produit calculee.
|
||||
|
||||
## Ce qui est livre
|
||||
- `IngredientRepository` : CRUD, stock %/bande calcules, `restock` (tx), `inventoryCount`
|
||||
(tx, ecrit une ligne meme a delta=0, RG-3), `movements` (borne), `isReferenced`.
|
||||
- `IngredientController` : CRUD (`ingredient.manage`, sans PIN), RESTOCK (`stock.manage`,
|
||||
sans PIN), INVENTORY_COUNT (`stock.count` + PIN), mouvements (`stock.read`).
|
||||
- `ProductRepository` : composition (`product_ingredient`), `setComposition`
|
||||
(delete-and-reinsert tx), `isOrderable` (RG-T21), `autoUnavailableIds`.
|
||||
- Editeur de recette (`ProductController::recipeForm/saveRecipe`, `ingredient.manage`).
|
||||
|
||||
## Regles metier
|
||||
- RG-T13 : INVENTORY_COUNT seule action sensible du stock (PIN equipier) ; succes ->
|
||||
`stock_movement.user_id`, **sans** `audit_log` (RG-T14 : le mouvement EST la trace).
|
||||
RESTOCK et CRUD ingredient ne sont PAS sensibles.
|
||||
- RG-T22 : echec PIN inventaire -> `pin.failed` + throttle dans une transaction.
|
||||
- RG-T21 : disponibilite produit calculee (cf. [ADR-0003](../adr/0003-stock-pourcentage-dispo-calculee.md)).
|
||||
- FK : `product_ingredient`/`stock_movement` RESTRICT sur l'ingredient (hard-delete -> 409) ;
|
||||
`product_ingredient.product_id` CASCADE (trace du nombre de lignes a la suppression, dette #27).
|
||||
|
||||
## Decisions
|
||||
[ADR-0003](../adr/0003-stock-pourcentage-dispo-calculee.md) (stock % + RG-T21),
|
||||
[ADR-0004](../adr/0004-pin-action-sensible-audit.md) / RG-T14 (attribution sans double-journal).
|
||||
|
||||
## Tables
|
||||
`ingredient`, `product_ingredient`, `stock_movement`, `allergen`, `ingredient_allergen`
|
||||
(mapping differe). Detail : `docs/merise/mlt.md` sections 8.8 + 9.
|
||||
31
docs/domaines/users.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Domaine — Comptes utilisateurs
|
||||
|
||||
## Perimetre
|
||||
Gestion des comptes back-office (mlt domaine 10.1-10.3 + 10.5) : creation, edition,
|
||||
desactivation, reinitialisation de PIN, effacement RGPD.
|
||||
|
||||
## Ce qui est livre
|
||||
- `UserRepository` (App\Auth) : all (JOIN role) / find / emailExists / activeRoleExists /
|
||||
create / update (allowlist) / setPasswordHash / clearPin / deactivate / anonymise /
|
||||
activeAdminCount / isAdmin.
|
||||
- `UserController` : index (`user.read`), create/store (`user.create`), edit/update
|
||||
(`user.update`), deactivate (`user.deactivate`), reset-pin, erase-PII. Vues
|
||||
`admin/users/{index,form,confirm}`.
|
||||
|
||||
## Regles metier
|
||||
- RG-T13/14 : **toutes** les mutations sont sensibles -> PIN equipier + `audit_log`
|
||||
(`user.create/update/deactivate/erase_pii`) dans la meme transaction ; `details` JSON =
|
||||
noms de champs / role (pas de PII). Throttle RG-T22.
|
||||
- RG-T16 : allowlist (email/prenom/nom/role_id/is_active) ; `is_active` pose serveur a
|
||||
la creation. Unicite email -> 409.
|
||||
- Self-protection : pas d'auto-desactivation (403 SELF_DEACTIVATION) ; on ne retire pas
|
||||
le statut du **dernier admin actif** (update/deactivate/erase) ; effacement deja fait -> 409.
|
||||
|
||||
## Decisions
|
||||
[ADR-0004](../adr/0004-pin-action-sensible-audit.md) (PIN + audit),
|
||||
[ADR-0007](../adr/0007-rgpd-anonymisation-tombstone.md) (anonymisation RGPD),
|
||||
[ADR-0006](../adr/0006-http-409-conflit-422-validation.md) (409/422).
|
||||
|
||||
## Tables
|
||||
`user` (+ `anonymized_at` pour RGPD), `audit_log`, `role` (FK). Detail :
|
||||
`docs/merise/mlt.md` section 10.1-10.5.
|
||||
211
docs/journal/2026-06-15--p3-throttle-pin-rg-t22.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# P3 securite — throttle du PIN d'action sensible (RG-T22), design multi-agents + verification adversariale
|
||||
|
||||
**Date** : 2026-06-15 (suite de la session CRUD Produits #17)
|
||||
**Branche** : working tree sur `dev` (chunk non commite ; base `dev` = `2756fb4`)
|
||||
**PR** : ouverte vers `dev` apres revue de l'implementation (auto-merge sur CI verte)
|
||||
**Duree estimee** : session longue (finalisation + merge Produits, puis design + build + docs Merise du throttle)
|
||||
|
||||
---
|
||||
|
||||
## Ce qui a ete fait
|
||||
|
||||
Deux temps dans la session.
|
||||
|
||||
### 1. Finalisation et merge du CRUD Produits (PR #17)
|
||||
|
||||
Le CRUD produits (cas riche : `price_cents`, `vat_rate {55,100}`, `category_id`, suppression FK-safe)
|
||||
a ete termine, revu (6 findings : 1 HIGH, 1 LOW, 4 MEDIUM de couverture), corrige, puis merge sur `dev`
|
||||
en auto-merge sur CI verte (squash, `dev` = `2756fb4`). La revue avait remonte un finding **HIGH** : le PIN
|
||||
d'action sensible (`PinVerifier`) verifie le PIN avec parite de timing mais **sans limitation de
|
||||
tentatives**. Mitigation shippee dans #17 : chaque echec ecrit une ligne `audit_log` `pin.failed`
|
||||
(detectable). Le throttle complet a ete arbitre comme chunk dedie — ce qui suit.
|
||||
|
||||
### 2. Throttle du PIN (RG-T22) — conception puis construction
|
||||
|
||||
**Conception via un panel multi-agents** (3 lentilles independantes : Ockham / efficacite-menace /
|
||||
anti-DoS) -> synthese -> passe adversariale. Le panel a tranche la **dimension** du compteur et a integre
|
||||
deux correctifs d'emblee. Verdict de l'adversaire : la conception tient (`holds = true`).
|
||||
|
||||
Artefacts produits (tous dans le working tree, **non commites**) :
|
||||
|
||||
- `db/migrations/0002_pin_throttle.sql` — nouvelle table (entite 22), cle sur `actor_user_id`
|
||||
(UNIQUE, FK -> `user` ON DELETE CASCADE), separee des compteurs de connexion. **Appliquee a la base
|
||||
dev** via `bash db/migrate.sh`.
|
||||
- `src/app/Auth/ThrottlePolicy.php` — dimension `'pin'` ajoutee a `fromConfig` (bornes propres
|
||||
`PIN_THROTTLE_*` : base 30s, plafond 300s).
|
||||
- `src/app/Auth/PinThrottle.php` (nouveau) — `isLocked` / `recordFailure` (upsert atomique + backoff,
|
||||
une transaction) / `reset`.
|
||||
- `src/app/Auth/PinVerifier.php` — methode additive `payTimingDecoy` (parite de timing du chemin
|
||||
verrouille).
|
||||
- `src/app/Controllers/ProductController.php` — cablage dans `update` (branche prix/TVA) et `destroy` :
|
||||
gate avant verification, `recordFailure` sur PIN faux, `reset` apres l'effet reussi.
|
||||
- Config : `.env.example` + `docker-compose.yml` (`PIN_THROTTLE_THRESHOLD/BASE/MAX/WINDOW`).
|
||||
- Docs Merise portees de 21 a 22 entites : RG-T22 dans `mlt.md`, entite 22 `pin_throttle` dans
|
||||
`mcd.md` / `mld.md` / `dictionary.md`, couverture MCT 22/22 dans `mct.md`.
|
||||
- Tests : +16 (dimension `pin` de `ThrottlePolicy` ; `PinThrottleTest` ; cas de controleur ; leurre de
|
||||
timing ; integration `PinThrottleDbTest`). **188 tests / 525 assertions verts, PHPStan L6 propre.**
|
||||
|
||||
---
|
||||
|
||||
## Pourquoi — decisions et alternatives
|
||||
|
||||
### Decision 1 — Compter les echecs par utilisateur AGISSANT (et non par email cible ni par IP)
|
||||
|
||||
- **Decision** : la dimension du throttle est l'identite de session authentifiee qui realise l'action
|
||||
(`$guard->userId`), stockee dans une table dediee `pin_throttle` cle sur `actor_user_id`.
|
||||
- **Alternatives considerees** :
|
||||
- *par email cible* : contournable par rotation des emails (le modele "identifiant equipier + PIN"
|
||||
verifie un email arbitraire) ;
|
||||
- *par IP* : sur un poste a session partagee, tous les equipiers sortent par la meme IP ; un verrou IP
|
||||
priverait de re-autorisation l'ensemble des equipiers honnetes du comptoir ;
|
||||
- *hybride cible + IP avec delai `usleep`* : ajoute une colonne de portee, ~6 cles de config, un `usleep`
|
||||
qui retient un worker PHP-FPM, et une surface de blocage d'un collegue ;
|
||||
- *globale* : un seul attaquant degraderait l'autorisation sensible de tout le magasin.
|
||||
- **Raison du choix** : la cle "acteur" est la seule non-contournable (changer d'acteur impose une
|
||||
reconnexion, elle-meme throttlee et auditee cote login) ET sans collateral sur un poste partage
|
||||
(verrouiller l'attaquant n'affecte aucun autre `user_id`). Elle dissout la tension rotation/collateral
|
||||
qui force les autres pistes a un delai par IP. Rasoir d'Ockham (#37) : une table, un collaborateur, deux
|
||||
points d'appel, `PinVerifier` inchange.
|
||||
|
||||
### Decision 2 — Table dediee, separee des compteurs de connexion
|
||||
|
||||
- **Decision** : compteurs `pin_throttle` physiquement distincts de `user.failed_login_attempts` /
|
||||
`user.lockout_until` / `login_throttle`.
|
||||
- **Alternative** : reutiliser les colonnes de login existantes.
|
||||
- **Raison** : un echec de PIN n'incremente aucun compteur de login ; sinon, marteler le PIN d'une victime
|
||||
verrouillerait sa connexion (escalade de deni de service vers une surface plus sensible). Un test de
|
||||
regression verifie l'absence d'ecriture vers `user`/`login_throttle` sur le chemin d'echec.
|
||||
|
||||
### Decision 3 — Backoff plus permissif que le login
|
||||
|
||||
- **Decision** : base 30s, plafond 300s (le login est a 60s / 900s).
|
||||
- **Raison** : RG-T13 cadre le PIN comme un controle de dissuasion (risque residuel Faible) ; un faux
|
||||
positif bloque un manager en plein rush. Le backoff reste degressif, pas un verrou definitif.
|
||||
|
||||
### Decision 4 — Correctifs adversariaux integres a la conception (pas en second passage)
|
||||
|
||||
- **Anti-flood de l'audit** : sous verrou actif, aucune nouvelle ligne `pin.failed` (les echecs ayant
|
||||
arme le verrou sont deja audites) — sinon le chemin verrouille, moins couteux, gonflerait le journal
|
||||
append-only et noierait l'alerte de volume.
|
||||
- **Parite de timing** : `payTimingDecoy` paie le cout argon2id sur le chemin verrouille, pour que la
|
||||
latence ne distingue pas "verrouille" de "mauvais PIN".
|
||||
|
||||
### Methodo — pourquoi un panel + une passe adversariale
|
||||
|
||||
Challenge Before Confirm (mantra IA-16) sur un finding de severite HIGH avec migration de schema (peu
|
||||
reversible) : faire produire trois conceptions independantes, les arbitrer, puis tenter de casser la
|
||||
retenue. La passe adversariale a confirme que les quatre attaques visees (rotation d'email, falsification
|
||||
de `X-Forwarded-For`, contamination du compteur de login, collateral de borne partagee) echouent par
|
||||
construction, et a remonte les deux correctifs ci-dessus.
|
||||
|
||||
---
|
||||
|
||||
## Comment — points techniques cles
|
||||
|
||||
- **Upsert atomique, miroir de la dimension IP d'`AuthService`** : `INSERT ... ON DUPLICATE KEY UPDATE
|
||||
failed_attempts = IF(window_started_at < :cutoff, 1, failed_attempts + 1) ...`. L'increment est calcule
|
||||
cote SQL sous le verrou de ligne pris sur la cle UNIQUE, ce qui serialise les POST concurrents (anti
|
||||
lost-update). Placeholders nommes distincts car `PDO::ATTR_EMULATE_PREPARES = false` interdit de lier un
|
||||
meme nom deux fois (`src/app/Auth/PinThrottle.php`).
|
||||
- **Gate-before-verify** : `isLocked($actorId)` est evalue AVANT `resolveActingUser`. Un acteur verrouille
|
||||
recoit le meme 422 generique "Email ou PIN invalide" (anti-enumeration) ; meme un PIN correct est bloque
|
||||
tant que le verrou court.
|
||||
- **Le piege du `reset`** : a un succes, deux identites sont en portee — l'acteur de session
|
||||
(`$guard->userId`, celui qui a ete incremente) et l'equipier resolu par le PIN (`$actor['id']`, ecrit
|
||||
dans `audit_log`). Le `reset` cible l'acteur de **session** ; le confondre laisserait le compteur de
|
||||
l'agissant sans purge. Un test l'asserte explicitement (`ProductControllerTest`).
|
||||
- **FK ON DELETE CASCADE** (contrairement a `login_throttle`, sans FK) : la cle est un utilisateur
|
||||
back-office authentifie, donc supprimer/anonymiser le compte retire proprement sa ligne de throttle
|
||||
(etat ephemere, par opposition a `audit_log` qui est permanent et en SET NULL).
|
||||
|
||||
---
|
||||
|
||||
## Criteres RNCP couverts
|
||||
|
||||
- **Bloc 2 - Cr 3.a / 3.b** : extension du modele Merise (dictionnaire/MCD/MLD) — entite 22 `pin_throttle`,
|
||||
FK et cardinalite (assoc R9), coherence 22/22 verifiee dans les quatre docs.
|
||||
- **Bloc 2 - Cr 4.e (securite)** : requetes preparees (anti-injection), reponse generique
|
||||
(anti-enumeration), separation dure des compteurs (anti escalade de DoS), gate avant verification.
|
||||
- **Bloc 2 - Cr 4.c (POO / namespaces)** : `PinThrottle` (classe dediee), reutilisation de `ThrottlePolicy`
|
||||
(math pure), cablage via les controleurs heritant d'`AdminController`.
|
||||
- **Bloc 2 - Cr 4.g (preparation livraison)** : 188 tests PHPUnit verts, PHPStan niveau 6 propre, test
|
||||
d'integration contre une vraie MariaDB.
|
||||
- **Bloc 2 - Cr 3.d (RGPD)** : FK ON DELETE CASCADE (l'etat de throttle suit l'anonymisation du compte) et
|
||||
purge cron documentee (minimisation / limitation de conservation).
|
||||
- **Bloc 5 - Cr 7.b.3 (cron) / Cr 7.d.2 (tests avant deploiement)** : predicat de purge `pin_throttle`
|
||||
aligne sur `login_throttle` ; le chunk passera la CI (PHPUnit + PHPStan + secret-scan) avant merge.
|
||||
|
||||
---
|
||||
|
||||
## Questions anticipees du jury
|
||||
|
||||
- **Q** : "Pourquoi compter les echecs de PIN sur l'utilisateur agissant plutot que sur l'IP, comme pour le login ?"
|
||||
**R** : Sur une borne a session partagee, tous les equipiers sortent par la meme IP ; un verrou par IP
|
||||
les priverait tous de re-autorisation. La cle "acteur" verrouille seulement l'individu qui multiplie les
|
||||
echecs, sans toucher ses collegues, et reste non-contournable (changer d'acteur impose une reconnexion,
|
||||
deja throttlee cote login).
|
||||
|
||||
- **Q** : "Un attaquant qui martele le PIN d'un collegue peut-il bloquer sa connexion ?"
|
||||
**R** : Non. Les compteurs du PIN vivent dans une table separee (`pin_throttle`), distincte de
|
||||
`user.failed_login_attempts` et de `login_throttle`. Un echec de PIN n'ecrit aucun compteur de login ;
|
||||
un test de regression le verifie.
|
||||
|
||||
- **Q** : "Pourquoi un backoff degressif et pas un verrou definitif ?"
|
||||
**R** : Le PIN est un controle de dissuasion a risque residuel Faible ; un verrou dur bloquerait un
|
||||
manager sur quelques fautes de frappe en plein service. Le backoff ralentit la force brute (de quelques
|
||||
essais a une poignee par fenetre) tout en s'auto-resorbant.
|
||||
|
||||
- **Q** : "Comment avez-vous valide cette conception de securite ?"
|
||||
**R** : Trois conceptions independantes ont ete produites puis arbitrees, et une passe adversariale a
|
||||
tente de casser la retenue (rotation d'email, falsification d'en-tete proxy, contamination du login,
|
||||
collateral de borne). Les quatre echouent par construction ; la passe a aussi remonte deux correctifs
|
||||
(anti-flood de l'audit, parite de timing) integres avant la fin.
|
||||
|
||||
- **Q** : "Pourquoi ajouter une 22e table plutot que des colonnes sur `user` ?"
|
||||
**R** : Des colonnes sur `user` devraient porter sur l'utilisateur cible (contournable par rotation) ou
|
||||
ajouter une 4e dimension de verrou sur la table de comptes. Une table dediee, cle sur l'acteur, garde
|
||||
`user` epuree et garantit la separation des compteurs par construction.
|
||||
|
||||
---
|
||||
|
||||
## Points d'amelioration conscients
|
||||
|
||||
- **Couverture CI de l'increment SQL** : les tests unitaires stubbent le compteur relu apres l'upsert
|
||||
(`FakeDatabase.pinThrottleAttempts` fixe), donc la semantique reelle de l'increment + fenetre glissante
|
||||
n'est prouvee que par `PinThrottleDbTest` (integration), auto-skippee sans MariaDB. C'est la posture
|
||||
STANDARD du projet (CI sans Composer ni base : `AuthServiceDbTest`, `PinVerifierDbTest`... skippent de
|
||||
meme) ; verifiee en local avec `WAKDO_DB_TESTS=1`. A garder en tete si la CI gagne un service DB.
|
||||
- **Cron de purge non encore etendu** : le predicat de purge `pin_throttle` est documente (`mlt.md` 13.5)
|
||||
mais le job cron lui-meme (`docker/cron`) n'a pas ete edite. Sans impact fonctionnel (la table tient une
|
||||
ligne par utilisateur back-office) ; a brancher avec le job `login_throttle` existant.
|
||||
- **Dimension par IP volontairement absente** : choix documente (collateral de borne partagee). A
|
||||
reconsiderer seulement si un abus par IP est observe en pratique.
|
||||
- **Detection** : l'alerte sur le volume de `pin.failed` est le vrai controle detectif ; elle reste a
|
||||
outiller cote supervision (hors code applicatif). Un PIN de plus de 4 chiffres pour les roles sensibles
|
||||
est recommande.
|
||||
|
||||
---
|
||||
|
||||
## Etat a la reprise
|
||||
|
||||
- Chunk throttle PIN complet (source + tests + migration + docs Merise + `.env.example` + compose + ce
|
||||
journal), vert (188 tests, PHPStan L6), revue adversariale de l'implementation passee (`holds = true`),
|
||||
commite et pousse cette session avec PR vers `dev` (auto-merge sur CI verte). Migration `0002` deja
|
||||
appliquee a la base dev.
|
||||
- **Prochaine action** : suite P3 : Menus (+ slots), Ingredients/stock, Users + matrice RBAC, Stats.
|
||||
Differe : etendre le cron de purge a `pin_throttle` ; alerte de volume `pin.failed` (supervision).
|
||||
|
||||
---
|
||||
|
||||
## Liens vers artefacts
|
||||
|
||||
- CRUD Produits merge : commit `49ab77b` -> `dev` `2756fb4` (PR #17, squash).
|
||||
- Throttle PIN (non commite) : `src/app/Auth/PinThrottle.php`, `src/app/Auth/ThrottlePolicy.php`,
|
||||
`src/app/Auth/PinVerifier.php`, `src/app/Controllers/ProductController.php`,
|
||||
`db/migrations/0002_pin_throttle.sql`.
|
||||
- Tests : `tests/Unit/Auth/PinThrottleTest.php`, `tests/Unit/Auth/ThrottlePolicyTest.php`,
|
||||
`tests/Unit/Admin/ProductControllerTest.php`, `tests/Integration/PinThrottleDbTest.php`,
|
||||
`tests/Support/FakeDatabase.php`.
|
||||
- Docs Merise (RG-T22, entite 22) : `docs/merise/{mlt,mcd,mld,dictionary,mct}.md`.
|
||||
- Config : `.env.example`, `docker-compose.yml` (`PIN_THROTTLE_*`).
|
||||
- Resume roulant : `docs/SESSION_RESUME.md` (entree Produits #17 = suite 4).
|
||||
187
docs/journal/2026-06-16--audit-reel-livrables-p2-p3.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Audit reel des livrables P2/P3 — verification sur pieces
|
||||
|
||||
**Date** : 2026-06-16
|
||||
**Branche** : `docs/journal-audit-2026-06-16` -> `dev`
|
||||
**PR** : cette note (PR dediee) ; remediations associees : #19, #20, #21
|
||||
**Auteur** : BYAN
|
||||
**Duree estimee** : 1 session
|
||||
|
||||
---
|
||||
|
||||
## Ce qui a ete fait
|
||||
|
||||
Verification du travail livre le 2026-06-15 (8 PR : P2 auth/RBAC/PIN, P3 shell +
|
||||
CRUD categories/produits + set-PIN + throttle PIN), a la demande explicite de
|
||||
controler "le reel, pas le journal" — suspicion d'un ecart non documente.
|
||||
|
||||
Methode : controle sur pieces uniquement.
|
||||
|
||||
- git : timeline du 2026-06-15, parents de commit, branches reellement presentes
|
||||
cote Forgejo (2 : `dev`, `main`) ;
|
||||
- code lu a la ligne (`file:line`) ;
|
||||
- base MariaDB live interrogee (schema, seed, migrations trackees) ;
|
||||
- suite de tests rejouee en conteneur ; API CI Forgejo (264 runs analyses) ;
|
||||
- sweep multi-agents : 10 dimensions (PR #11-#18 + regles SbD RG-T01..T22 +
|
||||
infra/config), chaque finding re-verifie en adversarial (confirmer le miss ou
|
||||
le refuter), plus un critique de completude.
|
||||
|
||||
---
|
||||
|
||||
## Resultat — le socle metier tient
|
||||
|
||||
Confirme enforced dans le code (pas seulement documente), `file:line` a l'appui :
|
||||
RG-T01 (CSRF sur les mutations), T02 (garde de session + re-verif `is_active`),
|
||||
T03 (autorisation par permission, pas par nom de role), T06 (requetes preparees),
|
||||
T08 (mutation + `audit_log` dans une seule transaction), T13 (PIN d'action
|
||||
sensible), T14 (audit append-only), T16 (allowlist de colonnes), T18 (validation
|
||||
serveur bornee), T22 (throttle PIN isole du login).
|
||||
|
||||
Base live conforme au seed documente (5 roles / 23 permissions / 57 lignes de
|
||||
matrice / 14 allergenes / 9 categories / 53 produits / 13 menus) ; migrations
|
||||
`0001` + `0002` trackees appliquees. 188 tests / PHPStan L6 : reproduits verts en
|
||||
conteneur.
|
||||
|
||||
---
|
||||
|
||||
## Miss confirmes (par gravite)
|
||||
|
||||
Severite issue de la passe adversariale, qui a parfois revu a la baisse
|
||||
l'evaluation initiale.
|
||||
|
||||
### CRITIQUE — durcissement php.ini absent du conteneur en service [OUVERT]
|
||||
|
||||
`docker/php-fpm/php.ini` (durci le 2026-06-15 : `allow_url_fopen=Off`,
|
||||
`disable_functions`, `cgi.fix_pathinfo=0`, `enable_dl=Off`) n'est pas actif sur
|
||||
`wakdo-app`. `docker exec wakdo-app php -i` renvoie `allow_url_fopen=On`,
|
||||
`disable_functions` vide, `enable_dl=On`. Cause : l'image date du 2026-04-30 (le
|
||||
`php.ini` est `COPY`-e a la build, pas monte) et n'a pas ete reconstruite depuis
|
||||
le durcissement. Correctif : rebuild de `wakdo-app` puis re-verif via `php -i`.
|
||||
|
||||
### HIGH — la CI n'executait aucun test d'integration DB [CORRIGE, PR #21]
|
||||
|
||||
`static-tests` lancait `phpunit` sans base ni `WAKDO_DB_TESTS=1` : les 7
|
||||
`tests/Integration/*DbTest` s'auto-skippaient (13 skips), donc le SQL porteur de
|
||||
securite (upsert atomique du throttle, predicat `AND r.is_active = 1`, audit
|
||||
in-transaction, FK RESTRICT/CASCADE) n'etait valide par aucun test en pipeline.
|
||||
Le double `FakeDatabase` n'execute pas le SQL : une regression de ces requetes
|
||||
passait la CI au vert. Corrige par un service MariaDB ephemere + application
|
||||
schema/seed + `WAKDO_DB_TESTS=1` + `--fail-on-skipped`. CI verte verifiee sur le
|
||||
runner (run #78 push + run #79 PR #21 : `secret-scan` / `php-lint` /
|
||||
`static-tests` au vert).
|
||||
|
||||
### MEDIUM
|
||||
|
||||
- XSS stockee latente dans la borne (RG-T15) : 3 scripts injectaient
|
||||
`product.nom` / `item.libelle` / `product.image` dans `innerHTML` sans
|
||||
echappement (seul `page-product-menu.js` etait conforme). Donnees statiques
|
||||
aujourd'hui, mais `data.js` documente la bascule P4 vers `/api/products`
|
||||
(valeurs CRUD admin). [CORRIGE, PR #20]
|
||||
- Liens de nav admin morts : `/admin/menus|orders|users|roles` exposes dans le
|
||||
layout (conditionnes par permission) sans route -> 404 JSON. [OUVERT]
|
||||
- Utilisateur DB applicatif en `GRANT ALL PRIVILEGES` alors que la doc
|
||||
(compose, `backup-db.sh`) decrit un moindre privilege (SELECT / LOCK TABLES /
|
||||
SHOW VIEW). [OUVERT]
|
||||
- Retention RGPD (audit / order) + purge throttle : documentees comme purges
|
||||
cron mais non implementees (pas de job actif, pas de script, vars non
|
||||
injectees au conteneur cron). [OUVERT]
|
||||
|
||||
### LOW
|
||||
|
||||
- Enumeration d'email sur le reset de mot de passe : reponse instantanee sur
|
||||
email inconnu vs travail + ecriture sur email connu. La parite timing/ecritures
|
||||
tient sur le login, pas sur le reset.
|
||||
- Suppression produit non entierement FK-safe : `product_ingredient.product_id`
|
||||
est `ON DELETE CASCADE` (omis du docblock) -> suppression silencieuse de
|
||||
recette possible, sans trace dans l'audit. Latent : table vide au seed actuel.
|
||||
- Page `/admin/profile/pin` non liee dans la nav (joignable par URL directe).
|
||||
- `PASSWORD_ALGO` expose en env mais code en dur (`PASSWORD_ARGON2ID`) : un
|
||||
changement de valeur serait sans effet.
|
||||
- Chemin d'echec PIN non atomique (`logFailedPin` hors transaction puis
|
||||
`recordFailure` dans sa propre transaction), en tension avec RG-T08 (qui tient
|
||||
sur le chemin de succes).
|
||||
- `borne/data/produits.json` (66 produits, maquette statique) diverge de la
|
||||
table `product` (53).
|
||||
|
||||
### Faux positifs ecartes par la passe adversariale
|
||||
|
||||
- "Throttle login partiel / non teste" : la double porte compte + IP est
|
||||
complete, l'increment atomique et le predicat de fenetre sont couverts (unit +
|
||||
integration), l'IP du dernier hop `X-Forwarded-For` n'est pas falsifiable.
|
||||
- "Code mort `userId === null` post-guard" : c'est le narrowing `?int -> int`
|
||||
requis par PHPStan L6, pas un defaut.
|
||||
|
||||
---
|
||||
|
||||
## Remediations livrees cette session
|
||||
|
||||
- **PR #19** : suppression des 6 maquettes `.html` du back-office servies sans
|
||||
authentification (exposition / information disclosure).
|
||||
- **PR #20** : echappement (`escHtml` centralise dans `state.js`) des chaines
|
||||
data-derived injectees en `innerHTML` dans les 3 scripts kiosk (RG-T15).
|
||||
- **PR #21** : execution des tests d'integration DB en CI (service MariaDB +
|
||||
`WAKDO_DB_TESTS=1` + `--fail-on-skipped`). Recette validee en local (188 tests
|
||||
/ 525 assertions / 0 skip) puis sur le runner.
|
||||
|
||||
---
|
||||
|
||||
## Reste a traiter (ordre suggere)
|
||||
|
||||
1. **CRITIQUE** : reconstruire l'image `wakdo-app` pour activer le `php.ini`
|
||||
durci.
|
||||
2. **MEDIUM** : retirer ou router les liens de nav morts ; appliquer un GRANT de
|
||||
moindre privilege au user DB ; implementer la purge RGPD / throttle (job cron
|
||||
+ script).
|
||||
3. **LOW** : decoy de timing sur le reset ; pre-check FK + trace audit a la
|
||||
suppression produit ; lien de nav vers la page PIN ; honorer ou retirer
|
||||
`PASSWORD_ALGO` ; atomiser le chemin d'echec PIN.
|
||||
|
||||
---
|
||||
|
||||
## Criteres RNCP couverts
|
||||
|
||||
- **Bloc 5 - Cr 7.d.2 / 7.d.3** (CI/CD : application testee avant deploiement,
|
||||
integration continue testee) : PR #21 fait reellement tourner les tests
|
||||
d'integration en pipeline (avant, ils etaient skippes).
|
||||
- **Bloc 2 - Cr 4.f.2** (maitrise de l'outil collaboratif : Git, PR, branches,
|
||||
hooks) : remediation via PR dediees et branches courtes, CI gardee.
|
||||
- **Securite (transverse)** : verification que les regles SbD documentees sont
|
||||
effectivement appliquees ; fermeture d'une exposition (maquettes non gardees)
|
||||
et d'une XSS latente.
|
||||
|
||||
---
|
||||
|
||||
## Questions anticipees du jury
|
||||
|
||||
- **Q** : "Vos tests etaient verts ; comment un trou a-t-il pu subsister ?"
|
||||
**R** : la suite unitaire (188 verts) ne touchait pas le SQL reel (double en
|
||||
memoire), et les tests d'integration s'auto-skippaient en CI. Le badge vert ne
|
||||
couvrait pas la couche SQL. Corrige (PR #21) et garde par `--fail-on-skipped`.
|
||||
- **Q** : "Le graphe des branches semble casse."
|
||||
**R** : workflow squash-merge -> historique `dev` lineaire (1 PR = 1 commit) ;
|
||||
les branches de feature apparaissent en moignons car le squash ne cree pas le
|
||||
2e parent d'un merge classique. Choix assume.
|
||||
- **Q** : "Pourquoi le durcissement php.ini n'etait-il pas actif ?"
|
||||
**R** : le `php.ini` est `COPY`-e dans l'image, pas monte ; l'image n'avait pas
|
||||
ete reconstruite depuis le durcissement. Detecte par `php -i` sur le conteneur,
|
||||
corrige par un rebuild.
|
||||
|
||||
---
|
||||
|
||||
## Points d'amelioration conscients
|
||||
|
||||
- Les findings MEDIUM / LOW restants sont traces ici et priorises ; ils ne
|
||||
bloquent pas la suite P3, mais sont a fermer avant une mise en avant securite
|
||||
au jury.
|
||||
- `--fail-on-skipped` est volontairement strict : tout futur test legitimement
|
||||
skippe devra etre justifie explicitement.
|
||||
|
||||
---
|
||||
|
||||
## Liens vers artefacts
|
||||
|
||||
- PR : #19 (maquettes), #20 (escHtml RG-T15), #21 (tests DB en CI).
|
||||
- Fichiers cles : `.forgejo/workflows/ci.yml`, `docker/php-fpm/php.ini`,
|
||||
`src/public/borne/assets/js/{state,page-products,page-product,page-cart}.js`,
|
||||
`src/app/Auth/*`, `src/app/Controllers/*`, `db/migrations/`, `db/seeds/`.
|
||||
- Methode : sweep multi-agents (10 dimensions) + verifications adversariales,
|
||||
pilote depuis Claude Code.
|
||||
88
docs/journal/2026-06-17--makefile-to-compose-migrate.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# 2026-06-17 — Du Makefile a `docker compose up` (service wakdo-migrate)
|
||||
|
||||
**Auteur : BYAN.** Remplacement de l'orchestration locale par Makefile par un service
|
||||
compose one-shot. Objectif : que `docker compose up` amene a lui seul une stack
|
||||
complete et utilisable, et retirer un Makefile devenu en partie trompeur.
|
||||
|
||||
## Pourquoi ce changement
|
||||
|
||||
### Le declencheur : le Makefile mentait en partie
|
||||
Un audit du Makefile (24 cibles) a montre trois categories :
|
||||
- **Cibles mortes / trompeuses** : `test`, `test-unit`, `test-integration`, `lint`
|
||||
affichaient *« Pas encore implemente … en P2 »* alors que les tests EXISTENT et
|
||||
tournent (PHPUnit via `.phar`, 263 tests unit + 301/916 en integration ; PHPStan
|
||||
L6 ; tests JS node:test+jsdom). `install-hooks` referencait un `.githooks/` et un
|
||||
`scripts/install-hooks.sh` absents. Une cible qui annonce un faux est pire qu'une
|
||||
cible absente.
|
||||
- **Wrappers fins** : `up/down/logs/shell/...` = une ligne au-dessus de
|
||||
`docker compose`, valeur surtout de decouvrabilite (`make help`).
|
||||
- **Une seule cible reellement porteuse** : `init` (build -> up -> wait-db ->
|
||||
migrate), citee comme la preuve du critere RNCP **Cr 7.c.4** (*« lancer la stack
|
||||
complete avec une seule ligne de commande »*).
|
||||
|
||||
### Le point cle : Cr 7.c.4 parle d'un RESULTAT, pas de `make`
|
||||
Le critere exige *une commande -> stack complete*. Il ne mentionne pas `make` ;
|
||||
`make init` n'etait qu'un choix d'implementation. Or `docker compose up` seul ne
|
||||
suffisait pas : il demarre les conteneurs mais **n'applique pas les migrations**
|
||||
(base vide -> stack non « complete »). C'etait l'unique raison d'etre de `make init`.
|
||||
|
||||
En deplacant migration + seed DANS la stack (un service one-shot qui tourne au
|
||||
boot), c'est `docker compose up` LUI-MEME qui amene la stack complete. Avantages :
|
||||
- **Commande universelle** : `docker compose up`, sans dependance a l'outil `make`
|
||||
sur l'hote (un correcteur n'a pas a installer/connaitre `make`).
|
||||
- **Comportement = documentation** : l'ancien `make init` ne faisait meme PAS le
|
||||
seed (il s'arretait a `migrate`), alors que le README annoncait « migrate + seed ».
|
||||
Le nouveau chemin seed pour de vrai, donc la stack est *loginnable* (admin present)
|
||||
en une commande.
|
||||
- **Plus idiomatique** : faire porter l'init par la couche d'orchestration (compose)
|
||||
plutot que par un outil hote externe.
|
||||
|
||||
## Ce qui a ete fait
|
||||
|
||||
- **`db/migrate-container.sh`** : runner in-container. Applique `db/migrations/*.sql`
|
||||
(suivi `schema_migrations`) PUIS `db/seeds/*.sql` (suivi `seeds_applied`), de
|
||||
maniere idempotente, en se connectant a la base par le reseau compose (DB_HOST).
|
||||
Distinct de `db/migrate.sh` (hote, via `docker exec`), conserve pour l'usage manuel
|
||||
(`--status`) et la CI.
|
||||
- **Service `wakdo-migrate`** (image mariadb, `restart: "no"`) : `depends_on`
|
||||
`wakdo-db: service_healthy`, lance le runner puis sort. `wakdo-app` et `wakdo-web`
|
||||
gagnent `depends_on wakdo-migrate: service_completed_successfully` -> ils ne servent
|
||||
qu'une fois le schema + le seed en place.
|
||||
- **Makefile supprime.** Les commandes equivalentes en clair :
|
||||
`docker compose up -d` (= ex-`make init`/`up`), `docker compose down` (`make down`),
|
||||
`docker compose down -v` (`make clean`), `docker compose build --no-cache && up -d`
|
||||
(`make rebuild`), `docker compose logs -f` (`make logs`),
|
||||
`docker compose exec wakdo-db mariadb -uroot -p"$DB_ROOT_PASSWORD"` (`make shell-db`).
|
||||
Tests : `docker run --rm -v "$PWD":/app -w /app wakdo-wakdo-app php phpunit.phar -c phpunit.xml`
|
||||
(cf. README / SESSION_RESUME).
|
||||
|
||||
## Verification
|
||||
|
||||
Base MariaDB ephemere et vierge (pour ne pas toucher la dev) :
|
||||
- Run #1 : 2 migrations + 2 seeds appliques.
|
||||
- Run #2 : 0 nouveau (idempotent, tout ignore).
|
||||
- Donnees : 5 roles, 23 permissions, admin `admin@wakdo.local` present ;
|
||||
`schema_migrations`=2, `seeds_applied`=2.
|
||||
|
||||
`docker compose -p wakdo config` valide. La CI n'utilise pas `make` (0 appel) :
|
||||
elle garde sa propre boucle migrate -> non impactee.
|
||||
|
||||
## Mapping Cr 7.c.4 (apres ce changement)
|
||||
*« Le fichier de configuration permet de lancer la stack applicative complete avec
|
||||
une seule ligne de commande »* -> **`docker compose up`** : `docker-compose.yml`
|
||||
decrit la stack, le service `wakdo-migrate` applique schema + donnees, app/web
|
||||
attendent sa completion. Une commande, aucune dependance hote.
|
||||
|
||||
## Note de deploiement (environnements deja seedes)
|
||||
Sur une base existante deja migree ET seedee AVANT l'introduction du suivi
|
||||
(`seeds_applied` absente), le premier `docker compose up` avec le nouveau service
|
||||
tenterait de rejouer les seeds (INSERT non idempotents) -> conflits d'unicite. Pour
|
||||
ces environnements : back-fill une fois la table de suivi
|
||||
(`CREATE TABLE seeds_applied(...)` + INSERT des noms de fichiers seed deja appliques,
|
||||
idem `schema_migrations` si besoin) AVANT le premier up. Les deploiements sur volume
|
||||
VIERGE ne sont pas concernes (le service applique tout proprement, comme verifie).
|
||||
|
||||
## Compromis assumes
|
||||
- migrations + seeds evalues a CHAQUE `up` : cout negligeable (le suivi rend les
|
||||
re-runs sans effet).
|
||||
- `wakdo-migrate` se connecte en root (DDL + INSERT de reference), comme `migrate.sh`.
|
||||
55
docs/journal/2026-06-17--session-infra-doc-e2e.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# 2026-06-17 — Session : infra compose, documentation, E2E Playwright
|
||||
|
||||
**Auteur : BYAN.** Retrospective de session. Suite a l'achevement du back-office P3
|
||||
(Stats/Users/RBAC), cette session a porte sur l'infra de demarrage, un jeu de
|
||||
documentation pour la Forge, et l'amorce des tests E2E.
|
||||
|
||||
## Contexte de depart
|
||||
P3 back-office complet et merge (#37 Stats, #38 Users, #39 RBAC). `dev` propre.
|
||||
|
||||
## Ce qui a ete livre (PR mergees sur dev)
|
||||
|
||||
| PR | Objet |
|
||||
|----|-------|
|
||||
| #40 | Makefile -> `docker compose up` : service one-shot `wakdo-migrate` (migrate + seed idempotents, suivi `schema_migrations`/`seeds_applied`). Makefile supprime. |
|
||||
| #41 | `docker-compose.yml` **standalone portable** (port hote, sans Traefik) ; prod = `docker-compose.prod.yml` **gitignore** par hote. Renommage `TRAEFIK_DOMAIN_*` -> `APP_HOST_*`. `.env.example` local-first. |
|
||||
| #42 | Doc socle : `ARCHITECTURE.md` (10 sections) + `DEVELOPER.md`. |
|
||||
| #43 | Registre `docs/adr/` : 9 ADR (puis 10, cf. #46). |
|
||||
| #44 | Doc par domaine `docs/domaines/` : 7 fiches (auth, catalogue, stock-recettes, users, rbac, stats, borne). |
|
||||
| #45 | E2E Playwright **etape 1** : parcours borne (welcome -> confirmation). |
|
||||
| #46 | E2E Playwright **etape 2** : parcours admin (garde -> login -> dashboard -> logout) + fix securite (cf. ADR-0010). |
|
||||
|
||||
## Decisions notables
|
||||
- **`docker compose up` comme commande unique** (Cr 7.c.4) sans `make` ni dependance
|
||||
hote, via le service `wakdo-migrate`. Cf. ADR-0008, journal makefile-to-compose.
|
||||
- **Deux fichiers compose pleins et independants** (pas d'overlay `-f`) : un standalone
|
||||
versionne pour tous, un prod gitignore par hote. Choix de simplicite assume sur
|
||||
demande du user (clarte > DRY pour l'infra). Cf. ADR-0009.
|
||||
- **Playwright en conteneur officiel, contre une stack jetable isolee** (`run.sh`,
|
||||
projet `-p wakdoe2e`, override container_name, joint par `--add-host`) : aucune
|
||||
dependance browser sur l'hote, ne touche aucune stack existante. Hostnames de test
|
||||
en `.test` (Chromium force `*.localhost` vers 127.0.0.1, RFC 6761).
|
||||
|
||||
## Ce que l'E2E a fait remonter (sa valeur)
|
||||
1. **a11y** : le bouton "Valider ma commande" (`<a>`) gardait `aria-disabled="true"`
|
||||
(`.disabled` est un no-op sur un `<a>`) -> annonce desactive panier rempli. Corrige.
|
||||
2. **securite/usage** : cookie de session `secure=true` en dur -> session intenable en
|
||||
HTTP, donc admin inconnectable en local. Rendu **conditionnel au HTTPS** (prod
|
||||
inchange). Cf. ADR-0010.
|
||||
|
||||
## Verifications
|
||||
PHPStan L6 OK ; 263 tests unit ; 7 tests JS ; 2 parcours E2E verts (borne + admin) ;
|
||||
smoke-test standalone (stack jetable, migrate + seed + vhosts) ; CI Forgejo verte sur
|
||||
chaque PR (auto-merge sur label). `.env` LOCAL migre vers `APP_HOST_*`.
|
||||
|
||||
## Reste a faire (file d'attente)
|
||||
- **Deploiement serveur (Thanos)** : migrer le `.env` serveur (`APP_HOST_*`), placer son
|
||||
`docker-compose.prod.yml`, back-fill `seeds_applied` avant le 1er up.
|
||||
- **E2E etape 3** : job CI Forgejo (stack jetable + Playwright conteneur) ; verifier que
|
||||
le runner peut lancer Docker.
|
||||
- **Front** : page de **login** a retravailler (signalee comme "moche" ; pas le dashboard).
|
||||
- **Doc** : enrichissements (diagrammes), doc commande quand P4 sort.
|
||||
- **P4** : domaine commande (KPIs vente, nav orders, swap borne -> API DB-backed).
|
||||
|
||||
## Reprise
|
||||
`docs/SESSION_RESUME.md` tient l'etat detaille et les commandes de reprise.
|
||||
80
docs/journal/2026-06-18--front-login-ui-admin-p4-commande.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# 2026-06-18 — Session : page login, refonte UI admin, humanisation, P4 commande
|
||||
|
||||
**Auteur : BYAN.** Retrospective de session. Apres la session infra/doc/E2E du 2026-06-17,
|
||||
cette session a porte sur le front (pages auth), une refonte du back-office pour des
|
||||
equipiers non-techniques (UX + UI), l'humanisation des libelles, et l'amorce du domaine
|
||||
P4 commande (creation + encaissement cote API).
|
||||
|
||||
## Contexte de depart
|
||||
Back-office P3 complet et merge. `dev` propre. Pistes ouvertes : E2E-CI, page login
|
||||
signalee comme "moche", domaine P4 commande.
|
||||
|
||||
## Ce qui a ete livre (PR mergees sur dev)
|
||||
|
||||
| PR | Objet |
|
||||
|----|-------|
|
||||
| #48 | Relooking des pages auth (login / forgot / reset) : `.login-card`, logo reel, `.form-input`, `.btn`. La page login servie via `layout.php` (passe sur `admin.css`). |
|
||||
| #49 | Design system back-office (direction A+C, lot 1) : shell en grille `sidebar/topbar/content`, tokens (jaune Wakdo doux, ombres, rayons), `.sidebar-item`, `.tile`, `.alert`. |
|
||||
| #50 | Dashboard donnees reelles (lot 2) : tuiles KPI alimentees par `StatsRepository` (produits dispo, categories, menus, stock critique). |
|
||||
| #51 | Fix borne : logo d'en-tete centre (etait a droite). |
|
||||
| #52 | Modal de re-autorisation PIN : le PIN d'action sensible devient un modal au clic (email pre-rempli), au lieu d'un fieldset inline. CSP-safe (`pin-modal.js`). |
|
||||
| #54 | Humanise les libelles restants : Slug -> Reference, Delta -> Variation, Acteur -> Auteur (vues + messages de validation + tests). |
|
||||
| #55 | P4 chunk 1a : creation de commande (`OrderRepository::createPending`, RG-5 etapes 1-4, calcul RG-4, numero K+id, idempotence) + migration `service_tag` (chevalet, B4). |
|
||||
| #56 | Fix : logo reel dans la sidebar (un "W" dessine a la main avait ete introduit ; remplace par `logo.png` + mot-symbole, comme la page login). |
|
||||
| #57 | P4 chunk 1b : encaissement (`OrderController` `POST /api/orders` + `/{number}/pay`, `OrderRepository::pay`, transition gardee -> paid + decrement de stock atomique RG-T20, idempotence). |
|
||||
| #58 | CI : retire le job `auto-merge` redondant (bruit HTTP 405). En cours de merge a la redaction. |
|
||||
|
||||
## Decisions notables
|
||||
- **E2E-CI abandonne.** Le log reel du runner a montre `Cannot connect to the Docker
|
||||
daemon` : le runner de prod (`forgejo_forgejo_internal`) n'expose pas le socket Docker
|
||||
aux jobs, et les jobs se repartissent sur plusieurs runners. L'E2E reste manuel via
|
||||
`tests/e2e/run.sh`. Le`s emulations locales contre le mauvais runner tournaient a blanc.
|
||||
- **UI pour equipiers non-techniques.** Zero jargon dev dans les ecrans ; le PIN (action
|
||||
sensible) est un modal au moment de l'action, pas un champ inline ; direction visuelle
|
||||
"Mix A+C" (neutre + accent jaune parcimonieux).
|
||||
- **P4 commande, regles tranchees** :
|
||||
- `order_number` = `K` + id auto-increment (plus simple que `K-AAAA-MM-JJ-NNN`, pas de
|
||||
compteur jour) ;
|
||||
- TVA portee par le produit (`product.vat_rate`), independante du mode de service
|
||||
(la distinction sur place / a emporter est surtout fiscale mais la TVA reste celle
|
||||
du produit) ; TVA d'une ligne menu = `vat_rate` du burger du menu ;
|
||||
- flux en **deux etapes** (creation `pending_payment` puis paiement -> `paid` +
|
||||
decrement), divergence assumee du flux mono-transaction de la spec, alignee sur un
|
||||
parcours borne reel (ecran paiement).
|
||||
- **Modele de menu borne** : B1 burger impose, B2 Normal/Maxi, B3 salades-en-menus,
|
||||
B4 numero de chevalet quand "sur place".
|
||||
- **Auto-merge** : bascule definitive sur l'auto-merge NATIF Forgejo
|
||||
(`merge_when_checks_succeed`) ; le job CI `auto-merge` (par label) est retire (#58).
|
||||
|
||||
## Ce que la session a fait remonter (dette / pistes)
|
||||
1. **PR #53 (ecran Roles humanise) restee ouverte et en conflit avec `dev`.** CI verte sur
|
||||
sa tete, mais `mergeable: false` (5 commits de retard, 2 d'avance) : les vues Roles et
|
||||
`admin.css` ont bouge avec le design system (#49) et les relabels (#54). A rebaser sur
|
||||
`dev` et resoudre avant merge. Dette principale a resorber.
|
||||
2. **CORS PHP manquant.** Le vhost admin delegue les headers CORS a "un middleware PHP" qui
|
||||
n'existe pas. Sans bloquer le chunk 1b (endpoints + tests cote serveur), cela bloquera
|
||||
la borne des qu'elle appellera `/api` cross-origin. Prerequis de la fondation borne.
|
||||
3. **`pay()` / decrement de stock inerte** tant que les recettes (`product_ingredient`) ne
|
||||
sont pas seedees : la transition `paid` s'applique, mais aucun `stock_movement` n'est
|
||||
produit faute de composition. La logique s'active des le seed des recettes.
|
||||
4. **Logo admin** : un "W" dessine a la main avait remplace le vrai logo ; corrige (#56).
|
||||
|
||||
## Verifications
|
||||
PHPStan L6 0 erreur ; PHPUnit 284 tests unit (chunk 1b : +7 tests `pay`, +6 tests
|
||||
controleur) ; php -l propre ; CI Forgejo verte par PR (merge natif squash sur les checks
|
||||
requis : secret-scan, php-lint, static-tests).
|
||||
|
||||
## Reste a faire (file d'attente)
|
||||
- **Rebaser + merger PR #53** (ecran Roles humanise) — conflit avec `dev`.
|
||||
- **Middleware CORS PHP** sur `/api` (prerequis borne cross-origin).
|
||||
- **Seed des recettes** (`product_ingredient`) -> active le decrement de `pay()`.
|
||||
- **Fondation borne** : read API (`GET /api/categories|products|menus`) pour consommer le
|
||||
vrai modele -> B1 (burger impose) + B2 (Normal/Maxi).
|
||||
- **B3** salades-en-menus (Cesar Classic / Italienne Mozza en menus) ; **B4** etape
|
||||
chevalet a la borne.
|
||||
- **Optionnel** : prix en euros vs centimes ; flux d'activite du dashboard (audit).
|
||||
|
||||
## Reprise
|
||||
`dev` porte tout le livre de la session sauf #53 (ouverte) et #58 (en cours de merge).
|
||||
Domaine commande : `src/app/Order/` (OrderRepository create + pay), routes anonymes
|
||||
`/api/orders` dans `src/public/admin/index.php`.
|
||||
|
|
@ -31,6 +31,8 @@ Les fichiers sont ordonnes chronologiquement par leur nom.
|
|||
| 2026-04-24 | [infra-docker](2026-04-24--infra-docker.md) | Stack Docker complete (compose + 4 services), referentiel RNCP integre, cross-check mappings Cr 4.f | `feat/infra-docker` |
|
||||
| 2026-04-30 | [smoke-test-infra](2026-04-30--smoke-test-infra.md) | Smoke test bout-en-bout sur serveur reel : fusion .env, switch FQDN sur stark.a3n.fr, subnet explicite RFC 1918, fix init cron + healthz | `feat/infra-docker` |
|
||||
| 2026-06-04 | [conception-prodlike-revision](2026-06-04--conception-prodlike-revision.md) | Revue d'alignement P1 + decisions prod-like du modele de donnees (drop commande_event, nommage EN, TVA par produit apres fact-check BOFiP, perso menus/ingredients, allergenes, ~16 entites) | `feat/p1-conception` |
|
||||
| 2026-06-15 | [p3-throttle-pin-rg-t22](2026-06-15--p3-throttle-pin-rg-t22.md) | P3 securite : throttle du PIN d'action sensible (RG-T22) — design multi-agents + verification adversariale, dimension "utilisateur agissant", entite 22 `pin_throttle` | `feat/p3-pin-throttle` -> `dev` |
|
||||
| 2026-06-16 | [audit-reel-livrables-p2-p3](2026-06-16--audit-reel-livrables-p2-p3.md) | Verification sur pieces des livrables du 2026-06-15 (sweep 10 dimensions + adversarial) : socle SbD confirme, miss confirmes par gravite (php.ini non deploye, CI sans tests DB, XSS kiosk, liens morts...) et remediations | `docs/journal-audit-2026-06-16` -> `dev` (PR #19/#20/#21) |
|
||||
|
||||
*Mis a jour a chaque nouvelle entree.*
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MCD - Catalogue" id="mcd-catalogue">
|
||||
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="categorie" value="<b>CATEGORIE</b><hr>id : INT (PK)<br>libelle : VARCHAR (UNIQUE)<br>slug : VARCHAR (UNIQUE)<br>image_path : VARCHAR<br>ordre : SMALLINT<br>est_actif : BOOLEAN" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="40" width="280" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="produit" value="<b>PRODUIT</b><hr>id : INT (PK)<br>categorie_id : INT (FK)<br>libelle : VARCHAR<br>description : TEXT<br>prix_ttc_cents : INT<br>image_path : VARCHAR<br>est_disponible : BOOLEAN<br>ordre : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="60" y="320" width="280" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="menu" value="<b>MENU</b><hr>id : INT (PK)<br>categorie_id : INT (FK)<br>libelle : VARCHAR<br>description : TEXT<br>prix_ttc_cents : INT<br>image_path : VARCHAR<br>est_disponible : BOOLEAN<br>ordre : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="820" y="320" width="280" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="menu_produit" value="<b>MENU_PRODUIT</b> <i>(associative)</i><hr>menu_id : INT (PK, FK)<br>produit_id : INT (PK, FK)<br>role : ENUM<br>position : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="640" width="280" height="140" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_cat_prod" value="regroupe" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_cat_prod_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_prod">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_cat_prod_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_prod">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_cat_menu" value="regroupe" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="menu">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_cat_menu_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_menu">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_cat_menu_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_menu">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_prod_mp" value="fait_partie_de" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="produit" target="menu_produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_prod_mp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_prod_mp">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_prod_mp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_prod_mp">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_menu_mp" value="compose" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="menu" target="menu_produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_menu_mp_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_menu_mp">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_menu_mp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_menu_mp">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
51
docs/merise/_diagrams/mcd-catalogue.mmd
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
erDiagram
|
||||
category {
|
||||
int id PK
|
||||
varchar name
|
||||
varchar slug
|
||||
varchar image_path
|
||||
smallint display_order
|
||||
tinyint is_active
|
||||
}
|
||||
product {
|
||||
int id PK
|
||||
int category_id FK
|
||||
varchar name
|
||||
text description
|
||||
int price_cents
|
||||
smallint vat_rate
|
||||
varchar image_path
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
}
|
||||
menu {
|
||||
int id PK
|
||||
int category_id FK
|
||||
int burger_product_id FK
|
||||
varchar name
|
||||
text description
|
||||
int price_normal_cents
|
||||
int price_maxi_cents
|
||||
varchar image_path
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
}
|
||||
menu_slot {
|
||||
int id PK
|
||||
int menu_id FK
|
||||
varchar name
|
||||
enum slot_type
|
||||
tinyint is_required
|
||||
smallint display_order
|
||||
}
|
||||
menu_slot_option {
|
||||
int menu_slot_id FK
|
||||
int product_id FK
|
||||
}
|
||||
|
||||
category ||--o{ product : "groups"
|
||||
category ||--o{ menu : "groups"
|
||||
menu ||--|| product : "anchors (burger_product_id)"
|
||||
menu ||--o{ menu_slot : "defines_slot"
|
||||
menu_slot ||--o{ menu_slot_option : "lists"
|
||||
product ||--o{ menu_slot_option : "is_eligible_for"
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 119 KiB |
|
|
@ -1,93 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MCD - Commande" id="mcd-commande">
|
||||
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="commande" value="<b>COMMANDE</b><hr>id : INT (PK)<br>numero : VARCHAR (UNIQUE)<br>source : ENUM (kiosk|comptoir|drive)<br>mode_consommation : ENUM (sur_place|a_emporter|drive)<br>statut : ENUM<br>total_ht_cents : INT<br>total_tva_cents : INT<br>total_ttc_cents : INT<br>tva_taux_pourmille : SMALLINT<br>paye_a : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="40" width="320" height="240" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="user_stub" value="<b>USER</b><hr>id : INT (PK)<br><i>(detail dans RBAC)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="880" y="40" width="240" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="commande_event" value="<b>COMMANDE_EVENT</b><hr>id : INT (PK)<br>commande_id : INT (FK)<br>event_type : ENUM<br>from_statut : ENUM (NULL)<br>to_statut : ENUM<br>user_id : INT (FK, NULL)<br>payload : JSON (NULL)<br>created_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="360" width="280" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="ligne_commande" value="<b>LIGNE_COMMANDE</b><hr>id : INT (PK)<br>commande_id : INT (FK)<br>type_item : ENUM (produit|menu)<br>produit_id : INT (FK, NULL)<br>menu_id : INT (FK, NULL)<br>libelle_snapshot : VARCHAR<br>prix_unitaire_ttc_cents_snapshot : INT<br>quantite : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="360" width="280" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="produit" value="<b>PRODUIT</b><hr>id : INT (PK)<br><i>(detail dans Catalogue)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="700" width="240" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="menu" value="<b>MENU</b><hr>id : INT (PK)<br><i>(detail dans Catalogue)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="700" width="240" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_cmd_lc" value="contient" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="ligne_commande">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_cmd_lc_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_lc">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_cmd_lc_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_lc">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_lc_prod" value="refere_si_type_produit" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_lc_prod_a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_prod">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_lc_prod_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_prod">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_lc_menu" value="refere_si_type_menu" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="menu">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_lc_menu_a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_menu">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_lc_menu_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_menu">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="note_poly" value="<b>Polymorphisme</b><br>Exactement UNE des deux references est non-nulle.<br>Discriminateur : type_item &isin; {produit, menu}.<br>Contrainte CHECK SQL au MLD." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="360" width="280" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_cmd_evt" value="journalise" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="commande_event">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_cmd_evt_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_evt">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_cmd_evt_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_evt">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_user_evt" value="declenche" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="user_stub" target="commande_event">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_user_evt_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_evt">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_user_evt_b" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_evt">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="note_audit" value="<b>Journal d'audit (event sourcing)</b><br>Append-only : aucun UPDATE / DELETE applicatif.<br>user_id NULL si auto-validation kiosk.<br>ON DELETE CASCADE cote commande_id.<br>ON DELETE SET NULL cote user_id." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="580" width="280" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
Before Width: | Height: | Size: 704 KiB |
|
|
@ -1,182 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MCD - Global" id="mcd-global">
|
||||
<mxGraphModel dx="1800" dy="1100" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="categorie" value="<b>CATEGORIE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="40" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="produit" value="<b>PRODUIT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="240" y="220" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="menu_produit" value="<b>MENU_PRODUIT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;align=center;verticalAlign=middle;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="220" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="menu" value="<b>MENU</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="960" y="220" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="ligne_commande" value="<b>LIGNE_COMMANDE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="400" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="commande" value="<b>COMMANDE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="540" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="commande_event" value="<b>COMMANDE_EVENT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="960" y="540" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="user" value="<b>USER</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="780" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="role" value="<b>ROLE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="780" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="role_permission" value="<b>ROLE_PERMISSION</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;align=center;verticalAlign=middle;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="760" y="780" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="permission" value="<b>PERMISSION</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="1080" y="780" width="200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e1" value="regroupe" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e1a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e1">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e1b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e1">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e2" value="regroupe" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="menu">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e2a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e2">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e2b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e2">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e3" value="fait_partie_de" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="produit" target="menu_produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e3a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e3">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e3b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e3">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e4" value="compose" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="menu" target="menu_produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e4a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e4">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e4b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e4">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e5" value="contient" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="ligne_commande">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e5a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e5">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e5b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e5">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e6" value="refere_si_type_produit" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="produit">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="120" y="425" />
|
||||
<mxPoint x="120" y="245" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e6a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e6">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e6b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e6">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e7" value="refere_si_type_menu" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="menu">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="1280" y="425" />
|
||||
<mxPoint x="1280" y="245" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e7a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e7">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e7b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e7">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e8" value="a_pour_role" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="user" target="role">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e8a" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e8">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e8b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e8">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e9" value="possede" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="role" target="role_permission">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e9a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e9">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e9b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e9">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e10" value="assignee_a" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="permission" target="role_permission">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e10a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e10">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e10b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e10">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e11" value="journalise" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="commande_event">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e11a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e11">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e11b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e11">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e12" value="declenche" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="user" target="commande_event">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="1280" y="805" />
|
||||
<mxPoint x="1280" y="565" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e12a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e12">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e12b" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e12">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
Before Width: | Height: | Size: 363 KiB |
61
docs/merise/_diagrams/mcd-ingredients-stock.mmd
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
erDiagram
|
||||
product {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
ingredient {
|
||||
int id PK
|
||||
varchar name
|
||||
varchar unit
|
||||
int stock_quantity
|
||||
int stock_capacity
|
||||
smallint pack_size
|
||||
varchar pack_label
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
tinyint is_active
|
||||
}
|
||||
product_ingredient {
|
||||
int product_id FK
|
||||
int ingredient_id FK
|
||||
smallint quantity_normal
|
||||
smallint quantity_maxi
|
||||
tinyint is_removable
|
||||
tinyint is_addable
|
||||
int extra_price_cents
|
||||
}
|
||||
allergen {
|
||||
int id PK
|
||||
varchar code
|
||||
varchar name
|
||||
text description
|
||||
}
|
||||
ingredient_allergen {
|
||||
int ingredient_id FK
|
||||
int allergen_id FK
|
||||
}
|
||||
customer_order {
|
||||
int id PK
|
||||
varchar order_number
|
||||
}
|
||||
user {
|
||||
int id PK
|
||||
varchar email
|
||||
}
|
||||
stock_movement {
|
||||
int id PK
|
||||
int ingredient_id FK
|
||||
enum movement_type
|
||||
int delta
|
||||
int order_id FK
|
||||
int user_id FK
|
||||
varchar note
|
||||
}
|
||||
|
||||
product ||--o{ product_ingredient : "is_composed_of"
|
||||
ingredient ||--o{ product_ingredient : "appears_in"
|
||||
ingredient ||--o{ ingredient_allergen : "contains"
|
||||
allergen ||--o{ ingredient_allergen : "is_present_in"
|
||||
ingredient ||--o{ stock_movement : "decrements"
|
||||
customer_order |o--o{ stock_movement : "triggers"
|
||||
user |o--o{ stock_movement : "logs"
|
||||
1
docs/merise/_diagrams/mcd-ingredients-stock.svg
Normal file
|
After Width: | Height: | Size: 141 KiB |
67
docs/merise/_diagrams/mcd-order.mmd
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
erDiagram
|
||||
customer_order {
|
||||
int id PK
|
||||
varchar order_number
|
||||
varchar idempotency_key
|
||||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
enum status
|
||||
int total_ht_cents
|
||||
int total_vat_cents
|
||||
int total_ttc_cents
|
||||
datetime paid_at
|
||||
datetime delivered_at
|
||||
datetime cancelled_at
|
||||
}
|
||||
order_item {
|
||||
int id PK
|
||||
int order_id FK
|
||||
enum item_type
|
||||
int product_id FK
|
||||
int menu_id FK
|
||||
enum format
|
||||
varchar label_snapshot
|
||||
int unit_price_cents_snapshot
|
||||
smallint vat_rate_snapshot
|
||||
smallint quantity
|
||||
}
|
||||
order_item_selection {
|
||||
int id PK
|
||||
int order_item_id FK
|
||||
int menu_slot_id FK
|
||||
int product_id FK
|
||||
varchar label_snapshot
|
||||
}
|
||||
order_item_modifier {
|
||||
int id PK
|
||||
int order_item_id FK
|
||||
int ingredient_id FK
|
||||
enum action
|
||||
int extra_price_cents
|
||||
}
|
||||
product {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
menu {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
menu_slot {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
ingredient {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
|
||||
customer_order ||--o{ order_item : "contains"
|
||||
order_item }o--o| product : "references_product"
|
||||
order_item }o--o| menu : "references_menu"
|
||||
order_item ||--o{ order_item_selection : "fills_slot"
|
||||
order_item ||--o{ order_item_modifier : "modifies_ingredient"
|
||||
menu_slot ||--o{ order_item_selection : "slot_filled_by"
|
||||
product ||--o{ order_item_selection : "chosen_for_slot"
|
||||
ingredient ||--o{ order_item_modifier : "modified_by"
|
||||
1
docs/merise/_diagrams/mcd-order.svg
Normal file
|
After Width: | Height: | Size: 156 KiB |
|
|
@ -1,57 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MCD - RBAC" id="mcd-rbac">
|
||||
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="user" value="<b>USER</b><hr>id : INT (PK)<br>email : VARCHAR (UNIQUE, RFC 5321)<br>password_hash : VARCHAR (argon2id)<br>nom : VARCHAR<br>prenom : VARCHAR<br>role_id : INT (FK)<br>est_actif : BOOLEAN<br>last_login_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="60" y="80" width="280" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="role" value="<b>ROLE</b><hr>id : INT (PK)<br>code : VARCHAR (UNIQUE)<br>libelle : VARCHAR<br>description : TEXT<br>est_actif : BOOLEAN" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="80" width="280" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="permission" value="<b>PERMISSION</b><hr>id : INT (PK)<br>code : VARCHAR (UNIQUE, resource.action)<br>libelle : VARCHAR<br>description : TEXT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="820" y="80" width="280" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="role_permission" value="<b>ROLE_PERMISSION</b> <i>(associative)</i><hr>role_id : INT (PK, FK)<br>permission_id : INT (PK, FK)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="630" y="440" width="300" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_user_role" value="a_pour_role" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="user" target="role">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_user_role_a" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_role">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_user_role_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_role">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_role_rp" value="possede" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="role" target="role_permission">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_role_rp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_role_rp">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_role_rp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_role_rp">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_perm_rp" value="assignee_a" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="permission" target="role_permission">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e_perm_rp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_perm_rp">
|
||||
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_perm_rp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_perm_rp">
|
||||
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
64
docs/merise/_diagrams/mcd-rbac.mmd
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
erDiagram
|
||||
user {
|
||||
int id PK
|
||||
varchar email
|
||||
varchar password_hash
|
||||
varchar pin_hash
|
||||
varchar first_name
|
||||
varchar last_name
|
||||
int role_id FK
|
||||
tinyint is_active
|
||||
datetime last_login_at
|
||||
smallint failed_login_attempts
|
||||
datetime lockout_until
|
||||
datetime anonymized_at
|
||||
}
|
||||
role {
|
||||
int id PK
|
||||
varchar code
|
||||
varchar label
|
||||
text description
|
||||
varchar default_route
|
||||
enum order_source
|
||||
tinyint is_active
|
||||
}
|
||||
role_visible_source {
|
||||
int role_id FK
|
||||
enum source
|
||||
}
|
||||
permission {
|
||||
int id PK
|
||||
varchar code
|
||||
varchar label
|
||||
text description
|
||||
}
|
||||
role_permission {
|
||||
int role_id FK
|
||||
int permission_id FK
|
||||
}
|
||||
audit_log {
|
||||
int id PK
|
||||
int actor_user_id FK
|
||||
int actor_role_id FK
|
||||
varchar action_code
|
||||
varchar entity_type
|
||||
int entity_id
|
||||
varchar summary
|
||||
json details
|
||||
datetime created_at
|
||||
}
|
||||
login_throttle {
|
||||
int id PK
|
||||
varchar ip_address UK
|
||||
smallint failed_attempts
|
||||
datetime window_started_at
|
||||
datetime lockout_until
|
||||
datetime last_attempt_at
|
||||
}
|
||||
|
||||
user }o--|| role : "holds"
|
||||
role ||--o{ role_visible_source : "sees_source"
|
||||
role ||--o{ role_permission : "grants"
|
||||
permission ||--o{ role_permission : "granted_to"
|
||||
user |o--o{ audit_log : "performs"
|
||||
role |o--o{ audit_log : "context_of"
|
||||
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 151 KiB |
|
|
@ -1,59 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MLD - Catalogue" id="mld-catalogue">
|
||||
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="t_categorie" value="<b>categorie</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK libelle : VARCHAR(80)<br>UK slug : VARCHAR(60)<br>image_path : VARCHAR(255) NULL<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>est_actif : TINYINT(1) DEFAULT 1<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="40" width="300" height="180" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_produit" value="<b>produit</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK categorie_id : INT UNSIGNED<br>libelle : VARCHAR(120)<br>description : TEXT NULL<br>prix_ttc_cents : INT UNSIGNED (CHECK > 0)<br>image_path : VARCHAR(255) NULL<br>est_disponible : TINYINT(1) DEFAULT 1<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="280" width="320" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_menu" value="<b>menu</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK categorie_id : INT UNSIGNED<br>libelle : VARCHAR(120)<br>description : TEXT NULL<br>prix_ttc_cents : INT UNSIGNED (CHECK > 0)<br>image_path : VARCHAR(255) NULL<br>est_disponible : TINYINT(1) DEFAULT 1<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="820" y="280" width="320" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_menu_produit" value="<b>menu_produit</b> (jointure)<hr><u>PK FK menu_id : INT UNSIGNED</u><br><u>PK FK produit_id : INT UNSIGNED</u><br>role : ENUM(burger,accompagnement,boisson,sauce,dessert)<br>position : SMALLINT UNSIGNED DEFAULT 0" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="560" width="380" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_prod_cat" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_produit" target="t_categorie">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_prod_cat_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_prod_cat">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_menu_cat" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu" target="t_categorie">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_menu_cat_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_menu_cat">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_mp_menu" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu_produit" target="t_menu">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_mp_menu_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_mp_menu">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_mp_prod" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu_produit" target="t_produit">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_mp_prod_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_mp_prod">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="legende" value="<b>Legende</b><br><u>PK</u> : cle primaire<br>FK : cle etrangere (fleche -> table referencee)<br>UK : contrainte unique<br>Bleu = table principale<br>Jaune pointille = table de jointure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="280" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
46
docs/merise/_diagrams/mld-catalogue.mmd
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
erDiagram
|
||||
category {
|
||||
int id PK
|
||||
varchar name UK
|
||||
varchar slug UK
|
||||
smallint display_order
|
||||
tinyint is_active
|
||||
}
|
||||
product {
|
||||
int id PK
|
||||
int category_id FK
|
||||
varchar name
|
||||
int price_cents
|
||||
smallint vat_rate
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
}
|
||||
menu {
|
||||
int id PK
|
||||
int category_id FK
|
||||
int burger_product_id FK
|
||||
varchar name
|
||||
int price_normal_cents
|
||||
int price_maxi_cents
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
}
|
||||
menu_slot {
|
||||
int id PK
|
||||
int menu_id FK
|
||||
varchar name
|
||||
enum slot_type
|
||||
tinyint is_required
|
||||
smallint display_order
|
||||
}
|
||||
menu_slot_option {
|
||||
int menu_slot_id PK,FK
|
||||
int product_id PK,FK
|
||||
}
|
||||
|
||||
category ||--o{ product : "category_id (RESTRICT)"
|
||||
category ||--o{ menu : "category_id (RESTRICT)"
|
||||
product ||--o{ menu : "burger_product_id (RESTRICT)"
|
||||
menu ||--o{ menu_slot : "menu_id (CASCADE)"
|
||||
menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)"
|
||||
product ||--o{ menu_slot_option : "product_id (RESTRICT)"
|
||||
1
docs/merise/_diagrams/mld-catalogue.svg
Normal file
|
After Width: | Height: | Size: 107 KiB |
|
|
@ -1,78 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||
<diagram name="MLD - Commande" id="mld-commande">
|
||||
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="t_commande" value="<b>commande</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK numero : VARCHAR(20)<br>source : ENUM(kiosk,comptoir,drive)<br>mode_consommation : ENUM(sur_place,a_emporter,drive)<br>statut : ENUM DEFAULT pending_payment<br>total_ht_cents : INT UNSIGNED<br>total_tva_cents : INT UNSIGNED<br>total_ttc_cents : INT UNSIGNED<br>tva_taux_pourmille : SMALLINT UNSIGNED<br>paye_a : DATETIME NULL<br>created_at : DATETIME<br>updated_at : DATETIME<hr>CHECK (source != drive OR mode = drive)<br>CHECK (ttc = ht + tva)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="500" y="40" width="380" height="290" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_ligne_commande" value="<b>ligne_commande</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK commande_id : INT UNSIGNED<br>type_item : ENUM(produit,menu)<br>FK produit_id : INT UNSIGNED NULL<br>FK menu_id : INT UNSIGNED NULL<br>libelle_snapshot : VARCHAR(120)<br>prix_unitaire_ttc_cents_snapshot : INT UNSIGNED<br>quantite : SMALLINT UNSIGNED DEFAULT 1<br>created_at : DATETIME<hr>CHECK polymorphisme exclusif" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="400" width="380" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_commande_event" value="<b>commande_event</b> (append-only)<hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK commande_id : INT UNSIGNED<br>event_type : ENUM(CREATED,PAID,...)<br>from_statut : ENUM NULL<br>to_statut : ENUM<br>FK user_id : INT UNSIGNED NULL<br>payload : JSON NULL<br>created_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="960" y="400" width="380" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_produit_stub" value="<b>produit</b> <i>(cf. Catalogue)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="680" width="200" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_menu_stub" value="<b>menu</b> <i>(cf. Catalogue)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="680" width="200" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="t_user_stub" value="<b>user</b> <i>(cf. RBAC)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||
<mxGeometry x="1140" y="680" width="200" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_lc_cmd" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_ligne_commande" target="t_commande">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_lc_cmd_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_cmd">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_lc_prod" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_ligne_commande" target="t_produit_stub">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_lc_prod_lbl" value="FK NULL ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_prod">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_lc_menu" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_ligne_commande" target="t_menu_stub">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_lc_menu_lbl" value="FK NULL ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_menu">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_evt_cmd" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_commande_event" target="t_commande">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_evt_cmd_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_evt_cmd">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="fk_evt_user" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_commande_event" target="t_user_stub">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fk_evt_user_lbl" value="FK NULL ON DELETE SET NULL" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_evt_user">
|
||||
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="note_audit" value="<b>Journal d'audit (event sourcing)</b><br>Append-only : aucun UPDATE / DELETE applicatif.<br>3 IDX : (commande_id, created_at), (user_id, created_at), (event_type, created_at).<br>Pattern d'ecriture : transaction qui modifie commande.statut insere aussi une ligne d'event." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="960" y="620" width="380" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="legende" value="<b>Legende</b><br><u>PK</u> : cle primaire<br>FK : cle etrangere (fleche -> table referencee)<br>UK : contrainte unique<br>Bleu = table principale<br>Vert = journal d'audit<br>Violet = stub d'un autre sous-domaine<br>Pointille = FK nullable" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="280" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
59
docs/merise/_diagrams/mld-ingredients-stock.mmd
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
erDiagram
|
||||
ingredient {
|
||||
int id PK
|
||||
varchar name UK
|
||||
varchar unit
|
||||
int stock_quantity
|
||||
int stock_capacity
|
||||
smallint pack_size
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
tinyint is_active
|
||||
}
|
||||
product_ingredient {
|
||||
int product_id PK,FK
|
||||
int ingredient_id PK,FK
|
||||
smallint quantity_normal
|
||||
smallint quantity_maxi
|
||||
tinyint is_removable
|
||||
tinyint is_addable
|
||||
int extra_price_cents
|
||||
}
|
||||
allergen {
|
||||
int id PK
|
||||
varchar code UK
|
||||
varchar name
|
||||
}
|
||||
ingredient_allergen {
|
||||
int ingredient_id PK,FK
|
||||
int allergen_id PK,FK
|
||||
}
|
||||
stock_movement {
|
||||
int id PK
|
||||
int ingredient_id FK
|
||||
enum movement_type
|
||||
int delta
|
||||
int order_id FK
|
||||
int user_id FK
|
||||
varchar note
|
||||
}
|
||||
product {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
customer_order {
|
||||
int id PK
|
||||
varchar order_number
|
||||
}
|
||||
user {
|
||||
int id PK
|
||||
varchar email
|
||||
}
|
||||
|
||||
product ||--o{ product_ingredient : "product_id (CASCADE)"
|
||||
ingredient ||--o{ product_ingredient : "ingredient_id (RESTRICT)"
|
||||
ingredient ||--o{ ingredient_allergen : "ingredient_id (CASCADE)"
|
||||
allergen ||--o{ ingredient_allergen : "allergen_id (RESTRICT)"
|
||||
ingredient ||--o{ stock_movement : "ingredient_id (RESTRICT)"
|
||||
customer_order ||--o{ stock_movement : "order_id (SET NULL, nullable)"
|
||||
user ||--o{ stock_movement : "user_id (SET NULL, nullable)"
|
||||
1
docs/merise/_diagrams/mld-ingredients-stock.svg
Normal file
|
After Width: | Height: | Size: 135 KiB |
72
docs/merise/_diagrams/mld-order.mmd
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
erDiagram
|
||||
customer_order {
|
||||
int id PK
|
||||
varchar order_number UK
|
||||
varchar idempotency_key UK
|
||||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
enum status
|
||||
int total_ht_cents
|
||||
int total_vat_cents
|
||||
int total_ttc_cents
|
||||
datetime paid_at
|
||||
datetime delivered_at
|
||||
datetime cancelled_at
|
||||
}
|
||||
order_item {
|
||||
int id PK
|
||||
int order_id FK
|
||||
enum item_type
|
||||
int product_id FK
|
||||
int menu_id FK
|
||||
enum format
|
||||
varchar label_snapshot
|
||||
int unit_price_cents_snapshot
|
||||
smallint vat_rate_snapshot
|
||||
smallint quantity
|
||||
}
|
||||
order_item_selection {
|
||||
int id PK
|
||||
int order_item_id FK
|
||||
int menu_slot_id FK
|
||||
int product_id FK
|
||||
varchar label_snapshot
|
||||
}
|
||||
order_item_modifier {
|
||||
int id PK
|
||||
int order_item_id FK
|
||||
int ingredient_id FK
|
||||
enum action
|
||||
int extra_price_cents
|
||||
}
|
||||
user {
|
||||
int id PK
|
||||
varchar email
|
||||
}
|
||||
product {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
menu {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
menu_slot {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
ingredient {
|
||||
int id PK
|
||||
varchar name
|
||||
}
|
||||
|
||||
user ||--o{ customer_order : "acting_user_id (SET NULL, nullable)"
|
||||
customer_order ||--o{ order_item : "order_id (CASCADE)"
|
||||
product ||--o{ order_item : "product_id (RESTRICT, polymorphic)"
|
||||
menu ||--o{ order_item : "menu_id (RESTRICT, polymorphic)"
|
||||
order_item ||--o{ order_item_selection : "order_item_id (CASCADE)"
|
||||
menu_slot ||--o{ order_item_selection : "menu_slot_id (RESTRICT)"
|
||||
product ||--o{ order_item_selection : "product_id (RESTRICT)"
|
||||
order_item ||--o{ order_item_modifier : "order_item_id (CASCADE)"
|
||||
ingredient ||--o{ order_item_modifier : "ingredient_id (RESTRICT)"
|
||||
1
docs/merise/_diagrams/mld-order.svg
Normal file
|
After Width: | Height: | Size: 167 KiB |