/** * ⚠️ SCHEMA RENAME NOTICE: Many tables in this file have been renamed. * DO NOT use names from this file directly - check the rename migrations: * * - companies → clients (see 20251003000001_company_to_client_migration.cjs) * - channels → boards (see 20250930000001_rename_channels_to_boards.cjs) * - billing_plans → contract_lines (see 20251008000001_rename_billing_to_contracts.cjs) */ exports.up = async function(knex) { await knex.schema.createTable('tenants', (table) => { table.uuid('tenant').primary().defaultTo(knex.raw('gen_random_uuid()')); table.text('company_name').notNullable(); table.text('phone_number'); table.text('email').notNullable(); table.text('industry'); table.timestamp('created_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.text('payment_platform_id'); table.text('payment_method_id'); table.text('auth_service_id'); table.text('plan'); }); await knex.schema.createTable('users', (table) => { table.uuid('tenant').notNullable(); table.uuid('user_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('username').notNullable(); table.text('hashed_password').notNullable(); table.text('first_name'); table.text('last_name'); table.timestamp('date_of_birth'); table.text('role').notNullable(); table.text('email'); table.text('icon'); table.text('auth_method'); table.timestamp('created_at',{ useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.boolean('two_factor_enabled').defaultTo(false); table.text('two_factor_secret'); table.boolean('is_google_user').defaultTo(false); table.bigInteger('rate'); table.jsonb('roles').defaultTo('[]'); table.primary(['tenant', 'user_id', 'email']); table.foreign('tenant').references('tenants.tenant'); table.unique(['tenant', 'user_id']); }); await knex.schema.createTable('sessions', (table) => { table.uuid('tenant').notNullable(); table.uuid('session_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('user_id').notNullable(); table.text('token').notNullable(); table.timestamp('created_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'session_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.raw('CREATE INDEX "session_user_id_index" ON "sessions"("user_id")'); await knex.schema.createTable('companies', (table) => { // ⚠️ RENAMED to 'clients' (see 20251003000001_company_to_client_migration.cjs) table.uuid('tenant').notNullable(); table.uuid('company_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('company_name').notNullable(); table.text('phone_no'); table.text('url'); table.text('address'); table.text('status'); table.text('type'); table.jsonb('properties'); table.text('billing_type'); table.text('payment_terms'); table.text('billing_cycle'); table.bigInteger('credit_limit'); table.text('preferred_payment_method'); table.boolean('auto_invoice').defaultTo(false); table.text('invoice_delivery_method'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'company_id']); table.foreign('tenant').references('tenants.tenant'); }); // ⚠️ RENAMED: 'companies' → 'clients' (see 20251003000001_company_to_client_migration.cjs) await knex.schema.createTable('contacts', (table) => { table.uuid('tenant').notNullable(); table.uuid('contact_name_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('full_name'); table.uuid('company_id'); table.text('phone_number'); table.text('email'); table.text('role'); table.boolean('approver'); table.timestamp('date_of_birth'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'contact_name_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); }); await knex.schema.createTable('statuses', (table) => { table.uuid('tenant').notNullable(); table.uuid('status_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('status_name').notNullable(); table.text('status_type').notNullable().checkIn(['project', 'ticket', 'project_task']); table.integer('order_number').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'status_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('channels', (table) => { // ⚠️ RENAMED to 'boards' (see 20250930000001_rename_channels_to_boards.cjs) table.uuid('tenant').notNullable(); table.uuid('channel_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('channel_name').notNullable(); table.boolean('display_contact_name_id').defaultTo(true); table.boolean('display_priority').defaultTo(true); table.boolean('display_severity').defaultTo(true); table.boolean('display_urgency').defaultTo(true); table.boolean('display_impact').defaultTo(true); table.boolean('display_category').defaultTo(true); table.boolean('display_subcategory').defaultTo(true); table.boolean('display_assigned_to').defaultTo(true); table.boolean('display_status').defaultTo(true); table.boolean('display_due_date').defaultTo(true); table.primary(['tenant', 'channel_id']); table.foreign('tenant').references('tenants.tenant'); }); // ⚠️ RENAMED: 'channels' → 'boards' (see 20250930000001_rename_channels_to_boards.cjs) // `categories` is the TICKET categories table (NOT `ticket_categories`, which // does not exist). Note: no description/is_active/updated_* columns, and // display_order is NOT NULL — inserts must supply it. await knex.schema.createTable('categories', (table) => { table.uuid('tenant').notNullable(); table.uuid('category_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('category_name').notNullable(); table.uuid('parent_category'); table.uuid('channel_id').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'category_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels'); table.foreign(['tenant', 'parent_category']).references(['tenant', 'category_id']).inTable('categories'); }); await knex.schema.createTable('priorities', (table) => { table.uuid('tenant').notNullable(); table.uuid('priority_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('priority_name').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'priority_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('severities', (table) => { table.uuid('tenant').notNullable(); table.uuid('severity_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('severity_name').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'severity_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('urgencies', (table) => { table.uuid('tenant').notNullable(); table.uuid('urgency_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('urgency_name').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'urgency_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('impacts', (table) => { table.uuid('tenant').notNullable(); table.uuid('impact_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('impact_name').notNullable(); table.uuid('created_by').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'impact_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('next_number', (table) => { table.uuid('tenant').notNullable(); table.text('entity_type').notNullable(); table.bigInteger('last_number').notNullable().defaultTo(0); table.bigInteger('initial_value').notNullable().defaultTo(1000); table.text('prefix').defaultTo('TIC'); table.primary(['tenant', 'entity_type']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.raw('CREATE INDEX idx_next_number_tenant_entity ON next_number (tenant, entity_type)'); await knex.schema.createTable('attribute_definitions', (table) => { table.uuid('tenant').notNullable(); table.text('attribute_name').notNullable(); table.text('description'); table.text('type').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'attribute_name']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.raw(` CREATE OR REPLACE FUNCTION user_has_role(user_roles JSONB, role_name TEXT) RETURNS BOOLEAN AS $$ BEGIN RETURN EXISTS (SELECT 1 FROM jsonb_array_elements(user_roles) AS role WHERE role->>'role_name' = role_name); END; $$ LANGUAGE plpgsql; `); await knex.schema.raw(` CREATE OR REPLACE FUNCTION user_has_permission(user_roles JSONB, resource TEXT, action TEXT) RETURNS BOOLEAN AS $$ BEGIN RETURN EXISTS ( SELECT 1 FROM jsonb_array_elements(user_roles) AS role, jsonb_array_elements(role->'permissions') AS permission WHERE permission->>'resource' = resource AND permission->>'action' = action ); END; $$ LANGUAGE plpgsql; `); await knex.schema.createTable('tickets', (table) => { table.uuid('tenant').notNullable(); table.uuid('ticket_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('ticket_number').notNullable(); table.text('title'); table.text('url'); table.uuid('company_id'); table.uuid('contact_name_id'); table.uuid('status_id'); table.uuid('channel_id'); table.uuid('category_id'); table.uuid('subcategory_id'); table.uuid('priority_id'); table.uuid('severity_id'); table.uuid('urgency_id'); table.uuid('impact_id'); table.uuid('entered_by'); table.uuid('updated_by'); table.uuid('assigned_to'); table.uuid('closed_by'); table.timestamp('entered_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('closed_at', { useTz: true }); table.boolean('is_closed'); table.jsonb('attributes'); table.primary(['tenant', 'ticket_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); table.foreign(['tenant', 'contact_name_id']).references(['tenant', 'contact_name_id']).inTable('contacts'); table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels'); table.foreign(['tenant', 'status_id']).references(['tenant', 'status_id']).inTable('statuses'); table.foreign(['tenant', 'category_id']).references(['tenant', 'category_id']).inTable('categories'); table.foreign(['tenant', 'subcategory_id']).references(['tenant', 'category_id']).inTable('categories'); table.foreign(['tenant', 'priority_id']).references(['tenant', 'priority_id']).inTable('priorities'); table.foreign(['tenant', 'severity_id']).references(['tenant', 'severity_id']).inTable('severities'); table.foreign(['tenant', 'urgency_id']).references(['tenant', 'urgency_id']).inTable('urgencies'); table.foreign(['tenant', 'impact_id']).references(['tenant', 'impact_id']).inTable('impacts'); table.unique(['tenant', 'ticket_id', 'assigned_to']); table.unique(['tenant', 'ticket_number']); }); await knex.schema.createTable('comments', (table) => { table.uuid('tenant').notNullable(); table.uuid('comment_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('ticket_id').notNullable(); table.uuid('user_id'); table.uuid('contact_name_id'); table.text('note').notNullable(); table.boolean('is_internal').notNullable(); table.boolean('is_resolution').notNullable(); table.boolean('is_initial_description').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'comment_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'ticket_id']).references(['tenant', 'ticket_id']).inTable('tickets'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('schedules', (table) => { table.uuid('tenant').notNullable(); table.uuid('schedule_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('ticket_id').notNullable(); table.uuid('user_id'); table.uuid('contact_name_id'); table.uuid('company_id'); table.text('status').notNullable(); table.timestamp('scheduled_start', { useTz: true }); table.timestamp('scheduled_end', { useTz: true }); table.timestamp('actual_start', { useTz: true }); table.timestamp('actual_end', { useTz: true }); table.integer('duration_minutes'); table.text('description'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'schedule_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'ticket_id']).references(['tenant', 'ticket_id']).inTable('tickets'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'contact_name_id']).references(['tenant', 'contact_name_id']).inTable('contacts'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); }); await knex.schema.createTable('document_types', (table) => { table.uuid('tenant').notNullable(); table.uuid('type_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('type_name').notNullable(); table.text('icon'); table.primary(['tenant', 'type_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('documents', (table) => { table.uuid('tenant').notNullable(); table.uuid('document_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('document_name').notNullable(); table.uuid('type_id').notNullable(); table.uuid('user_id').notNullable(); table.uuid('contact_name_id'); table.uuid('company_id'); table.uuid('ticket_id'); table.uuid('schedule_id'); table.increments('order_number').notNullable(); table.uuid('created_by').notNullable(); table.uuid('edited_by'); table.timestamp('entered_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.text('content'); table.primary(['tenant', 'document_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'type_id']).references(['tenant', 'type_id']).inTable('document_types'); table.foreign(['tenant', 'created_by']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'contact_name_id']).references(['tenant', 'contact_name_id']).inTable('contacts'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); }); await knex.schema.createTable('tags', (table) => { table.uuid('tenant').notNullable(); table.uuid('tag_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('channel_id'); table.text('tag_text').notNullable(); table.uuid('tagged_id').notNullable(); table.text('tagged_type').notNullable(); table.primary(['tenant', 'tag_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels'); }); await knex.schema.createTable('interaction_types', (table) => { table.uuid('tenant').notNullable(); table.uuid('type_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('type_name').notNullable(); table.text('icon'); table.primary(['tenant', 'type_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('interactions', (table) => { table.uuid('tenant').notNullable(); table.uuid('interaction_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('type_id').notNullable(); table.uuid('contact_name_id'); table.uuid('company_id'); table.uuid('user_id').notNullable(); table.uuid('ticket_id'); table.text('description'); table.timestamp('interaction_date', { useTz: true }).defaultTo(knex.fn.now()); table.integer('duration'); table.primary(['tenant', 'interaction_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'type_id']).references(['tenant', 'type_id']).inTable('interaction_types'); table.foreign(['tenant', 'contact_name_id']).references(['tenant', 'contact_name_id']).inTable('contacts'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'ticket_id']).references(['tenant', 'ticket_id']).inTable('tickets'); }); await knex.schema.createTable('service_categories', (table) => { table.uuid('tenant').notNullable(); table.uuid('category_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('category_name').notNullable(); table.text('description'); table.primary(['tenant', 'category_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('service_catalog', (table) => { table.uuid('tenant').notNullable(); table.uuid('service_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('service_name').notNullable(); table.text('description'); table.text('service_type').notNullable(); table.bigInteger('default_rate'); table.text('unit_of_measure'); table.uuid('category_id'); table.primary(['tenant', 'service_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'category_id']).references(['tenant', 'category_id']).inTable('service_categories'); }); await knex.schema.createTable('billing_plans', (table) => { // ⚠️ RENAMED to 'contract_lines' (see 20251008000001_rename_billing_to_contracts.cjs) table.uuid('tenant').notNullable(); table.uuid('plan_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('plan_name').notNullable(); table.text('description'); table.text('billing_frequency').notNullable(); table.boolean('is_custom').defaultTo(false); table.text('plan_type').checkIn(['Fixed', 'Hourly', 'Usage', 'Bucket']); table.primary(['tenant', 'plan_id']); table.foreign('tenant').references('tenants.tenant'); }); // ⚠️ RENAMED: 'billing_plans' → 'contract_lines' (see 20251008000001_rename_billing_to_contracts.cjs) await knex.schema.createTable('bucket_plans', (table) => { table.uuid('tenant').notNullable(); table.uuid('bucket_plan_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('plan_id').notNullable(); table.integer('total_hours').notNullable(); table.text('billing_period').notNullable(); table.bigInteger('overage_rate').notNullable(); table.primary(['tenant', 'bucket_plan_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'plan_id']).references(['tenant', 'plan_id']).inTable('billing_plans'); }); await knex.schema.createTable('bucket_usage', (table) => { table.uuid('tenant').notNullable(); table.uuid('usage_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('bucket_plan_id').notNullable(); table.uuid('company_id').notNullable(); table.timestamp('period_start').notNullable(); table.timestamp('period_end').notNullable(); table.bigInteger('hours_used').notNullable(); table.bigInteger('overage_hours').notNullable(); table.primary(['tenant', 'usage_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'bucket_plan_id']).references(['tenant', 'bucket_plan_id']).inTable('bucket_plans'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); }); await knex.schema.createTable('plan_services', (table) => { table.uuid('tenant').notNullable(); table.uuid('plan_id').notNullable(); table.uuid('service_id').notNullable(); table.integer('quantity'); table.bigInteger('custom_rate'); table.primary(['tenant', 'plan_id', 'service_id']); table.foreign(['tenant', 'plan_id']).references(['tenant', 'plan_id']).inTable('billing_plans'); table.foreign(['tenant', 'service_id']).references(['tenant', 'service_id']).inTable('service_catalog'); }); await knex.schema.createTable('client_billing', (table) => { table.uuid('tenant').notNullable(); table.uuid('billing_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('company_id').notNullable(); table.uuid('plan_id').notNullable(); table.uuid('service_category'); table.boolean('is_active').defaultTo(true); table.timestamp('start_date').notNullable(); table.timestamp('end_date'); table.primary(['tenant', 'billing_id']); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); table.foreign(['tenant', 'plan_id']).references(['tenant', 'plan_id']).inTable('billing_plans'); table.foreign(['tenant', 'service_category']).references(['tenant', 'category_id']).inTable('service_categories'); }); await knex.schema.createTable('projects', (table) => { table.uuid('tenant').notNullable(); table.uuid('project_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('company_id').notNullable(); table.text('project_name').notNullable(); table.text('description'); table.timestamp('start_date'); table.timestamp('end_date'); table.uuid('status').notNullable(); table.text('wbs_code').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'project_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); table.unique(['tenant', 'wbs_code']); }); await knex.schema.createTable('time_period_types', (table) => { table.uuid('tenant').notNullable(); table.uuid('type_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('type_name').notNullable(); table.text('description'); table.primary(['tenant', 'type_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('time_periods', (table) => { table.uuid('tenant').notNullable(); table.uuid('period_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('type_id').notNullable(); table.timestamp('start_date', { useTz: true }).notNullable(); table.timestamp('end_date', { useTz: true }).notNullable(); table.boolean('is_closed').defaultTo(false); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'period_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'type_id']).references(['tenant', 'type_id']).inTable('time_period_types'); }); await knex.schema.createTable('tenant_time_period_settings', (table) => { table.uuid('tenant').notNullable(); table.uuid('type_id').notNullable(); table.integer('start_day'); table.integer('start_month'); table.integer('frequency'); table.text('frequency_unit').checkIn(['day', 'week', 'month', 'year']); table.boolean('is_active').defaultTo(true); table.timestamp('effective_from', { useTz: true }).notNullable(); table.timestamp('effective_to', { useTz: true }); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'type_id', 'effective_from']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'type_id']).references(['tenant', 'type_id']).inTable('time_period_types'); }); await knex.schema.raw(`CREATE TYPE ApprovalStatus AS ENUM ('DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED', 'CHANGES_REQUESTED')`); await knex.schema.createTable('time_sheets', (table) => { table.uuid('tenant').notNullable(); table.uuid('id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('user_id').notNullable(); table.uuid('period_id').notNullable(); table.specificType('approval_status', 'ApprovalStatus').defaultTo('DRAFT'); table.timestamp('submitted_at', { useTz: true }); table.timestamp('approved_at', { useTz: true }); table.uuid('approved_by'); table.primary(['tenant', 'id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'period_id']).references(['tenant', 'period_id']).inTable('time_periods'); table.foreign(['tenant', 'approved_by']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('time_entries', (table) => { table.uuid('tenant').notNullable(); table.uuid('entry_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('user_id').notNullable(); table.timestamp('start_time', { useTz: true }).notNullable(); table.timestamp('end_time', { useTz: true }).notNullable(); table.text('notes'); table.uuid('work_item_id'); table.integer('billable_duration'); table.text('work_item_type'); table.specificType('approval_status', 'ApprovalStatus').defaultTo('DRAFT'); table.uuid('time_sheet_id'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'entry_id']); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'time_sheet_id']).references(['tenant', 'id']).inTable('time_sheets'); }); await knex.schema.createTable('usage_tracking', (table) => { table.uuid('tenant').notNullable(); table.uuid('usage_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('company_id').notNullable(); table.uuid('service_id').notNullable(); table.timestamp('usage_date', { useTz: true }).notNullable(); table.bigInteger('quantity').notNullable(); table.primary(['tenant', 'usage_id']); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); table.foreign(['tenant', 'service_id']).references(['tenant', 'service_id']).inTable('service_catalog'); }); await knex.schema.createTable('invoices', (table) => { table.uuid('tenant').notNullable(); table.uuid('invoice_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('company_id').notNullable(); table.text('invoice_number').notNullable(); table.timestamp('invoice_date', { useTz: true }).notNullable(); table.timestamp('due_date', { useTz: true }).notNullable(); table.bigInteger('total_amount').notNullable(); table.text('status').notNullable(); table.uuid('template_id'); table.jsonb('custom_fields'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'invoice_id']); table.foreign(['tenant', 'company_id']).references(['tenant', 'company_id']).inTable('companies'); }); await knex.schema.createTable('invoice_items', (table) => { table.uuid('tenant').notNullable(); table.uuid('item_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('invoice_id').notNullable(); table.uuid('service_id').notNullable(); table.text('description').notNullable(); table.bigInteger('quantity').notNullable(); table.bigInteger('unit_price').notNullable(); table.bigInteger('total_price').notNullable(); table.primary(['tenant', 'item_id']); table.foreign(['tenant', 'invoice_id']).references(['tenant', 'invoice_id']).inTable('invoices'); table.foreign(['tenant', 'service_id']).references(['tenant', 'service_id']).inTable('service_catalog'); }); await knex.schema.createTable('project_phases', (table) => { table.uuid('tenant').notNullable(); table.uuid('phase_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('project_id').notNullable(); table.text('phase_name').notNullable(); table.text('description'); table.timestamp('start_date', { useTz: true }); table.timestamp('end_date', { useTz: true }); table.text('status').notNullable(); table.integer('order_number').notNullable(); table.text('wbs_code').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'phase_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'project_id']).references(['tenant', 'project_id']).inTable('projects'); table.unique(['tenant', 'project_id', 'wbs_code']); }); await knex.schema.createTable('project_tasks', (table) => { table.uuid('tenant').notNullable(); table.uuid('task_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('phase_id').notNullable(); table.text('task_name').notNullable(); table.text('description'); table.uuid('assigned_to'); table.bigInteger('estimated_hours'); table.timestamp('due_date', { useTz: true }); table.bigInteger('actual_hours'); table.uuid('status_id'); table.text('wbs_code').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'task_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'phase_id']).references(['tenant', 'phase_id']).inTable('project_phases'); table.foreign(['tenant', 'assigned_to']).references(['tenant', 'user_id']).inTable('users'); table.unique(['tenant', 'phase_id', 'wbs_code']); table.foreign(['tenant', 'status_id']).references(['tenant', 'status_id']).inTable('statuses'); }); await knex.schema.createTable('project_ticket_links', (table) => { table.uuid('tenant').notNullable(); table.uuid('link_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('project_id').notNullable(); table.uuid('phase_id'); table.uuid('task_id'); table.uuid('ticket_id').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'link_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'project_id']).references(['tenant', 'project_id']).inTable('projects'); table.foreign(['tenant', 'phase_id']).references(['tenant', 'phase_id']).inTable('project_phases'); table.foreign(['tenant', 'task_id']).references(['tenant', 'task_id']).inTable('project_tasks'); table.foreign(['tenant', 'ticket_id']).references(['tenant', 'ticket_id']).inTable('tickets'); }); await knex.schema.createTable('schedule_entries', (table) => { table.uuid('tenant').notNullable(); table.uuid('entry_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('title').notNullable(); table.uuid('work_item_id').notNullable(); table.uuid('user_id').notNullable(); table.timestamp('scheduled_start', { useTz: true }).notNullable(); table.timestamp('scheduled_end', { useTz: true }).notNullable(); table.text('status').notNullable(); table.text('notes'); table.jsonb('recurrence_pattern'); table.text('work_item_type').notNullable().checkIn(['project_task', 'ticket']); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'entry_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('resources', (table) => { table.uuid('tenant').notNullable(); table.uuid('resource_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('user_id').notNullable(); table.jsonb('availability'); table.specificType('skills', 'TEXT[]'); table.integer('max_daily_capacity'); table.integer('max_weekly_capacity'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'resource_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('schedule_conflicts', (table) => { table.uuid('tenant').notNullable(); table.uuid('conflict_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('entry_id_1').notNullable(); table.uuid('entry_id_2').notNullable(); table.text('conflict_type').notNullable(); table.boolean('resolved').defaultTo(false); table.text('resolution_notes'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'conflict_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'entry_id_1']).references(['tenant', 'entry_id']).inTable('schedule_entries'); table.foreign(['tenant', 'entry_id_2']).references(['tenant', 'entry_id']).inTable('schedule_entries'); }); await knex.schema.createTable('teams', (table) => { table.uuid('tenant').notNullable(); table.uuid('team_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('team_name').notNullable(); table.uuid('manager_id').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'team_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'manager_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('team_members', (table) => { table.uuid('tenant').notNullable(); table.uuid('team_id').notNullable(); table.uuid('user_id').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'team_id', 'user_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'team_id']).references(['tenant', 'team_id']).inTable('teams'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('approval_levels', (table) => { table.uuid('tenant').notNullable(); table.uuid('id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('name').notNullable(); table.integer('order_num').notNullable(); table.primary(['tenant', 'id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('approval_thresholds', (table) => { table.uuid('tenant').notNullable(); table.uuid('id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('type').checkIn(['OVERTIME', 'HIGH_VALUE']); table.bigInteger('threshold').notNullable(); table.uuid('approval_level_id').notNullable(); table.primary(['tenant', 'id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'approval_level_id']).references(['tenant', 'id']).inTable('approval_levels'); }); await knex.raw(` CREATE OR REPLACE FUNCTION generate_next_number(p_tenant_id UUID, p_entity_type TEXT) RETURNS TEXT AS $$ DECLARE new_number BIGINT; number_prefix TEXT; initial_val BIGINT; formatted_number TEXT; BEGIN PERFORM pg_advisory_xact_lock(hashtext(p_tenant_id::text || p_entity_type)); INSERT INTO next_number (tenant, entity_type) VALUES (p_tenant_id, p_entity_type) ON CONFLICT (tenant, entity_type) DO UPDATE SET last_number = CASE WHEN next_number.last_number = 0 THEN next_number.initial_value ELSE next_number.last_number + 1 END RETURNING last_number, initial_value, prefix INTO new_number, initial_val, number_prefix; IF new_number = initial_val AND new_number != 1 THEN ELSE UPDATE next_number SET last_number = new_number WHERE tenant = p_tenant_id AND entity_type = p_entity_type; END IF; IF number_prefix IS NOT NULL THEN formatted_number := number_prefix || new_number::TEXT; ELSE formatted_number := new_number::TEXT; END IF; RETURN formatted_number; END; $$ LANGUAGE plpgsql; `); await knex.raw(` CREATE OR REPLACE FUNCTION set_ticket_number() RETURNS TRIGGER AS $$ BEGIN IF NEW.ticket_number IS NULL OR NEW.ticket_number = '' THEN NEW.ticket_number := generate_next_number(NEW.tenant, 'TICKET'); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; `); await knex.raw(` CREATE TRIGGER trigger_set_ticket_number BEFORE INSERT ON tickets FOR EACH ROW EXECUTE FUNCTION set_ticket_number(); `); await knex.raw(` CREATE OR REPLACE FUNCTION get_current_tenant_id() RETURNS UUID AS $$ DECLARE tenant_id UUID; BEGIN BEGIN tenant_id := current_setting('app.current_tenant')::uuid; EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'Invalid tenant ID'; END; RETURN tenant_id; END; $$ LANGUAGE plpgsql SECURITY DEFINER; `); await knex.schema.createTable('roles', (table) => { table.uuid('tenant').notNullable(); table.uuid('role_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('role_name').notNullable(); table.text('description'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'role_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('user_roles', (table) => { table.uuid('tenant').notNullable(); table.uuid('user_id').notNullable(); table.uuid('role_id').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'user_id', 'role_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'role_id']).references(['tenant', 'role_id']).inTable('roles'); }); await knex.schema.createTable('permissions', (table) => { table.uuid('tenant').notNullable(); table.uuid('permission_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('resource').notNullable(); table.text('action').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'permission_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('role_permissions', (table) => { table.uuid('tenant').notNullable(); table.uuid('role_id').notNullable(); table.uuid('permission_id').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'role_id', 'permission_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'role_id']).references(['tenant', 'role_id']).inTable('roles'); table.foreign(['tenant', 'permission_id']).references(['tenant', 'permission_id']).inTable('permissions'); }); await knex.schema.createTable('policies', (table) => { table.uuid('tenant').notNullable(); table.uuid('policy_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('policy_name').notNullable(); table.text('resource').notNullable(); table.text('action').notNullable(); table.jsonb('conditions').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'policy_id']); table.foreign('tenant').references('tenants.tenant'); }); await knex.schema.createTable('ticket_resources', (table) => { table.uuid('tenant').notNullable(); table.uuid('assignment_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('ticket_id').notNullable(); table.uuid('assigned_to').notNullable(); table.uuid('additional_user_id'); table.text('role'); table.timestamp('assigned_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'assignment_id']); table.foreign('tenant').references('tenants.tenant'); table.foreign(['tenant', 'ticket_id', 'assigned_to']).references(['tenant', 'ticket_id', 'assigned_to']).inTable('tickets'); table.foreign(['tenant', 'assigned_to']).references(['tenant', 'user_id']).inTable('users'); table.foreign(['tenant', 'additional_user_id']).references(['tenant', 'user_id']).inTable('users'); table.check('assigned_to != additional_user_id'); }); await knex.schema.createTable('invoice_templates', (table) => { table.uuid('tenant').notNullable(); table.uuid('template_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('name').notNullable(); table.integer('version').notNullable(); table.text('dsl').notNullable(); table.boolean('is_default').defaultTo(false); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'template_id']); }); await knex.schema.createTable('template_sections', (table) => { table.uuid('tenant').notNullable(); table.uuid('section_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('template_id').notNullable(); table.text('section_type').notNullable().checkIn(['header', 'list', 'footer']); table.text('name').notNullable(); table.integer('grid_rows').notNullable(); table.integer('grid_columns').notNullable(); table.integer('order_number').notNullable(); table.primary(['tenant', 'section_id']); table.foreign(['tenant', 'template_id']).references(['tenant', 'template_id']).inTable('invoice_templates'); }); await knex.schema.createTable('layout_blocks', (table) => { table.uuid('tenant').notNullable(); table.uuid('block_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('section_id').notNullable(); table.text('type').notNullable(); table.text('content'); table.integer('grid_column').notNullable(); table.integer('grid_row').notNullable(); table.integer('grid_column_span').notNullable(); table.integer('grid_row_span').notNullable(); table.jsonb('styles'); table.primary(['tenant', 'block_id']); table.foreign(['tenant', 'section_id']).references(['tenant', 'section_id']).inTable('template_sections'); }); await knex.schema.createTable('custom_fields', (table) => { table.uuid('tenant').notNullable(); table.uuid('field_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('name').notNullable(); table.text('type').notNullable(); table.jsonb('default_value'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'field_id']); }); await knex.schema.createTable('invoice_annotations', (table) => { table.uuid('tenant').notNullable(); table.uuid('annotation_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('invoice_id').notNullable(); table.uuid('user_id').notNullable(); table.text('content').notNullable(); table.boolean('is_internal').notNullable(); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'annotation_id']); table.foreign(['tenant', 'invoice_id']).references(['tenant', 'invoice_id']).inTable('invoices'); table.foreign(['tenant', 'user_id']).references(['tenant', 'user_id']).inTable('users'); }); await knex.schema.createTable('conditional_display_rules', (table) => { table.uuid('tenant').notNullable(); table.uuid('rule_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('template_id').notNullable(); table.jsonb('condition').notNullable(); table.text('action').notNullable(); table.text('target').notNullable(); table.jsonb('format'); table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now()); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now()); table.primary(['tenant', 'rule_id']); table.foreign(['tenant', 'template_id']).references(['tenant', 'template_id']).inTable('invoice_templates'); }); await knex.schema.raw('CREATE INDEX idx_ticket_resources_ticket_user ON ticket_resources (tenant, ticket_id, additional_user_id)'); const dbUserServer = process.env.DB_USER_SERVER; const envDbNameServer = process.env.DB_NAME_SERVER; if (dbUserServer) { const currentDatabaseResult = await knex.raw('SELECT current_database()'); const currentDatabaseRow = Array.isArray(currentDatabaseResult.rows) ? currentDatabaseResult.rows[0] : currentDatabaseResult[0]; const currentDatabaseName = currentDatabaseRow?.current_database; if (!currentDatabaseName) { throw new Error('Failed to resolve current database name when applying permissions'); } let targetDbName = envDbNameServer && envDbNameServer !== 'default_db_name' ? envDbNameServer : currentDatabaseName; try { const databaseCheck = await knex.raw('SELECT 1 FROM pg_database WHERE datname = ?', [targetDbName]); const hasDatabase = Array.isArray(databaseCheck.rows) ? databaseCheck.rows.length > 0 : Array.isArray(databaseCheck) && databaseCheck.length > 0; if (!hasDatabase) { targetDbName = currentDatabaseName; } } catch (error) { targetDbName = currentDatabaseName; } const escapedUser = dbUserServer.replace(/"/g, '""'); const escapedDbName = targetDbName.replace(/"/g, '""'); await knex.schema.raw(` GRANT USAGE ON SCHEMA public TO "${escapedUser}"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "${escapedUser}"; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO "${escapedUser}"; GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO "${escapedUser}"; GRANT ALL PRIVILEGES ON DATABASE "${escapedDbName}" TO "${escapedUser}"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO "${escapedUser}"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO "${escapedUser}"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO "${escapedUser}"; `); } } exports.down = async function(knex) { throw new Error('Cannot rollback the initial migration.'); };