|
|
@@ -1,4 +1,8 @@
|
|
|
-import { Injectable } from '@nestjs/common';
|
|
|
+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';
|
|
|
@@ -8,7 +12,14 @@ import { CommonStatus } from '../common/status.enum';
|
|
|
export class AdsService {
|
|
|
constructor(private readonly mongoPrismaService: MongoPrismaService) {}
|
|
|
|
|
|
- private now() {
|
|
|
+ /**
|
|
|
+ * 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();
|
|
|
}
|
|
|
|
|
|
@@ -18,19 +29,29 @@ export class AdsService {
|
|
|
return typeof value === 'string' ? value.trim() : value;
|
|
|
}
|
|
|
|
|
|
- private ensureTimeRange(startDt: number, expiryDt: number) {
|
|
|
+ /**
|
|
|
+ * Validate that expiryDt is not earlier than startDt.
|
|
|
+ * Throws 400 if invalid.
|
|
|
+ */
|
|
|
+ private ensureTimeRange(startDt: number, expiryDt: number): void {
|
|
|
if (expiryDt < startDt) {
|
|
|
- throw new Error('expiryDt must be greater than or equal to startDt');
|
|
|
+ throw new BadRequestException(
|
|
|
+ 'expiryDt must be greater than or equal to startDt',
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async assertChannelExists(channelId: string) {
|
|
|
+ /**
|
|
|
+ * Ensure the channel exists.
|
|
|
+ */
|
|
|
+ private async assertChannelExists(channelId: string): Promise<void> {
|
|
|
const exists = await this.mongoPrismaService.channel.findUnique({
|
|
|
where: { id: channelId },
|
|
|
select: { id: true },
|
|
|
});
|
|
|
+
|
|
|
if (!exists) {
|
|
|
- throw new Error('Channel not found');
|
|
|
+ throw new NotFoundException('Channel not found');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -39,6 +60,7 @@ export class AdsService {
|
|
|
this.ensureTimeRange(dto.startDt, dto.expiryDt);
|
|
|
|
|
|
const now = this.now();
|
|
|
+
|
|
|
return this.mongoPrismaService.ads.create({
|
|
|
data: {
|
|
|
channelId: dto.channelId,
|
|
|
@@ -63,27 +85,36 @@ export class AdsService {
|
|
|
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,
|
|
|
+ adsModule: dto.adsModule,
|
|
|
+ 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: {
|
|
|
- channelId: dto.channelId,
|
|
|
- adsModule: dto.adsModule,
|
|
|
- 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,
|
|
|
- updateAt: now,
|
|
|
- },
|
|
|
+ data,
|
|
|
});
|
|
|
} catch (e) {
|
|
|
if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
|
|
|
- throw new Error('Ads not found');
|
|
|
+ throw new NotFoundException('Ads not found');
|
|
|
}
|
|
|
throw e;
|
|
|
}
|
|
|
@@ -93,7 +124,11 @@ export class AdsService {
|
|
|
const row = await this.mongoPrismaService.ads.findUnique({
|
|
|
where: { id },
|
|
|
});
|
|
|
- if (!row) throw new Error('Ads not found');
|
|
|
+
|
|
|
+ if (!row) {
|
|
|
+ throw new NotFoundException('Ads not found');
|
|
|
+ }
|
|
|
+
|
|
|
return row;
|
|
|
}
|
|
|
|
|
|
@@ -109,25 +144,33 @@ export class AdsService {
|
|
|
if (dto.adsModule) {
|
|
|
where.adsModule = { contains: dto.adsModule, mode: 'insensitive' };
|
|
|
}
|
|
|
- if (dto.channelId) where.channelId = dto.channelId;
|
|
|
- if (dto.status !== undefined) where.status = dto.status;
|
|
|
+ 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: (dto.page - 1) * dto.size,
|
|
|
- take: dto.size,
|
|
|
+ skip: (page - 1) * size,
|
|
|
+ take: size,
|
|
|
}),
|
|
|
]);
|
|
|
|
|
|
return {
|
|
|
total,
|
|
|
data,
|
|
|
- totalPages: Math.ceil(total / dto.size),
|
|
|
- page: dto.page,
|
|
|
- size: dto.size,
|
|
|
+ totalPages: Math.ceil(total / size),
|
|
|
+ page,
|
|
|
+ size,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -137,7 +180,7 @@ export class AdsService {
|
|
|
return { message: 'Deleted' };
|
|
|
} catch (e) {
|
|
|
if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
|
|
|
- throw new Error('Ads not found');
|
|
|
+ throw new NotFoundException('Ads not found');
|
|
|
}
|
|
|
throw e;
|
|
|
}
|