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

300 lines
9.2 KiB
TypeScript

import { Coupon, ICoupon, CouponDiscountType, CouponApplicableService } from '../models/Coupon';
export interface CreateCouponDTO {
codigo: string;
descripcion?: string;
tipoDescuento: CouponDiscountType;
valorDescuento: number;
aplicaPara: CouponApplicableService;
montoMinimoSubtotal?: number;
limiteUsosTotales?: number | null;
fechaExpiracion?: Date | string | null;
estaActivo?: boolean;
}
export interface CouponValidationResult {
valid: boolean;
coupon?: {
id: string;
codigo: string;
descripcion: string;
tipoDescuento: CouponDiscountType;
valorDescuento: number;
aplicaPara: CouponApplicableService;
};
subtotalOriginal: number;
descuentoCalculado: number;
totalFinal: number;
message: string;
}
export class CouponService {
private static instance: CouponService;
private constructor() {}
public static getInstance(): CouponService {
if (!CouponService.instance) {
CouponService.instance = new CouponService();
}
return CouponService.instance;
}
/**
* Inicializa cupones predeterminados si la colección está vacía
*/
public async ensureSeedCoupons(): Promise<void> {
try {
const count = await Coupon.countDocuments();
if (count === 0) {
await Coupon.create([
{
codigo: 'PACIFICO10',
descripcion: '10% de descuento en viajes de pasajeros y encomiendas',
tipoDescuento: 'PORCENTAJE',
valorDescuento: 10,
aplicaPara: 'TODOS',
montoMinimoSubtotal: 20000,
estaActivo: true,
},
{
codigo: 'BONOVIAJE5K',
descripcion: 'Bono fijo de $5.000 COP de descuento en reserva de cupos de viaje',
tipoDescuento: 'MONTO_FIJO',
valorDescuento: 5000,
aplicaPara: 'VIAJES',
montoMinimoSubtotal: 35000,
estaActivo: true,
},
{
codigo: 'ENVIOYA',
descripcion: 'Bono de $4.000 COP de descuento en fletes de mensajería y paquetería',
tipoDescuento: 'MONTO_FIJO',
valorDescuento: 4000,
aplicaPara: 'ENCOMIENDAS',
montoMinimoSubtotal: 25000,
estaActivo: true,
},
{
codigo: 'PROMO20',
descripcion: 'Super Bono del 20% de descuento especial',
tipoDescuento: 'PORCENTAJE',
valorDescuento: 20,
aplicaPara: 'TODOS',
montoMinimoSubtotal: 50000,
estaActivo: true,
},
]);
console.log('🎟️ [Coupons Seeded] Cupones iniciales creados exitosamente en MongoDB.');
}
} catch (err) {
console.warn('⚠️ [Coupons Seed Warning]:', err);
}
}
/**
* Obtiene todos los cupones para el panel de administración
*/
public async getAllCoupons(): Promise<ICoupon[]> {
await this.ensureSeedCoupons();
return Coupon.find().sort({ createdAt: -1 });
}
/**
* Crea un nuevo cupón desde el panel administrativo
*/
public async createCoupon(data: CreateCouponDTO): Promise<ICoupon> {
const cleanCode = (data.codigo || '').trim().toUpperCase();
if (!cleanCode) {
throw new Error('El código del cupón es obligatorio.');
}
const existing = await Coupon.findOne({ codigo: cleanCode });
if (existing) {
throw new Error(`El cupón con código "${cleanCode}" ya existe.`);
}
const valor = parseFloat(String(data.valorDescuento));
if (isNaN(valor) || valor <= 0) {
throw new Error('El valor del descuento debe ser mayor a cero.');
}
if (data.tipoDescuento === 'PORCENTAJE' && valor > 100) {
throw new Error('El porcentaje de descuento no puede ser mayor al 100%.');
}
let expDate: Date | null = null;
if (data.fechaExpiracion) {
expDate = new Date(data.fechaExpiracion);
}
const coupon = await Coupon.create({
codigo: cleanCode,
descripcion: (data.descripcion || '').trim(),
tipoDescuento: data.tipoDescuento || 'PORCENTAJE',
valorDescuento: valor,
aplicaPara: data.aplicaPara || 'TODOS',
montoMinimoSubtotal: data.montoMinimoSubtotal ? Math.max(0, parseFloat(String(data.montoMinimoSubtotal))) : 0,
limiteUsosTotales: data.limiteUsosTotales ? parseInt(String(data.limiteUsosTotales), 10) : null,
fechaExpiracion: expDate,
estaActivo: data.estaActivo !== undefined ? data.estaActivo : true,
});
console.log(`🎟️ [Coupon Created] Cupón creado: ${coupon.codigo} (${coupon.tipoDescuento}: ${coupon.valorDescuento})`);
return coupon;
}
/**
* Valida un cupón de descuento para un servicio y monto determinados
*/
public async validateCoupon(
code: string,
serviceType: 'VIAJES' | 'ENCOMIENDAS',
subtotal: number
): Promise<CouponValidationResult> {
await this.ensureSeedCoupons();
const cleanCode = (code || '').trim().toUpperCase();
if (!cleanCode) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: 'Por favor ingresa un código de cupón.',
};
}
const coupon = await Coupon.findOne({ codigo: cleanCode });
if (!coupon) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `El cupón "${cleanCode}" no existe o no es válido.`,
};
}
if (!coupon.estaActivo) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `El cupón "${cleanCode}" se encuentra actualmente inactivo.`,
};
}
if (coupon.fechaExpiracion && new Date() > new Date(coupon.fechaExpiracion)) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `El cupón "${cleanCode}" ha expirado el ${coupon.fechaExpiracion.toLocaleDateString('es-CO')}.`,
};
}
if (coupon.limiteUsosTotales && coupon.canjesActuales >= coupon.limiteUsosTotales) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `El cupón "${cleanCode}" ha alcanzado su límite máximo de canjes disponibles.`,
};
}
if (coupon.aplicaPara !== 'TODOS' && coupon.aplicaPara !== serviceType) {
const servicioPermitido = coupon.aplicaPara === 'VIAJES' ? 'viajes de pasajeros' : 'encomiendas y fletes';
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `Este cupón solo es válido para ${servicioPermitido}.`,
};
}
if (coupon.montoMinimoSubtotal > 0 && subtotal < coupon.montoMinimoSubtotal) {
return {
valid: false,
subtotalOriginal: subtotal,
descuentoCalculado: 0,
totalFinal: subtotal,
message: `Este cupón requiere un subtotal mínimo de $${coupon.montoMinimoSubtotal.toLocaleString('es-CO')} COP.`,
};
}
// Cálculo del descuento
let descuento = 0;
if (coupon.tipoDescuento === 'PORCENTAJE') {
descuento = Math.round(subtotal * (coupon.valorDescuento / 100));
} else {
descuento = Math.min(coupon.valorDescuento, subtotal);
}
// Evitar que el total quede menor a cero
descuento = Math.min(descuento, subtotal);
const totalFinal = Math.max(0, subtotal - descuento);
const descTexto = coupon.tipoDescuento === 'PORCENTAJE'
? `${coupon.valorDescuento}%`
: `$${coupon.valorDescuento.toLocaleString('es-CO')} COP`;
return {
valid: true,
coupon: {
id: coupon._id.toString(),
codigo: coupon.codigo,
descripcion: coupon.descripcion,
tipoDescuento: coupon.tipoDescuento,
valorDescuento: coupon.valorDescuento,
aplicaPara: coupon.aplicaPara,
},
subtotalOriginal: subtotal,
descuentoCalculado: descuento,
totalFinal,
message: `✅ ¡Cupón "${coupon.codigo}" aplicado con éxito! Ahorro: $${descuento.toLocaleString('es-CO')} COP (${descTexto} OFF).`,
};
}
/**
* Canjea formalmente el cupón al crearse un viaje o encomienda
*/
public async redeemCoupon(code: string): Promise<boolean> {
const cleanCode = (code || '').trim().toUpperCase();
if (!cleanCode) return false;
const res = await Coupon.findOneAndUpdate(
{ codigo: cleanCode, estaActivo: true },
{ $inc: { canjesActuales: 1 } },
{ new: true }
);
return !!res;
}
/**
* Alterna el estado activo/inactivo de un cupón
*/
public async toggleCouponStatus(id: string, estaActivo?: boolean): Promise<ICoupon | null> {
const coupon = await Coupon.findById(id);
if (!coupon) return null;
coupon.estaActivo = estaActivo !== undefined ? estaActivo : !coupon.estaActivo;
await coupon.save();
return coupon;
}
/**
* Elimina un cupón del sistema
*/
public async deleteCoupon(id: string): Promise<boolean> {
const res = await Coupon.findByIdAndDelete(id);
return !!res;
}
}
export const couponService = CouponService.getInstance();