apps/recallassess/recallassess-api/src/api/admin/report/reports/invoice/invoice-filters.service.ts
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).
Methods |
|
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| Async getInvoiceDetail | ||||||
getInvoiceDetail(invoiceId: number)
|
||||||
|
Parameters :
Returns :
Promise<InvoiceDetailDto | null>
|
| Private invoiceDetailPlain |
invoiceDetailPlain(row: unknown, course_name: string)
|
|
Returns :
Record<string, unknown>
|
| Async listInvoicesForCompany |
listInvoicesForCompany(companyId: number, courseId?: number)
|
|
Lists invoices for a company. Optional
Returns :
Promise<InvoiceRowDto[]>
|
| Private Async resolveLearningGroupCourseTitlesForCompany | ||||||
resolveLearningGroupCourseTitlesForCompany(companyId: number)
|
||||||
|
Distinct
Parameters :
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,
};
}
}