Initial commit - Horux Despachos NL
This commit is contained in:
147
apps/api/src/services/despacho.service.ts
Normal file
147
apps/api/src/services/despacho.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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: (despacho.plan === 'business_control' ? 'BYO' : 'MANAGED') as any,
|
||||
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));
|
||||
|
||||
// If paid plan, create MP checkout via subscriptionService.subscribe()
|
||||
// que también crea la fila Subscription en BD (clave para que el webhook
|
||||
// pueda aplicar la dualidad firstYear→renewal tras el primer cobro aprobado).
|
||||
let paymentUrl: string | undefined;
|
||||
if (data.despacho.plan && data.despacho.plan !== 'trial') {
|
||||
try {
|
||||
const subscriptionService = await import('./payment/subscription.service.js');
|
||||
const result2 = await subscriptionService.subscribe({
|
||||
tenantId: result.tenant.id,
|
||||
plan: data.despacho.plan as any,
|
||||
// mi_empresa(+) acepta monthly/annual; los demás solo annual
|
||||
// — el subscribe valida y rechaza monthly cuando no aplica.
|
||||
frequency: data.despacho.frequency || 'annual',
|
||||
payerEmail: owner.email,
|
||||
});
|
||||
paymentUrl = result2.paymentUrl;
|
||||
} catch (err: any) {
|
||||
// Rollback: delete tenant + user since payment couldn't be set up
|
||||
await prisma.tenantMembership.deleteMany({ where: { tenantId: result.tenant.id } }).catch(() => {});
|
||||
await prisma.refreshToken.deleteMany({ where: { userId: result.user.id } }).catch(() => {});
|
||||
await prisma.tenant.delete({ where: { id: result.tenant.id } }).catch(() => {});
|
||||
await prisma.user.delete({ where: { id: result.user.id } }).catch(() => {});
|
||||
const msg = err?.message || '';
|
||||
if (msg.includes('MercadoPago no está configurado') || msg.includes('Unauthorized access')) {
|
||||
throw new Error('No se pudo procesar el cobro. Verifica que el sistema de pagos esté configurado o selecciona el plan Trial.');
|
||||
}
|
||||
throw new Error(msg || 'No se pudo procesar el cobro.');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
paymentUrl,
|
||||
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,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user