import { Types } from 'mongoose'; import { User, IUser } from '../models/User'; import { Trip, ITrip } from '../models/Trip'; import { TarifasConfig } from '../models/TarifasConfig'; import { boldPaymentService, BoldPaymentResponse } from './bold.service'; import { pushNotificationService } from './push-notification.service'; import { whatsAppNotificationService } from './whatsapp-notification.service'; export interface CancelTripResult { success: boolean; message: string; penalidadAplicada: number; nuevoSaldoPenalidad: number; usuarioBloqueado: boolean; linkPagoLiquidacion?: string; qrPagoLiquidacion?: string; } export class CancellationService { private static instance: CancellationService; private constructor() {} public static getInstance(): CancellationService { if (!CancellationService.instance) { CancellationService.instance = new CancellationService(); } return CancellationService.instance; } /** * Procesa la cancelación de un viaje evaluando el estado y aplicando penalidades según las reglas de negocio */ public async procesarCancelacion( tripId: string, usuarioId: string, motivo: string = 'Cancelación solicitada por el usuario' ): Promise { const trip = await Trip.findById(tripId); if (!trip) { throw new Error(`El viaje con ID ${tripId} no fue encontrado.`); } const user = await User.findById(usuarioId); if (!user) { throw new Error(`El usuario con ID ${usuarioId} no fue encontrado.`); } // Regla 1: Viajes ya en curso o completados no admiten cancelación directa if (trip.estado === 'IN_PROGRESS') { throw new Error('No es posible cancelar un viaje que ya se encuentra en progreso. Comuníquese con soporte.'); } if (trip.estado === 'COMPLETED' || trip.estado === 'CANCELLED') { throw new Error(`El viaje ya se encuentra en estado final: ${trip.estado}.`); } // Regla 2: Cancelación cuando el viaje aún está en PENDING_APPROVAL (Gratuita sin penalidad) if (trip.estado === 'PENDING_APPROVAL') { // Remover al pasajero del pool const pasajerosRestantes = trip.pasajeros.filter((p: any) => p.usuarioId.toString() !== usuarioId); if (pasajerosRestantes.length === 0) { trip.estado = 'CANCELLED'; trip.canceladoInfo = { canceladoPor: user._id, motivo, fechaCancelacion: new Date(), penalidadAplicada: 0, }; } else { trip.pasajeros = pasajerosRestantes; // Recalcular finanzas del viaje const config = (await TarifasConfig.findOne({ estaActiva: true })) || (await TarifasConfig.create({})); trip.tarifaTotal = trip.pasajeros.length * (config.tarifaPasajeroFija || 35000); trip.comisionAdmin = Math.round(trip.tarifaTotal * (config.porcentajeComisionAdmin / 100)); trip.pagoConductor = trip.tarifaTotal - trip.comisionAdmin; } await trip.save(); return { success: true, message: 'Viaje cancelado sin penalidad ya que aún no había sido aprobado por el Administrador.', penalidadAplicada: 0, nuevoSaldoPenalidad: user.saldoPenalidad || 0, usuarioBloqueado: (user.saldoPenalidad || 0) > 0, }; } // Regla 3: Cancelación cuando el viaje ya está en ACTIVE (Aprobado y conductor confirmado) // Se genera automáticamente un cargo de penalidad en su cuenta y queda bloqueado if (trip.estado === 'ACTIVE') { const config = (await TarifasConfig.findOne({ estaActiva: true })) || (await TarifasConfig.create({})); const penalidad = config.costoPenalidadCancelacion || 15000; // Aplicar penalidad a la cuenta del usuario en MongoDB user.saldoPenalidad = (user.saldoPenalidad || 0) + penalidad; user.totalViajesCancelados = (user.totalViajesCancelados || 0) + 1; await user.save(); // Actualizar el estado del viaje trip.estado = 'CANCELLED'; trip.canceladoInfo = { canceladoPor: user._id, motivo: `Cancelado por el usuario en estado ACTIVE. Penalidad aplicada: $${penalidad.toLocaleString('es-CO')} COP. Motivo: ${motivo}`, fechaCancelacion: new Date(), penalidadAplicada: penalidad, }; await trip.save(); // Notificar al conductor asignado sobre la cancelación if (trip.conductorId) { await pushNotificationService.notificarCancelacionAConductor( trip.conductorId.toString(), user.nombre, tripId ); } // Generar link de pago inmediato vía Bold para liquidar la sanción const boldPayment = await boldPaymentService.createPayment({ referenceId: user._id.toString(), amount: user.saldoPenalidad, description: `Liquidación de penalidad por cancelación de viaje Cali-Buenaventura #${tripId.slice(-6)}`, payerEmail: user.email, payerPhone: user.telefono, paymentType: 'PENALTY_SETTLEMENT', }); // Notificación inmediata por WhatsApp al usuario con link de pago Bold if (boldPayment.checkoutUrl) { await whatsAppNotificationService.notificarUsuarioPenalidad({ telefono: user.telefono, nombreUsuario: user.nombre, tripId, montoPenalidad: penalidad, linkBoldLiquidacion: boldPayment.checkoutUrl, }); } return { success: true, message: `El viaje ya había sido aprobado. Se aplicó una sanción de $${penalidad.toLocaleString( 'es-CO' )} COP. Su cuenta queda bloqueada para nuevos servicios hasta saldar la penalidad.`, penalidadAplicada: penalidad, nuevoSaldoPenalidad: user.saldoPenalidad, usuarioBloqueado: true, linkPagoLiquidacion: boldPayment.checkoutUrl, qrPagoLiquidacion: boldPayment.qrImageUrl, }; } throw new Error(`Estado de viaje no reconocido para cancelación: ${trip.estado}`); } /** * Valida si un usuario tiene autorización para solicitar servicios (debe tener saldoPenalidad === 0) */ public async validarUsuarioHabilitado(usuarioId: string): Promise<{ habilitado: boolean; saldoPenalidad: number; mensaje?: string; }> { const user = await User.findById(usuarioId); if (!user) { return { habilitado: false, saldoPenalidad: 0, mensaje: 'Usuario no encontrado' }; } const deuda = user.saldoPenalidad || 0; if (deuda > 0) { return { habilitado: false, saldoPenalidad: deuda, mensaje: `Usuario bloqueado. Presenta un saldo pendiente de penalidad de $${deuda.toLocaleString('es-CO')} COP.`, }; } return { habilitado: true, saldoPenalidad: 0, }; } /** * Liquida y restaura el estado del usuario tras el pago verificado de la penalidad */ public async liquidarPenalidad( usuarioId: string, montoPagado: number, referenciaComprobante: string ): Promise<{ success: boolean; nuevoSaldo: number; mensaje: string }> { const user = await User.findById(usuarioId); if (!user) throw new Error('Usuario no encontrado'); const saldoActual = user.saldoPenalidad || 0; const nuevoSaldo = Math.max(0, saldoActual - montoPagado); user.saldoPenalidad = nuevoSaldo; await user.save(); console.log( `💰 [Penalidad Liquidada] Usuario ${user.nombre} (${user.telefono}) pagó $${montoPagado} COP. Ref: ${referenciaComprobante}. Nuevo saldo: $${nuevoSaldo}` ); return { success: true, nuevoSaldo, mensaje: nuevoSaldo === 0 ? 'Penalidad saldada en su totalidad. Su cuenta ha sido desbloqueada satisfactoriamente.' : `Abono de $${montoPagado} procesado. Saldo restante: $${nuevoSaldo}`, }; } } export const cancellationService = CancellationService.getInstance();