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:
@@ -39,21 +39,24 @@ export async function createTenant(req: Request, res: Response, next: NextFuncti
|
|||||||
throw new AppError(403, 'Solo administradores pueden crear clientes');
|
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) {
|
if (!nombre || !rfc || !adminEmail || !adminNombre) {
|
||||||
throw new AppError(400, 'Nombre y RFC son requeridos');
|
throw new AppError(400, 'Nombre, RFC, adminEmail y adminNombre son requeridos');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenant = await tenantsService.createTenant({
|
const result = await tenantsService.createTenant({
|
||||||
nombre,
|
nombre,
|
||||||
rfc,
|
rfc,
|
||||||
plan,
|
plan,
|
||||||
cfdiLimit,
|
cfdiLimit,
|
||||||
usersLimit,
|
usersLimit,
|
||||||
|
adminEmail,
|
||||||
|
adminNombre,
|
||||||
|
amount: amount || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(tenant);
|
res.status(201).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|||||||
import { prisma } from '../config/database.js';
|
import { prisma } from '../config/database.js';
|
||||||
import { env } from '../config/env.js';
|
import { env } from '../config/env.js';
|
||||||
import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js';
|
import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js';
|
||||||
|
import { emailService } from './email/email.service.js';
|
||||||
import type { FielStatus } from '@horux/shared';
|
import type { FielStatus } from '@horux/shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,10 +145,22 @@ export async function uploadFiel(
|
|||||||
console.error('[FIEL] Filesystem storage failed (DB storage OK):', fsError);
|
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(
|
const daysUntilExpiration = Math.ceil(
|
||||||
(validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
(validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'FIEL configurada correctamente',
|
message: 'FIEL configurada correctamente',
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { prisma, tenantDb } from '../config/database.js';
|
import { prisma, tenantDb } from '../config/database.js';
|
||||||
import { PLANS } from '@horux/shared';
|
import { PLANS } from '@horux/shared';
|
||||||
|
import { emailService } from './email/email.service.js';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
export async function getAllTenants() {
|
export async function getAllTenants() {
|
||||||
return prisma.tenant.findMany({
|
return prisma.tenant.findMany({
|
||||||
@@ -41,13 +44,17 @@ export async function createTenant(data: {
|
|||||||
plan?: 'starter' | 'business' | 'professional' | 'enterprise';
|
plan?: 'starter' | 'business' | 'professional' | 'enterprise';
|
||||||
cfdiLimit?: number;
|
cfdiLimit?: number;
|
||||||
usersLimit?: number;
|
usersLimit?: number;
|
||||||
|
adminEmail: string;
|
||||||
|
adminNombre: string;
|
||||||
|
amount: number;
|
||||||
}) {
|
}) {
|
||||||
const plan = data.plan || 'starter';
|
const plan = data.plan || 'starter';
|
||||||
const planConfig = PLANS[plan];
|
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);
|
const databaseName = await tenantDb.provisionDatabase(data.rfc);
|
||||||
|
|
||||||
|
// 2. Create tenant record
|
||||||
const tenant = await prisma.tenant.create({
|
const tenant = await prisma.tenant.create({
|
||||||
data: {
|
data: {
|
||||||
nombre: data.nombre,
|
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: {
|
export async function updateTenant(id: string, data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user