// ============================================================================ // 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}