ads.service.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {
  2. Injectable,
  3. BadRequestException,
  4. NotFoundException,
  5. } from '@nestjs/common';
  6. import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
  7. import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
  8. import { CreateAdsDto, ListAdsDto, UpdateAdsDto } from './ads.dto';
  9. import { CommonStatus } from '../common/status.enum';
  10. @Injectable()
  11. export class AdsService {
  12. constructor(private readonly mongoPrismaService: MongoPrismaService) {}
  13. /**
  14. * Current epoch time in milliseconds.
  15. *
  16. * NOTE:
  17. * - Kept as `number` for backward compatibility.
  18. * - If you migrate to BigInt timestamps, update this and the Prisma schema.
  19. */
  20. private now(): number {
  21. return Date.now();
  22. }
  23. private trimOptional(value?: string | null) {
  24. if (value === undefined) return undefined;
  25. if (value === null) return null;
  26. return typeof value === 'string' ? value.trim() : value;
  27. }
  28. /**
  29. * Validate that expiryDt is not earlier than startDt.
  30. * Throws 400 if invalid.
  31. */
  32. private ensureTimeRange(startDt: number, expiryDt: number): void {
  33. if (expiryDt < startDt) {
  34. throw new BadRequestException(
  35. 'expiryDt must be greater than or equal to startDt',
  36. );
  37. }
  38. }
  39. /**
  40. * Ensure the channel exists.
  41. */
  42. private async assertChannelExists(channelId: string): Promise<void> {
  43. const exists = await this.mongoPrismaService.channel.findUnique({
  44. where: { id: channelId },
  45. select: { id: true },
  46. });
  47. if (!exists) {
  48. throw new NotFoundException('Channel not found');
  49. }
  50. }
  51. async create(dto: CreateAdsDto) {
  52. await this.assertChannelExists(dto.channelId);
  53. this.ensureTimeRange(dto.startDt, dto.expiryDt);
  54. const now = this.now();
  55. return this.mongoPrismaService.ads.create({
  56. data: {
  57. channelId: dto.channelId,
  58. adsModuleId: dto.adsModuleId,
  59. advertiser: dto.advertiser,
  60. title: dto.title,
  61. adsContent: this.trimOptional(dto.adsContent) ?? null,
  62. adsCoverImg: this.trimOptional(dto.adsCoverImg) ?? null,
  63. adsUrl: this.trimOptional(dto.adsUrl) ?? null,
  64. startDt: dto.startDt,
  65. expiryDt: dto.expiryDt,
  66. seq: dto.seq ?? 0,
  67. status: dto.status ?? CommonStatus.enabled,
  68. createAt: now,
  69. updateAt: now,
  70. },
  71. include: { channel: true, adsModule: true },
  72. });
  73. }
  74. async update(dto: UpdateAdsDto) {
  75. await this.assertChannelExists(dto.channelId);
  76. this.ensureTimeRange(dto.startDt, dto.expiryDt);
  77. const now = this.now();
  78. // Build data object carefully to avoid unintended field changes
  79. const data: any = {
  80. channelId: dto.channelId,
  81. adsModuleId: dto.adsModuleId,
  82. advertiser: dto.advertiser,
  83. title: dto.title,
  84. adsContent: this.trimOptional(dto.adsContent) ?? null,
  85. adsCoverImg: this.trimOptional(dto.adsCoverImg) ?? null,
  86. adsUrl: this.trimOptional(dto.adsUrl) ?? null,
  87. startDt: dto.startDt,
  88. expiryDt: dto.expiryDt,
  89. seq: dto.seq ?? 0,
  90. updateAt: now,
  91. };
  92. // Only update status if explicitly provided,
  93. // to avoid silently re-enabling disabled ads.
  94. if (dto.status !== undefined) {
  95. data.status = dto.status;
  96. }
  97. try {
  98. return await this.mongoPrismaService.ads.update({
  99. where: { id: dto.id },
  100. data,
  101. include: { channel: true, adsModule: true },
  102. });
  103. } catch (e) {
  104. if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
  105. throw new NotFoundException('Ads not found');
  106. }
  107. throw e;
  108. }
  109. }
  110. async findOne(id: string) {
  111. const row = await this.mongoPrismaService.ads.findUnique({
  112. where: { id },
  113. include: { channel: true, adsModule: true },
  114. });
  115. if (!row) {
  116. throw new NotFoundException('Ads not found');
  117. }
  118. return row;
  119. }
  120. async list(dto: ListAdsDto) {
  121. const where: any = {};
  122. if (dto.title) {
  123. where.title = { contains: dto.title, mode: 'insensitive' };
  124. }
  125. if (dto.advertiser) {
  126. where.advertiser = { contains: dto.advertiser, mode: 'insensitive' };
  127. }
  128. if (dto.adsModuleId) {
  129. where.adsModuleId = dto.adsModuleId;
  130. }
  131. if (dto.channelId) {
  132. where.channelId = dto.channelId;
  133. }
  134. if (dto.status !== undefined) {
  135. where.status = dto.status;
  136. }
  137. // Defensive guards for pagination
  138. const page = dto.page && dto.page > 0 ? dto.page : 1;
  139. const size = dto.size && dto.size > 0 ? dto.size : 10;
  140. const [total, data] = await this.mongoPrismaService.$transaction([
  141. this.mongoPrismaService.ads.count({ where }),
  142. this.mongoPrismaService.ads.findMany({
  143. where,
  144. orderBy: { updateAt: 'desc' },
  145. skip: (page - 1) * size,
  146. take: size,
  147. include: {
  148. channel: true,
  149. adsModule: true,
  150. },
  151. }),
  152. ]);
  153. return {
  154. total,
  155. data,
  156. totalPages: Math.ceil(total / size),
  157. page,
  158. size,
  159. };
  160. }
  161. async remove(id: string) {
  162. try {
  163. await this.mongoPrismaService.ads.delete({ where: { id } });
  164. return { message: 'Deleted' };
  165. } catch (e) {
  166. if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
  167. throw new NotFoundException('Ads not found');
  168. }
  169. throw e;
  170. }
  171. }
  172. // get list of ads modules
  173. async listAdsModules() {
  174. const adsModules = await this.mongoPrismaService.adsModule.findMany({
  175. orderBy: { seq: 'asc' },
  176. });
  177. return adsModules;
  178. }
  179. }