Backend: - Notificación email al admin cuando llega primer pago aprobado (sin factura auto) - Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global - Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d) - Fix webhook MP: validación defensiva de x-signature header - Fix autocompleto RFCs: eliminado filtro por contribuyenteId - Fix autocompleto conceptos: eliminado filtro por contribuyenteId - SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds - SAT sync request reuse across jobs para evitar agotar cuota diaria - Typo fix MP_ACCESS_TOKEN en .env - Trial invitations system backend Frontend: - Nueva página /admin/facturas-pendientes con tabla y emisión manual - Métrica 'Facturas pendientes' en /clientes (clickable) - Navegación onboarding FIEL/CSD corregida - Sidebar themes sincronizados - Fix SAT portal migration scraper (NetIQ) - Trial invitation acceptance pages
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { prisma, tenantDb } from '../config/database.js';
|
|
import { hashPassword } from '../auth/passwords.js';
|
|
import { generateAccessToken, generateRefreshToken } from '../auth/tokens.js';
|
|
import type { DespachoSignupRequest } from '@horux/shared';
|
|
import type { JWTPayload, Role } from '@horux/shared';
|
|
import { emailService } from './email/email.service.js';
|
|
|
|
export async function signupDespacho(data: DespachoSignupRequest) {
|
|
const { despacho, owner } = data;
|
|
|
|
const existingUser = await prisma.user.findUnique({ where: { email: owner.email } });
|
|
if (existingUser) {
|
|
throw new Error('Ya existe un usuario con este email');
|
|
}
|
|
|
|
const passwordHash = await hashPassword(owner.password);
|
|
|
|
const tenantSlug = `despacho_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
const databaseName = `horux_${tenantSlug}`;
|
|
|
|
const result = await prisma.$transaction(async (tx) => {
|
|
const tenant = await tx.tenant.create({
|
|
data: {
|
|
nombre: despacho.nombre,
|
|
rfc: tenantSlug.toUpperCase(),
|
|
plan: 'trial',
|
|
databaseName: databaseName,
|
|
verticalProfile: despacho.verticalProfile as any,
|
|
dbMode: 'MANAGED',
|
|
dbSchemaVersion: 0,
|
|
trialEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
codigoPostal: despacho.codigoPostal,
|
|
},
|
|
});
|
|
|
|
const user = await tx.user.create({
|
|
data: {
|
|
email: owner.email.toLowerCase(),
|
|
passwordHash,
|
|
nombre: owner.nombre,
|
|
lastTenantId: tenant.id,
|
|
},
|
|
});
|
|
|
|
const ownerRole = await tx.rol.findUnique({ where: { nombre: 'owner' } });
|
|
if (!ownerRole) throw new Error('Rol owner no encontrado en BD');
|
|
|
|
await tx.tenantMembership.create({
|
|
data: {
|
|
userId: user.id,
|
|
tenantId: tenant.id,
|
|
rolId: ownerRole.id,
|
|
isOwner: true,
|
|
},
|
|
});
|
|
|
|
return { tenant, user };
|
|
});
|
|
|
|
try {
|
|
await tenantDb.provisionDatabase(tenantSlug, databaseName);
|
|
} catch (err: any) {
|
|
await prisma.tenant.delete({ where: { id: result.tenant.id } }).catch(() => {});
|
|
await prisma.user.delete({ where: { id: result.user.id } }).catch(() => {});
|
|
throw new Error(`Error al crear base de datos del despacho: ${err.message}`);
|
|
}
|
|
|
|
const payload: Omit<JWTPayload, 'iat' | 'exp'> = {
|
|
userId: result.user.id,
|
|
email: result.user.email,
|
|
role: 'owner' as Role,
|
|
tenantId: result.tenant.id,
|
|
tokenVersion: 0,
|
|
};
|
|
|
|
const accessToken = generateAccessToken(payload);
|
|
const refreshToken = generateRefreshToken(payload);
|
|
|
|
await prisma.refreshToken.create({
|
|
data: {
|
|
userId: result.user.id,
|
|
token: refreshToken,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
},
|
|
});
|
|
|
|
// Send welcome email (fire-and-forget)
|
|
emailService.sendDespachoWelcome(owner.email, {
|
|
nombre: result.user.nombre,
|
|
despachoNombre: result.tenant.nombre,
|
|
email: result.user.email,
|
|
}).catch(err => console.error('[Signup] Welcome email failed:', err));
|
|
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
user: {
|
|
id: result.user.id,
|
|
email: result.user.email,
|
|
nombre: result.user.nombre,
|
|
role: 'owner' as Role,
|
|
tenantId: result.tenant.id,
|
|
tenantName: result.tenant.nombre,
|
|
tenantRfc: result.tenant.rfc,
|
|
plan: result.tenant.plan,
|
|
tenants: [{
|
|
id: result.tenant.id,
|
|
nombre: result.tenant.nombre,
|
|
rfc: result.tenant.rfc,
|
|
plan: result.tenant.plan,
|
|
role: 'owner' as Role,
|
|
isOwner: true,
|
|
}],
|
|
},
|
|
};
|
|
}
|