/** * Migration 1: Company → Client Rename - Base Migration * * This migration creates new client-related tables and adds client_id columns * to all dependent tables while keeping the old company tables intact. * * Two-step approach: * 1. Create new tables/columns (this migration) * 2. Drop old tables/columns (cleanup migration after app cutover) */ exports.config = { transaction: false }; exports.up = async function(knex) { console.log('Starting company to client migration...'); const createdTables = []; const addedColumns = []; try { // Step 0: Rename tenants.company_name → tenants.client_name (if not already renamed) const tenantHasCompanyName = await knex.schema.hasColumn('tenants', 'company_name'); const tenantHasClientName = await knex.schema.hasColumn('tenants', 'client_name'); if (tenantHasCompanyName && !tenantHasClientName) { console.log('Renaming tenants.company_name → tenants.client_name...'); await knex.raw('ALTER TABLE tenants RENAME COLUMN company_name TO client_name'); console.log('✓ tenants table column renamed to client_name'); } else if (!tenantHasCompanyName && tenantHasClientName) { console.log('tenants table already uses client_name column, skipping rename...'); } else if (!tenantHasCompanyName && !tenantHasClientName) { throw new Error('❌ tenants table missing both company_name and client_name columns. Cannot proceed.'); } else { console.log('Both company_name and client_name columns exist on tenants table. Please resolve manually.'); throw new Error('❌ Unexpected tenants table state - both company_name and client_name present'); } // Step 1: Create clients table const clientsExists = await knex.schema.hasTable('clients'); if (clientsExists) { console.log('clients table already exists, skipping creation...'); } else { console.log('Creating clients table...'); createdTables.push('clients'); // Clone structure from companies table with ALL columns await knex.schema.createTable('clients', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.text('client_name').notNullable(); table.text('url'); table.jsonb('properties'); table.text('billing_type'); table.text('payment_terms'); table.bigInteger('credit_limit'); table.text('preferred_payment_method'); table.boolean('auto_invoice').defaultTo(false); table.text('invoice_delivery_method'); table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); table.boolean('is_inactive').defaultTo(false); table.text('client_type'); table.boolean('is_tax_exempt').notNullable().defaultTo(false); table.string('tax_exemption_certificate', 255); table.string('tax_id_number', 255); table.text('notes'); table.integer('credit_balance'); table.text('billing_cycle').notNullable().defaultTo('monthly'); table.string('timezone', 255); table.uuid('notes_document_id'); table.uuid('invoice_template_id'); table.uuid('billing_contact_id'); table.string('billing_email', 255); table.string('region_code', 255); table.uuid('account_manager_id'); table.primary(['tenant', 'client_id']); }); // Add constraints // Citus cannot distribute a table with a unique constraint that // excludes the partition column, and nothing references client_id // alone (all FKs are composite on tenant). Skip it on Citus so the // clients table stays distributable. const citusForUnique = await knex.raw( "SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'citus') AS enabled" ); if (!citusForUnique.rows[0].enabled) await knex.raw(` ALTER TABLE clients ADD CONSTRAINT clients_client_id_unique UNIQUE (client_id) `); await knex.raw(` ALTER TABLE clients ADD CONSTRAINT clients_tenant_client_name_unique UNIQUE (tenant, client_name) `); // Add CHECK constraint for billing_cycle await knex.raw(` ALTER TABLE clients ADD CONSTRAINT clients_billing_cycle_check CHECK (billing_cycle = ANY (ARRAY['weekly'::text, 'bi-weekly'::text, 'monthly'::text, 'quarterly'::text, 'semi-annually'::text, 'annually'::text])) `); // Add indexes matching companies table await knex.raw('CREATE INDEX idx_clients_tenant_client_name ON clients(tenant, client_name)'); await knex.raw('CREATE INDEX idx_clients_tenant_inactive_name ON clients(tenant, is_inactive, client_name)'); await knex.raw('CREATE INDEX idx_clients_tenant_client_type ON clients(tenant, client_type)'); await knex.raw('CREATE INDEX idx_clients_tenant_url ON clients(tenant, url)'); await knex.raw('CREATE INDEX idx_clients_tenant_region_code ON clients(tenant, region_code)'); await knex.raw('CREATE INDEX idx_clients_tenant_account_manager ON clients(tenant, account_manager_id)'); // Add foreign keys (will be validated after backfill) // Skip FK in Citus - it will be added by Citus migration after distribution const citusEnabled = await knex.raw(` SELECT EXISTS ( SELECT 1 FROM pg_extension WHERE extname = 'citus' ) as enabled `); if (!citusEnabled.rows[0].enabled) { await knex.raw(` ALTER TABLE clients ADD CONSTRAINT clients_tenant_foreign FOREIGN KEY (tenant) REFERENCES tenants(tenant) NOT VALID `); console.log('✓ Added FK to tenants (non-Citus)'); } else { console.log('⊘ Skipped FK to tenants (Citus - will be added after distribution)'); } console.log('✓ clients table created'); } // Step 2: Backfill clients from companies with ALL columns console.log('Backfilling clients from companies...'); await knex.raw(` INSERT INTO clients ( tenant, client_id, client_name, url, properties, billing_type, payment_terms, credit_limit, preferred_payment_method, auto_invoice, invoice_delivery_method, created_at, updated_at, is_inactive, client_type, is_tax_exempt, tax_exemption_certificate, tax_id_number, notes, credit_balance, billing_cycle, timezone, notes_document_id, invoice_template_id, billing_contact_id, billing_email, region_code, account_manager_id ) SELECT tenant, company_id, company_name, url, properties, billing_type, payment_terms, credit_limit, preferred_payment_method, auto_invoice, invoice_delivery_method, created_at, updated_at, is_inactive, client_type, is_tax_exempt, tax_exemption_certificate, tax_id_number, notes, credit_balance, billing_cycle, timezone, notes_document_id, invoice_template_id, billing_contact_id, billing_email, region_code, account_manager_id FROM companies ON CONFLICT (tenant, client_id) DO NOTHING `); const count = await knex('clients').count('* as count'); console.log(`✓ Backfilled ${count[0].count} clients`); // Step 3: Create other renamed tables await createClientLocations(knex, createdTables); await createClientBillingCycles(knex, createdTables); await createClientBillingSettings(knex, createdTables); await createClientTaxSettings(knex, createdTables); await createClientTaxRates(knex, createdTables); await createClientBillingPlans(knex, createdTables); await createClientPlanBundles(knex, createdTables); // Step 4: Add client_id columns to dependent tables await addClientIdColumns(knex, addedColumns); // Step 5: Backfill client_id from company_id await backfillClientIds(knex); // Step 6: Verify row counts await verifyRowCounts(knex); console.log('✓ Company to client migration completed successfully'); } catch (error) { console.error('❌ Migration failed:', error.message); console.log('Rolling back changes...'); try { // Drop created tables in reverse order for (const table of createdTables.reverse()) { console.log(` Dropping table: ${table}`); await knex.schema.dropTableIfExists(table); } // Drop added columns for (const {table, column} of addedColumns.reverse()) { console.log(` Dropping column: ${table}.${column}`); const tableExists = await knex.schema.hasTable(table); if (tableExists) { const hasColumn = await knex.schema.hasColumn(table, column); if (hasColumn) { await knex.schema.table(table, (t) => { t.dropColumn(column); }); } } } console.log('✓ Rollback completed'); } catch (rollbackError) { console.error('❌ Rollback failed:', rollbackError.message); console.error('Manual cleanup may be required!'); } throw error; // Re-throw to mark migration as failed } }; async function createClientLocations(knex, createdTables) { const exists = await knex.schema.hasTable('client_locations'); if (!exists) { console.log('Creating client_locations...'); createdTables.push('client_locations'); await knex.schema.createTable('client_locations', (table) => { table.uuid('location_id').notNullable(); table.uuid('tenant').notNullable(); table.uuid('client_id').notNullable(); table.string('location_name'); table.string('address_line1').notNullable(); table.string('address_line2'); table.string('address_line3'); table.string('city').notNullable(); table.string('state_province'); table.string('postal_code'); table.string('country_code').notNullable(); table.string('country_name').notNullable(); table.string('region_code'); table.boolean('is_billing_address').defaultTo(false); table.boolean('is_shipping_address').defaultTo(false); table.boolean('is_default').defaultTo(false); table.string('phone'); table.string('fax'); table.string('email'); table.text('notes'); table.boolean('is_active').defaultTo(true); table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); table.primary(['location_id', 'tenant']); }); // Add indexes await knex.raw('CREATE INDEX idx_client_locations_tenant_client_id ON client_locations(tenant, client_id)'); await knex.raw('CREATE INDEX idx_client_locations_tenant_is_default ON client_locations(tenant, is_default)'); await knex.raw('CREATE INDEX idx_client_locations_tenant_is_active ON client_locations(tenant, is_active)'); await knex.raw('CREATE INDEX idx_client_locations_default_phone ON client_locations(tenant, client_id, is_default, phone)'); await knex.raw('CREATE INDEX idx_client_locations_default_address ON client_locations(tenant, client_id, is_default, address_line1)'); } else { console.log('client_locations already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_locations from company_locations...'); await knex.raw(` INSERT INTO client_locations ( location_id, tenant, client_id, location_name, address_line1, address_line2, address_line3, city, state_province, postal_code, country_code, country_name, region_code, is_billing_address, is_shipping_address, is_default, phone, fax, email, notes, is_active, created_at, updated_at ) SELECT location_id, tenant, company_id, location_name, address_line1, address_line2, address_line3, city, state_province, postal_code, country_code, country_name, region_code, is_billing_address, is_shipping_address, is_default, phone, fax, email, notes, is_active, created_at, updated_at FROM company_locations ON CONFLICT (location_id, tenant) DO NOTHING `); const count = await knex('client_locations').count('* as count'); console.log(`✓ client_locations has ${count[0].count} rows`); } async function createClientBillingCycles(knex, createdTables) { const exists = await knex.schema.hasTable('client_billing_cycles'); if (!exists) { console.log('Creating client_billing_cycles...'); createdTables.push('client_billing_cycles'); await knex.schema.createTable('client_billing_cycles', (table) => { table.uuid('tenant').notNullable(); table.uuid('billing_cycle_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('client_id').notNullable(); table.string('billing_cycle').notNullable().defaultTo('monthly'); table.timestamp('effective_date').notNullable().defaultTo(knex.fn.now()); table.timestamp('period_start_date').notNullable().defaultTo(knex.fn.now()); table.timestamp('period_end_date'); table.boolean('is_active').notNullable().defaultTo(true); table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); table.primary(['tenant', 'billing_cycle_id']); }); } else { console.log('client_billing_cycles already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_billing_cycles from company_billing_cycles...'); await knex.raw(` INSERT INTO client_billing_cycles ( tenant, billing_cycle_id, client_id, billing_cycle, effective_date, period_start_date, period_end_date, is_active, created_at, updated_at ) SELECT tenant, billing_cycle_id, company_id, billing_cycle, effective_date, period_start_date, period_end_date, is_active, created_at, updated_at FROM company_billing_cycles ON CONFLICT (tenant, billing_cycle_id) DO NOTHING `); const count = await knex('client_billing_cycles').count('* as count'); console.log(`✓ client_billing_cycles has ${count[0].count} rows`); } async function createClientBillingSettings(knex, createdTables) { const exists = await knex.schema.hasTable('client_billing_settings'); if (!exists) { console.log('Creating client_billing_settings...'); createdTables.push('client_billing_settings'); await knex.schema.createTable('client_billing_settings', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_id').notNullable(); table.text('zero_dollar_invoice_handling').notNullable(); table.boolean('suppress_zero_dollar_invoices').notNullable(); table.integer('credit_expiration_days'); table.specificType('credit_expiration_notification_days', 'integer[]'); table.boolean('enable_credit_expiration'); table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()); table.primary(['tenant', 'client_id']); }); } else { console.log('client_billing_settings already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_billing_settings from company_billing_settings...'); await knex.raw(` INSERT INTO client_billing_settings ( tenant, client_id, zero_dollar_invoice_handling, suppress_zero_dollar_invoices, credit_expiration_days, credit_expiration_notification_days, enable_credit_expiration, created_at, updated_at ) SELECT tenant, company_id, zero_dollar_invoice_handling, suppress_zero_dollar_invoices, credit_expiration_days, credit_expiration_notification_days, enable_credit_expiration, created_at, updated_at FROM company_billing_settings ON CONFLICT (tenant, client_id) DO NOTHING `); const count = await knex('client_billing_settings').count('* as count'); console.log(`✓ client_billing_settings has ${count[0].count} rows`); } async function createClientTaxSettings(knex, createdTables) { const exists = await knex.schema.hasTable('client_tax_settings'); if (!exists) { console.log('Creating client_tax_settings...'); createdTables.push('client_tax_settings'); await knex.schema.createTable('client_tax_settings', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_id').notNullable(); table.boolean('is_reverse_charge_applicable').defaultTo(false); table.primary(['tenant', 'client_id']); }); } else { console.log('client_tax_settings already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_tax_settings from company_tax_settings...'); await knex.raw(` INSERT INTO client_tax_settings ( tenant, client_id, is_reverse_charge_applicable ) SELECT tenant, company_id, is_reverse_charge_applicable FROM company_tax_settings ON CONFLICT (tenant, client_id) DO NOTHING `); const count = await knex('client_tax_settings').count('* as count'); console.log(`✓ client_tax_settings has ${count[0].count} rows`); } async function createClientTaxRates(knex, createdTables) { const exists = await knex.schema.hasTable('client_tax_rates'); if (!exists) { console.log('Creating client_tax_rates...'); createdTables.push('client_tax_rates'); await knex.schema.createTable('client_tax_rates', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_tax_rates_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('client_id').notNullable(); table.uuid('tax_rate_id').notNullable(); table.uuid('location_id'); table.boolean('is_default').notNullable().defaultTo(false); table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()); table.primary(['client_tax_rates_id', 'tenant']); }); } else { console.log('client_tax_rates already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_tax_rates from company_tax_rates...'); await knex.raw(` INSERT INTO client_tax_rates ( tenant, client_tax_rates_id, client_id, tax_rate_id, location_id, is_default, created_at, updated_at ) SELECT tenant, company_tax_rates_id, company_id, tax_rate_id, location_id, is_default, created_at, updated_at FROM company_tax_rates ON CONFLICT (client_tax_rates_id, tenant) DO NOTHING `); const count = await knex('client_tax_rates').count('* as count'); console.log(`✓ client_tax_rates has ${count[0].count} rows`); } async function createClientBillingPlans(knex, createdTables) { const exists = await knex.schema.hasTable('client_billing_plans'); if (!exists) { console.log('Creating client_billing_plans...'); createdTables.push('client_billing_plans'); await knex.schema.createTable('client_billing_plans', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_billing_plan_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('client_id').notNullable(); table.uuid('plan_id').notNullable(); table.uuid('service_category'); table.boolean('is_active').defaultTo(true); table.timestamp('start_date', { useTz: true }).notNullable(); table.timestamp('end_date', { useTz: true }); table.uuid('client_bundle_id'); table.primary(['tenant', 'client_billing_plan_id']); }); // Add indexes await knex.raw('CREATE INDEX idx_client_billing_plans_tenant ON client_billing_plans(tenant)'); await knex.raw('CREATE INDEX idx_client_billing_plans_tenant_client_id ON client_billing_plans(tenant, client_id)'); await knex.raw('CREATE INDEX idx_client_billing_plans_plan_id ON client_billing_plans(tenant, plan_id)'); await knex.raw('CREATE INDEX client_billing_plans_client_bundle_id_index ON client_billing_plans(client_bundle_id)'); } else { console.log('client_billing_plans already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_billing_plans from company_billing_plans...'); await knex.raw(` INSERT INTO client_billing_plans ( tenant, client_billing_plan_id, client_id, plan_id, service_category, is_active, start_date, end_date, client_bundle_id ) SELECT tenant, company_billing_plan_id, company_id, plan_id, service_category, is_active, start_date, end_date, company_bundle_id FROM company_billing_plans ON CONFLICT (tenant, client_billing_plan_id) DO NOTHING `); const count = await knex('client_billing_plans').count('* as count'); console.log(`✓ client_billing_plans has ${count[0].count} rows`); } async function createClientPlanBundles(knex, createdTables) { const exists = await knex.schema.hasTable('client_plan_bundles'); if (!exists) { console.log('Creating client_plan_bundles...'); createdTables.push('client_plan_bundles'); await knex.schema.createTable('client_plan_bundles', (table) => { table.uuid('tenant').notNullable(); table.uuid('client_bundle_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable(); table.uuid('client_id').notNullable(); table.uuid('bundle_id').notNullable(); table.timestamp('start_date', { useTz: true }).notNullable(); table.timestamp('end_date', { useTz: true }); table.boolean('is_active').defaultTo(true); table.timestamp('created_at', { useTz: true }).defaultTo(knex.raw('CURRENT_TIMESTAMP')); table.timestamp('updated_at', { useTz: true }).defaultTo(knex.raw('CURRENT_TIMESTAMP')); table.primary(['tenant', 'client_bundle_id']); }); // Add unique constraint await knex.raw(` ALTER TABLE client_plan_bundles ADD CONSTRAINT client_plan_bundles_tenant_client_bundle_id_unique UNIQUE (tenant, client_bundle_id) `); // Add indexes await knex.raw('CREATE INDEX client_plan_bundles_tenant_index ON client_plan_bundles(tenant)'); await knex.raw('CREATE INDEX client_plan_bundles_client_id_index ON client_plan_bundles(client_id)'); await knex.raw('CREATE INDEX client_plan_bundles_bundle_id_index ON client_plan_bundles(bundle_id)'); } else { console.log('client_plan_bundles already exists, skipping creation...'); } // Backfill (always run, idempotent) console.log('Backfilling client_plan_bundles from company_plan_bundles...'); await knex.raw(` INSERT INTO client_plan_bundles ( tenant, client_bundle_id, client_id, bundle_id, start_date, end_date, is_active, created_at, updated_at ) SELECT tenant, company_bundle_id, company_id, bundle_id, start_date, end_date, is_active, created_at, updated_at FROM company_plan_bundles ON CONFLICT (tenant, client_bundle_id) DO NOTHING `); const count = await knex('client_plan_bundles').count('* as count'); console.log(`✓ client_plan_bundles has ${count[0].count} rows`); } async function addClientIdColumns(knex, addedColumns) { console.log('Adding client_id columns to dependent tables...'); const tables = [ 'assets', 'bucket_usage', 'company_billing_plans', 'company_plan_bundles', 'contacts', 'credit_reconciliation_reports', 'credit_tracking', 'inbound_ticket_defaults', 'interactions', 'invoices', 'payment_methods', 'plan_discounts', 'projects', 'tenant_companies', 'tickets', 'transactions', 'usage_tracking' ]; for (const table of tables) { const tableExists = await knex.schema.hasTable(table); if (!tableExists) { console.log(` ⊘ Table ${table} does not exist, skipping...`); continue; } const hasColumn = await knex.schema.hasColumn(table, 'client_id'); if (!hasColumn) { console.log(` Adding client_id to ${table}...`); await knex.schema.table(table, (t) => { t.uuid('client_id'); }); addedColumns.push({ table, column: 'client_id' }); } else { console.log(` ✓ ${table}.client_id already exists`); } } console.log('✓ Finished adding client_id columns'); } async function backfillClientIds(knex) { console.log('Backfilling client_id columns...'); const tables = [ 'assets', 'bucket_usage', 'company_billing_plans', 'company_plan_bundles', 'contacts', 'credit_reconciliation_reports', 'credit_tracking', 'inbound_ticket_defaults', 'interactions', 'invoices', 'payment_methods', 'plan_discounts', 'projects', 'tenant_companies', 'tickets', 'transactions', 'usage_tracking' ]; for (const table of tables) { const tableExists = await knex.schema.hasTable(table); if (!tableExists) { console.log(` ⊘ Table ${table} does not exist, skipping...`); continue; } const hasCompanyId = await knex.schema.hasColumn(table, 'company_id'); if (!hasCompanyId) { console.log(` ⊘ ${table} has no company_id column, skipping...`); continue; } console.log(` Backfilling ${table}.client_id...`); const result = await knex.raw(` UPDATE ${table} SET client_id = company_id WHERE client_id IS NULL AND company_id IS NOT NULL `); console.log(` ✓ Updated ${result.rowCount || 0} rows`); } console.log('✓ Finished backfilling client_id columns'); } async function verifyRowCounts(knex) { console.log('\nVerifying row counts...'); // Verify main table const companiesCount = await knex('companies').count('* as count'); const clientsCount = await knex('clients').count('* as count'); if (companiesCount[0].count !== clientsCount[0].count) { throw new Error(`Row count mismatch: companies=${companiesCount[0].count}, clients=${clientsCount[0].count}`); } console.log(` ✓ clients: ${clientsCount[0].count} rows match companies`); // Verify related tables const relatedTables = [ { old: 'company_locations', new: 'client_locations' }, { old: 'company_billing_cycles', new: 'client_billing_cycles' }, { old: 'company_billing_settings', new: 'client_billing_settings' }, { old: 'company_tax_settings', new: 'client_tax_settings' }, { old: 'company_tax_rates', new: 'client_tax_rates' }, { old: 'company_billing_plans', new: 'client_billing_plans' }, { old: 'company_plan_bundles', new: 'client_plan_bundles' } ]; for (const { old: oldTable, new: newTable } of relatedTables) { const oldTableExists = await knex.schema.hasTable(oldTable); const newTableExists = await knex.schema.hasTable(newTable); if (!oldTableExists || !newTableExists) { console.log(` ⊘ Skipping ${oldTable} → ${newTable} (table missing)`); continue; } const oldCount = await knex(oldTable).count('* as count'); const newCount = await knex(newTable).count('* as count'); if (oldCount[0].count !== newCount[0].count) { throw new Error(`Row count mismatch: ${oldTable}=${oldCount[0].count}, ${newTable}=${newCount[0].count}`); } console.log(` ✓ ${newTable}: ${newCount[0].count} rows match ${oldTable}`); } console.log('\n✓ All row count verifications passed'); } exports.down = async function(knex) { console.log('Rolling back company to client migration...'); // Restore tenants.client_name → tenants.company_name if needed const tenantHasClientName = await knex.schema.hasColumn('tenants', 'client_name'); const tenantHasCompanyName = await knex.schema.hasColumn('tenants', 'company_name'); if (tenantHasClientName && !tenantHasCompanyName) { console.log('Renaming tenants.client_name back to tenants.company_name...'); await knex.raw('ALTER TABLE tenants RENAME COLUMN client_name TO company_name'); console.log('✓ tenants table column restored to company_name'); } // Drop new tables await knex.schema.dropTableIfExists('client_plan_bundles'); await knex.schema.dropTableIfExists('client_billing_plans'); await knex.schema.dropTableIfExists('client_tax_rates'); await knex.schema.dropTableIfExists('client_tax_settings'); await knex.schema.dropTableIfExists('client_billing_settings'); await knex.schema.dropTableIfExists('client_billing_cycles'); await knex.schema.dropTableIfExists('client_locations'); await knex.schema.dropTableIfExists('clients'); // Drop client_id columns const tables = [ 'assets', 'bucket_usage', 'company_billing_plans', 'company_plan_bundles', 'contacts', 'credit_reconciliation_reports', 'credit_tracking', 'inbound_ticket_defaults', 'interactions', 'invoices', 'payment_methods', 'plan_discounts', 'projects', 'tenant_companies', 'tickets', 'transactions', 'usage_tracking' ]; for (const table of tables) { const tableExists = await knex.schema.hasTable(table); if (!tableExists) continue; const hasColumn = await knex.schema.hasColumn(table, 'client_id'); if (hasColumn) { await knex.schema.table(table, (t) => { t.dropColumn('client_id'); }); } } console.log('✓ Rollback completed'); };