import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { MailService } from '../mail/mail.service'; import { NotificationsService } from '../notifications/notifications.service'; @Injectable() export class ProfessionalsService { constructor( private prisma: PrismaService, private mail: MailService, private notifications: NotificationsService, ) {} private async tryPush(userId: string, title: string, body: string) { try { const u = await this.prisma.users.findUnique({ where: { id: userId }, select: { fcm_token: true }, }); if (u?.fcm_token) await this.notifications.send(u.fcm_token, title, body); } catch (_) {} } async findAllActive(page = 1, limit = 20, city?: string) { const skip = (page - 1) * limit; const where: any = { is_active: true }; if (city) where.users = { city: { contains: city, mode: 'insensitive' } }; const [data, total] = await Promise.all([ this.prisma.professionals.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, email: true, phone: true, picture: true, city: true } }, schedules: true, specializations: true, payment_methods: true, }, }), this.prisma.professionals.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async findById(id: string) { // Accept either the professional UUID or the user_id (legacy Flutter behavior) let prof = await this.prisma.professionals.findUnique({ where: { id }, include: { users: { select: { id: true, name: true, email: true, phone: true, picture: true, city: true } }, schedules: { orderBy: { day_of_week: 'asc' } }, specializations: true, payment_methods: true, }, }); if (!prof) { prof = await this.prisma.professionals.findUnique({ where: { user_id: id }, include: { users: { select: { id: true, name: true, email: true, phone: true, picture: true, city: true } }, schedules: { orderBy: { day_of_week: 'asc' } }, specializations: true, payment_methods: true, }, }); } if (!prof) throw new NotFoundException('Profesional no encontrado'); return prof; } async findByUserId(userId: string) { const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId }, include: { schedules: true, specializations: true, payment_methods: true }, }); if (!prof) throw new NotFoundException('No eres un profesional registrado'); return prof; } async updateById(id: string, data: any) { const prof = await this.prisma.professionals.findUnique({ where: { id } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); return this.prisma.professionals.update({ where: { id }, data }); } async upsert(userId: string, data: any) { const { payment_methods: pm, ...profData } = data; const buildPm = (forCreate = false) => { if (!pm) return undefined; const pmFields = { nequi: pm.nequi ?? false, datafono: pm.datafono ?? false, transferencia: pm.transferencia ?? false }; return forCreate ? { create: pmFields } : { upsert: { update: pmFields, create: pmFields } }; }; const existing = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (!existing) { await this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } }); const createData: any = { user_id: userId, is_active: false, ...profData }; if (pm) createData.payment_methods = buildPm(true); const prof = await this.prisma.professionals.create({ data: createData }); const u = await this.prisma.users.findUnique({ where: { id: userId }, select: { name: true, email: true } }); if (u?.email) this.mail.sendProfessionalSubmitted(u.name || 'Usuario', u.email, data.profession || '').catch(() => {}); this.tryPush(userId, 'Solicitud recibida', 'Hemos recibido tu solicitud profesional. Te avisaremos cuando sea revisada.'); return prof; } // Re-submitting after rejection reset (pro_state=0): move back to pending const user = await this.prisma.users.findUnique({ where: { id: userId }, select: { pro_state: true, name: true, email: true } }); if (user?.pro_state === 0) { const resubData: any = { ...profData, is_active: false }; if (pm) resubData.payment_methods = buildPm(); await this.prisma.$transaction([ this.prisma.professionals.update({ where: { user_id: userId }, data: resubData }), this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } }), ]); if (user.email) this.mail.sendProfessionalSubmitted(user.name || 'Usuario', user.email, data.profession || '').catch(() => {}); this.tryPush(userId, 'Solicitud recibida', 'Hemos recibido tu solicitud profesional. Te avisaremos cuando sea revisada.'); return this.prisma.professionals.findUnique({ where: { user_id: userId } }); } const updateData: any = { ...profData }; if (pm) updateData.payment_methods = buildPm(); return this.prisma.professionals.update({ where: { user_id: userId }, data: updateData }); } async updateSchedules(professionalId: string, schedules: any[]) { const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); for (const s of schedules) { if (s.range1_hour1 && s.range1_hour2 && s.range1_hour1 >= s.range1_hour2) { throw new BadRequestException(`Día ${s.day_of_week}: range1_hour1 debe ser menor que range1_hour2`); } } const result = await this.prisma.$transaction(async (tx: any) => { await tx.schedules.deleteMany({ where: { professional_id: professionalId } }); if (schedules.length > 0) { await tx.schedules.createMany({ data: schedules.map((s: any) => ({ professional_id: professionalId, day_of_week: s.day_of_week, enabled: s.enabled ?? false, continuous_day: s.continuous_day ?? false, range1_hour1: s.range1_hour1 ? new Date(`1970-01-01T${s.range1_hour1}:00`) : null, range1_hour2: s.range1_hour2 ? new Date(`1970-01-01T${s.range1_hour2}:00`) : null, range2_hour1: s.range2_hour1 ? new Date(`1970-01-01T${s.range2_hour1}:00`) : null, range2_hour2: s.range2_hour2 ? new Date(`1970-01-01T${s.range2_hour2}:00`) : null, })), }); } return tx.schedules.findMany({ where: { professional_id: professionalId } }); }); return result; } async findPendingApprovals(page = 1, limit = 20) { const skip = (page - 1) * limit; const where = { is_active: false, users: { pro_state: 1 } }; const [data, total] = await Promise.all([ this.prisma.professionals.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, email: true, phone: true, created_at: true, pro_state: true } } }, }), this.prisma.professionals.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async findRejected(page = 1, limit = 20) { const skip = (page - 1) * limit; const where = { is_active: false, users: { pro_state: 3 } }; const [data, total] = await Promise.all([ this.prisma.professionals.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, email: true, phone: true, created_at: true, pro_state: true } } }, }), this.prisma.professionals.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async approve(professionalId: string) { const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); await this.prisma.$transaction([ this.prisma.professionals.update({ where: { id: professionalId }, data: { is_active: true } }), this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 2 } }), ]); const u = await this.prisma.users.findUnique({ where: { id: prof.user_id }, select: { name: true, email: true } }); if (u?.email) this.mail.sendStatusChanged(u.name || 'Profesional', u.email, 'approved').catch(() => {}); this.tryPush(prof.user_id, '¡Solicitud aprobada! 🎉', 'Tu perfil profesional ha sido aprobado en ProsApp.'); return { message: 'Profesional aprobado' }; } async deny(professionalId: string) { const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); await this.prisma.$transaction([ this.prisma.professionals.update({ where: { id: professionalId }, data: { is_active: false, updated_at: new Date() } }), this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 3 } }), ]); const u = await this.prisma.users.findUnique({ where: { id: prof.user_id }, select: { name: true, email: true } }); if (u?.email) this.mail.sendStatusChanged(u.name || 'Profesional', u.email, 'rejected').catch(() => {}); this.tryPush(prof.user_id, 'Actualización de tu solicitud', 'Tu solicitud profesional fue rechazada. Puedes volver a intentarlo.'); return { message: 'Solicitud rechazada' }; } async resetRejected(userId: string) { // Always reset pro_state regardless of whether a professional record exists await this.prisma.users.update({ where: { id: userId }, data: { pro_state: 0 } }); return { message: 'Solicitud reiniciada' }; } async deactivate(id: string) { const prof = await this.prisma.professionals.findUnique({ where: { id } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); await this.prisma.$transaction([ this.prisma.professionals.update({ where: { id }, data: { is_active: false } }), this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 3 } }), ]); const u = await this.prisma.users.findUnique({ where: { id: prof.user_id }, select: { name: true, email: true } }); if (u?.email) this.mail.sendStatusChanged(u.name || 'Profesional', u.email, 'deactivated').catch(() => {}); this.tryPush(prof.user_id, 'Cuenta desactivada', 'Tu cuenta profesional ha sido desactivada.'); return { message: 'Profesional desactivado' }; } async setPending(id: string) { const prof = await this.prisma.professionals.findUnique({ where: { id } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); await this.prisma.$transaction([ this.prisma.professionals.update({ where: { id }, data: { is_active: false } }), this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 1 } }), ]); const u = await this.prisma.users.findUnique({ where: { id: prof.user_id }, select: { name: true, email: true } }); if (u?.email) this.mail.sendStatusChanged(u.name || 'Profesional', u.email, 'pending').catch(() => {}); this.tryPush(prof.user_id, 'Solicitud en revisión', 'Tu solicitud está siendo revisada por el equipo de ProsApp.'); return { message: 'Profesional puesto en revisión' }; } async validateRethus(id: string) { const prof = await this.prisma.professionals.findUnique({ where: { id } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); await this.prisma.professionals.update({ where: { id }, data: { rethus_validated: true } }); return { message: 'RETHUS validado correctamente' }; } async requestProfessional(userId: string, data: any) { const existing = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (existing) throw new BadRequestException('Ya tienes una solicitud de profesional'); await this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } }); return this.prisma.professionals.create({ data: { user_id: userId, identification: data.identification, profession: data.profession, address: data.address, additional_address: data.additional_address, rethus_code: data.rethus_code ?? null, identification_picture: data.identification_picture, certificate_picture: data.certificate_picture, }, }); } }