File

apps/recallassess/recallassess-api/src/api/admin/promo-code/promo-code.service.ts

Extends

BNestBaseModuleService

Index

Properties
Methods

Constructor

constructor(prisma: BNestPrismaService, systemLogService: SystemLogService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
systemLogService SystemLogService No

Methods

Async add
add(data: any)

Override add method to log creation

Parameters :
Name Type Optional
data any No
Returns : Promise<any>
Async delete
delete(id: number)

Override delete method to log deletion

Parameters :
Name Type Optional
id number No
Returns : Promise<void>
Private escapeLikeSegment
escapeLikeSegment(s: string)
Parameters :
Name Type Optional
s string No
Returns : string
Async getDetail
getDetail(id: number)

Override getDetail to ensure proper date transformation

Parameters :
Name Type Optional
id number No
Async getList
getList(paginationOptions: PaginationOptions)

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.

Parameters :
Name Type Optional
paginationOptions PaginationOptions No
Returns : Promise<ListResponseDataInterface<any>>
Private Async promoCodeIdsForScalarColumnTextOp
promoCodeIdsForScalarColumnTextOp(column: "usage_count" | "usage_limit" | "discount_percentage", operator: string, rawValue: string)
Parameters :
Name Type Optional
column "usage_count" | "usage_limit" | "discount_percentage" No
operator string No
rawValue string No
Returns : Promise<number[]>
Async save
save(id: number, data: any)

Override save method to log update

Parameters :
Name Type Optional
id number No
data any No
Returns : Promise<any>
Private Async withPromoCodeScalarTextSearchPagination
withPromoCodeScalarTextSearchPagination(opts: PaginationOptions)
Parameters :
Name Type Optional
opts PaginationOptions No
Returns : Promise<PaginationOptions>

Properties

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);
  }
}

results matching ""

    No results matching ""