almeira-web/transporte-cali-buenaventura/backend/src/services/cancellation.service.ts

219 lines
7.7 KiB
TypeScript

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<CancelTripResult> {
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();