Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1122 lines
42 KiB
JavaScript
1122 lines
42 KiB
JavaScript
/**
|
||
* Migration 1 Cleanup: Company to Client - Final Cleanup
|
||
*
|
||
* IMPORTANT: Run this ONLY after:
|
||
* - All application code uses client_id instead of company_id
|
||
* - Feature flags fully rolled out
|
||
* - Production verification passes
|
||
* - Dual-write period is complete (1-2 weeks minimum)
|
||
* - All dependent systems updated
|
||
*
|
||
* This migration:
|
||
* 1. Drops company_id columns from all dependent tables
|
||
* 2. Drops old company_* tables (companies, company_locations, etc.)
|
||
* 3. Makes client_id columns NOT NULL where appropriate
|
||
* 4. Adds proper foreign key constraints to new tables
|
||
* 5. Includes verification steps before dropping anything
|
||
*
|
||
* RISK LEVEL: HIGH - This is a one-way migration affecting core entities
|
||
*
|
||
* @param { import("knex").Knex } knex
|
||
* @returns { Promise<void> }
|
||
*/
|
||
|
||
exports.config = { transaction: false };
|
||
|
||
exports.up = async function(knex) {
|
||
console.log('='.repeat(80));
|
||
console.log('Starting company to client CLEANUP migration...');
|
||
console.log('WARNING: This migration will DROP old tables and columns!');
|
||
console.log('Ensure dual-write period is complete and application is stable.');
|
||
console.log('='.repeat(80));
|
||
|
||
// Step 1: Verify data integrity before cleanup
|
||
await verifyDataIntegrityBeforeCleanup(knex);
|
||
|
||
// Step 2: Add foreign key constraints to new tables
|
||
await addForeignKeyConstraints(knex);
|
||
|
||
// Step 3: Make client_id columns NOT NULL where appropriate
|
||
await makeClientIdNotNull(knex);
|
||
|
||
// Step 3b: Update tenant_companies constraints prior to dropping company_id
|
||
await updateTenantCompaniesConstraints(knex);
|
||
|
||
// Step 4: Drop company_id columns from dependent tables
|
||
await dropOldCompanyIdColumns(knex);
|
||
|
||
// Step 5: Drop old company_* tables
|
||
await dropOldCompanyTables(knex);
|
||
|
||
// Step 6: Final verification
|
||
await finalVerification(knex);
|
||
|
||
console.log('='.repeat(80));
|
||
console.log('✓ Company to client cleanup migration completed successfully');
|
||
console.log('='.repeat(80));
|
||
};
|
||
|
||
/**
|
||
* Helper: Verify data integrity before cleanup
|
||
*/
|
||
async function verifyDataIntegrityBeforeCleanup(knex) {
|
||
console.log('Verifying data integrity before cleanup...');
|
||
|
||
// Check that clients and companies have same row counts
|
||
const companiesExists = await knex.schema.hasTable('companies');
|
||
const clientsExists = await knex.schema.hasTable('clients');
|
||
|
||
if (!clientsExists) {
|
||
throw new Error('❌ clients table does not exist - cleanup cannot proceed');
|
||
}
|
||
|
||
if (companiesExists) {
|
||
const companiesCount = await knex('companies').count('* as count');
|
||
const clientsCount = await knex('clients').count('* as count');
|
||
|
||
const companyTotal = parseInt(companiesCount[0].count);
|
||
const clientTotal = parseInt(clientsCount[0].count);
|
||
|
||
if (clientTotal < companyTotal) {
|
||
throw new Error(
|
||
`❌ Data loss detected: companies=${companyTotal}, clients=${clientTotal}. ` +
|
||
`Clients table has fewer rows than companies table!`
|
||
);
|
||
}
|
||
|
||
if (clientTotal > companyTotal) {
|
||
console.log(` ℹ️ Note: clients table has more rows (${clientTotal}) than companies (${companyTotal})`);
|
||
console.log(` This is normal if new clients were added after the initial migration.`);
|
||
} else {
|
||
console.log(` ✓ Verified: ${clientTotal} rows in both companies and clients`);
|
||
}
|
||
|
||
// Verify per-tenant that all companies have been migrated to clients
|
||
console.log(' Checking per-tenant migration completeness...');
|
||
const missingClients = await knex.raw(`
|
||
SELECT c.tenant, COUNT(*) as missing_count
|
||
FROM companies c
|
||
LEFT JOIN clients cl ON c.tenant = cl.tenant AND c.company_id = cl.client_id
|
||
WHERE cl.client_id IS NULL
|
||
GROUP BY c.tenant
|
||
`);
|
||
|
||
if (missingClients.rows.length > 0) {
|
||
console.error(' ❌ Found companies not migrated to clients:');
|
||
for (const row of missingClients.rows) {
|
||
console.error(` Tenant ${row.tenant}: ${row.missing_count} missing clients`);
|
||
}
|
||
throw new Error(
|
||
`❌ Found ${missingClients.rows.length} tenant(s) with companies not migrated to clients`
|
||
);
|
||
}
|
||
console.log(' ✓ All companies have been migrated to clients');
|
||
}
|
||
|
||
// Verify all dependent tables have client_id populated
|
||
// NOTE: company_billing_plans and company_plan_bundles are excluded because they will be
|
||
// migrated to contracts/client_contract_lines in later migrations (20251003000006 and 20251003000009)
|
||
const dependentTables = [
|
||
'assets', 'bucket_usage',
|
||
'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 dependentTables) {
|
||
const tableExists = await knex.schema.hasTable(table);
|
||
if (!tableExists) {
|
||
console.log(` ⚠ Table ${table} does not exist, skipping verification`);
|
||
continue;
|
||
}
|
||
|
||
const hasCompanyId = await knex.schema.hasColumn(table, 'company_id');
|
||
const hasClientId = await knex.schema.hasColumn(table, 'client_id');
|
||
|
||
if (!hasClientId && hasCompanyId) {
|
||
throw new Error(`❌ Table ${table} has company_id but not client_id - migration not complete`);
|
||
}
|
||
|
||
if (hasCompanyId && hasClientId) {
|
||
// Check for rows where company_id is set but client_id is not
|
||
const missingClientId = await knex(table)
|
||
.whereNotNull('company_id')
|
||
.whereNull('client_id')
|
||
.count('* as count');
|
||
|
||
if (parseInt(missingClientId[0].count) > 0) {
|
||
throw new Error(
|
||
`❌ Table ${table} has ${missingClientId[0].count} rows with company_id but no client_id`
|
||
);
|
||
}
|
||
console.log(` ✓ Verified ${table}: all company_id rows have client_id`);
|
||
}
|
||
}
|
||
|
||
console.log('✓ Data integrity verification complete');
|
||
}
|
||
|
||
/**
|
||
* Helper: Add foreign key constraints to new tables
|
||
*/
|
||
async function addForeignKeyConstraints(knex) {
|
||
console.log('Adding foreign key constraints to new tables...');
|
||
|
||
// Check if Citus is enabled
|
||
const citusEnabled = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_extension WHERE extname = 'citus'
|
||
) as enabled
|
||
`);
|
||
|
||
const isCitus = citusEnabled.rows[0].enabled;
|
||
|
||
// Add FK from clients to tenants (should already exist from Citus migration)
|
||
try {
|
||
console.log(' Checking FK: clients → tenants...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'clients'::regclass
|
||
AND conname = 'clients_tenant_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: clients → tenants...');
|
||
await knex.raw(`
|
||
ALTER TABLE clients
|
||
ADD CONSTRAINT clients_tenant_foreign
|
||
FOREIGN KEY (tenant) REFERENCES tenants(tenant)
|
||
`);
|
||
console.log(' ✓ Added FK: clients → tenants');
|
||
} else {
|
||
console.log(' ✓ FK already exists: clients → tenants');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK clients → tenants: ${error.message}`);
|
||
}
|
||
|
||
// Add FK from client_locations to clients (should already exist from Citus migration)
|
||
const clientLocationsExists = await knex.schema.hasTable('client_locations');
|
||
if (clientLocationsExists) {
|
||
try {
|
||
console.log(' Checking FK: client_locations → clients...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'client_locations'::regclass
|
||
AND conname = 'client_locations_client_id_tenant_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: client_locations → clients...');
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE client_locations
|
||
ADD CONSTRAINT client_locations_client_id_tenant_foreign
|
||
FOREIGN KEY (client_id, tenant) REFERENCES clients(client_id, tenant) ON DELETE CASCADE
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE client_locations
|
||
ADD CONSTRAINT client_locations_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(' ✓ Added FK: client_locations → clients');
|
||
} else {
|
||
console.log(' ✓ FK already exists: client_locations → clients');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add FK from client_billing_cycles to clients (should already exist from Citus migration)
|
||
const clientBillingCyclesExists = await knex.schema.hasTable('client_billing_cycles');
|
||
if (clientBillingCyclesExists) {
|
||
try {
|
||
console.log(' Checking FK: client_billing_cycles → clients...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'client_billing_cycles'::regclass
|
||
AND conname = 'client_billing_cycles_client_id_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: client_billing_cycles → clients...');
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE client_billing_cycles
|
||
ADD CONSTRAINT client_billing_cycles_client_id_foreign
|
||
FOREIGN KEY (tenant, client_id) REFERENCES clients(tenant, client_id) ON DELETE CASCADE
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE client_billing_cycles
|
||
ADD CONSTRAINT client_billing_cycles_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(' ✓ Added FK: client_billing_cycles → clients');
|
||
} else {
|
||
console.log(' ✓ FK already exists: client_billing_cycles → clients');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add FK from client_billing_settings to clients (should already exist from Citus migration)
|
||
const clientBillingSettingsExists = await knex.schema.hasTable('client_billing_settings');
|
||
if (clientBillingSettingsExists) {
|
||
try {
|
||
console.log(' Checking FK: client_billing_settings → clients...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'client_billing_settings'::regclass
|
||
AND conname = 'client_billing_settings_tenant_client_id_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: client_billing_settings → clients...');
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE client_billing_settings
|
||
ADD CONSTRAINT client_billing_settings_tenant_client_id_foreign
|
||
FOREIGN KEY (tenant, client_id) REFERENCES clients(tenant, client_id)
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE client_billing_settings
|
||
ADD CONSTRAINT client_billing_settings_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(' ✓ Added FK: client_billing_settings → clients');
|
||
} else {
|
||
console.log(' ✓ FK already exists: client_billing_settings → clients');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add FK from client_tax_settings to clients (should already exist from Citus migration)
|
||
const clientTaxSettingsExists = await knex.schema.hasTable('client_tax_settings');
|
||
if (clientTaxSettingsExists) {
|
||
try {
|
||
console.log(' Checking FK: client_tax_settings → clients...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'client_tax_settings'::regclass
|
||
AND conname = 'client_tax_settings_client_id_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: client_tax_settings → clients...');
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE client_tax_settings
|
||
ADD CONSTRAINT client_tax_settings_client_id_foreign
|
||
FOREIGN KEY (tenant, client_id) REFERENCES clients(tenant, client_id)
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE client_tax_settings
|
||
ADD CONSTRAINT client_tax_settings_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(' ✓ Added FK: client_tax_settings → clients');
|
||
} else {
|
||
console.log(' ✓ FK already exists: client_tax_settings → clients');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add FK from client_tax_rates to clients (should already exist from Citus migration)
|
||
const clientTaxRatesExists = await knex.schema.hasTable('client_tax_rates');
|
||
if (clientTaxRatesExists) {
|
||
try {
|
||
console.log(' Checking FK: client_tax_rates → clients...');
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = 'client_tax_rates'::regclass
|
||
AND conname = 'client_tax_rates_client_id_foreign'
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(' Adding FK: client_tax_rates → clients...');
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE client_tax_rates
|
||
ADD CONSTRAINT client_tax_rates_client_id_foreign
|
||
FOREIGN KEY (tenant, client_id) REFERENCES clients(tenant, client_id)
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE client_tax_rates
|
||
ADD CONSTRAINT client_tax_rates_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(' ✓ Added FK: client_tax_rates → clients');
|
||
} else {
|
||
console.log(' ✓ FK already exists: client_tax_rates → clients');
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add FK from dependent tables to clients
|
||
// NOTE: company_billing_plans and company_plan_bundles are excluded because they will be
|
||
// migrated to contracts/client_contract_lines in later migrations
|
||
const dependentTables = [
|
||
'assets', 'bucket_usage', '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 dependentTables) {
|
||
const tableExists = await knex.schema.hasTable(table);
|
||
if (!tableExists) {
|
||
console.log(` ⚠ Table ${table} does not exist, skipping FK`);
|
||
continue;
|
||
}
|
||
|
||
const hasClientId = await knex.schema.hasColumn(table, 'client_id');
|
||
if (!hasClientId) {
|
||
console.log(` ⚠ Table ${table} does not have client_id column, skipping FK`);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
console.log(` Checking FK: ${table} → clients...`);
|
||
const fkExists = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conrelid = '${table}'::regclass
|
||
AND (conname LIKE '%client_id%' OR conname LIKE '%_tenant_client_id_%')
|
||
) as exists
|
||
`);
|
||
|
||
if (!fkExists.rows[0].exists) {
|
||
console.log(` Adding FK: ${table} → clients...`);
|
||
const constraintName = `${table}_tenant_client_id_foreign`;
|
||
if (isCitus) {
|
||
await knex.raw(`
|
||
ALTER TABLE ${table}
|
||
ADD CONSTRAINT ${constraintName}
|
||
FOREIGN KEY (tenant, client_id) REFERENCES clients(tenant, client_id)
|
||
`);
|
||
} else {
|
||
await knex.raw(`
|
||
ALTER TABLE ${table}
|
||
ADD CONSTRAINT ${table}_client_id_foreign
|
||
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE
|
||
`);
|
||
}
|
||
console.log(` ✓ Added FK: ${table} → clients`);
|
||
} else {
|
||
console.log(` ✓ FK already exists: ${table} → clients`);
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not add FK for ${table}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
console.log('✓ Foreign key constraints added');
|
||
}
|
||
|
||
/**
|
||
* Helper: Make client_id NOT NULL where appropriate
|
||
*/
|
||
async function makeClientIdNotNull(knex) {
|
||
console.log('Making client_id columns NOT NULL...');
|
||
|
||
// Tables where client_id should be NOT NULL
|
||
const requiredClientIdTables = [
|
||
'assets', 'contacts', 'invoices', 'projects', 'tickets', 'tenant_companies'
|
||
];
|
||
|
||
for (const table of requiredClientIdTables) {
|
||
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(` ⚠ Table ${table} does not have client_id column, skipping...`);
|
||
continue;
|
||
}
|
||
|
||
// Check for NULL values
|
||
const nullCount = await knex(table).whereNull('client_id').count('* as count');
|
||
if (parseInt(nullCount[0].count) > 0) {
|
||
console.log(` ⚠ Table ${table} has ${nullCount[0].count} NULL client_id values, skipping NOT NULL constraint`);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
console.log(` Setting client_id NOT NULL in ${table}...`);
|
||
await knex.raw(`ALTER TABLE ${table} ALTER COLUMN client_id SET NOT NULL`);
|
||
console.log(` ✓ Set client_id NOT NULL in ${table}`);
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not set NOT NULL on ${table}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Some tables may have optional client_id (e.g., bucket_usage, usage_tracking)
|
||
console.log('✓ Completed NOT NULL enforcement (some tables allow NULL by design)');
|
||
}
|
||
|
||
/**
|
||
* Helper: Update tenant_companies primary key and indexes to use client_id
|
||
*/
|
||
async function updateTenantCompaniesConstraints(knex) {
|
||
const tableExists = await knex.schema.hasTable('tenant_companies');
|
||
if (!tableExists) {
|
||
console.log('tenant_companies table does not exist, skipping constraint refresh');
|
||
return;
|
||
}
|
||
|
||
const hasClientId = await knex.schema.hasColumn('tenant_companies', 'client_id');
|
||
if (!hasClientId) {
|
||
console.log('tenant_companies does not have client_id column yet, skipping constraint refresh');
|
||
return;
|
||
}
|
||
|
||
console.log('Refreshing tenant_companies constraints...');
|
||
|
||
try {
|
||
console.log(' Dropping existing primary key (if any)...');
|
||
await knex.raw('ALTER TABLE tenant_companies DROP CONSTRAINT IF EXISTS tenant_companies_pkey');
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not drop tenant_companies primary key: ${error.message}`);
|
||
}
|
||
|
||
try {
|
||
console.log(' Adding primary key on (tenant, client_id)...');
|
||
await knex.raw('ALTER TABLE tenant_companies ADD CONSTRAINT tenant_companies_pkey PRIMARY KEY (tenant, client_id)');
|
||
console.log(' ✓ tenant_companies primary key updated');
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not add tenant_companies primary key: ${error.message}`);
|
||
}
|
||
|
||
try {
|
||
console.log(' Dropping legacy default-company index if present...');
|
||
await knex.raw('DROP INDEX IF EXISTS idx_tenant_default_company');
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not drop idx_tenant_default_company: ${error.message}`);
|
||
}
|
||
|
||
try {
|
||
console.log(' Creating unique default-client index...');
|
||
await knex.raw(`
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_default_client
|
||
ON tenant_companies (tenant)
|
||
WHERE is_default = TRUE
|
||
`);
|
||
console.log(' ✓ Created idx_tenant_default_client');
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not create idx_tenant_default_client: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper: Drop company_id columns from dependent tables
|
||
*/
|
||
async function dropOldCompanyIdColumns(knex) {
|
||
console.log('Dropping company_id columns from dependent tables...');
|
||
|
||
// NOTE: company_billing_plans and company_plan_bundles are excluded because they will be
|
||
// migrated to contracts/client_contract_lines in later migrations
|
||
const dependentTables = [
|
||
'assets', 'bucket_usage',
|
||
'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 dependentTables) {
|
||
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 ${table} already has company_id dropped`);
|
||
continue;
|
||
}
|
||
|
||
// Drop any foreign key constraints on company_id first
|
||
try {
|
||
const fks = await knex.raw(`
|
||
SELECT constraint_name
|
||
FROM information_schema.table_constraints
|
||
WHERE table_name = '${table}'
|
||
AND constraint_type = 'FOREIGN KEY'
|
||
AND constraint_name LIKE '%company_id%'
|
||
AND table_schema = current_schema()
|
||
`);
|
||
|
||
for (const fk of fks.rows) {
|
||
console.log(` Dropping FK constraint ${fk.constraint_name}...`);
|
||
await knex.raw(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk.constraint_name}`);
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not drop FK constraints: ${error.message}`);
|
||
}
|
||
|
||
// Drop the column
|
||
try {
|
||
console.log(` Dropping company_id from ${table}...`);
|
||
await knex.schema.table(table, (t) => {
|
||
t.dropColumn('company_id');
|
||
});
|
||
console.log(` ✓ Dropped company_id from ${table}`);
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not drop company_id from ${table}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
console.log('✓ Old company_id columns dropped');
|
||
}
|
||
|
||
/**
|
||
* Helper: Drop old company_* tables
|
||
*/
|
||
async function dropOldCompanyTables(knex) {
|
||
console.log('Dropping old company_* tables...');
|
||
|
||
// Check if Citus is enabled
|
||
const citusEnabled = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_extension WHERE extname = 'citus'
|
||
) as enabled
|
||
`);
|
||
|
||
const isCitus = citusEnabled.rows[0].enabled;
|
||
|
||
// Drop in order of dependencies (child tables first, parent tables last)
|
||
// NOTE: company_billing_plans and company_plan_bundles are included here now
|
||
// since migration 1 creates client_billing_plans and client_plan_bundles
|
||
const tablesToDrop = [
|
||
'company_plan_bundles',
|
||
'company_billing_plans',
|
||
'company_tax_rates',
|
||
'company_tax_settings',
|
||
'company_billing_settings',
|
||
'company_billing_cycles',
|
||
'company_locations',
|
||
'companies'
|
||
];
|
||
|
||
for (const tableName of tablesToDrop) {
|
||
const exists = await knex.schema.hasTable(tableName);
|
||
if (!exists) {
|
||
console.log(` ✓ ${tableName} already dropped`);
|
||
continue;
|
||
}
|
||
|
||
// If Citus, check if distributed and undistribute first
|
||
if (isCitus) {
|
||
try {
|
||
const isDistributed = await knex.raw(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM pg_dist_partition
|
||
WHERE logicalrelid = '${tableName}'::regclass
|
||
) as distributed
|
||
`);
|
||
|
||
if (isDistributed.rows[0].distributed) {
|
||
console.log(` Undistributing ${tableName}...`);
|
||
await knex.raw(`SELECT undistribute_table('${tableName}', cascade_via_foreign_keys=>true)`);
|
||
}
|
||
} catch (error) {
|
||
console.log(` - Could not check/undistribute ${tableName}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Drop the table with CASCADE to handle any remaining dependencies
|
||
try {
|
||
console.log(` Dropping ${tableName}...`);
|
||
await knex.raw(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
|
||
console.log(` ✓ Dropped ${tableName}`);
|
||
} catch (error) {
|
||
console.log(` ⚠ Could not drop ${tableName}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
console.log('✓ Old company_* tables dropped');
|
||
}
|
||
|
||
/**
|
||
* Helper: Final verification
|
||
*/
|
||
async function finalVerification(knex) {
|
||
console.log('='.repeat(80));
|
||
console.log('FINAL VERIFICATION');
|
||
console.log('='.repeat(80));
|
||
|
||
// Verify new tables exist
|
||
const clientsExists = await knex.schema.hasTable('clients');
|
||
const clientLocationsExists = await knex.schema.hasTable('client_locations');
|
||
const clientBillingCyclesExists = await knex.schema.hasTable('client_billing_cycles');
|
||
const clientBillingSettingsExists = await knex.schema.hasTable('client_billing_settings');
|
||
const clientTaxSettingsExists = await knex.schema.hasTable('client_tax_settings');
|
||
const clientTaxRatesExists = await knex.schema.hasTable('client_tax_rates');
|
||
const clientBillingPlansExists = await knex.schema.hasTable('client_billing_plans');
|
||
const clientPlanBundlesExists = await knex.schema.hasTable('client_plan_bundles');
|
||
|
||
console.log(`clients table exists: ${clientsExists ? '✓' : '✗'}`);
|
||
console.log(`client_locations table exists: ${clientLocationsExists ? '✓' : '✗'}`);
|
||
console.log(`client_billing_cycles table exists: ${clientBillingCyclesExists ? '✓' : '✗'}`);
|
||
console.log(`client_billing_settings table exists: ${clientBillingSettingsExists ? '✓' : '✗'}`);
|
||
console.log(`client_tax_settings table exists: ${clientTaxSettingsExists ? '✓' : '✗'}`);
|
||
console.log(`client_tax_rates table exists: ${clientTaxRatesExists ? '✓' : '✗'}`);
|
||
console.log(`client_billing_plans table exists: ${clientBillingPlansExists ? '✓' : '✗'}`);
|
||
console.log(`client_plan_bundles table exists: ${clientPlanBundlesExists ? '✓' : '✗'}`);
|
||
|
||
if (!clientsExists) {
|
||
throw new Error('❌ clients table does not exist - cleanup verification failed');
|
||
}
|
||
|
||
// Verify old tables are dropped
|
||
const companiesExists = await knex.schema.hasTable('companies');
|
||
const companyLocationsExists = await knex.schema.hasTable('company_locations');
|
||
const companyBillingCyclesExists = await knex.schema.hasTable('company_billing_cycles');
|
||
const companyBillingSettingsExists = await knex.schema.hasTable('company_billing_settings');
|
||
const companyTaxSettingsExists = await knex.schema.hasTable('company_tax_settings');
|
||
const companyTaxRatesExists = await knex.schema.hasTable('company_tax_rates');
|
||
const companyBillingPlansExists = await knex.schema.hasTable('company_billing_plans');
|
||
const companyPlanBundlesExists = await knex.schema.hasTable('company_plan_bundles');
|
||
|
||
console.log(`companies table dropped: ${!companiesExists ? '✓' : '✗'}`);
|
||
console.log(`company_locations table dropped: ${!companyLocationsExists ? '✓' : '✗'}`);
|
||
console.log(`company_billing_cycles table dropped: ${!companyBillingCyclesExists ? '✓' : '✗'}`);
|
||
console.log(`company_billing_settings table dropped: ${!companyBillingSettingsExists ? '✓' : '✗'}`);
|
||
console.log(`company_tax_settings table dropped: ${!companyTaxSettingsExists ? '✓' : '✗'}`);
|
||
console.log(`company_tax_rates table dropped: ${!companyTaxRatesExists ? '✓' : '✗'}`);
|
||
console.log(`company_billing_plans table dropped: ${!companyBillingPlansExists ? '✓' : '✗'}`);
|
||
console.log(`company_plan_bundles table dropped: ${!companyPlanBundlesExists ? '✓' : '✗'}`);
|
||
|
||
// Verify critical tables were dropped
|
||
const failedDrops = [];
|
||
if (companiesExists) failedDrops.push('companies');
|
||
if (companyLocationsExists) failedDrops.push('company_locations');
|
||
if (companyBillingCyclesExists) failedDrops.push('company_billing_cycles');
|
||
if (companyBillingSettingsExists) failedDrops.push('company_billing_settings');
|
||
if (companyTaxSettingsExists) failedDrops.push('company_tax_settings');
|
||
if (companyTaxRatesExists) failedDrops.push('company_tax_rates');
|
||
if (companyBillingPlansExists) failedDrops.push('company_billing_plans');
|
||
if (companyPlanBundlesExists) failedDrops.push('company_plan_bundles');
|
||
|
||
if (failedDrops.length > 0) {
|
||
throw new Error(
|
||
`❌ Failed to drop the following company_* tables: ${failedDrops.join(', ')}. ` +
|
||
`These tables must be dropped for the cleanup migration to succeed. ` +
|
||
`Check for remaining foreign key constraints or other dependencies.`
|
||
);
|
||
}
|
||
|
||
// Count records in new tables
|
||
if (clientsExists) {
|
||
const clientsCount = await knex('clients').count('* as count');
|
||
console.log(`clients record count: ${clientsCount[0].count}`);
|
||
}
|
||
|
||
// Verify dependent tables no longer have company_id
|
||
const dependentTables = [
|
||
'assets', 'bucket_usage', 'contacts', 'invoices', 'projects', 'tickets'
|
||
];
|
||
|
||
for (const table of dependentTables) {
|
||
const tableExists = await knex.schema.hasTable(table);
|
||
if (tableExists) {
|
||
const hasCompanyId = await knex.schema.hasColumn(table, 'company_id');
|
||
const hasClientId = await knex.schema.hasColumn(table, 'client_id');
|
||
console.log(`${table}: company_id dropped=${!hasCompanyId ? '✓' : '✗'}, client_id exists=${hasClientId ? '✓' : '✗'}`);
|
||
}
|
||
}
|
||
|
||
console.log('='.repeat(80));
|
||
console.log('✓ FINAL VERIFICATION COMPLETE');
|
||
console.log('='.repeat(80));
|
||
}
|
||
|
||
/**
|
||
* Rollback - WARNING: This is extremely risky and may not fully restore data
|
||
*/
|
||
exports.down = async function(knex) {
|
||
console.log('='.repeat(80));
|
||
console.log('WARNING: Rollback of cleanup migration is EXTREMELY RISKY');
|
||
console.log('This will attempt to restore old tables but data may be lost');
|
||
console.log('This assumes data is still available in the new tables');
|
||
console.log('='.repeat(80));
|
||
|
||
// Restore tenants.client_name back to 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 (cleanup rollback)...');
|
||
await knex.raw('ALTER TABLE tenants RENAME COLUMN client_name TO company_name');
|
||
console.log('✓ tenants column restored to company_name');
|
||
}
|
||
|
||
// Step 1: Recreate old company_* tables
|
||
await recreateOldCompanyTables(knex);
|
||
|
||
// Step 2: Restore company_id columns to dependent tables
|
||
await restoreCompanyIdColumns(knex);
|
||
|
||
// Step 3: Backfill old tables from new tables
|
||
await backfillOldTables(knex);
|
||
|
||
console.log('='.repeat(80));
|
||
console.log('✓ Rollback completed - Manual verification strongly recommended');
|
||
console.log('='.repeat(80));
|
||
};
|
||
|
||
/**
|
||
* Helper: Recreate old company_* tables
|
||
*/
|
||
async function recreateOldCompanyTables(knex) {
|
||
console.log('Recreating old company_* tables...');
|
||
|
||
// Recreate companies table
|
||
const companiesExists = await knex.schema.hasTable('companies');
|
||
if (!companiesExists) {
|
||
console.log(' Recreating companies table...');
|
||
console.log(' ⚠️ Note: Some columns from the original companies table were moved to');
|
||
console.log(' client_locations and cannot be fully restored (address, city, etc.)');
|
||
|
||
await knex.schema.createTable('companies', (table) => {
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('company_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
||
table.text('company_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', 'company_id']);
|
||
});
|
||
|
||
// Copy data back from clients (only columns that exist in both tables)
|
||
await knex.raw(`
|
||
INSERT INTO companies (
|
||
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
|
||
)
|
||
SELECT
|
||
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
|
||
FROM clients
|
||
`);
|
||
console.log(' ✓ Restored companies data from clients');
|
||
}
|
||
|
||
// Recreate company_locations
|
||
const companyLocationsExists = await knex.schema.hasTable('company_locations');
|
||
if (!companyLocationsExists) {
|
||
console.log(' Recreating company_locations...');
|
||
await knex.schema.createTable('company_locations', (table) => {
|
||
table.uuid('location_id').notNullable();
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('company_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']);
|
||
});
|
||
|
||
// Copy data back
|
||
const clientLocationsExists = await knex.schema.hasTable('client_locations');
|
||
if (clientLocationsExists) {
|
||
await knex.raw(`
|
||
INSERT INTO company_locations
|
||
SELECT
|
||
location_id, tenant, client_id as 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 client_locations
|
||
`);
|
||
console.log(' ✓ Restored company_locations data');
|
||
}
|
||
}
|
||
|
||
// Recreate other company_* tables following similar pattern
|
||
await recreateCompanyBillingCycles(knex);
|
||
await recreateCompanyBillingSettings(knex);
|
||
await recreateCompanyTaxSettings(knex);
|
||
await recreateCompanyTaxRates(knex);
|
||
|
||
console.log('✓ Old company_* tables recreated');
|
||
}
|
||
|
||
async function recreateCompanyBillingCycles(knex) {
|
||
const exists = await knex.schema.hasTable('company_billing_cycles');
|
||
if (exists) return;
|
||
|
||
console.log(' Recreating company_billing_cycles...');
|
||
await knex.schema.createTable('company_billing_cycles', (table) => {
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('billing_cycle_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
||
table.uuid('company_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']);
|
||
});
|
||
|
||
const clientBillingCyclesExists = await knex.schema.hasTable('client_billing_cycles');
|
||
if (clientBillingCyclesExists) {
|
||
await knex.raw(`
|
||
INSERT INTO company_billing_cycles
|
||
SELECT
|
||
tenant, billing_cycle_id, client_id as company_id, billing_cycle,
|
||
effective_date, period_start_date, period_end_date, is_active,
|
||
created_at, updated_at
|
||
FROM client_billing_cycles
|
||
`);
|
||
console.log(' ✓ Restored company_billing_cycles data');
|
||
}
|
||
}
|
||
|
||
async function recreateCompanyBillingSettings(knex) {
|
||
const exists = await knex.schema.hasTable('company_billing_settings');
|
||
if (exists) return;
|
||
|
||
console.log(' Recreating company_billing_settings...');
|
||
await knex.schema.createTable('company_billing_settings', (table) => {
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('company_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', 'company_id']);
|
||
});
|
||
|
||
const clientBillingSettingsExists = await knex.schema.hasTable('client_billing_settings');
|
||
if (clientBillingSettingsExists) {
|
||
await knex.raw(`
|
||
INSERT INTO company_billing_settings
|
||
SELECT
|
||
tenant, client_id as 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 client_billing_settings
|
||
`);
|
||
console.log(' ✓ Restored company_billing_settings data');
|
||
}
|
||
}
|
||
|
||
async function recreateCompanyTaxSettings(knex) {
|
||
const exists = await knex.schema.hasTable('company_tax_settings');
|
||
if (exists) return;
|
||
|
||
console.log(' Recreating company_tax_settings...');
|
||
await knex.schema.createTable('company_tax_settings', (table) => {
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('company_id').notNullable();
|
||
table.boolean('is_reverse_charge_applicable').defaultTo(false);
|
||
|
||
table.primary(['tenant', 'company_id']);
|
||
});
|
||
|
||
const clientTaxSettingsExists = await knex.schema.hasTable('client_tax_settings');
|
||
if (clientTaxSettingsExists) {
|
||
await knex.raw(`
|
||
INSERT INTO company_tax_settings
|
||
SELECT tenant, client_id as company_id, is_reverse_charge_applicable
|
||
FROM client_tax_settings
|
||
`);
|
||
console.log(' ✓ Restored company_tax_settings data');
|
||
}
|
||
}
|
||
|
||
async function recreateCompanyTaxRates(knex) {
|
||
const exists = await knex.schema.hasTable('company_tax_rates');
|
||
if (exists) return;
|
||
|
||
console.log(' Recreating company_tax_rates...');
|
||
await knex.schema.createTable('company_tax_rates', (table) => {
|
||
table.uuid('tenant').notNullable();
|
||
table.uuid('company_tax_rates_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
||
table.uuid('company_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(['company_tax_rates_id', 'tenant']);
|
||
});
|
||
|
||
const clientTaxRatesExists = await knex.schema.hasTable('client_tax_rates');
|
||
if (clientTaxRatesExists) {
|
||
await knex.raw(`
|
||
INSERT INTO company_tax_rates
|
||
SELECT
|
||
tenant, client_tax_rates_id as company_tax_rates_id,
|
||
client_id as company_id, tax_rate_id, location_id,
|
||
is_default, created_at, updated_at
|
||
FROM client_tax_rates
|
||
`);
|
||
console.log(' ✓ Restored company_tax_rates data');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper: Restore company_id columns to dependent tables
|
||
*/
|
||
async function restoreCompanyIdColumns(knex) {
|
||
console.log('Restoring company_id columns to dependent tables...');
|
||
|
||
const dependentTables = [
|
||
'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 dependentTables) {
|
||
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 ${table} already has company_id column`);
|
||
continue;
|
||
}
|
||
|
||
console.log(` Adding company_id to ${table}...`);
|
||
await knex.schema.table(table, (t) => {
|
||
t.uuid('company_id');
|
||
});
|
||
}
|
||
|
||
console.log('✓ company_id columns restored');
|
||
}
|
||
|
||
/**
|
||
* Helper: Backfill old tables from new tables
|
||
*/
|
||
async function backfillOldTables(knex) {
|
||
console.log('Backfilling company_id from client_id in dependent tables...');
|
||
|
||
const dependentTables = [
|
||
'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 dependentTables) {
|
||
const tableExists = await knex.schema.hasTable(table);
|
||
if (!tableExists) continue;
|
||
|
||
const hasCompanyId = await knex.schema.hasColumn(table, 'company_id');
|
||
const hasClientId = await knex.schema.hasColumn(table, 'client_id');
|
||
|
||
if (hasCompanyId && hasClientId) {
|
||
console.log(` Backfilling ${table}.company_id from client_id...`);
|
||
await knex.raw(`
|
||
UPDATE ${table}
|
||
SET company_id = client_id
|
||
WHERE company_id IS NULL AND client_id IS NOT NULL
|
||
`);
|
||
}
|
||
}
|
||
|
||
console.log('✓ Backfill complete');
|
||
}
|