feat: Stripe Checkout, Zeptomail, pre-alert linking, perfil 3-tab, carga-pesada landing

- payments: real Stripe Checkout Session (redirect flow), confirmBySession, webhook handler
- notifications: Zeptomail REST email, wa.me WhatsApp links, IntegrationsService injection
- auth: changePassword, updateProfile, disableMfa (TOTP-verified) endpoints
- packages: tryLinkPreAlert() auto-links on create (non-blocking)
- integrations: catalog updated (zeptomail, stripe_webhook_secret; removed sendgrid/whatsapp-api)
- web/portal/perfil: 3-tab layout (datos / contraseña / MFA)
- web/portal/pago: Stripe redirect + ?success=1&session_id= callback, cancelled banner
- web/admin/b2b: fix enum values (EN_COTIZACION, ACEPTADO)
- web/carga-pesada: rich marketing landing (FCL/LCL, 7-step process, sectors, INEN callout)
- tests: fix payments.service.spec (IntegrationsService+ConfigService mocks)
- tests: fix notifications.service.spec (IntegrationsService mock, phone in mockUser)
- all 104 tests passing, API + web builds clean
This commit is contained in:
Lizandro Guarnizo
2026-06-01 20:24:35 -05:00
parent b2f03e654f
commit 98ab5a309c
19 changed files with 979 additions and 134 deletions
+45
View File
@@ -185,6 +185,51 @@ export class AuthService {
return { mfaEnabled: true };
}
// ─── Change Password ─────────────────────────────────────────
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });
if (!user) throw new UnauthorizedException();
const valid = await bcrypt.compare(oldPassword, user.passwordHash);
if (!valid) throw new BadRequestException("La contraseña actual es incorrecta.");
if (oldPassword === newPassword) throw new BadRequestException("La nueva contraseña debe ser diferente.");
const passwordHash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
await this.prisma.client.user.update({ where: { id: userId }, data: { passwordHash } });
await this.audit(user.tenantId, userId, "PASSWORD_CHANGED", "User", userId);
}
// ─── Update Profile ───────────────────────────────────────────
async updateProfile(userId: string, data: { firstName?: string; lastName?: string; phone?: string }): Promise<any> {
const user = await this.prisma.client.user.update({
where: { id: userId },
data: {
...(data.firstName ? { firstName: data.firstName } : {}),
...(data.lastName ? { lastName: data.lastName } : {}),
...(data.phone !== undefined ? { phone: data.phone || null } : {}),
},
});
return this.sanitizeUser(user);
}
// ─── Disable MFA ─────────────────────────────────────────────
async disableMfa(userId: string, totpCode: string): Promise<any> {
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });
if (!user) throw new UnauthorizedException();
if (!user.mfaEnabled) throw new BadRequestException("MFA no está activada.");
const ok = totpVerify({ token: totpCode, secret: user.mfaSecret! });
if (!ok) throw new BadRequestException("Código TOTP inválido.");
await this.prisma.client.user.update({
where: { id: userId },
data: { mfaEnabled: false, mfaSecret: null },
});
await this.audit(user.tenantId, userId, "MFA_DISABLED", "User", userId);
return { mfaEnabled: false };
}
// ─── Profile ─────────────────────────────────────────────────
async getProfile(userId: string): Promise<any> {
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });