docs: add ERD diagrams (v2.4/v2.5/v2.6) and material-supplier-category migration plan
This commit is contained in:
+300
@@ -0,0 +1,300 @@
|
||||
// ============================================================================
|
||||
// PROCUREMENT SCHEMA v2.4 — Final Enterprise Architecture
|
||||
// ============================================================================
|
||||
|
||||
// ---------- ENUMS ----------
|
||||
|
||||
Enum quote_status {
|
||||
offered
|
||||
accepted
|
||||
arrived
|
||||
rejected
|
||||
expired
|
||||
}
|
||||
|
||||
Enum termsheet_status {
|
||||
offered
|
||||
accepted
|
||||
rejected
|
||||
}
|
||||
|
||||
Enum material_status {
|
||||
active
|
||||
discontinued
|
||||
draft
|
||||
}
|
||||
|
||||
Enum attribute_data_type {
|
||||
number
|
||||
integer
|
||||
string
|
||||
enum
|
||||
boolean
|
||||
date
|
||||
}
|
||||
|
||||
Enum discount_type {
|
||||
none
|
||||
percent
|
||||
fixed
|
||||
}
|
||||
|
||||
Enum item_decision_status {
|
||||
pending
|
||||
chosen
|
||||
rejected
|
||||
}
|
||||
|
||||
Enum quote_event_type {
|
||||
offered
|
||||
accepted
|
||||
rejected
|
||||
arrived
|
||||
note
|
||||
edited
|
||||
expired
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CATALOG DOMAIN
|
||||
// ============================================================================
|
||||
|
||||
Table units {
|
||||
id integer [pk]
|
||||
code varchar [not null, unique]
|
||||
name_fa varchar
|
||||
dimension varchar [note: 'length, mass, pressure, area, volume, voltage, count, ratio, time, currency']
|
||||
si_factor decimal [not null, default: 1.0, note: 'Universal multiply factor to convert to base SI unit within same dimension']
|
||||
is_base boolean [default: false]
|
||||
}
|
||||
|
||||
Table brands {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
country text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(name, country) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table material_categories {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
slug text [not null, unique]
|
||||
name text [not null]
|
||||
description text
|
||||
is_active boolean [not null, default: true]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
// Global Attribute Dictionary
|
||||
Table attribute_definitions {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
attribute_key text [not null, unique]
|
||||
label text [not null]
|
||||
data_type attribute_data_type [not null]
|
||||
unit_id integer [ref: > units.id]
|
||||
enum_values jsonb [note: 'Allowed options for enum types']
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
// Category-Attribute Junction (Scopes global attributes & sets category rules)
|
||||
Table category_attributes {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
category_id uuid [not null, ref: > material_categories.id]
|
||||
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
||||
is_required boolean [not null, default: false]
|
||||
is_filterable boolean [not null, default: false]
|
||||
conditional_rules jsonb [note: 'UI Wizard rules e.g. {"when": {"is_colored": true}, "then": {"required": ["color"]}}']
|
||||
sort_order smallint [not null, default: 0]
|
||||
|
||||
indexes {
|
||||
(category_id, attribute_definition_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract Base Products / Materials
|
||||
Table materials {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
category_id uuid [not null, ref: > material_categories.id]
|
||||
name text [not null]
|
||||
canonical_unit_id integer [not null, ref: > units.id]
|
||||
status material_status [not null, default: `'active'`]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(category_id, name) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
// M:N Brand <-> Material Junction
|
||||
Table brand_materials {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
brand_id uuid [not null, ref: > brands.id]
|
||||
material_id uuid [not null, ref: > materials.id]
|
||||
catalog_code text
|
||||
is_active boolean [not null, default: true]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(brand_id, material_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
// Material & Brand Specific Packaging / Unit Overrides
|
||||
Table material_units {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
material_id uuid [not null, ref: > materials.id]
|
||||
brand_material_id uuid [ref: > brand_materials.id, note: 'NULL = generic material fallback; set = brand-specific packaging override']
|
||||
unit_id integer [not null, ref: > units.id]
|
||||
conversion_factor "numeric(18,6)" [not null, note: 'Multiplier to convert to canonical_unit_id']
|
||||
is_default boolean [not null, default: false]
|
||||
|
||||
indexes {
|
||||
(material_id, brand_material_id, unit_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
// Typed Specs Source of Truth (EAV)
|
||||
Table material_attribute_values {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
material_id uuid [not null, ref: > materials.id]
|
||||
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
||||
value_number "numeric(18,6)"
|
||||
value_string text
|
||||
value_boolean boolean
|
||||
value_date date
|
||||
|
||||
indexes {
|
||||
(material_id, attribute_definition_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUOTATION & PROCUREMENT DOMAIN
|
||||
// ============================================================================
|
||||
|
||||
Table suppliers {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
location text
|
||||
phone text
|
||||
contact_person text
|
||||
tax_id text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table projects {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
description text
|
||||
start_date date
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table quotes {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
supplier_id uuid [not null, ref: > suppliers.id]
|
||||
project_id uuid [ref: > projects.id]
|
||||
quote_date timestamptz [not null, default: `now()`]
|
||||
expiry_date date
|
||||
accepted_date timestamptz
|
||||
arrived_date timestamptz
|
||||
status quote_status [not null, default: `'offered'`]
|
||||
is_official boolean [not null, default: true, note: 'True = formal binding quote; False = informal price estimate / draft']
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
Table quote_events {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
quote_id uuid [not null, ref: > quotes.id]
|
||||
event_type quote_event_type [not null]
|
||||
payload jsonb [not null, default: `'{}'`]
|
||||
occurred_at timestamptz [not null, default: `now()`]
|
||||
actor text
|
||||
}
|
||||
|
||||
Table line_items {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
quote_id uuid [not null, ref: > quotes.id]
|
||||
material_id uuid [not null, ref: > materials.id]
|
||||
brand_material_id uuid [ref: > brand_materials.id, note: 'Optional: set when a specific branded product is quoted']
|
||||
unit_id integer [not null, ref: > units.id]
|
||||
quantity "numeric(18,4)" [not null]
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
// Historical Snapshot: Freezes material specs at quote generation time
|
||||
Table line_item_specs {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
line_item_id uuid [not null, ref: > line_items.id]
|
||||
attribute_definition_id uuid [not null, ref: > attribute_definitions.id]
|
||||
value_number "numeric(18,6)"
|
||||
value_string text
|
||||
value_boolean boolean
|
||||
value_date date
|
||||
unit_id_at_time integer [ref: > units.id]
|
||||
|
||||
indexes {
|
||||
(line_item_id, attribute_definition_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table termsheets {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
quote_id uuid [not null, ref: > quotes.id]
|
||||
label text [not null]
|
||||
status termsheet_status [not null, default: `'offered'`]
|
||||
discount_type discount_type [not null, default: `'none'`]
|
||||
discount_value "numeric(18,4)" [not null, default: 0]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table termsheet_schedule_steps {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
termsheet_id uuid [not null, ref: > termsheets.id]
|
||||
step_no smallint [not null]
|
||||
due_date date [not null]
|
||||
amount "numeric(18,2)" [not null]
|
||||
note text
|
||||
|
||||
indexes {
|
||||
(termsheet_id, step_no) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table item_termsheets {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
line_item_id uuid [not null, ref: > line_items.id]
|
||||
termsheet_id uuid [not null, ref: > termsheets.id]
|
||||
unit_price "numeric(18,4)" [not null]
|
||||
min_order_qty "numeric(18,4)"
|
||||
total_price "numeric(18,2)" [note: 'Calculated at query/view time: unit_price * line_items.quantity']
|
||||
|
||||
indexes {
|
||||
(line_item_id, termsheet_id) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table item_decisions {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
line_item_id uuid [not null, unique, ref: - line_items.id]
|
||||
chosen_item_termsheet_id uuid [ref: > item_termsheets.id]
|
||||
status item_decision_status [not null, default: `'pending'`]
|
||||
adjusted_unit_price "numeric(18,4)"
|
||||
adjusted_notes text
|
||||
decided_at timestamptz
|
||||
}
|
||||
|
||||
Table item_receipts {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
item_decision_id uuid [not null, unique, ref: - item_decisions.id]
|
||||
received_at timestamptz [not null, default: `now()`]
|
||||
received_qty "numeric(18,4)" [not null]
|
||||
on_time boolean
|
||||
quality_note text}
|
||||
Reference in New Issue
Block a user