apps/recallassess/recallassess-api/src/api/admin/promo-code/promo-code.service.ts
BNestBaseModuleService
Properties |
|
Methods |
|
constructor(prisma: BNestPrismaService, systemLogService: SystemLogService)
|
|||||||||
|
Parameters :
|
| Async add | ||||||
add(data: any)
|
||||||
|
Override add method to log creation
Parameters :
Returns :
Promise<any>
|
| Async delete | ||||||
delete(id: number)
|
||||||
|
Override delete method to log deletion
Parameters :
Returns :
Promise<void>
|
| Private escapeLikeSegment | ||||||
escapeLikeSegment(s: string)
|
||||||
|
Parameters :
Returns :
string
|
| Async getDetail | ||||||
getDetail(id: number)
|
||||||
|
Override getDetail to ensure proper date transformation
Parameters :
|
| Private Async promoCodeIdsForScalarColumnTextOp | ||||||||||||
promoCodeIdsForScalarColumnTextOp(column: "usage_count" | "usage_limit" | "discount_percentage", operator: string, rawValue: string)
|
||||||||||||
|
Parameters :
Returns :
Promise<number[]>
|
| Async save |
save(id: number, data: any)
|
|
Override save method to log update
Returns :
Promise<any>
|
| Private Async withPromoCodeScalarTextSearchPagination | ||||||
withPromoCodeScalarTextSearchPagination(opts: PaginationOptions)
|
||||||
|
Parameters :
Returns :
Promise<PaginationOptions>
|
| Private Readonly logger |
Type : unknown
|
Default value : new Logger(PromoCodeService.name)
|
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
import { PaginationOptions } from "@bish-nest/core/data/pagination/pagination-options.interface";
import { BNestPrismaService } from "@bish-nest/core/services";
import { SystemLogService } from "@api/shared/services";
import { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { ListResponseDataInterface } from "@bish-nest/core/interfaces/list-response-data.interface";
import { Injectable, Logger, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
import { Prisma, SystemLogEntityType } from "@prisma/client";
import { plainToInstance } from "class-transformer";
@Injectable()
export class PromoCodeService extends BNestBaseModuleService {
private readonly logger = new Logger(PromoCodeService.name);
constructor(
protected prisma: BNestPrismaService,
private readonly systemLogService: SystemLogService,
) {
super();
}
/**
* Advanced search can send text operators (contains / startsWith / endsWith) on numeric columns.
* Prisma does not support those on Int/Decimal; resolve matching ids via SQL and AND them into the list query.
*/
override async getList(paginationOptions: PaginationOptions): Promise<ListResponseDataInterface<any>> {
const listOpts = await this.withPromoCodeScalarTextSearchPagination(paginationOptions);
return super.getList(listOpts);
}
private async withPromoCodeScalarTextSearchPagination(opts: PaginationOptions): Promise<PaginationOptions> {
let listOpts: PaginationOptions = { ...opts };
const extraAnd: Record<string, unknown>[] = [...(listOpts.additionalWhereAnd ?? [])];
if (!listOpts.where || typeof listOpts.where !== "object" || Array.isArray(listOpts.where)) {
return listOpts;
}
const w = { ...(listOpts.where as Record<string, { operator: string; value: string }>) };
let touched = false;
const stringOpsOnScalar = new Set(["contains", "startsWith", "endsWith"]);
for (const fld of ["usage_count", "usage_limit", "discount_percentage"] as const) {
const cell = w[fld];
if (!cell || !stringOpsOnScalar.has(cell.operator)) {
continue;
}
delete w[fld];
touched = true;
const ids = await this.promoCodeIdsForScalarColumnTextOp(fld, cell.operator, String(cell.value ?? ""));
extraAnd.push({ id: { in: ids } });
}
if (!touched) {
return listOpts;
}
return {
...listOpts,
where: Object.keys(w).length > 0 ? (w as PaginationOptions["where"]) : undefined,
additionalWhereAnd: extraAnd,
};
}
private escapeLikeSegment(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
}
private async promoCodeIdsForScalarColumnTextOp(
column: "usage_count" | "usage_limit" | "discount_percentage",
operator: string,
rawValue: string,
): Promise<number[]> {
const v = rawValue.trim();
if (!v) {
return [];
}
const esc = this.escapeLikeSegment(v);
let pattern: string;
if (operator === "contains") {
pattern = `%${esc}%`;
} else if (operator === "startsWith") {
pattern = `${esc}%`;
} else if (operator === "endsWith") {
pattern = `%${esc}`;
} else {
return [];
}
const colFragment =
column === "usage_limit"
? Prisma.sql`COALESCE(usage_limit::text, '')`
: column === "discount_percentage"
? Prisma.sql`discount_percentage::text`
: Prisma.sql`usage_count::text`;
const rows = await this.prisma.client.$queryRaw<{ id: number }[]>(Prisma.sql`
SELECT id FROM promo_code
WHERE ${colFragment} LIKE ${pattern} ESCAPE '\\'
`);
return rows.map((r) => r.id);
}
/**
* Override add method to log creation
*/
async add(data: any): Promise<any> {
const addResponse = await super.add(data);
const promoCode = addResponse.data;
// Log the creation
await this.systemLogService.logInsert(
SystemLogEntityType.MEDIA, // Note: PromoCode might not have its own entity type, using MEDIA as placeholder
promoCode.id,
promoCode as Record<string, unknown>,
);
return addResponse;
}
/**
* Override save method to log update
*/
async save(id: number, data: any): Promise<any> {
// Get old data before update
const oldPromoCode = await this.prisma.client.promoCode.findUnique({
where: { id },
});
if (!oldPromoCode) {
throw new NotFoundException(`Promo code with ID ${id} not found`);
}
const saveResponse = await super.save(id, data);
const updatedPromoCode = saveResponse.data;
// Calculate changed fields and log the update
const changedFields = SystemLogService.calculateChangedFields(
oldPromoCode as Record<string, unknown>,
updatedPromoCode as Record<string, unknown>,
);
await this.systemLogService.logUpdate(
SystemLogEntityType.MEDIA, // Note: PromoCode might not have its own entity type
id,
oldPromoCode as Record<string, unknown>,
updatedPromoCode as Record<string, unknown>,
changedFields,
);
return saveResponse;
}
/**
* Override delete method to log deletion
*/
async delete(id: number): Promise<void> {
// Get promo code data before deletion
const promoCode = await this.prisma.client.promoCode.findUnique({
where: { id },
});
if (!promoCode) {
throw new NotFoundException(`Promo code with ID ${id} not found`);
}
// Call parent delete method
await super.delete(id);
// Log the deletion
await this.systemLogService.logDelete(
SystemLogEntityType.MEDIA, // Note: PromoCode might not have its own entity type
id,
promoCode as Record<string, unknown>,
);
}
/**
* Override getDetail to ensure proper date transformation
*/
async getDetail(id: number): Promise<DetailResponseDataInterface<unknown>> {
const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
const repoName = moduleCurrentCfg.repoName;
const repo = this.commonMethods.getRepo(repoName);
const include: Record<string, unknown> = {
userCreatedBy: {
select: {
id: true,
first_name: true,
last_name: true,
},
},
userUpdatedBy: {
select: {
id: true,
first_name: true,
last_name: true,
},
},
};
const findParams = {
where: { id },
include,
};
let data: any = await repo.findUnique(findParams);
if (!data) {
const msg = "The record you are looking for is not found.";
throw new UnprocessableEntityException(msg);
}
// Sanitize data to ensure dates are properly formatted
const sanitizedData: Record<string, unknown> = { ...data };
// Ensure dates are Date objects (not strings)
if (data['valid_from']) {
sanitizedData['valid_from'] = data['valid_from'] instanceof Date
? data['valid_from']
: new Date(data['valid_from']);
}
if (data['valid_until']) {
sanitizedData['valid_until'] = data['valid_until'] instanceof Date
? data['valid_until']
: new Date(data['valid_until']);
}
// Transform to DTO with excludeExtraneousValues to properly handle @Exclude() and @Expose()
try {
data = plainToInstance(moduleCurrentCfg.detailDto, sanitizedData, {
excludeExtraneousValues: true,
});
} catch (error) {
this.logger.error("Error transforming promo code detail data to DTO:", error);
data = sanitizedData; // Return raw data if transformation fails
}
return this.moduleMethods.getReturnDataForDetail(data);
}
}