diff --git a/docs/merise/_diagrams/mld-catalogue.mmd b/docs/merise/_diagrams/mld-catalogue.mmd new file mode 100644 index 0000000..7519fdb --- /dev/null +++ b/docs/merise/_diagrams/mld-catalogue.mmd @@ -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)" diff --git a/docs/merise/_diagrams/mld-catalogue.svg b/docs/merise/_diagrams/mld-catalogue.svg new file mode 100644 index 0000000..7d501b3 --- /dev/null +++ b/docs/merise/_diagrams/mld-catalogue.svg @@ -0,0 +1 @@ +category_id (RESTRICT)category_id (RESTRICT)burger_product_id (RESTRICT)menu_id (CASCADE)menu_slot_id (CASCADE)product_id (RESTRICT)categoryintidPKvarcharnameUKvarcharslugUKsmallintdisplay_ordertinyintis_activeproductintidPKintcategory_idFKvarcharnameintprice_centssmallintvat_ratetinyintis_availablesmallintdisplay_ordermenuintidPKintcategory_idFKintburger_product_idFKvarcharnameintprice_normal_centsintprice_maxi_centstinyintis_availablesmallintdisplay_ordermenu_slotintidPKintmenu_idFKvarcharnameenumslot_typetinyintis_requiredsmallintdisplay_ordermenu_slot_optionintmenu_slot_idPK,FKintproduct_idPK,FK \ No newline at end of file diff --git a/docs/merise/_diagrams/mld-ingredients-stock.mmd b/docs/merise/_diagrams/mld-ingredients-stock.mmd new file mode 100644 index 0000000..556f22f --- /dev/null +++ b/docs/merise/_diagrams/mld-ingredients-stock.mmd @@ -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)" diff --git a/docs/merise/_diagrams/mld-ingredients-stock.svg b/docs/merise/_diagrams/mld-ingredients-stock.svg new file mode 100644 index 0000000..22a377f --- /dev/null +++ b/docs/merise/_diagrams/mld-ingredients-stock.svg @@ -0,0 +1 @@ +product_id (CASCADE)ingredient_id (RESTRICT)ingredient_id (CASCADE)allergen_id (RESTRICT)ingredient_id (RESTRICT)order_id (SET NULL, nullable)user_id (SET NULL, nullable)ingredientintidPKvarcharnameUKvarcharunitintstock_quantityintstock_capacitysmallintpack_sizesmallintlow_stock_pctsmallintcritical_stock_pcttinyintis_activeproduct_ingredientintproduct_idPK,FKintingredient_idPK,FKsmallintquantity_normalsmallintquantity_maxitinyintis_removabletinyintis_addableintextra_price_centsallergenintidPKvarcharcodeUKvarcharnameingredient_allergenintingredient_idPK,FKintallergen_idPK,FKstock_movementintidPKintingredient_idFKenummovement_typeintdeltaintorder_idFKintuser_idFKvarcharnoteproductintidPKvarcharnamecustomer_orderintidPKvarcharorder_numberuserintidPKvarcharemail \ No newline at end of file diff --git a/docs/merise/_diagrams/mld-order.mmd b/docs/merise/_diagrams/mld-order.mmd new file mode 100644 index 0000000..f6ec1e3 --- /dev/null +++ b/docs/merise/_diagrams/mld-order.mmd @@ -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)" diff --git a/docs/merise/_diagrams/mld-order.svg b/docs/merise/_diagrams/mld-order.svg new file mode 100644 index 0000000..3cbc2f8 --- /dev/null +++ b/docs/merise/_diagrams/mld-order.svg @@ -0,0 +1 @@ +acting_user_id (SET NULL, nullable)order_id (CASCADE)product_id (RESTRICT, polymorphic)menu_id (RESTRICT, polymorphic)order_item_id (CASCADE)menu_slot_id (RESTRICT)product_id (RESTRICT)order_item_id (CASCADE)ingredient_id (RESTRICT)customer_orderintidPKvarcharorder_numberUKvarcharidempotency_keyUKenumsourceintacting_user_idFKenumservice_modeenumstatusinttotal_ht_centsinttotal_vat_centsinttotal_ttc_centsdatetimepaid_atdatetimedelivered_atdatetimecancelled_atorder_itemintidPKintorder_idFKenumitem_typeintproduct_idFKintmenu_idFKenumformatvarcharlabel_snapshotintunit_price_cents_snapshotsmallintvat_rate_snapshotsmallintquantityorder_item_selectionintidPKintorder_item_idFKintmenu_slot_idFKintproduct_idFKvarcharlabel_snapshotorder_item_modifierintidPKintorder_item_idFKintingredient_idFKenumactionintextra_price_centsuserintidPKvarcharemailproductintidPKvarcharnamemenuintidPKvarcharnamemenu_slotintidPKvarcharnameingredientintidPKvarcharname \ No newline at end of file diff --git a/docs/merise/_diagrams/mld-rbac.mmd b/docs/merise/_diagrams/mld-rbac.mmd new file mode 100644 index 0000000..2bc76b8 --- /dev/null +++ b/docs/merise/_diagrams/mld-rbac.mmd @@ -0,0 +1,61 @@ +erDiagram + role { + int id PK + varchar code UK + varchar label + varchar default_route + enum order_source + tinyint is_active + } + user { + int id PK + varchar email UK + varchar password_hash + varchar pin_hash + varchar first_name + varchar last_name + int role_id FK + tinyint is_active + smallint failed_login_attempts + datetime lockout_until + datetime anonymized_at + } + role_visible_source { + int role_id PK,FK + enum source PK + } + permission { + int id PK + varchar code UK + varchar label + } + role_permission { + int role_id PK,FK + int permission_id PK,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 + } + + role ||--o{ user : "role_id (RESTRICT)" + role ||--o{ role_visible_source : "role_id (CASCADE)" + role ||--o{ role_permission : "role_id (CASCADE)" + permission ||--o{ role_permission : "permission_id (CASCADE)" + user ||--o{ audit_log : "actor_user_id (SET NULL, nullable)" + role ||--o{ audit_log : "actor_role_id (SET NULL, nullable)" diff --git a/docs/merise/_diagrams/mld-rbac.svg b/docs/merise/_diagrams/mld-rbac.svg new file mode 100644 index 0000000..ad68a9f --- /dev/null +++ b/docs/merise/_diagrams/mld-rbac.svg @@ -0,0 +1 @@ +role_id (RESTRICT)role_id (CASCADE)role_id (CASCADE)permission_id (CASCADE)actor_user_id (SET NULL, nullable)actor_role_id (SET NULL, nullable)roleintidPKvarcharcodeUKvarcharlabelvarchardefault_routeenumorder_sourcetinyintis_activeuserintidPKvarcharemailUKvarcharpassword_hashvarcharpin_hashvarcharfirst_namevarcharlast_nameintrole_idFKtinyintis_activesmallintfailed_login_attemptsdatetimelockout_untildatetimeanonymized_atrole_visible_sourceintrole_idPK,FKenumsourcePKpermissionintidPKvarcharcodeUKvarcharlabelrole_permissionintrole_idPK,FKintpermission_idPK,FKaudit_logintidPKintactor_user_idFKintactor_role_idFKvarcharaction_codevarcharentity_typeintentity_idvarcharsummaryjsondetailsdatetimecreated_atlogin_throttleintidPKvarcharip_addressUKsmallintfailed_attemptsdatetimewindow_started_atdatetimelockout_untildatetimelast_attempt_at \ No newline at end of file diff --git a/docs/merise/mld.md b/docs/merise/mld.md index 5cd6563..c701f63 100644 --- a/docs/merise/mld.md +++ b/docs/merise/mld.md @@ -97,6 +97,279 @@ in addition to the composite FK PK. Applied to `product_ingredient`. Tables are ordered by dependency (no-FK tables first, then tables that depend on them). +### Relational diagrams (by sub-domain) + +The relational schema is shown as four Mermaid `erDiagram` views, one per sub-domain (same +decomposition as the MCD; a single 21-table diagram would not lay out cleanly). These differ +from the MCD: associative entities are resolved into join tables with composite PKs, the +`order_item` polymorphism appears as two nullable FKs (`product_id` / `menu_id`), and every +foreign key is explicit. Audit timestamps (`created_at` / `updated_at`) are present on most +tables (see the per-table sections below) but omitted from the diagrams to keep them readable. +Relationship labels carry the FK column and its `ON DELETE` behaviour. Cross-sub-domain FK +targets are shown as stub tables (id + name). Portable SVG renders live in `_diagrams/` +(`mld-catalogue.svg`, `mld-ingredients-stock.svg`, `mld-order.svg`, `mld-rbac.svg`). + +#### Catalogue + +```mermaid +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)" +``` + +#### Ingredients & Stock + +```mermaid +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)" +``` + +#### Order + +```mermaid +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)" +``` + +#### RBAC & security + +```mermaid +erDiagram + role { + int id PK + varchar code UK + varchar label + varchar default_route + enum order_source + tinyint is_active + } + user { + int id PK + varchar email UK + varchar password_hash + varchar pin_hash + varchar first_name + varchar last_name + int role_id FK + tinyint is_active + smallint failed_login_attempts + datetime lockout_until + datetime anonymized_at + } + role_visible_source { + int role_id PK,FK + enum source PK + } + permission { + int id PK + varchar code UK + varchar label + } + role_permission { + int role_id PK,FK + int permission_id PK,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 + } + + role ||--o{ user : "role_id (RESTRICT)" + role ||--o{ role_visible_source : "role_id (CASCADE)" + role ||--o{ role_permission : "role_id (CASCADE)" + permission ||--o{ role_permission : "permission_id (CASCADE)" + user ||--o{ audit_log : "actor_user_id (SET NULL, nullable)" + role ||--o{ audit_log : "actor_role_id (SET NULL, nullable)" +``` + +> `login_throttle` has no FK (an IP is not a modelled entity); it stands alone, keyed by +> `ip_address`. + --- ### 4.1 `category`
category_id (RESTRICT)
burger_product_id (RESTRICT)
menu_id (CASCADE)
menu_slot_id (CASCADE)
product_id (RESTRICT)
category
int
id
PK
varchar
name
UK
slug
smallint
display_order
tinyint
is_active
product
category_id
FK
price_cents
vat_rate
is_available
menu
burger_product_id
price_normal_cents
price_maxi_cents
menu_slot
menu_id
enum
slot_type
is_required
menu_slot_option
menu_slot_id
PK,FK
product_id
product_id (CASCADE)
ingredient_id (RESTRICT)
ingredient_id (CASCADE)
allergen_id (RESTRICT)
order_id (SET NULL, nullable)
user_id (SET NULL, nullable)
ingredient
unit
stock_quantity
stock_capacity
pack_size
low_stock_pct
critical_stock_pct
product_ingredient
ingredient_id
quantity_normal
quantity_maxi
is_removable
is_addable
extra_price_cents
allergen
code
ingredient_allergen
allergen_id
stock_movement
movement_type
delta
order_id
user_id
note
customer_order
order_number
user
email
acting_user_id (SET NULL, nullable)
order_id (CASCADE)
product_id (RESTRICT, polymorphic)
menu_id (RESTRICT, polymorphic)
order_item_id (CASCADE)
menu_slot_id (RESTRICT)
idempotency_key
source
acting_user_id
service_mode
status
total_ht_cents
total_vat_cents
total_ttc_cents
datetime
paid_at
delivered_at
cancelled_at
order_item
item_type
format
label_snapshot
unit_price_cents_snapshot
vat_rate_snapshot
quantity
order_item_selection
order_item_id
order_item_modifier
action
role_id (RESTRICT)
role_id (CASCADE)
permission_id (CASCADE)
actor_user_id (SET NULL, nullable)
actor_role_id (SET NULL, nullable)
role
label
default_route
order_source
password_hash
pin_hash
first_name
last_name
role_id
failed_login_attempts
lockout_until
anonymized_at
role_visible_source
permission
role_permission
permission_id
audit_log
actor_user_id
actor_role_id
action_code
entity_type
entity_id
summary
json
details
created_at
login_throttle
ip_address
failed_attempts
window_started_at
last_attempt_at