219 lines
7.7 KiB
TypeScript
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();
|