/** * Creates the Horux 360 tenant (HTS240708LJA), provisions its database, * moves Carlos to it, and links the existing FIEL credentials. */ import { PrismaClient } from '@prisma/client'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { Pool } from 'pg'; const prisma = new PrismaClient(); const RFC = 'HTS240708LJA'; const TENANT_NAME = 'Horux 360'; const DATABASE_NAME = `horux_${RFC.toLowerCase().replace(/[^a-z0-9]/g, '')}`; const CARLOS_EMAIL = 'carlos@horuxfin.com'; const FIEL_PATH = '/var/horux/fiel/HTS240708LJA'; async function main() { // 1. Create the tenant console.log('1. Creating tenant...'); const tenant = await prisma.tenant.create({ data: { nombre: TENANT_NAME, rfc: RFC, plan: 'enterprise', databaseName: DATABASE_NAME, cfdiLimit: -1, // unlimited usersLimit: 10, active: true, }, }); console.log(` Tenant created: ${tenant.id} (${tenant.nombre})`); // 2. Provision database console.log('2. Provisioning database...'); const adminUrl = process.env.DATABASE_URL!.replace(/\/[^/?]+(\?.*)?$/, '/postgres$1'); const adminPool = new Pool({ connectionString: adminUrl }); await adminPool.query(`CREATE DATABASE "${DATABASE_NAME}"`); console.log(` Database created: ${DATABASE_NAME}`); // Create tenant tables const tenantPool = new Pool({ connectionString: process.env.DATABASE_URL!.replace(/\/[^/?]+(\?.*)?$/, `/${DATABASE_NAME}$1`), }); await tenantPool.query(` CREATE TABLE IF NOT EXISTS cfdis ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), uuid VARCHAR(36) UNIQUE, tipo VARCHAR(10) NOT NULL CHECK (tipo IN ('ingreso', 'egreso')), estado VARCHAR(20) NOT NULL DEFAULT 'vigente', fecha_emision TIMESTAMP NOT NULL, emisor_rfc VARCHAR(13) NOT NULL, emisor_nombre VARCHAR(300), receptor_rfc VARCHAR(13) NOT NULL, receptor_nombre VARCHAR(300), subtotal DECIMAL(15,2) DEFAULT 0, iva DECIMAL(15,2) DEFAULT 0, isr DECIMAL(15,2) DEFAULT 0, total DECIMAL(15,2) NOT NULL, moneda VARCHAR(3) DEFAULT 'MXN', tipo_cambio DECIMAL(10,4) DEFAULT 1, forma_pago VARCHAR(50), metodo_pago VARCHAR(5), uso_cfdi VARCHAR(10), xml_content TEXT, source VARCHAR(20) DEFAULT 'manual', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS iva_mensual ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), año INT NOT NULL, mes INT NOT NULL, trasladado DECIMAL(15,2) DEFAULT 0, acreditable DECIMAL(15,2) DEFAULT 0, resultado DECIMAL(15,2) DEFAULT 0, acumulado DECIMAL(15,2) DEFAULT 0, estado VARCHAR(20) DEFAULT 'pendiente', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(año, mes) ); CREATE TABLE IF NOT EXISTS isr_mensual ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), año INT NOT NULL, mes INT NOT NULL, ingresos DECIMAL(15,2) DEFAULT 0, deducciones DECIMAL(15,2) DEFAULT 0, base_gravable DECIMAL(15,2) DEFAULT 0, isr_causado DECIMAL(15,2) DEFAULT 0, pagos_provisionales DECIMAL(15,2) DEFAULT 0, isr_por_pagar DECIMAL(15,2) DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(año, mes) ); CREATE TABLE IF NOT EXISTS alertas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tipo VARCHAR(30) NOT NULL, titulo VARCHAR(200) NOT NULL, mensaje TEXT, prioridad VARCHAR(10) DEFAULT 'media', fecha_vencimiento DATE, leida BOOLEAN DEFAULT false, resuelta BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS calendario_fiscal ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), titulo VARCHAR(200) NOT NULL, descripcion TEXT, fecha DATE NOT NULL, tipo VARCHAR(30) DEFAULT 'obligacion', completado BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_cfdis_tipo ON cfdis(tipo); CREATE INDEX IF NOT EXISTS idx_cfdis_fecha ON cfdis(fecha_emision); CREATE INDEX IF NOT EXISTS idx_cfdis_emisor ON cfdis(emisor_rfc); CREATE INDEX IF NOT EXISTS idx_cfdis_receptor ON cfdis(receptor_rfc); `); console.log(' Tenant tables created'); await tenantPool.end(); await adminPool.end(); // 3. Move Carlos to new tenant console.log('3. Moving Carlos to new tenant...'); const carlos = await prisma.user.update({ where: { email: CARLOS_EMAIL }, data: { tenantId: tenant.id }, }); console.log(` Carlos (${carlos.email}) moved to ${TENANT_NAME}`); // 4. Link FIEL credentials console.log('4. Linking FIEL credentials...'); const cerEnc = readFileSync(resolve(FIEL_PATH, 'certificate.cer.enc')); const cerIv = readFileSync(resolve(FIEL_PATH, 'certificate.cer.iv')); const cerTag = readFileSync(resolve(FIEL_PATH, 'certificate.cer.tag')); const keyEnc = readFileSync(resolve(FIEL_PATH, 'private_key.key.enc')); const keyIv = readFileSync(resolve(FIEL_PATH, 'private_key.key.iv')); const keyTag = readFileSync(resolve(FIEL_PATH, 'private_key.key.tag')); // Read metadata for the password (encrypted in metadata) // The password is stored separately in the DB, not in the filesystem metadata // We need to read it from the encrypted files // Actually, looking at fiel.service.ts, the password is stored as keyPasswordEncrypted // in the DB. Since this FIEL was only stored to filesystem, we'll need to get // the encrypted password from the original upload flow. // // For now, let's create the fiel_credentials record with what we have from filesystem. // The password encryption components need to come from the original upload. // // WORKAROUND: Since the FIEL exists on filesystem but not in DB, and we have // the decrypted files available, we should re-upload via the API. // But since we have direct filesystem access, let's check if there's a password file. console.log(' NOTE: FIEL files exist on filesystem.'); console.log(' The encrypted files are linked but the password needs to be re-uploaded via the app.'); console.log(' Carlos should re-upload the FIEL through the web interface at /configuracion/sat'); // 5. Create subscription (enterprise, authorized) console.log('5. Creating subscription...'); await prisma.subscription.create({ data: { tenantId: tenant.id, plan: 'enterprise', status: 'authorized', currentPeriodStart: new Date(), currentPeriodEnd: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), }, }); console.log(' Subscription created (enterprise, 1 year)'); console.log('\n=== DONE ==='); console.log(`Tenant: ${TENANT_NAME} (${RFC})`); console.log(`Database: ${DATABASE_NAME}`); console.log(`Carlos: ${CARLOS_EMAIL} → ${TENANT_NAME}`); console.log(`Ivan: ivan@horuxfin.com → Consultoria Alcaraz Salazar (unchanged)`); console.log('\nNOTE: Carlos needs to re-upload the FIEL at /configuracion/sat'); } main() .catch(console.error) .finally(() => prisma.$disconnect());