Files
core/src/modules/invoice/db.js
T
2026-04-12 12:50:14 -04:00

236 lines
8.5 KiB
JavaScript

/**
* Invoice Module - Database
* Database initialization and tables for invoice module
*/
import { query } from '@hykocx/zen/database';
/**
* Check if a table exists in the database
* @param {string} tableName - Name of the table to check
* @returns {Promise<boolean>}
*/
async function tableExists(tableName) {
const result = await query(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
)`,
[tableName]
);
return result.rows[0].exists;
}
/**
* Create all invoice-related tables (includes clients, items, and transactions)
* @returns {Promise<Object>}
*/
export async function createTables() {
const created = [];
const skipped = [];
// Import clients, items, categories, transactions and recurrences table creation
const { createClientsTable } = await import('../clients/db.js');
const { createItemsTable } = await import('./items/db.js');
const { createCategoriesTable } = await import('./categories/db.js');
const { createTransactionsTable } = await import('./transactions/db.js');
const { createRecurrencesTable } = await import('./recurrences/db.js');
// Create clients table first (dependency for invoices)
console.log('\n--- Clients (Invoice Module) ---');
const clientsResult = await createClientsTable();
if (clientsResult.created) created.push(clientsResult.tableName);
else skipped.push(clientsResult.tableName);
// Create categories table first (dependency for items)
console.log('\n--- Categories (Invoice Module) ---');
const categoriesResult = await createCategoriesTable();
if (categoriesResult.created) created.push(categoriesResult.tableName);
else skipped.push(categoriesResult.tableName);
// Create items table (depends on categories)
console.log('\n--- Items (Invoice Module) ---');
const itemsResult = await createItemsTable();
if (itemsResult.created) created.push(itemsResult.tableName);
else skipped.push(itemsResult.tableName);
// 1. Main invoices table
const invoicesTableExists = await tableExists('zen_invoices');
if (!invoicesTableExists) {
await query(`
CREATE TABLE zen_invoices (
id SERIAL PRIMARY KEY,
invoice_number VARCHAR(50) UNIQUE NOT NULL,
token VARCHAR(64) UNIQUE NOT NULL,
client_id INTEGER NOT NULL REFERENCES zen_clients(id) ON DELETE RESTRICT,
issue_date DATE NOT NULL,
due_date DATE NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL DEFAULT 0,
tax_rate DECIMAL(5, 2) DEFAULT 0,
tax_amount DECIMAL(10, 2) DEFAULT 0,
total_amount DECIMAL(10, 2) NOT NULL,
paid_amount DECIMAL(10, 2) DEFAULT 0,
interest_amount DECIMAL(10, 2) DEFAULT 0,
interest_last_calculated DATE,
notes TEXT,
status VARCHAR(50) DEFAULT 'draft',
paid_at TIMESTAMPTZ,
first_reminder_days INTEGER DEFAULT 30,
is_recurring BOOLEAN DEFAULT false,
recurring_frequency VARCHAR(50),
recurring_end_date DATE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
`);
await query(`CREATE INDEX idx_zen_invoices_invoice_number ON zen_invoices(invoice_number)`);
await query(`CREATE INDEX idx_zen_invoices_token ON zen_invoices(token)`);
await query(`CREATE INDEX idx_zen_invoices_client_id ON zen_invoices(client_id)`);
await query(`CREATE INDEX idx_zen_invoices_status ON zen_invoices(status)`);
await query(`CREATE INDEX idx_zen_invoices_due_date ON zen_invoices(due_date)`);
await query(`CREATE INDEX idx_zen_invoices_interest_calc ON zen_invoices(status, due_date, interest_last_calculated) WHERE status IN ('sent', 'partial', 'overdue')`);
created.push('zen_invoices');
console.log('✓ Created table: zen_invoices');
} else {
skipped.push('zen_invoices');
console.log('- Table already exists: zen_invoices');
}
// 2. Invoice items table
const itemsTableExists = await tableExists('zen_invoice_items');
if (!itemsTableExists) {
await query(`
CREATE TABLE zen_invoice_items (
id SERIAL PRIMARY KEY,
invoice_id INTEGER NOT NULL REFERENCES zen_invoices(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
quantity DECIMAL(10, 2) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
total DECIMAL(10, 2) NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
`);
await query(`CREATE INDEX idx_zen_invoice_items_invoice_id ON zen_invoice_items(invoice_id)`);
created.push('zen_invoice_items');
console.log('✓ Created table: zen_invoice_items');
} else {
skipped.push('zen_invoice_items');
console.log('- Table already exists: zen_invoice_items');
}
// 3. Invoice reminders table
const remindersTableExists = await tableExists('zen_invoice_reminders');
if (!remindersTableExists) {
await query(`
CREATE TABLE zen_invoice_reminders (
id SERIAL PRIMARY KEY,
invoice_id INTEGER NOT NULL REFERENCES zen_invoices(id) ON DELETE CASCADE,
reminder_type VARCHAR(50) NOT NULL,
days_before INTEGER NOT NULL,
sent_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
`);
await query(`CREATE INDEX idx_zen_invoice_reminders_invoice_id ON zen_invoice_reminders(invoice_id)`);
await query(`CREATE INDEX idx_zen_invoice_reminders_sent_at ON zen_invoice_reminders(sent_at)`);
created.push('zen_invoice_reminders');
console.log('✓ Created table: zen_invoice_reminders');
} else {
skipped.push('zen_invoice_reminders');
console.log('- Table already exists: zen_invoice_reminders');
}
// 4. Interac credentials table
const interacTableExists = await tableExists('zen_invoice_interac');
if (!interacTableExists) {
await query(`
CREATE TABLE zen_invoice_interac (
id SERIAL PRIMARY KEY,
client_id INTEGER NOT NULL UNIQUE REFERENCES zen_clients(id) ON DELETE CASCADE,
security_question VARCHAR(255) NOT NULL,
security_answer VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
`);
await query(`CREATE INDEX idx_zen_invoice_interac_client_id ON zen_invoice_interac(client_id)`);
created.push('zen_invoice_interac');
console.log('✓ Created table: zen_invoice_interac');
} else {
skipped.push('zen_invoice_interac');
console.log('- Table already exists: zen_invoice_interac');
}
// 5. Transactions table (depends on invoices)
console.log('\n--- Transactions (Invoice Module) ---');
const transactionsResult = await createTransactionsTable();
if (transactionsResult.created) created.push(transactionsResult.tableName);
else skipped.push(transactionsResult.tableName);
// 6. Recurrences table (depends on clients and invoices)
console.log('\n--- Recurrences (Invoice Module) ---');
const recurrencesResult = await createRecurrencesTable();
if (recurrencesResult.created) created.push(recurrencesResult.tableName);
else skipped.push(recurrencesResult.tableName);
return { created, skipped };
}
/**
* Drop all invoice-related tables (includes clients, items, and transactions)
* @returns {Promise<void>}
*/
export async function dropTables() {
// Import drop functions
const { dropClientsTable } = await import('../clients/db.js');
const { dropItemsTable } = await import('./items/db.js');
const { dropCategoriesTable } = await import('./categories/db.js');
const { dropTransactionsTable } = await import('./transactions/db.js');
const { dropRecurrencesTable } = await import('./recurrences/db.js');
// Drop recurrences first (depends on invoices)
await dropRecurrencesTable();
// Drop transactions (depends on invoices)
await dropTransactionsTable();
// Drop invoice tables
const tables = [
'zen_invoice_reminders',
'zen_invoice_interac',
'zen_invoice_items',
'zen_invoices'
];
for (const tableName of tables) {
const exists = await tableExists(tableName);
if (exists) {
await query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
console.log(`✓ Dropped table: ${tableName}`);
}
}
// Drop items (depends on categories)
await dropItemsTable();
// Drop categories
await dropCategoriesTable();
// Drop clients last
await dropClientsTable();
}
// Backward compatibility aliases
export const createInvoiceTables = createTables;
export const dropInvoiceTables = dropTables;