File

apps/recallassess/recallassess-api/src/api/admin/invoice/invoice.service.ts

Extends

BNestBaseModuleService

Index

Properties
Methods

Constructor

constructor(prisma: BNestPrismaService, systemLogService: SystemLogService, stripeService: StripeService, stripePaymentService: StripePaymentService, invoicePdfService: InvoicePdfService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
systemLogService SystemLogService No
stripeService StripeService No
stripePaymentService StripePaymentService No
invoicePdfService InvoicePdfService No

Methods

Async add
add(data: any)

Override add method to log creation

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

Override delete method to log deletion

Parameters :
Name Type Optional
id number No
Returns : Promise<void>
Private Async ensureTotalFromLineItemsIfZero
ensureTotalFromLineItemsIfZero(invoiceId: number)

When Stripe totals are 0/missing but line items were saved correctly, align total_amount for display.

Parameters :
Name Type Optional
invoiceId number No
Returns : Promise<void>
Async generatePdf
generatePdf(id: number, authorization?: string)

Generate or retrieve invoice PDF via the shared InvoicePdfService (Angular SSR fluid invoice report + Puppeteer — same as dashboard invoice PDF).

Parameters :
Name Type Optional
id number No
authorization string Yes
Async getDetail
getDetail(id: number)

Override getDetail to ensure proper data transformation with company and subscription names

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

Override getList method to transform raw data and include company/subscription names

Parameters :
Name Type Optional
paginationOptions PaginationOptions No
Async getPdfBuffer
getPdfBuffer(id: number, authorization?: string)

Get PDF buffer and filename for streaming download. Delegates to the shared InvoicePdfService; used by the download-pdf endpoint so the file is served by our API (avoids opening S3 URL in the browser on UAT).

Parameters :
Name Type Optional
id number No
authorization string Yes
Returns : Promise<literal type>
Async refreshFromStripe
refreshFromStripe(id: number)

Admin action: refresh a local Invoice row from Stripe using its Stripe ids.

  • If the invoice has a subscription-based Stripe invoice id, we delegate to StripePaymentService.handleInvoicePaid so renewal math + fields stay consistent.
  • If only a bare stripe_invoice_id exists (no subscription link), we still use Stripe to align amounts.
Parameters :
Name Type Optional
id number No
Async save
save(id: number, data: any)

Override save: invoices are otherwise read-only in admin UI — only billing_reference_notes may be updated.

Parameters :
Name Type Optional
id number No
data any No
Returns : Promise<any>

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(InvoiceService.name)
import { InvoicePdfService } from "@api/shared/invoice/invoice-pdf.service";
import { SystemLogService } from "@api/shared/services";
import { StripeService } from "@api/shared/stripe/services/stripe.service";
import { StripePaymentService } from "@api/shared/stripe/services/stripe-payment.service";
import { buildListResponseData } 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 { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { ListResponseDataInterface } from "@bish-nest/core/interfaces/list-response-data.interface";
import { BNestPrismaService } from "@bish-nest/core/services";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { SystemLogEntityType } from "@prisma/client";
import { plainToInstance } from "class-transformer";
import Stripe from "stripe";
import { InvoiceGeneratePdfResponseDto } from "./dto/invoice-generate-pdf-response.dto";

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

  constructor(
    protected prisma: BNestPrismaService,
    private readonly systemLogService: SystemLogService,
    private readonly stripeService: StripeService,
    private readonly stripePaymentService: StripePaymentService,
    private readonly invoicePdfService: InvoicePdfService,
  ) {
    super();
  }

  /**
   * Override add method to log creation
   */
  async add(data: any): Promise<any> {
    const addResponse = await super.add(data);
    const invoice = addResponse.data;

    // Log the creation
    await this.systemLogService.logInsert(
      SystemLogEntityType.INVOICE,
      invoice.id,
      invoice as Record<string, unknown>,
      { company_id: invoice.company_id },
    );

    return addResponse;
  }

  /**
   * Override save: invoices are otherwise read-only in admin UI — only {@link billing_reference_notes} may be updated.
   */
  async save(id: number, data: any): Promise<any> {
    const oldInvoice = await this.prisma.client.invoice.findUnique({
      where: { id },
    });

    if (!oldInvoice) {
      throw new NotFoundException(`Invoice with ID ${id} not found`);
    }

    if (!data || typeof data !== "object" || !("billing_reference_notes" in data)) {
      return this.getDetail(id);
    }

    const raw = data["billing_reference_notes"];
    const billing_reference_notes = raw === null || raw === undefined || raw === "" ? null : String(raw);

    await this.prisma.client.invoice.update({
      where: { id },
      data: { billing_reference_notes },
    });

    const newRow = await this.prisma.client.invoice.findUnique({ where: { id } });
    const changedFields = SystemLogService.calculateChangedFields(
      oldInvoice as Record<string, unknown>,
      (newRow ?? oldInvoice) as Record<string, unknown>,
    );

    if (Object.keys(changedFields).length > 0) {
      await this.systemLogService.logUpdate(
        SystemLogEntityType.INVOICE,
        id,
        oldInvoice as Record<string, unknown>,
        (newRow ?? oldInvoice) as Record<string, unknown>,
        changedFields,
        { company_id: oldInvoice.company_id },
      );
    }

    return this.getDetail(id);
  }

  /**
   * Override delete method to log deletion
   */
  async delete(id: number): Promise<void> {
    // Get invoice data before deletion
    const invoice = await this.prisma.client.invoice.findUnique({
      where: { id },
    });

    if (!invoice) {
      throw new NotFoundException(`Invoice with ID ${id} not found`);
    }

    // Call parent delete method
    await super.delete(id);

    // Log the deletion
    await this.systemLogService.logDelete(SystemLogEntityType.INVOICE, id, invoice as Record<string, unknown>, {
      company_id: invoice.company_id,
    });
  }

  /**
   * Generate or retrieve invoice PDF via the shared {@link InvoicePdfService}
   * (Angular SSR fluid **invoice** report + Puppeteer — same as dashboard invoice PDF).
   */
  async generatePdf(id: number, authorization?: string): Promise<InvoiceGeneratePdfResponseDto> {
    const url = await this.invoicePdfService.generateSignedUrl(id, 3600, authorization);
    return { url };
  }

  /**
   * Get PDF buffer and filename for streaming download. Delegates to the
   * shared {@link InvoicePdfService}; used by the download-pdf endpoint so
   * the file is served by our API (avoids opening S3 URL in the browser on UAT).
   */
  async getPdfBuffer(id: number, authorization?: string): Promise<{ buffer: Buffer; filename: string }> {
    return this.invoicePdfService.getPdfBuffer(id, authorization);
  }

  /**
   * Override getDetail to ensure proper data transformation with company and subscription names
   */
  async getDetail(id: number): Promise<DetailResponseDataInterface<unknown>> {
    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
    const repoName = moduleCurrentCfg.repoName;
    const repo = this.commonMethods.getRepo(repoName);

    const include = {
      company: {
        select: {
          id: true,
          name: true,
        },
      },
      subscription: {
        select: {
          id: true,
          company_id: true,
          package_id: true,
          status: true,
          billing_cycle: true,
          package: {
            select: {
              id: true,
              name: true,
            },
          },
        },
      },
      // Pull the parent invoice so the admin UI can render a "Relates to
      // #INV-…" link for amendment rows (UPGRADE_PRORATION, CYCLE_CHANGE,
      // DOWNGRADE_CREDIT, REFUND) in a single request.
      parentInvoice: {
        select: {
          id: true,
          invoice_number: true,
        },
      },
      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,
    };

    // Get the data
    let data: any = await repo.findUnique(findParams);
    if (!data) {
      throw new Error("The record you are looking for is not found.");
    }

    // Sanitize data to remove Decimal fields and set display names
    const sanitizedData: Record<string, unknown> = { ...data };

    // Extract company fields
    const company = data["company"] as { id?: number; name?: string } | null | undefined;
    if (company) {
      sanitizedData["company"] = {
        id: company.id,
        name: company.name || "",
      };
      sanitizedData["company_name"] = company.name || "";
    } else {
      sanitizedData["company_name"] = "";
    }

    // Extract subscription fields
    const subscription = data["subscription"] as
      | {
          id?: number;
          package?: { id?: number; name?: string } | null;
        }
      | null
      | undefined;
    if (subscription) {
      sanitizedData["subscription"] = {
        id: subscription.id,
      };
      // Add subscription display field with package name (e.g., "Trial - Subscription")
      const packageName = subscription.package?.name || "";
      if (packageName) {
        sanitizedData["subscription_display"] = `${packageName} - Subscription`;
      } else {
        sanitizedData["subscription_display"] = "Subscription";
      }
    } else {
      sanitizedData["subscription_display"] = "";
    }

    // Flatten parent invoice relation -> `parent_invoice_number` for the DTO,
    // so the admin UI can show "Relates to #INV-…" for amendment invoices
    // without a second lookup.
    const parentInvoice = data["parentInvoice"] as
      | { id?: number; invoice_number?: string }
      | null
      | undefined;
    sanitizedData["parent_invoice_number"] = parentInvoice?.invoice_number ?? null;

    // Normalize Decimal fields (convert Prisma Decimal to number, handle undefined) before plainToInstance
    const decimalFields = [
      "unit_price_per_license",
      "license_quantity",
      "proration_ratio",
      "proration_amount",
      "discount_amount",
      "processing_fee_percentage",
      "processing_fee",
      "vat_fee_percentage",
      "vat_fee",
      "gross_license_amount",
      "subtotal_amount",
      "pre_vat_total_amount",
      "total_amount",
    ];

    decimalFields.forEach((field) => {
      const value = (sanitizedData as any)[field];
      if (value === undefined || value === null) {
        // Optional money/percentage fields become null; required totals default to 0
        if (
          field === "proration_ratio" ||
          field === "proration_amount" ||
          field === "discount_amount" ||
          field === "processing_fee_percentage" ||
          field === "processing_fee" ||
          field === "vat_fee_percentage" ||
          field === "vat_fee" ||
          field === "gross_license_amount" ||
          field === "pre_vat_total_amount"
        ) {
          (sanitizedData as any)[field] = null;
        } else {
          (sanitizedData as any)[field] = 0;
        }
      } else if (typeof value === "object" && value !== null && "toNumber" in value) {
        // Prisma Decimal
        (sanitizedData as any)[field] = (value as { toNumber: () => number }).toNumber();
      } else if (typeof value === "string") {
        (sanitizedData as any)[field] = parseFloat(value) || 0;
      }
    });

    // 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 invoice detail data to DTO:", error);
      // Return sanitized data directly if transformation fails
      return this.moduleMethods.getReturnDataForDetail(sanitizedData);
    }

    return this.moduleMethods.getReturnDataForDetail(data);
  }

  private decimalLikeToNumber(v: unknown): number {
    if (v === null || v === undefined) return 0;
    if (typeof v === "number") return Number.isFinite(v) ? v : 0;
    if (typeof v === "bigint") return Number(v);
    if (typeof v === "string") {
      const n = parseFloat(v);
      return Number.isFinite(n) ? n : 0;
    }
    if (
      typeof v === "object" &&
      v !== null &&
      "toNumber" in v &&
      typeof (v as { toNumber: () => number }).toNumber === "function"
    ) {
      const n = (v as { toNumber: () => number }).toNumber();
      return Number.isFinite(n) ? n : 0;
    }
    const n = Number(v);
    return Number.isFinite(n) ? n : 0;
  }

  /**
   * When Stripe totals are 0/missing but line items were saved correctly, align total_amount for display.
   */
  private async ensureTotalFromLineItemsIfZero(invoiceId: number): Promise<void> {
    const row = await this.prisma.client.invoice.findUnique({
      where: { id: invoiceId },
      select: {
        total_amount: true,
        subtotal_amount: true,
        processing_fee: true,
        vat_fee: true,
        pre_vat_total_amount: true,
        gross_license_amount: true,
        discount_amount: true,
        proration_amount: true,
      },
    });
    if (!row) return;
    const total = this.decimalLikeToNumber(row["total_amount"]);
    if (total > 0.001) return;
    const preVatStored = this.decimalLikeToNumber(row["pre_vat_total_amount"]);
    const sub = this.decimalLikeToNumber(row["subtotal_amount"]);
    const proc = this.decimalLikeToNumber(row["processing_fee"]);
    const vat = this.decimalLikeToNumber(row["vat_fee"]);
    const proration = this.decimalLikeToNumber(row["proration_amount"]);
    const discount = this.decimalLikeToNumber(row["discount_amount"]);
    const preVat =
      preVatStored > 0.001 ? preVatStored : Math.round((sub + proration + proc) * 100) / 100;
    const computed = Math.round((preVat + vat) * 100) / 100;
    if (computed > 0.001) {
      const patch: {
        total_amount: number;
        pre_vat_total_amount: number;
        gross_license_amount?: number;
      } = {
        total_amount: computed,
        pre_vat_total_amount: preVat,
      };
      if (this.decimalLikeToNumber(row["gross_license_amount"]) <= 0.001) {
        patch.gross_license_amount = Math.round((sub + discount) * 100) / 100;
      }
      await this.prisma.client.invoice.update({
        where: { id: invoiceId },
        data: patch,
      });
      this.logger.log(
        `Invoice ${invoiceId}: set total_amount=${computed.toFixed(2)} from subtotal+fees (Stripe totals were unusable).`,
      );
    }
  }

  /**
   * Admin action: refresh a local Invoice row from Stripe using its Stripe ids.
   * - If the invoice has a subscription-based Stripe invoice id, we delegate to StripePaymentService.handleInvoicePaid
   *   so renewal math + fields stay consistent.
   * - If only a bare stripe_invoice_id exists (no subscription link), we still use Stripe to align amounts.
   */
  async refreshFromStripe(id: number): Promise<DetailResponseDataInterface<unknown>> {
    const invoice = await this.prisma.client.invoice.findUnique({
      where: { id },
      select: {
        id: true,
        company_id: true,
        stripe_invoice_id: true,
        total_amount: true,
      },
    });

    if (!invoice) {
      throw new NotFoundException(`Invoice with ID ${id} not found`);
    }
    if (!invoice.stripe_invoice_id) {
      throw new BadRequestException("Invoice has no stripe_invoice_id to refresh from.");
    }
    if (!this.stripeService.isConfigured()) {
      throw new BadRequestException("Stripe is not configured.");
    }

    // Fetch latest Stripe invoice.
    const stripeInv = (await this.stripeService.getInvoiceExpanded(
      invoice.stripe_invoice_id,
    )) as Stripe.Invoice & { subscription?: string | Stripe.Subscription | null };
    const pickPositive = (v: number | null | undefined): number | null =>
      typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
    // Stripe can report amount_paid=0 for paid invoices covered by credits/balance; total still reflects invoice amount.
    const stripeTotalCents =
      pickPositive(stripeInv.total) ??
      pickPositive(stripeInv.amount_paid) ??
      pickPositive(stripeInv.amount_due) ??
      0;

    // If this is a subscription invoice, reuse the webhook handler so logic stays consistent.
    const subRef = stripeInv.subscription;
    const subscriptionId = typeof subRef === "string" ? subRef : (subRef?.id ?? null);
    if (subscriptionId) {
      await this.stripePaymentService.handleInvoicePaid(stripeInv);
      // Safety net: if webhook-style handler kept/left total at 0 but Stripe total is known, align local value.
      if (stripeTotalCents > 0) {
        const existingTotal =
          typeof invoice.total_amount === "number"
            ? invoice.total_amount
            : invoice.total_amount && typeof (invoice.total_amount as any).toNumber === "function"
              ? (invoice.total_amount as any).toNumber()
              : Number(invoice.total_amount ?? 0);
        if (!Number.isFinite(existingTotal) || existingTotal <= 0) {
          await this.prisma.client.invoice.update({
            where: { id },
            data: { total_amount: stripeTotalCents / 100 },
          });
        }
      }
    } else {
      // One-off / standalone invoice (no subscription): align total_amount with Stripe total when known.
      if (stripeTotalCents > 0) {
        await this.prisma.client.invoice.update({
          where: { id },
          data: {
            total_amount: stripeTotalCents / 100,
          },
        });
      }
    }

    const extractedIds = this.stripePaymentService.extractStripePaymentIdsFromInvoice(stripeInv);
    const { paymentIntentId } = extractedIds;
    let { chargeId, checkoutSessionId } = extractedIds;

    if (paymentIntentId && !chargeId) {
      try {
        const pi = await this.stripeService.retrievePaymentIntent(paymentIntentId, ["latest_charge"]);
        const lc = pi.latest_charge;
        if (typeof lc === "string") {
          chargeId = lc;
        } else if (lc && typeof lc === "object" && "id" in lc) {
          chargeId = String((lc as Stripe.Charge).id);
        }
        const cs = (
          pi as Stripe.PaymentIntent & {
            checkout_session?: string | Stripe.Checkout.Session | null;
          }
        ).checkout_session;
        if (!checkoutSessionId) {
          checkoutSessionId =
            typeof cs === "string" ? cs : cs && typeof cs === "object" && "id" in cs ? cs.id : null;
        }
      } catch (e) {
        this.logger.warn(
          `refreshFromStripe: could not load PaymentIntent ${paymentIntentId} for charge id: ${
            e instanceof Error ? e.message : String(e)
          }`,
        );
      }
    }

    if (!chargeId) {
      const invCharge = (stripeInv as Stripe.Invoice & { charge?: string | Stripe.Charge | null }).charge;
      if (typeof invCharge === "string") {
        chargeId = invCharge;
      } else if (invCharge && typeof invCharge === "object" && "id" in invCharge) {
        chargeId = String(invCharge.id);
      }
    }

    const chargeCheckoutPatch: { stripe_charge_id?: string; stripe_checkout_session_id?: string } = {};
    if (chargeId) chargeCheckoutPatch.stripe_charge_id = chargeId;
    if (checkoutSessionId) chargeCheckoutPatch.stripe_checkout_session_id = checkoutSessionId;
    if (Object.keys(chargeCheckoutPatch).length > 0) {
      try {
        await this.prisma.client.invoice.update({
          where: { id },
          data: chargeCheckoutPatch,
        });
      } catch (e) {
        this.logger.warn(
          `refreshFromStripe: could not persist stripe_charge_id for invoice ${id}: ${
            e instanceof Error ? e.message : String(e)
          }`,
        );
      }
    }

    if (paymentIntentId) {
      try {
        await this.prisma.client.invoice.update({
          where: { id },
          data: { stripe_payment_intent_id: paymentIntentId },
        });
      } catch (e) {
        this.logger.warn(
          `refreshFromStripe: could not persist stripe_payment_intent_id for invoice ${id} (unique conflict or read-only): ${
            e instanceof Error ? e.message : String(e)
          }`,
        );
      }
    }

    await this.ensureTotalFromLineItemsIfZero(id);

    // Return updated detail using the existing method (includes company/subscription display fields).
    return this.getDetail(id);
  }

  /**
   * Override getList method to transform raw data and include company/subscription names
   */
  async getList(paginationOptions: PaginationOptions): Promise<ListResponseDataInterface<unknown>> {
    const { page, limit } = paginationOptions;

    // Get raw data from parent method's moduleMethods
    const [rawData, totalCount] = await this.moduleMethods.getListRows(paginationOptions);

    // Debug: Log if no data is returned
    if (!rawData || rawData.length === 0) {
      console.log("InvoiceService.getList: No raw data returned from getListRows");
      console.log("Pagination options:", paginationOptions);
    } else {
      console.log(`InvoiceService.getList: Found ${rawData.length} invoices, totalCount: ${totalCount}`);
    }

    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
    const packageRepo = this.commonMethods.getRepo("package");

    // Collect unique package IDs to fetch package names
    const packageIds = new Set<number>();
    rawData.forEach((row: Record<string, unknown>) => {
      const subscription = row["subscription"] as { package_id?: number } | null | undefined;
      if (subscription?.package_id) {
        packageIds.add(subscription.package_id);
      }
    });

    // Fetch all packages in one query
    const packages =
      packageIds.size > 0
        ? await packageRepo.findMany({
            where: { id: { in: Array.from(packageIds) } },
            select: { id: true, name: true },
          })
        : [];

    // Create a map of package_id -> package name
    const packageMap = new Map<number, string>();
    packages.forEach((pkg: { id: number; name: string }) => {
      packageMap.set(pkg.id, pkg.name);
    });

    // Transform raw data to add company_name and subscription info
    const sanitizedData = rawData.map((row: Record<string, unknown>) => {
      const sanitizedRow: Record<string, unknown> = { ...row };

      // Extract company name from company relation
      const company = row["company"] as { id?: number; name?: string } | null | undefined;
      if (company) {
        sanitizedRow["company"] = {
          id: company.id,
          name: company.name || "",
        };
        sanitizedRow["company_name"] = company.name || "";
      } else {
        sanitizedRow["company_name"] = "";
      }

      // Extract subscription info
      const subscription = row["subscription"] as
        | {
            id?: number;
            status?: string;
            package_id?: number;
          }
        | null
        | undefined;
      if (subscription) {
        sanitizedRow["subscription"] = {
          id: subscription.id,
          status: subscription.status,
          package_id: subscription.package_id,
        };
        // Add subscription display field with package name (e.g., "Growth Package - Subscription")
        const packageName = subscription.package_id ? packageMap.get(subscription.package_id) || "" : "";
        if (packageName) {
          sanitizedRow["subscription_display"] = `${packageName} - Subscription`;
        } else {
          sanitizedRow["subscription_display"] = "Subscription";
        }
      } else {
        sanitizedRow["subscription_display"] = "";
      }

      // Ensure all Decimal fields have valid values (convert Prisma Decimal to number, handle undefined)
      const decimalFields = [
        "unit_price_per_license",
        "license_quantity",
        "proration_ratio",
        "proration_amount",
        "discount_amount",
        "processing_fee_percentage",
        "processing_fee",
        "vat_fee_percentage",
        "vat_fee",
        "gross_license_amount",
        "subtotal_amount",
        "pre_vat_total_amount",
        "total_amount",
      ];

      decimalFields.forEach((field) => {
        const value = sanitizedRow[field];
        if (value === undefined || value === null) {
          // Set default based on field type
          if (
            field === "proration_ratio" ||
            field === "proration_amount" ||
            field === "discount_amount" ||
            field === "processing_fee_percentage" ||
            field === "processing_fee" ||
            field === "vat_fee_percentage" ||
            field === "vat_fee" ||
            field === "gross_license_amount" ||
            field === "pre_vat_total_amount"
          ) {
            sanitizedRow[field] = null;
          } else {
            sanitizedRow[field] = 0;
          }
        } else if (typeof value === "object" && value !== null && "toNumber" in value) {
          // Prisma Decimal object - convert to number
          sanitizedRow[field] = (value as { toNumber: () => number }).toNumber();
        } else if (typeof value === "string") {
          // String representation of decimal - convert to number
          sanitizedRow[field] = parseFloat(value) || 0;
        }
        // If it's already a number, leave it as is
      });

      return sanitizedRow;
    });

    // Convert the sanitized data to appropriate DTOs
    let data: any[] = sanitizedData;

    try {
      if (paginationOptions.vl) {
        if (moduleCurrentCfg.simpleDto) {
          data = sanitizedData.map((row: any) =>
            plainToInstance(moduleCurrentCfg.simpleDto, row, { excludeExtraneousValues: true }),
          );
        }
      } else {
        if (moduleCurrentCfg.listDto) {
          data = sanitizedData.map((row: any) =>
            plainToInstance(moduleCurrentCfg.listDto, row, { excludeExtraneousValues: true }),
          );
        }
      }
    } catch (error) {
      this.logger.error("Error transforming invoice data to DTO:", error);
      // Return raw data if transformation fails
      data = sanitizedData;
    }

    return buildListResponseData(data, page || 1, limit || 10, totalCount);
  }
}

results matching ""

    No results matching ""