import { Injectable, BadRequestException, NotFoundException, } from '@nestjs/common'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service'; import { CreateAdsDto, ListAdsDto, UpdateAdsDto } from './ads.dto'; import { CommonStatus } from '../common/status.enum'; @Injectable() export class AdsService { constructor(private readonly mongoPrismaService: MongoPrismaService) {} /** * Current epoch time in milliseconds. * * NOTE: * - Kept as `number` for backward compatibility. * - If you migrate to BigInt timestamps, update this and the Prisma schema. */ private now(): number { return Date.now(); } private trimOptional(value?: string | null) { if (value === undefined) return undefined; if (value === null) return null; return typeof value === 'string' ? value.trim() : value; } /** * Validate that expiryDt is not earlier than startDt. * Throws 400 if invalid. */ private ensureTimeRange(startDt: number, expiryDt: number): void { if (expiryDt < startDt) { throw new BadRequestException( 'expiryDt must be greater than or equal to startDt', ); } } /** * Ensure the channel exists. */ private async assertChannelExists(channelId: string): Promise { const exists = await this.mongoPrismaService.channel.findUnique({ where: { id: channelId }, select: { id: true }, }); if (!exists) { throw new NotFoundException('Channel not found'); } } async create(dto: CreateAdsDto) { await this.assertChannelExists(dto.channelId); this.ensureTimeRange(dto.startDt, dto.expiryDt); const now = this.now(); return this.mongoPrismaService.ads.create({ data: { channelId: dto.channelId, adsModuleId: dto.adsModuleId, advertiser: dto.advertiser, title: dto.title, adsContent: this.trimOptional(dto.adsContent) ?? null, adsCoverImg: this.trimOptional(dto.adsCoverImg) ?? null, adsUrl: this.trimOptional(dto.adsUrl) ?? null, startDt: dto.startDt, expiryDt: dto.expiryDt, seq: dto.seq ?? 0, status: dto.status ?? CommonStatus.enabled, createAt: now, updateAt: now, }, include: { channel: true, adsModule: true }, }); } async update(dto: UpdateAdsDto) { await this.assertChannelExists(dto.channelId); this.ensureTimeRange(dto.startDt, dto.expiryDt); const now = this.now(); // Build data object carefully to avoid unintended field changes const data: any = { channelId: dto.channelId, adsModuleId: dto.adsModuleId, advertiser: dto.advertiser, title: dto.title, adsContent: this.trimOptional(dto.adsContent) ?? null, adsCoverImg: this.trimOptional(dto.adsCoverImg) ?? null, adsUrl: this.trimOptional(dto.adsUrl) ?? null, startDt: dto.startDt, expiryDt: dto.expiryDt, seq: dto.seq ?? 0, updateAt: now, }; // Only update status if explicitly provided, // to avoid silently re-enabling disabled ads. if (dto.status !== undefined) { data.status = dto.status; } try { return await this.mongoPrismaService.ads.update({ where: { id: dto.id }, data, include: { channel: true, adsModule: true }, }); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') { throw new NotFoundException('Ads not found'); } throw e; } } async findOne(id: string) { const row = await this.mongoPrismaService.ads.findUnique({ where: { id }, include: { channel: true, adsModule: true }, }); if (!row) { throw new NotFoundException('Ads not found'); } return row; } async list(dto: ListAdsDto) { const where: any = {}; if (dto.title) { where.title = { contains: dto.title, mode: 'insensitive' }; } if (dto.advertiser) { where.advertiser = { contains: dto.advertiser, mode: 'insensitive' }; } if (dto.adsModuleId) { where.adsModuleId = dto.adsModuleId; } if (dto.channelId) { where.channelId = dto.channelId; } if (dto.status !== undefined) { where.status = dto.status; } // Defensive guards for pagination const page = dto.page && dto.page > 0 ? dto.page : 1; const size = dto.size && dto.size > 0 ? dto.size : 10; const [total, data] = await this.mongoPrismaService.$transaction([ this.mongoPrismaService.ads.count({ where }), this.mongoPrismaService.ads.findMany({ where, orderBy: { updateAt: 'desc' }, skip: (page - 1) * size, take: size, include: { channel: true, adsModule: true, }, }), ]); return { total, data, totalPages: Math.ceil(total / size), page, size, }; } async remove(id: string) { try { await this.mongoPrismaService.ads.delete({ where: { id } }); return { message: 'Deleted' }; } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') { throw new NotFoundException('Ads not found'); } throw e; } } // get list of ads modules async listAdsModules() { const adsModules = await this.mongoPrismaService.adsModule.findMany({ orderBy: { seq: 'asc' }, }); return adsModules; } }