File

apps/recallassess/recallassess-api/src/api/admin/report/reports/invoice/invoice-filters.service.ts

Description

Invoice queries for invoice-summary (list) and invoice (single document). Dashboard filter valuelists for invoice-summary use ParticipantScopeFiltersService (InvoiceSummaryController under api/admin/report/invoice-summary).

Index

Methods

Constructor

constructor(prisma: BNestPrismaService)
Parameters :
Name Type Optional
prisma BNestPrismaService No

Methods

Async getInvoiceDetail
getInvoiceDetail(invoiceId: number)
Parameters :
Name Type Optional
invoiceId number No
Returns : Promise<InvoiceDetailDto | null>
Private invoiceDetailPlain
invoiceDetailPlain(row: unknown, course_name: string)
Parameters :
Name Type Optional
row unknown No
course_name string No
Async listInvoicesForCompany
listInvoicesForCompany(companyId: number, courseId?: number)

Lists invoices for a company. Optional courseId narrows rows whose subscription package.name matches Course.title (case-insensitive). invoice has no course_id (see LearningGroup in Prisma).

course_name: filtered Course.title when courseId is set; else distinct Course.title values for the company via learningGroups, comma-separated when several; if none, subscription.package.name per row.

Parameters :
Name Type Optional
companyId number No
courseId number Yes
Private Async resolveLearningGroupCourseTitlesForCompany
resolveLearningGroupCourseTitlesForCompany(companyId: number)

Distinct Course.title for courses the company runs (LearningGroup).

Parameters :
Name Type Optional
companyId number No
Returns : Promise<string[]>
import {
  bnestPlainToDto,
  bnestPlainToDtoArray,
  dateToIsoString,
  decimalToNumber,
  humanizeEnumLabel,
} from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable } from "@nestjs/common";
import { type Company, type Invoice, Prisma } from "@prisma/client";
import {
  classifyInvoiceType,
  formatBillingCycle,
  getBillingCycleMonths,
  pickInvoicePeriodForPdf,
} from "@api/shared/invoice/invoice-pdf-meta";
import { InvoiceDetailDto, InvoiceRowDto } from "./invoice.dto";

const MAX_INVOICE_ROWS = 5000;

/**
 * Invoice queries for **`invoice-summary`** (list) and **`invoice`** (single document).
 * Dashboard filter valuelists for **`invoice-summary`** use {@link ParticipantScopeFiltersService}
 * (**`InvoiceSummaryController`** under **`api/admin/report/invoice-summary`**).
 */
@Injectable()
export class InvoiceFiltersService {
  constructor(private readonly prisma: BNestPrismaService) {}

  /**
   * Lists invoices for a company. Optional **`courseId`** narrows rows whose subscription **`package.name`**
   * matches **`Course.title`** (case-insensitive). **`invoice`** has no **`course_id`** (see **`LearningGroup`** in Prisma).
   *
   * **`course_name`**: filtered **`Course.title`** when **`courseId`** is set; else distinct **`Course.title`** values for
   * the company via **`learningGroups`**, comma-separated when several; if none, **`subscription.package.name`** per row.
   */
  async listInvoicesForCompany(companyId: number, courseId?: number): Promise<InvoiceRowDto[]> {
    let subscriptionWhere: Prisma.SubscriptionWhereInput | undefined;
    let titleWhenFilteredByCourse: string | undefined;
    if (courseId != null) {
      const course = await this.prisma.client.course.findFirst({
        where: { id: courseId },
        select: { title: true },
      });
      if (!course) {
        return [];
      }
      titleWhenFilteredByCourse = course.title;
      subscriptionWhere = {
        package: { name: { equals: course.title, mode: Prisma.QueryMode.insensitive } },
      };
    }

    const rows = await this.prisma.client.invoice.findMany({
      where: {
        company_id: companyId,
        ...(subscriptionWhere != null ? { subscription: subscriptionWhere } : {}),
      },
      include: {
        subscription: {
          select: {
            package: { select: { name: true } },
          },
        },
      },
      orderBy: [{ created_at: "desc" }, { id: "desc" }],
      take: MAX_INVOICE_ROWS,
    });

    if (titleWhenFilteredByCourse != null) {
      const course_name = titleWhenFilteredByCourse;
      return bnestPlainToDtoArray(
        rows.map((row) => ({ ...row, course_name })),
        InvoiceRowDto,
      );
    }

    const lgTitles = await this.resolveLearningGroupCourseTitlesForCompany(companyId);
    const fromLms = lgTitles.join(", ");
    if (fromLms.length > 0) {
      return bnestPlainToDtoArray(
        rows.map((row) => ({ ...row, course_name: fromLms })),
        InvoiceRowDto,
      );
    }

    return bnestPlainToDtoArray(
      rows.map((row) => ({
        ...row,
        course_name: row.subscription?.package?.name ?? "",
      })),
      InvoiceRowDto,
    );
  }

