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 { 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 { await this.ensureSeedCoupons(); return Coupon.find().sort({ createdAt: -1 }); } /** * Crea un nuevo cupón desde el panel administrativo */ public async createCoupon(data: CreateCouponDTO): Promise { 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 { 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 { 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 { 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 { const res = await Coupon.findByIdAndDelete(id); return !!res; } } export const couponService = CouponService.getInstance();