File

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

Description

PromoCodeService

Admin API — Promo Code (Promo Code). Business Logic Service for RecallAssess.

Extends

BNestBaseModuleService

Index

Properties
Methods

Methods

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" | "discount_amount", operator: string, rawValue: string)
Parameters :
Name Type Optional
column "usage_count" | "usage_limit" | "discount_percentage" | "discount_amount" No
operator string No
rawValue string No
Returns : Promise<number[]>
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 { bnestPlainToDto } from "@bish-nest/core";
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 { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { ListResponseDataInterface } from "@bish-nest/core/interfaces/list-response-data.interface";
import { Injectable, Logger, UnprocessableEntityException } from "@nestjs/common";
import { Prisma } from "@prisma/client";

/**
 * PromoCodeService
 *
 * Admin API — Promo Code (Promo Code). Business Logic Service for RecallAssess.
 */


@Injectable()
export class PromoCodeService extends BNestBaseModuleService {
  private readonly logger = new Logger(PromoCodeService.name);

  /**
   * 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", "discount_amount"] 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" | "discount_amount",
    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`COALESCE(discount_percentage::text, '')`
          : column === "discount_amount"
            ? Prisma.sql`COALESCE(discount_amount::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 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']);
    }

    try {
      data = bnestPlainToDto(sanitizedData, moduleCurrentCfg.detailDto);
    } catch (error) {
      this.logger.error("Error transforming promo code detail data to DTO:", error);
      data = sanitizedData;
    }

    return this.moduleMethods.getReturnDataForDetail(data);
  }
}

results matching ""

    No results matching ""