  async getInvoiceDetail(invoiceId: number): Promise<InvoiceDetailDto | null> {
    const row = await this.prisma.client.invoice.findUnique({
      where: { id: invoiceId },
      include: {
        company: true,
        parentInvoice: { select: { invoice_number: true } },
        subscription: {
          select: {
            start_date: true,
            end_date: true,
            package: { select: { name: true } },
          },
        },
      },
    });
    if (!row) {
      return null;
    }
    const lgTitles = await this.resolveLearningGroupCourseTitlesForCompany(row.company_id);
    const course_name =
      lgTitles.length > 0 ? lgTitles.join(", ") : row.subscription?.package?.name ?? "";
    return bnestPlainToDto(this.invoiceDetailPlain(row, course_name), InvoiceDetailDto);
  }

  /** Distinct **`Course.title`** for courses the company runs (**`LearningGroup`**). */
  private async resolveLearningGroupCourseTitlesForCompany(companyId: number): Promise<string[]> {
    const courses = await this.prisma.client.course.findMany({
      where: {
        learningGroups: {
          some: { company_id: companyId },
        },
      },
      select: { title: true },
      orderBy: { title: "asc" },
    });
    return courses.map((c) => c.title.trim()).filter((t) => t.length > 0);
  }

  private invoiceDetailPlain(
    row: Invoice & {
      company: Company;
      parentInvoice: { invoice_number: string } | null;
      subscription: {
        start_date: Date | null;
        end_date: Date | null;
        package: { name: string } | null;
      } | null;
    },
    course_name: string,
  ): Record<string, unknown> {
    const c = row.company;
    const addrParts = [c.address?.trim(), c.city?.trim(), c.country?.trim()].filter((x): x is string =>
      Boolean(x && x.length > 0),
    );
    const packageName = row.subscription?.package?.name ?? "";
    const subscription_plan_display = packageName ? `${packageName} - Subscription` : "Subscription";
    const typeInfo = classifyInvoiceType(String(row.invoice_type));
    const billingCycleStr = formatBillingCycle(row.billing_cycle);
    const billing_cycle_label = billingCycleStr.length > 0 ? billingCycleStr : null;
    const billing_cycle_months = getBillingCycleMonths(row.billing_cycle);
    const resolvedPeriod = pickInvoicePeriodForPdf(row);
    const period_display_start =
      resolvedPeriod.start != null ? resolvedPeriod.start.toISOString().slice(0, 10) : null;
    const period_display_end =
      resolvedPeriod.end != null ? resolvedPeriod.end.toISOString().slice(0, 10) : null;

    return {
      id: row.id,
      invoice_number: row.invoice_number,
      status: String(row.status),
      status_label: humanizeEnumLabel(String(row.status)),
      invoice_type: String(row.invoice_type),
      invoice_type_label: humanizeEnumLabel(String(row.invoice_type)),
      package_type: String(row.package_type),
      package_type_label: humanizeEnumLabel(String(row.package_type)),
      license_quantity: row.license_quantity,
      unit_price_per_license: decimalToNumber(row.unit_price_per_license),
      total_amount: decimalToNumber(row.total_amount),
      paid_date: dateToIsoString(row.paid_date ?? undefined),
      period_start: dateToIsoString(row.period_start ?? undefined),
      period_end: dateToIsoString(row.period_end ?? undefined),
      created_at: dateToIsoString(row.created_at) ?? "",
      company_id: row.company_id,
      company_name: c.name,
      company_email: c.email,
      company_address: addrParts.length ? addrParts.join(", ") : null,
      course_name,
      subtotal_amount: decimalToNumber(row.subtotal_amount),
      gross_license_amount: decimalToNumber(row.gross_license_amount),
      pre_vat_total_amount: decimalToNumber(row.pre_vat_total_amount),
      discount_amount: decimalToNumber(row.discount_amount),
      coupon_code: row.coupon_code,
      vat_fee: decimalToNumber(row.vat_fee),
      processing_fee: decimalToNumber(row.processing_fee),
      proration_amount: decimalToNumber(row.proration_amount),
      billing_reference_notes: row.billing_reference_notes,
      pdf_heading: typeInfo.heading,
      pdf_subheading: typeInfo.subheading,
      pdf_invoice_type_label: typeInfo.invoiceTypeLabel,
      pdf_is_credit_note: typeInfo.isCreditNote,
      billing_cycle_label,
      billing_cycle_months,
      parent_invoice_number: row.parentInvoice?.invoice_number ?? null,
      subscription_plan_display,
      processing_fee_percentage: decimalToNumber(row.processing_fee_percentage),
      vat_fee_percentage: decimalToNumber(row.vat_fee_percentage),
      period_display_start,
      period_display_end,
    };
  }
}

results matching ""

    No results matching ""