import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { CreateCouponDto } from '../dto/create-coupon.dto'; import { UpdateCouponDto } from '../dto/update-coupon.dto'; import { FindCouponsDto } from '../dto/find-coupons.dto'; import { CouponRepository } from '../repositories/coupon.repository'; import { RestRepository } from '../../restaurants/repositories/rest.repository'; import { EntityManager } from '@mikro-orm/postgresql'; import { RequiredEntityData } from '@mikro-orm/core'; import { Coupon, CouponType } from '../entities/coupon.entity'; import { CouponMessage, RestMessage } from 'src/common/enums/message.enum'; import { randomBytes } from 'node:crypto'; @Injectable() export class CouponService { constructor( private readonly couponRepository: CouponRepository, private readonly restRepository: RestRepository, private readonly em: EntityManager, ) {} async create(restId: string, createCouponDto: CreateCouponDto): Promise { const restaurant = await this.restRepository.findOne({ id: restId }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } // Check if code already exists const existingCoupon = await this.couponRepository.findOne({ code: createCouponDto.code }); if (existingCoupon) { throw new BadRequestException(CouponMessage.CODE_ALREADY_EXISTS); } // Validate dates if (createCouponDto.startDate && createCouponDto.endDate) { const startDate = new Date(createCouponDto.startDate); const endDate = new Date(createCouponDto.endDate); if (endDate <= startDate) { throw new BadRequestException(CouponMessage.END_DATE_MUST_BE_AFTER_START_DATE); } } // Validate percentage coupon if (createCouponDto.type === CouponType.PERCENTAGE) { if (createCouponDto.value < 0 || createCouponDto.value > 100) { throw new BadRequestException(CouponMessage.PERCENTAGE_MUST_BE_BETWEEN_0_AND_100); } } const data: RequiredEntityData = { restaurant, code: createCouponDto.code, name: createCouponDto.name, description: createCouponDto.description, type: createCouponDto.type, value: createCouponDto.value, maxDiscount: createCouponDto.maxDiscount, minOrderAmount: createCouponDto.minOrderAmount, maxUses: createCouponDto.maxUses, maxUsesPerUser: createCouponDto.maxUsesPerUser, startDate: createCouponDto.startDate ? new Date(createCouponDto.startDate) : undefined, endDate: createCouponDto.endDate ? new Date(createCouponDto.endDate) : undefined, isActive: createCouponDto.isActive ?? true, usedCount: 0, foodCategories: createCouponDto.foodCategories, foods: createCouponDto.foods, userPhone: createCouponDto.userPhone, }; const coupon = this.couponRepository.create(data); if (!coupon) { throw new Error('Failed to create coupon entity'); } await this.em.persistAndFlush(coupon); return coupon; } findAll(restId: string, dto: FindCouponsDto) { return this.couponRepository.findAllPaginated(restId, dto); } async findById(id: string): Promise { const coupon = await this.couponRepository.findOne({ id }, { populate: ['restaurant'] }); if (!coupon) { throw new NotFoundException(CouponMessage.NOT_FOUND); } return coupon; } async findByCode(code: string, restId: string): Promise { const coupon = await this.couponRepository.findOne({ code, restaurant: { id: restId } }); if (!coupon) { throw new NotFoundException(CouponMessage.NOT_FOUND); } return coupon; } async update(id: string, dto: UpdateCouponDto): Promise { const coupon = await this.couponRepository.findOne({ id }); if (!coupon) { throw new NotFoundException(CouponMessage.NOT_FOUND); } // Check if code is being changed and if it already exists if (dto.code && dto.code !== coupon.code) { const existingCoupon = await this.couponRepository.findOne({ code: dto.code }); if (existingCoupon) { throw new BadRequestException(CouponMessage.CODE_ALREADY_EXISTS); } } // Validate dates const startDate = dto.startDate ? new Date(dto.startDate) : coupon.startDate; const endDate = dto.endDate ? new Date(dto.endDate) : coupon.endDate; if (startDate && endDate && endDate <= startDate) { throw new BadRequestException(CouponMessage.END_DATE_MUST_BE_AFTER_START_DATE); } // Validate percentage coupon if (dto.type === CouponType.PERCENTAGE || coupon.type === CouponType.PERCENTAGE) { const value = dto.value ?? coupon.value; if (value < 0 || value > 100) { throw new BadRequestException(CouponMessage.PERCENTAGE_MUST_BE_BETWEEN_0_AND_100); } } // Update fields if (dto.code) coupon.code = dto.code; if (dto.name) coupon.name = dto.name; if (dto.description !== undefined) coupon.description = dto.description; if (dto.type) coupon.type = dto.type; if (dto.value !== undefined) coupon.value = dto.value; if (dto.maxDiscount !== undefined) coupon.maxDiscount = dto.maxDiscount; if (dto.minOrderAmount !== undefined) coupon.minOrderAmount = dto.minOrderAmount; if (dto.maxUses !== undefined) coupon.maxUses = dto.maxUses; if (dto.maxUsesPerUser !== undefined) coupon.maxUsesPerUser = dto.maxUsesPerUser; if (dto.startDate !== undefined) coupon.startDate = dto.startDate ? new Date(dto.startDate) : undefined; if (dto.endDate !== undefined) coupon.endDate = dto.endDate ? new Date(dto.endDate) : undefined; if (dto.isActive !== undefined) coupon.isActive = dto.isActive; if (dto.foodCategories !== undefined) coupon.foodCategories = dto.foodCategories; if (dto.foods !== undefined) coupon.foods = dto.foods; if (dto.userPhone !== undefined) coupon.userPhone = dto.userPhone; await this.em.persistAndFlush(coupon); return coupon; } async remove(id: string) { const coupon = await this.couponRepository.findOne({ id }); if (!coupon) { throw new NotFoundException(CouponMessage.NOT_FOUND); } coupon.deletedAt = new Date(); await this.em.persistAndFlush(coupon); } /** * Validate and calculate discount for a coupon */ async validateCoupon( code: string, restId: string, orderAmount: number, ): Promise<{ valid: boolean; coupon?: Coupon; message?: string; }> { const coupon = await this.couponRepository.findOne( { code, restaurant: { id: restId } }, { populate: ['restaurant'] }, ); if (!coupon) { return { valid: false, message: CouponMessage.NOT_FOUND, }; } // Check if coupon is active if (!coupon.isActive) { return { valid: false, message: CouponMessage.COUPON_INACTIVE, }; } // Check date validity const now = new Date(); if (coupon.startDate && now < coupon.startDate) { return { valid: false, message: CouponMessage.COUPON_NOT_STARTED, }; } if (coupon.endDate && now > coupon.endDate) { return { valid: false, message: CouponMessage.COUPON_EXPIRED, }; } // Check max uses if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { return { valid: false, message: CouponMessage.COUPON_MAX_USES_REACHED, }; } // Check minimum order amount if (coupon.minOrderAmount && orderAmount < coupon.minOrderAmount) { return { valid: false, message: CouponMessage.MIN_ORDER_AMOUNT_NOT_MET, }; } return { valid: true, coupon, }; } /** * Increment coupon usage count */ async incrementUsage(id: string): Promise { const coupon = await this.couponRepository.findOne({ id }); if (coupon) { coupon.usedCount += 1; await this.em.persistAndFlush(coupon); } } generateShortCode(length = 8) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const bytes = randomBytes(length); let result = ''; for (let i = 0; i < bytes.length; i++) { result += chars[bytes[i] % chars.length]; } return result; } async generateCouponCode(restId: string): Promise { const code = this.generateShortCode(8); const existing = await this.couponRepository.findOne({ code, restaurant: { id: restId } }); if (existing) return this.generateCouponCode(restId); return code; } }