feat: rewrite tenants service to use TenantConnectionManager
- Replace inline schema SQL with tenantDb.provisionDatabase - Delete now soft-deletes DB (rename) and invalidates pool - Use PLANS config for default limits per plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '../config/database.js';
|
import { prisma, tenantDb } from '../config/database.js';
|
||||||
|
import { PLANS } from '@horux/shared';
|
||||||
|
|
||||||
export async function getAllTenants() {
|
export async function getAllTenants() {
|
||||||
return prisma.tenant.findMany({
|
return prisma.tenant.findMany({
|
||||||
@@ -41,102 +42,23 @@ export async function createTenant(data: {
|
|||||||
cfdiLimit?: number;
|
cfdiLimit?: number;
|
||||||
usersLimit?: number;
|
usersLimit?: number;
|
||||||
}) {
|
}) {
|
||||||
const databaseName = `horux_${data.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
const plan = data.plan || 'starter';
|
||||||
|
const planConfig = PLANS[plan];
|
||||||
|
|
||||||
|
// Provision a dedicated database for this tenant
|
||||||
|
const databaseName = await tenantDb.provisionDatabase(data.rfc);
|
||||||
|
|
||||||
// Create tenant record
|
|
||||||
const tenant = await prisma.tenant.create({
|
const tenant = await prisma.tenant.create({
|
||||||
data: {
|
data: {
|
||||||
nombre: data.nombre,
|
nombre: data.nombre,
|
||||||
rfc: data.rfc.toUpperCase(),
|
rfc: data.rfc.toUpperCase(),
|
||||||
plan: data.plan || 'starter',
|
plan,
|
||||||
databaseName,
|
databaseName,
|
||||||
cfdiLimit: data.cfdiLimit || 500,
|
cfdiLimit: data.cfdiLimit || planConfig.cfdiLimit,
|
||||||
usersLimit: data.usersLimit || 3,
|
usersLimit: data.usersLimit || planConfig.usersLimit,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create schema and tables for the tenant
|
|
||||||
await prisma.$executeRawUnsafe(`CREATE SCHEMA IF NOT EXISTS "${databaseName}"`);
|
|
||||||
|
|
||||||
// Create CFDIs table
|
|
||||||
await prisma.$executeRawUnsafe(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "${databaseName}"."cfdis" (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
uuid_fiscal VARCHAR(36) UNIQUE NOT NULL,
|
|
||||||
tipo VARCHAR(20) NOT NULL,
|
|
||||||
serie VARCHAR(25),
|
|
||||||
folio VARCHAR(40),
|
|
||||||
fecha_emision TIMESTAMP NOT NULL,
|
|
||||||
fecha_timbrado TIMESTAMP NOT NULL,
|
|
||||||
rfc_emisor VARCHAR(13) NOT NULL,
|
|
||||||
nombre_emisor VARCHAR(300) NOT NULL,
|
|
||||||
rfc_receptor VARCHAR(13) NOT NULL,
|
|
||||||
nombre_receptor VARCHAR(300) NOT NULL,
|
|
||||||
subtotal DECIMAL(18,2) NOT NULL,
|
|
||||||
descuento DECIMAL(18,2) DEFAULT 0,
|
|
||||||
iva DECIMAL(18,2) DEFAULT 0,
|
|
||||||
isr_retenido DECIMAL(18,2) DEFAULT 0,
|
|
||||||
iva_retenido DECIMAL(18,2) DEFAULT 0,
|
|
||||||
total DECIMAL(18,2) NOT NULL,
|
|
||||||
moneda VARCHAR(3) DEFAULT 'MXN',
|
|
||||||
tipo_cambio DECIMAL(10,4) DEFAULT 1,
|
|
||||||
metodo_pago VARCHAR(3),
|
|
||||||
forma_pago VARCHAR(2),
|
|
||||||
uso_cfdi VARCHAR(4),
|
|
||||||
estado VARCHAR(20) DEFAULT 'vigente',
|
|
||||||
xml_url TEXT,
|
|
||||||
pdf_url TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create IVA monthly table
|
|
||||||
await prisma.$executeRawUnsafe(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "${databaseName}"."iva_mensual" (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
año INT NOT NULL,
|
|
||||||
mes INT NOT NULL,
|
|
||||||
iva_trasladado DECIMAL(18,2) NOT NULL,
|
|
||||||
iva_acreditable DECIMAL(18,2) NOT NULL,
|
|
||||||
iva_retenido DECIMAL(18,2) DEFAULT 0,
|
|
||||||
resultado DECIMAL(18,2) NOT NULL,
|
|
||||||
acumulado DECIMAL(18,2) NOT NULL,
|
|
||||||
estado VARCHAR(20) DEFAULT 'pendiente',
|
|
||||||
fecha_declaracion TIMESTAMP,
|
|
||||||
UNIQUE(año, mes)
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create alerts table
|
|
||||||
await prisma.$executeRawUnsafe(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "${databaseName}"."alertas" (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
tipo VARCHAR(50) NOT NULL,
|
|
||||||
titulo VARCHAR(200) NOT NULL,
|
|
||||||
mensaje TEXT NOT NULL,
|
|
||||||
prioridad VARCHAR(20) DEFAULT 'media',
|
|
||||||
fecha_vencimiento TIMESTAMP,
|
|
||||||
leida BOOLEAN DEFAULT FALSE,
|
|
||||||
resuelta BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create calendario_fiscal table
|
|
||||||
await prisma.$executeRawUnsafe(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "${databaseName}"."calendario_fiscal" (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
titulo VARCHAR(200) NOT NULL,
|
|
||||||
descripcion TEXT,
|
|
||||||
tipo VARCHAR(20) NOT NULL,
|
|
||||||
fecha_limite TIMESTAMP NOT NULL,
|
|
||||||
recurrencia VARCHAR(20) DEFAULT 'mensual',
|
|
||||||
completado BOOLEAN DEFAULT FALSE,
|
|
||||||
notas TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +95,20 @@ export async function updateTenant(id: string, data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTenant(id: string) {
|
export async function deleteTenant(id: string) {
|
||||||
// Soft delete - just mark as inactive
|
const tenant = await prisma.tenant.findUnique({
|
||||||
return prisma.tenant.update({
|
where: { id },
|
||||||
|
select: { databaseName: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Soft-delete the tenant record
|
||||||
|
await prisma.tenant.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { active: false }
|
data: { active: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Soft-delete the database (rename with _deleted_ suffix)
|
||||||
|
if (tenant) {
|
||||||
|
await tenantDb.deprovisionDatabase(tenant.databaseName);
|
||||||
|
tenantDb.invalidatePool(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user