-- Template migration for each organization schema. -- Replace {schema} with the real schema name, e.g. company_. create table if not exists {schema}.customers ( id uuid primary key, customer_number text unique, name_ciphertext bytea not null, name_nonce bytea not null, name_key_id text not null, status text not null default 'active', created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint customers_status_valid check (status in ('active', 'inactive', 'blocked')) ); create table if not exists {schema}.suppliers ( id uuid primary key, supplier_number text unique, name_ciphertext bytea not null, name_nonce bytea not null, name_key_id text not null, status text not null default 'active', created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint suppliers_status_valid check (status in ('active', 'inactive', 'blocked')) ); create table if not exists {schema}.items ( id uuid primary key, item_number text not null unique, name_ciphertext bytea not null, name_nonce bytea not null, name_key_id text not null, unit text not null default 'Stk', tax_rate numeric(7, 4) not null default 19.0, default_purchase_price numeric(14, 4), default_sales_price numeric(14, 4), status text not null default 'active', created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint items_status_valid check (status in ('active', 'inactive', 'blocked')), constraint items_tax_rate_non_negative check (tax_rate >= 0), constraint items_default_purchase_price_non_negative check ( default_purchase_price is null or default_purchase_price >= 0 ), constraint items_default_sales_price_non_negative check ( default_sales_price is null or default_sales_price >= 0 ) ); create table if not exists {schema}.cash_discount_terms ( id uuid primary key, code text not null unique, name text not null, discount_percent numeric(7, 4) not null, discount_days integer not null, net_days integer, valid_from date, valid_until date, is_default_customer_term boolean not null default false, is_default_supplier_term boolean not null default false, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint cash_discount_terms_percent_valid check ( discount_percent >= 0 and discount_percent <= 100 ), constraint cash_discount_terms_days_valid check ( discount_days >= 0 and (net_days is null or net_days >= discount_days) ), constraint cash_discount_terms_valid_range check ( valid_until is null or valid_from is null or valid_until >= valid_from ) ); create unique index if not exists idx_cash_discount_terms_default_customer on {schema}.cash_discount_terms (is_default_customer_term) where is_default_customer_term; create unique index if not exists idx_cash_discount_terms_default_supplier on {schema}.cash_discount_terms (is_default_supplier_term) where is_default_supplier_term; create table if not exists {schema}.customer_price_terms ( id uuid primary key, customer_id uuid not null references {schema}.customers(id) on delete cascade, standard_discount_percent numeric(7, 4) not null default 0, cash_discount_term_id uuid references {schema}.cash_discount_terms(id), valid_from date, valid_until date, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint customer_price_terms_discount_valid check ( standard_discount_percent >= 0 and standard_discount_percent <= 100 ), constraint customer_price_terms_valid_range check ( valid_until is null or valid_from is null or valid_until >= valid_from ) ); create index if not exists idx_customer_price_terms_customer_active on {schema}.customer_price_terms (customer_id, is_active, valid_from, valid_until); create table if not exists {schema}.supplier_price_terms ( id uuid primary key, supplier_id uuid not null references {schema}.suppliers(id) on delete cascade, standard_discount_percent numeric(7, 4) not null default 0, cash_discount_term_id uuid references {schema}.cash_discount_terms(id), payment_days integer, valid_from date, valid_until date, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint supplier_price_terms_discount_valid check ( standard_discount_percent >= 0 and standard_discount_percent <= 100 ), constraint supplier_price_terms_payment_days_valid check ( payment_days is null or payment_days >= 0 ), constraint supplier_price_terms_valid_range check ( valid_until is null or valid_from is null or valid_until >= valid_from ) ); create index if not exists idx_supplier_price_terms_supplier_active on {schema}.supplier_price_terms (supplier_id, is_active, valid_from, valid_until); create table if not exists {schema}.activities ( id uuid primary key, activity_type text not null, title_ciphertext bytea not null, title_nonce bytea not null, title_key_id text not null, body_ciphertext bytea, body_nonce bytea, body_key_id text, status text not null default 'open', priority text not null default 'normal', due_at timestamptz, starts_at timestamptz, ends_at timestamptz, assigned_to_user_id uuid, created_by_user_id uuid, completed_by_user_id uuid, completed_at timestamptz, visibility text not null default 'internal', system_source text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint activities_type_valid check ( activity_type in ( 'email_note', 'phone_note', 'internal_note', 'task', 'follow_up', 'calendar_event', 'system_event', 'work_step' ) ), constraint activities_status_valid check ( status in ('open', 'in_progress', 'done', 'cancelled') ), constraint activities_priority_valid check ( priority in ('low', 'normal', 'high', 'critical') ), constraint activities_visibility_valid check ( visibility in ('internal', 'organization') ), constraint activities_body_encryption_complete check ( ( body_ciphertext is null and body_nonce is null and body_key_id is null ) or ( body_ciphertext is not null and body_nonce is not null and body_key_id is not null ) ), constraint activities_time_range_valid check ( ends_at is null or starts_at is null or ends_at >= starts_at ) ); create index if not exists idx_activities_status_due on {schema}.activities (status, due_at); create index if not exists idx_activities_assigned_status on {schema}.activities (assigned_to_user_id, status); create table if not exists {schema}.activity_links ( activity_id uuid not null references {schema}.activities(id) on delete cascade, entity_type text not null, entity_id uuid not null, created_at timestamptz not null default now(), primary key (activity_id, entity_type, entity_id), constraint activity_links_entity_type_valid check ( entity_type in ( 'customer', 'supplier', 'contact', 'quote', 'outgoing_invoice', 'incoming_invoice', 'item', 'document', 'import' ) ) ); create index if not exists idx_activity_links_entity on {schema}.activity_links (entity_type, entity_id); create table if not exists {schema}.outgoing_invoices ( id uuid primary key, invoice_number text unique, customer_id uuid not null references {schema}.customers(id), status text not null default 'draft', cash_discount_term_id uuid references {schema}.cash_discount_terms(id), issued_at date, due_at date, created_by_user_id uuid, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), finalized_at timestamptz, constraint outgoing_invoices_status_valid check ( status in ('draft', 'finalized', 'sent', 'paid', 'cancelled', 'overdue') ) ); create index if not exists idx_outgoing_invoices_customer_status on {schema}.outgoing_invoices (customer_id, status); create table if not exists {schema}.outgoing_invoice_items ( id uuid primary key, invoice_id uuid not null references {schema}.outgoing_invoices(id) on delete cascade, line_number integer not null, item_id uuid not null references {schema}.items(id), description_ciphertext bytea, description_nonce bytea, description_key_id text, quantity numeric(14, 4) not null, unit_price numeric(14, 4) not null, original_unit_price numeric(14, 4), discount_percent numeric(7, 4) not null default 0, price_overridden boolean not null default false, price_override_reason_ciphertext bytea, price_override_reason_nonce bytea, price_override_reason_key_id text, price_overridden_by_user_id uuid, price_overridden_at timestamptz, tax_rate numeric(7, 4) not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (invoice_id, line_number), constraint outgoing_invoice_items_quantity_positive check (quantity > 0), constraint outgoing_invoice_items_unit_price_non_negative check (unit_price >= 0), constraint outgoing_invoice_items_original_price_non_negative check ( original_unit_price is null or original_unit_price >= 0 ), constraint outgoing_invoice_items_discount_valid check ( discount_percent >= 0 and discount_percent <= 100 ), constraint outgoing_invoice_items_tax_rate_non_negative check (tax_rate >= 0), constraint outgoing_invoice_items_description_encryption_complete check ( ( description_ciphertext is null and description_nonce is null and description_key_id is null ) or ( description_ciphertext is not null and description_nonce is not null and description_key_id is not null ) ), constraint outgoing_invoice_items_override_reason_encryption_complete check ( ( price_override_reason_ciphertext is null and price_override_reason_nonce is null and price_override_reason_key_id is null ) or ( price_override_reason_ciphertext is not null and price_override_reason_nonce is not null and price_override_reason_key_id is not null ) ), constraint outgoing_invoice_items_override_complete check ( ( price_overridden = false and price_overridden_by_user_id is null and price_overridden_at is null ) or ( price_overridden = true and price_overridden_by_user_id is not null and price_overridden_at is not null ) ) ); create index if not exists idx_outgoing_invoice_items_item on {schema}.outgoing_invoice_items (item_id); create table if not exists {schema}.incoming_invoices ( id uuid primary key, invoice_number text, supplier_id uuid not null references {schema}.suppliers(id), status text not null default 'draft', cash_discount_term_id uuid references {schema}.cash_discount_terms(id), invoice_date date, due_at date, created_by_user_id uuid, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint incoming_invoices_status_valid check ( status in ('draft', 'received', 'approved', 'paid', 'cancelled', 'overdue') ) ); create index if not exists idx_incoming_invoices_supplier_status on {schema}.incoming_invoices (supplier_id, status); create table if not exists {schema}.incoming_invoice_items ( id uuid primary key, invoice_id uuid not null references {schema}.incoming_invoices(id) on delete cascade, line_number integer not null, item_id uuid references {schema}.items(id), description_ciphertext bytea, description_nonce bytea, description_key_id text, quantity numeric(14, 4) not null, unit_price numeric(14, 4) not null, tax_rate numeric(7, 4) not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (invoice_id, line_number), constraint incoming_invoice_items_quantity_positive check (quantity > 0), constraint incoming_invoice_items_unit_price_non_negative check (unit_price >= 0), constraint incoming_invoice_items_tax_rate_non_negative check (tax_rate >= 0), constraint incoming_invoice_items_description_encryption_complete check ( ( description_ciphertext is null and description_nonce is null and description_key_id is null ) or ( description_ciphertext is not null and description_nonce is not null and description_key_id is not null ) ) );