import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { NotificationsService } from '../notifications/notifications.service'; import { ServiceStatus, VALID_TRANSITIONS } from './dto/service.dto'; const STATUS_MESSAGES: Partial> = { [ServiceStatus.ACCEPTED]: { title: 'Servicio aceptado', body: 'El profesional aceptó tu solicitud.' }, [ServiceStatus.DENIED]: { title: 'Servicio rechazado', body: 'El profesional rechazó tu solicitud.' }, [ServiceStatus.CANCELLED]:{ title: 'Servicio cancelado', body: 'El servicio fue cancelado.' }, [ServiceStatus.ACTIVE]: { title: 'Servicio iniciado', body: 'El profesional inició el servicio.' }, [ServiceStatus.COMPLETED]:{ title: 'Servicio completado', body: '¡El servicio ha finalizado! Puedes dejar tu puntuación.' }, }; @Injectable() export class ServicesService { constructor( private prisma: PrismaService, private notifications: NotificationsService, ) {} async create(data: { professional_id: string; user_id: string; day: string; description?: string; rate?: number; range1_hour1?: string; range1_hour2?: string; address?: string; latitude?: number; longitude?: number; location_preference?: 'office' | 'delivery'; }) { // Accept either professional UUID or user_id (legacy Flutter behavior) let prof = await this.prisma.professionals.findUnique({ where: { id: data.professional_id } }); if (!prof) prof = await this.prisma.professionals.findUnique({ where: { user_id: data.professional_id } }); if (!prof || !prof.is_active) throw new BadRequestException('Profesional no disponible'); return this.prisma.services.create({ data: { professional_id: prof.id, user_id: data.user_id, day: new Date(data.day), description: data.description, rate: data.rate ?? prof.rate, range1_hour1: data.range1_hour1 ? new Date(`1970-01-01T${data.range1_hour1}:00`) : null, range1_hour2: data.range1_hour2 ? new Date(`1970-01-01T${data.range1_hour2}:00`) : null, address: data.address, latitude: data.latitude, longitude: data.longitude, location_preference: data.location_preference as any, }, }); } async updateStatus(serviceId: string, newStatus: ServiceStatus, userId: string) { const service = await this.prisma.services.findUnique({ where: { id: serviceId } }); if (!service) throw new NotFoundException('Servicio no encontrado'); const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); const allowedTransitions = VALID_TRANSITIONS[service.status]; if (!allowedTransitions || !allowedTransitions.includes(newStatus)) { throw new BadRequestException( `Transición inválida: ${service.status} → ${newStatus}. Permitidas: ${allowedTransitions?.join(', ') || 'ninguna'}`, ); } if (newStatus === ServiceStatus.ACCEPTED || newStatus === ServiceStatus.DENIED) { if (!prof || prof.id !== service.professional_id) { throw new ForbiddenException('Solo el profesional puede aceptar/rechazar'); } } if (newStatus === ServiceStatus.CANCELLED) { if (service.user_id !== userId && (!prof || prof.id !== service.professional_id)) { throw new ForbiddenException('Solo el usuario o el profesional pueden cancelar'); } } if (newStatus === ServiceStatus.COMPLETED) { if (!prof || prof.id !== service.professional_id) { throw new ForbiddenException('Solo el profesional puede marcar como completado'); } } const updated = await this.prisma.services.update({ where: { id: serviceId }, data: { status: newStatus as any }, }); // Notify the other party const msg = STATUS_MESSAGES[newStatus]; if (msg) { const targetUserId = userId === service.user_id ? (await this.prisma.professionals.findUnique({ where: { id: service.professional_id } }))?.user_id : service.user_id; if (targetUserId) { const target = await this.prisma.users.findUnique({ where: { id: targetUserId }, select: { fcm_token: true } }); if (target?.fcm_token) { this.notifications.send(target.fcm_token, msg.title, msg.body).catch(() => {}); } } } return updated; } async findByUser(userId: string, page = 1, limit = 20) { const skip = (page - 1) * limit; const where = { user_id: userId, status: { notIn: ['completed' as const, 'cancelled' as const, 'denied' as const] } }; const [data, total] = await Promise.all([ this.prisma.services.findMany({ where, skip, take: limit, include: { professionals: { include: { users: true } } }, orderBy: { created_at: 'desc' }, }), this.prisma.services.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async findByProfessional(userId: string, page = 1, limit = 20) { const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (!prof) throw new NotFoundException('No eres un profesional'); const where = { professional_id: prof.id }; const skip = (page - 1) * limit; const [data, total] = await Promise.all([ this.prisma.services.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, picture: true, phone: true } } }, orderBy: { created_at: 'desc' }, }), this.prisma.services.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async findRequestsByProfessional(userId: string, page = 1, limit = 20) { const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (!prof) throw new NotFoundException('No eres un profesional'); const where = { professional_id: prof.id, status: 'pending' as const }; const skip = (page - 1) * limit; const [data, total] = await Promise.all([ this.prisma.services.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, picture: true, phone: true } } }, orderBy: { created_at: 'desc' }, }), this.prisma.services.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async findById(id: string) { const service = await this.prisma.services.findUnique({ where: { id }, include: { users: { select: { id: true, name: true, picture: true, phone: true } }, professionals: { include: { users: { select: { name: true, picture: true } } } }, }, }); if (!service) throw new NotFoundException('Servicio no encontrado'); return service; } async getHistoryByUser(userId: string, page = 1, limit = 20) { const skip = (page - 1) * limit; const where = { user_id: userId, status: { in: ['completed' as const, 'cancelled' as const] } }; const [data, total] = await Promise.all([ this.prisma.services.findMany({ where, skip, take: limit, include: { professionals: { include: { users: { select: { name: true, picture: true } } } } }, orderBy: { day: 'desc' }, }), this.prisma.services.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async getHistoryByProfessional(userId: string, page = 1, limit = 20) { const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (!prof) throw new NotFoundException('No eres un profesional'); const where = { professional_id: prof.id, status: { in: ['completed' as const, 'cancelled' as const] } }; const skip = (page - 1) * limit; const [data, total] = await Promise.all([ this.prisma.services.findMany({ where, skip, take: limit, include: { users: { select: { id: true, name: true, picture: true } } }, orderBy: { day: 'desc' }, }), this.prisma.services.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } async getCalendarByProfessional(userId: string) { const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); if (!prof) throw new NotFoundException('No eres un profesional'); return this.prisma.services.findMany({ where: { professional_id: prof.id, status: { notIn: ['denied', 'cancelled'] } }, orderBy: { day: 'asc' }, }); } async getPublicCalendar(professionalId: string) { const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } }); if (!prof) throw new NotFoundException('Profesional no encontrado'); const schedules = await this.prisma.schedules.findMany({ where: { professional_id: professionalId, enabled: true }, }); const services = await this.prisma.services.findMany({ where: { professional_id: professionalId, status: { notIn: ['denied', 'cancelled'] } }, }); return { schedules, services }; } async findAll(page = 1, limit = 20) { const skip = (page - 1) * limit; const [data, total] = await Promise.all([ this.prisma.services.findMany({ skip, take: limit, include: { users: { select: { id: true, name: true } }, professionals: { include: { users: { select: { name: true } } } }, }, orderBy: { created_at: 'desc' }, }), this.prisma.services.count(), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } }; } }