import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; import { CreatePromotionDto } from './promos.dto'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; @Injectable() export class PromosService { constructor(private prisma: PrismaService) {} getActive() { return this.prisma.promotion.findMany({ where: { active: true, validUntil: { gte: new Date() } }, orderBy: { createdAt: 'desc' }, }); } async getAll(filters: { search?: string; active?: boolean; page?: number; pageSize?: number }) { const { search, active, page = 1, pageSize = 10 } = filters; const skip = (page - 1) * pageSize; const where: any = {}; if (search) { where.OR = [ { code: { contains: search, mode: 'insensitive' } }, { title: { contains: search, mode: 'insensitive' } }, ]; } if (active !== undefined) { where.active = active; } const [items, total] = await Promise.all([ this.prisma.promotion.findMany({ where, skip, take: pageSize, orderBy: { createdAt: 'desc' }, }), this.prisma.promotion.count({ where }), ]); return { items: this.formatItems(items), total, page, pageSize }; } async getById(id: string) { const promo = await this.prisma.promotion.findUnique({ where: { id } }); if (!promo) throw new NotFoundException('Promo not found'); return this.formatItem(promo); } async validate(code: string) { const promo = await this.prisma.promotion.findUnique({ where: { code } }); if (!promo || !promo.active || promo.validUntil < new Date()) return { applicable: false, message: 'Promo code invalid or expired' }; return { code: promo.code, percentOff: promo.percentOff, amountOffMinor: promo.amountOffMinor, validUntil: promo.validUntil, applicable: true, message: promo.percentOff ? `${promo.percentOff}% off` : `ETB ${((promo.amountOffMinor ?? 0) / 100).toFixed(2)} off`, }; } async create(dto: CreatePromotionDto & { discountType?: string; discountValue?: number }) { try { // Map frontend fields to database fields let percentOff: number | undefined; let amountOffMinor: number | undefined; if (dto.discountType && dto.discountValue !== undefined) { if (dto.discountType === 'PERCENTAGE') { percentOff = dto.discountValue; } else if (dto.discountType === 'FIXED') { amountOffMinor = dto.discountValue; } } else { // Fallback to direct fields percentOff = dto.percentOff; amountOffMinor = dto.amountOffMinor; } const promo = await this.prisma.promotion.create({ data: { code: dto.code, title: dto.title, subtitle: dto.subtitle, percentOff, amountOffMinor, validUntil: new Date(dto.validUntil), ctaLabel: dto.ctaLabel, deepLink: dto.deepLink, active: dto.active ?? true, }, }); return this.formatItem(promo); } catch (error) { if (error instanceof PrismaClientKnownRequestError) { if (error.code === 'P2002') { const field = (error.meta?.target as string[])?.[0]; throw new BadRequestException( `A promo code with this ${field} already exists. Please use a different ${field}.`, ); } } throw error; } } async update(id: string, dto: Partial & { discountType?: string; discountValue?: number }) { const promo = await this.prisma.promotion.findUnique({ where: { id } }); if (!promo) throw new NotFoundException('Promo not found'); const updateData: any = {}; // Map frontend fields to database fields if (dto.discountType && dto.discountValue !== undefined) { // Clear existing discount fields updateData.percentOff = null; updateData.amountOffMinor = null; if (dto.discountType === 'PERCENTAGE') { updateData.percentOff = dto.discountValue; } else if (dto.discountType === 'FIXED') { updateData.amountOffMinor = dto.discountValue; } } else { // Only include fields that are explicitly provided if (dto.percentOff !== undefined) updateData.percentOff = dto.percentOff; if (dto.amountOffMinor !== undefined) updateData.amountOffMinor = dto.amountOffMinor; } if (dto.title !== undefined) updateData.title = dto.title; if (dto.subtitle !== undefined) updateData.subtitle = dto.subtitle; if (dto.ctaLabel !== undefined) updateData.ctaLabel = dto.ctaLabel; if (dto.deepLink !== undefined) updateData.deepLink = dto.deepLink; if (dto.active !== undefined) updateData.active = dto.active; if (dto.validUntil !== undefined) updateData.validUntil = new Date(dto.validUntil); // Don't allow updating code - it's immutable after creation try { const updated = await this.prisma.promotion.update({ where: { id }, data: updateData, }); return this.formatItem(updated); } catch (error) { if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') { const field = (error.meta?.target as string[])?.[0]; throw new BadRequestException( `A promo code with this ${field} already exists. Please use a different ${field}.`, ); } throw error; } } async delete(id: string) { const promo = await this.prisma.promotion.findUnique({ where: { id } }); if (!promo) throw new NotFoundException('Promo not found'); return this.prisma.promotion.delete({ where: { id } }); } private formatItem(promo: any) { return { id: promo.id, code: promo.code, title: promo.title, discountType: promo.percentOff ? 'PERCENTAGE' : 'FIXED', discountValue: promo.percentOff || promo.amountOffMinor || 0, maxDiscount: undefined, minBookingAmount: undefined, maxUsagePerUser: undefined, totalUsageLimit: undefined, usageCount: 0, validFrom: promo.createdAt, validUntil: promo.validUntil, isActive: promo.active, createdAt: promo.createdAt, updatedAt: promo.createdAt, }; } private formatItems(promos: any[]) { return promos.map((promo) => this.formatItem(promo)); } }