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
221 lines
9.0 KiB
JavaScript
221 lines
9.0 KiB
JavaScript
/**
|
|
* Migration: Add External Tax Support
|
|
*
|
|
* This migration adds database support for delegating tax calculation to external
|
|
* accounting systems (Xero, QuickBooks, etc.) and importing the calculated taxes back.
|
|
*
|
|
* @param { import("knex").Knex } knex
|
|
* @returns { Promise<void> }
|
|
*/
|
|
exports.up = async function(knex) {
|
|
console.log('Adding external tax support...');
|
|
|
|
// B.1.1 Add tax_source column to invoices table
|
|
console.log(' Adding tax_source to invoices...');
|
|
const hasInvoiceTaxSource = await knex.schema.hasColumn('invoices', 'tax_source');
|
|
if (!hasInvoiceTaxSource) {
|
|
await knex.schema.alterTable('invoices', (table) => {
|
|
table.string('tax_source', 20).defaultTo('internal');
|
|
});
|
|
|
|
// Add check constraint
|
|
await knex.raw(`
|
|
ALTER TABLE invoices
|
|
ADD CONSTRAINT invoices_tax_source_check
|
|
CHECK (tax_source IN ('internal', 'external', 'pending_external'))
|
|
`);
|
|
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN invoices.tax_source IS
|
|
'Source of tax calculation: internal (Alga), external (accounting package), pending_external (awaiting import)'
|
|
`);
|
|
}
|
|
|
|
// B.1.2 Add external tax fields to invoice_charges table
|
|
console.log(' Adding external tax fields to invoice_charges...');
|
|
const hasExternalTaxAmount = await knex.schema.hasColumn('invoice_charges', 'external_tax_amount');
|
|
if (!hasExternalTaxAmount) {
|
|
await knex.schema.alterTable('invoice_charges', (table) => {
|
|
table.integer('external_tax_amount').nullable();
|
|
table.string('external_tax_code', 50).nullable();
|
|
table.decimal('external_tax_rate', 5, 2).nullable();
|
|
});
|
|
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN invoice_charges.external_tax_amount IS
|
|
'Tax amount calculated by external accounting system (in cents)'
|
|
`);
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN invoice_charges.external_tax_code IS
|
|
'Tax code from external accounting system'
|
|
`);
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN invoice_charges.external_tax_rate IS
|
|
'Tax rate from external accounting system'
|
|
`);
|
|
}
|
|
|
|
// B.1.3 Add tax delegation settings to tenant_settings table
|
|
// Note: external_tax_adapter is NOT stored here - the adapter is determined automatically
|
|
// based on which accounting system the invoice is exported to (from tenant_external_entity_mappings)
|
|
console.log(' Adding tax delegation settings to tenant_settings...');
|
|
const hasDefaultTaxSource = await knex.schema.hasColumn('tenant_settings', 'default_tax_source');
|
|
if (!hasDefaultTaxSource) {
|
|
await knex.schema.alterTable('tenant_settings', (table) => {
|
|
table.string('default_tax_source', 20).defaultTo('internal');
|
|
table.boolean('allow_external_tax_override').defaultTo(false);
|
|
});
|
|
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN tenant_settings.default_tax_source IS
|
|
'Default tax calculation source for new invoices: internal (Alga calculates) or external (accounting package calculates after export)'
|
|
`);
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN tenant_settings.allow_external_tax_override IS
|
|
'Whether clients can override the default tax source'
|
|
`);
|
|
}
|
|
|
|
// B.1.4 Add client-level tax source override to client_tax_settings
|
|
// Note: No adapter override needed - the adapter is determined by which accounting system
|
|
// the invoice is exported to, not by a pre-configured setting
|
|
console.log(' Adding client-level tax source override...');
|
|
const hasTaxSourceOverride = await knex.schema.hasColumn('client_tax_settings', 'tax_source_override');
|
|
if (!hasTaxSourceOverride) {
|
|
await knex.schema.alterTable('client_tax_settings', (table) => {
|
|
table.string('tax_source_override', 20).nullable();
|
|
});
|
|
|
|
await knex.raw(`
|
|
COMMENT ON COLUMN client_tax_settings.tax_source_override IS
|
|
'Per-client override of tenant tax source setting (internal or external)'
|
|
`);
|
|
}
|
|
|
|
// B.1.5 Create external_tax_imports tracking table
|
|
console.log(' Creating external_tax_imports table...');
|
|
const hasExternalTaxImports = await knex.schema.hasTable('external_tax_imports');
|
|
if (!hasExternalTaxImports) {
|
|
await knex.schema.createTable('external_tax_imports', (table) => {
|
|
table.uuid('import_id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
|
table.uuid('tenant').notNullable().references('tenant').inTable('tenants');
|
|
// Note: invoice_id FK added below with composite key (tenant, invoice_id)
|
|
table.uuid('invoice_id').notNullable();
|
|
table.string('adapter_type', 50).notNullable();
|
|
table.string('external_invoice_ref', 255).nullable();
|
|
table.timestamp('imported_at', { useTz: true }).defaultTo(knex.fn.now());
|
|
// imported_by is nullable and references users(user_id) within the same tenant
|
|
// Note: FK constraint added below since users table has composite primary key (tenant, user_id)
|
|
table.uuid('imported_by').nullable();
|
|
table.string('import_status', 20).defaultTo('success');
|
|
table.integer('original_internal_tax').nullable();
|
|
table.integer('imported_external_tax').nullable();
|
|
table.integer('tax_difference').nullable();
|
|
table.jsonb('metadata').nullable();
|
|
table.timestamp('created_at', { useTz: true }).defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at', { useTz: true }).defaultTo(knex.fn.now());
|
|
|
|
table.index('invoice_id', 'idx_external_tax_imports_invoice');
|
|
table.index('tenant', 'idx_external_tax_imports_tenant');
|
|
});
|
|
|
|
// Add composite foreign key constraint for invoice_id
|
|
// Since invoices table has composite primary key (tenant, invoice_id), we need to reference both
|
|
await knex.raw(`
|
|
ALTER TABLE external_tax_imports
|
|
ADD CONSTRAINT external_tax_imports_invoice_id_foreign
|
|
FOREIGN KEY (tenant, invoice_id)
|
|
REFERENCES invoices(tenant, invoice_id)
|
|
ON DELETE CASCADE
|
|
`);
|
|
|
|
// Add composite foreign key constraint for imported_by
|
|
// Since users table has composite primary key (tenant, user_id), we need to reference both
|
|
// PostgreSQL allows NULL values in foreign key columns, so this works even when imported_by is NULL
|
|
await knex.raw(`
|
|
ALTER TABLE external_tax_imports
|
|
ADD CONSTRAINT external_tax_imports_imported_by_foreign
|
|
FOREIGN KEY (tenant, imported_by)
|
|
REFERENCES users(tenant, user_id)
|
|
ON DELETE SET NULL
|
|
`);
|
|
|
|
// Add RLS policy
|
|
await knex.raw(`
|
|
ALTER TABLE external_tax_imports ENABLE ROW LEVEL SECURITY
|
|
`);
|
|
|
|
await knex.raw(`
|
|
CREATE POLICY external_tax_imports_tenant_isolation ON external_tax_imports
|
|
FOR ALL
|
|
USING (tenant::text = current_setting('app.current_tenant', true))
|
|
WITH CHECK (tenant::text = current_setting('app.current_tenant', true))
|
|
`);
|
|
|
|
await knex.raw(`
|
|
COMMENT ON TABLE external_tax_imports IS
|
|
'Tracks tax imports from external accounting systems for reconciliation and audit'
|
|
`);
|
|
}
|
|
|
|
console.log('✓ External tax support added');
|
|
};
|
|
|
|
/**
|
|
* @param { import("knex").Knex } knex
|
|
* @returns { Promise<void> }
|
|
*/
|
|
exports.down = async function(knex) {
|
|
console.log('Removing external tax support...');
|
|
|
|
// Remove external_tax_imports table
|
|
console.log(' Dropping external_tax_imports table...');
|
|
const hasExternalTaxImports = await knex.schema.hasTable('external_tax_imports');
|
|
if (hasExternalTaxImports) {
|
|
await knex.raw('DROP POLICY IF EXISTS external_tax_imports_tenant_isolation ON external_tax_imports');
|
|
await knex.schema.dropTable('external_tax_imports');
|
|
}
|
|
|
|
// Remove client_tax_settings columns
|
|
console.log(' Removing client-level tax source override column...');
|
|
const hasTaxSourceOverride = await knex.schema.hasColumn('client_tax_settings', 'tax_source_override');
|
|
if (hasTaxSourceOverride) {
|
|
await knex.schema.alterTable('client_tax_settings', (table) => {
|
|
table.dropColumn('tax_source_override');
|
|
});
|
|
}
|
|
|
|
// Remove tenant_settings columns
|
|
console.log(' Removing tenant tax delegation settings...');
|
|
const hasDefaultTaxSource = await knex.schema.hasColumn('tenant_settings', 'default_tax_source');
|
|
if (hasDefaultTaxSource) {
|
|
await knex.schema.alterTable('tenant_settings', (table) => {
|
|
table.dropColumn('default_tax_source');
|
|
table.dropColumn('allow_external_tax_override');
|
|
});
|
|
}
|
|
|
|
// Remove invoice_charges columns
|
|
console.log(' Removing external tax fields from invoice_charges...');
|
|
const hasExternalTaxAmount = await knex.schema.hasColumn('invoice_charges', 'external_tax_amount');
|
|
if (hasExternalTaxAmount) {
|
|
await knex.schema.alterTable('invoice_charges', (table) => {
|
|
table.dropColumn('external_tax_amount');
|
|
table.dropColumn('external_tax_code');
|
|
table.dropColumn('external_tax_rate');
|
|
});
|
|
}
|
|
|
|
// Remove invoices column
|
|
console.log(' Removing tax_source from invoices...');
|
|
const hasInvoiceTaxSource = await knex.schema.hasColumn('invoices', 'tax_source');
|
|
if (hasInvoiceTaxSource) {
|
|
await knex.raw('ALTER TABLE invoices DROP CONSTRAINT IF EXISTS invoices_tax_source_check');
|
|
await knex.schema.alterTable('invoices', (table) => {
|
|
table.dropColumn('tax_source');
|
|
});
|
|
}
|
|
|
|
console.log('✓ External tax support removed');
|
|
};
|