From 41f9c96d33eb32510cc97e5b02a508e14ac8cfd0 Mon Sep 17 00:00:00 2001 From: Corentin JOGUET Date: Mon, 15 Jun 2026 15:36:05 +0200 Subject: [PATCH] feat(db): initial schema DDL (21 tables) + migration runner (#6) --- Makefile | 23 +- db/README.md | 38 +++ db/migrate.sh | 68 +++++ db/migrations/0001_init_schema.sql | 465 +++++++++++++++++++++++++++++ db/seed.sh | 69 +++++ 5 files changed, 659 insertions(+), 4 deletions(-) create mode 100644 db/README.md create mode 100755 db/migrate.sh create mode 100644 db/migrations/0001_init_schema.sql create mode 100755 db/seed.sh diff --git a/Makefile b/Makefile index 1c1ee94..d7734cb 100644 --- a/Makefile +++ b/Makefile @@ -156,12 +156,12 @@ wait-db: ## Attend que la base de donnees accepte les connexions (timeout 60s) @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/." +migrate: ## Applique les migrations SQL en attente (db/migrations/) + @bash db/migrate.sh .PHONY: seed -seed: ## Charge les donnees de demo [a venir] - @echo "[seed] Pas encore implemente. Les seeds seront dans db/seeds/." +seed: ## Charge les donnees de demo (db/seeds/) + @bash db/seed.sh .PHONY: backup backup: ## Declenche un dump SQL horodate immediat (via le container cron) @@ -211,6 +211,21 @@ clean: ## Stop + suppression containers + volumes (DESTRUCTIF, demande confirmat clean-force: ## Version non interactive de clean (pour CI uniquement) @$(COMPOSE) down -v +# === Documentation === + +.PHONY: docs-render +docs-render: ## Regenere les diagrammes Mermaid (docs/**/_diagrams/*.mmd -> *.svg) + @echo "[docs-render] Recherche des sources Mermaid sous docs/..." + @count=0; \ + for src in $$(find docs -name '*.mmd' -path '*/_diagrams/*'); do \ + out="$${src%.mmd}.svg"; \ + echo " $$src -> $$out"; \ + npx -y -p @mermaid-js/mermaid-cli mmdc -i "$$src" -o "$$out" >/dev/null 2>&1 \ + || { echo "[docs-render] ECHEC sur $$src"; exit 1; }; \ + count=$$((count + 1)); \ + done; \ + echo "[docs-render] $$count diagramme(s) genere(s)." + # === Hooks Git === .PHONY: install-hooks diff --git a/db/README.md b/db/README.md new file mode 100644 index 0000000..67384bd --- /dev/null +++ b/db/README.md @@ -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 `make migrate` 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. diff --git a/db/migrate.sh b/db/migrate.sh new file mode 100755 index 0000000..b05f65b --- /dev/null +++ b/db/migrate.sh @@ -0,0 +1,68 @@ +#!/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 +# (c'est ce que `make migrate` appellera). Identifiants lus dans .env. +# +# 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 (make up)" >&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))." diff --git a/db/migrations/0001_init_schema.sql b/db/migrations/0001_init_schema.sql new file mode 100644 index 0000000..9e564d8 --- /dev/null +++ b/db/migrations/0001_init_schema.sql @@ -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; diff --git a/db/seed.sh b/db/seed.sh new file mode 100755 index 0000000..9b0f398 --- /dev/null +++ b/db/seed.sh @@ -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 (make up)" >&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))."