feat: integrate email + subscription into tenant provisioning, FIEL notification

createTenant now: provisions DB, creates admin user with temp password,
creates initial subscription, and sends welcome email.
FIEL upload sends admin notification email.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-03-15 23:42:55 +00:00
parent b977f92141
commit c351b5aeda
3 changed files with 63 additions and 8 deletions

View File

@@ -39,21 +39,24 @@ export async function createTenant(req: Request, res: Response, next: NextFuncti
throw new AppError(403, 'Solo administradores pueden crear clientes');
}
const { nombre, rfc, plan, cfdiLimit, usersLimit } = req.body;
const { nombre, rfc, plan, cfdiLimit, usersLimit, adminEmail, adminNombre, amount } = req.body;
if (!nombre || !rfc) {
throw new AppError(400, 'Nombre y RFC son requeridos');
if (!nombre || !rfc || !adminEmail || !adminNombre) {
throw new AppError(400, 'Nombre, RFC, adminEmail y adminNombre son requeridos');
}
const tenant = await tenantsService.createTenant({
const result = await tenantsService.createTenant({
nombre,
rfc,
plan,
cfdiLimit,
usersLimit,
adminEmail,
adminNombre,
amount: amount || 0,
});
res.status(201).json(tenant);
res.status(201).json(result);
} catch (error) {
next(error);
}

View File

@@ -4,6 +4,7 @@ import { join } from 'path';
import { prisma } from '../config/database.js';
import { env } from '../config/env.js';
import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js';
import { emailService } from './email/email.service.js';
import type { FielStatus } from '@horux/shared';
/**
@@ -144,10 +145,22 @@ export async function uploadFiel(
console.error('[FIEL] Filesystem storage failed (DB storage OK):', fsError);
}
// Notify admin that client uploaded FIEL
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { nombre: true, rfc: true },
});
if (tenant) {
emailService.sendFielNotification({
clienteNombre: tenant.nombre,
clienteRfc: tenant.rfc,
}).catch(err => console.error('[EMAIL] FIEL notification failed:', err));
}
const daysUntilExpiration = Math.ceil(
(validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
return {
success: true,
message: 'FIEL configurada correctamente',

View File

@@ -1,5 +1,8 @@
import { prisma, tenantDb } from '../config/database.js';
import { PLANS } from '@horux/shared';
import { emailService } from './email/email.service.js';
import { randomBytes } from 'crypto';
import bcrypt from 'bcryptjs';
export async function getAllTenants() {
return prisma.tenant.findMany({
@@ -41,13 +44,17 @@ export async function createTenant(data: {
plan?: 'starter' | 'business' | 'professional' | 'enterprise';
cfdiLimit?: number;
usersLimit?: number;
adminEmail: string;
adminNombre: string;
amount: number;
}) {
const plan = data.plan || 'starter';
const planConfig = PLANS[plan];
// Provision a dedicated database for this tenant
// 1. Provision a dedicated database for this tenant
const databaseName = await tenantDb.provisionDatabase(data.rfc);
// 2. Create tenant record
const tenant = await prisma.tenant.create({
data: {
nombre: data.nombre,
@@ -59,7 +66,39 @@ export async function createTenant(data: {
}
});
return tenant;
// 3. Create admin user with temp password
const tempPassword = randomBytes(4).toString('hex'); // 8-char random
const hashedPassword = await bcrypt.hash(tempPassword, 10);
const user = await prisma.user.create({
data: {
tenantId: tenant.id,
email: data.adminEmail,
passwordHash: hashedPassword,
nombre: data.adminNombre,
role: 'admin',
},
});
// 4. Create initial subscription
await prisma.subscription.create({
data: {
tenantId: tenant.id,
plan,
status: 'pending',
amount: data.amount,
frequency: 'monthly',
},
});
// 5. Send welcome email (non-blocking)
emailService.sendWelcome(data.adminEmail, {
nombre: data.adminNombre,
email: data.adminEmail,
tempPassword,
}).catch(err => console.error('[EMAIL] Welcome email failed:', err));
return { tenant, user, tempPassword };
}
export async function updateTenant(id: string, data: {