apps/recallassess/recallassess-api/src/api/admin/report/reports/subscription/subscription-filters.service.ts
Methods |
|
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| Async listSubscriptionPayments |
listSubscriptionPayments(dateFrom: string, dateTo: string)
|
|
Lists paid invoices and refunds whose Invoice.paid_date falls in
Returns :
Promise<SubscriptionPaymentRowDto[]>
|
| Private Async loadCompanyAdminContacts | ||||||
loadCompanyAdminContacts(companyIds: number[])
|
||||||
|
Parameters :
Returns :
Promise<Map<number, literal type>>
|
| Private toPlainRow | |||||||||
toPlainRow(row: unknown, adminContact?: literal type)
|
|||||||||
|
Parameters :
Returns :
Record<string, unknown>
|
import { bnestPlainToDtoArray, decimalToNumber } from "@bish-nest/core";
import { roundMoney } from "../../../../../config/billing.config";
import { BNestPrismaService } from "@bish-nest/core/services";
import { BadRequestException, Injectable } from "@nestjs/common";
import { InvoiceStatus, ParticipantRole, type Invoice } from "@prisma/client";
import {
formatBillingCycle,
pickInvoicePeriodForPdf,
} from "@api/shared/invoice/invoice-pdf-meta";
import { normalizeReportDateOnlyParam } from "../../utils/report-date-params.util";
import { invoiceRowIsRefund, SubscriptionPaymentRowDto } from "./subscription.dto";
const MAX_SUBSCRIPTION_PAYMENT_ROWS = 10_000;
function parseInclusiveDateRange(fromDate: string, toDate: string): { from: Date; to: Date } {
const fromKey = normalizeReportDateOnlyParam(fromDate, "date_from");
const toKey = normalizeReportDateOnlyParam(toDate, "date_to");
const from = new Date(`${fromKey}T00:00:00.000Z`);
const to = new Date(`${toKey}T23:59:59.999Z`);
if (from > to) {
throw new BadRequestException("date_from must be on or before date_to");
}
return { from, to };
}
function formatIsoDate(d: Date | null): string {
if (!d) {
return "";
}
return d.toISOString().slice(0, 10);
}
function optionalMoney(n: number | null | undefined): number | null {
if (n == null || !Number.isFinite(n) || n === 0) {
return null;
}
return roundMoney(n);
}
@Injectable()
export class SubscriptionFiltersService {
constructor(private readonly prisma: BNestPrismaService) {}
/**
* Lists paid invoices and refunds whose {@link Invoice.paid_date} falls in `[date_from, date_to]` (inclusive, UTC).
*/
async listSubscriptionPayments(dateFrom: string, dateTo: string): Promise<SubscriptionPaymentRowDto[]> {
const { from, to } = parseInclusiveDateRange(dateFrom, dateTo);
const rows = await this.prisma.client.invoice.findMany({
where: {
status: InvoiceStatus.PAID,
paid_date: { gte: from, lte: to },
},
include: {
company: { select: { id: true, name: true, email: true } },
subscription: {
select: {
start_date: true,
end_date: true,
package: { select: { name: true } },
},
},
},
orderBy: [{ paid_date: "asc" }, { id: "asc" }],
take: MAX_SUBSCRIPTION_PAYMENT_ROWS,
});
const companyIds = [...new Set(rows.map((r) => r.company_id))];
const adminByCompany = await this.loadCompanyAdminContacts(companyIds);
const plainRows = rows.map((row) => this.toPlainRow(row, adminByCompany.get(row.company_id)));
return bnestPlainToDtoArray(plainRows, SubscriptionPaymentRowDto);
}
private async loadCompanyAdminContacts(
companyIds: number[],
): Promise<Map<number, { name: string; email: string }>> {
const map = new Map<number, { name: string; email: string }>();
if (companyIds.length === 0) {
return map;
}
const admins = await this.prisma.client.participant.findMany({
where: {
company_id: { in: companyIds },
role: ParticipantRole.PARTICIPANT_ADMIN,
is_active: true,
},
select: {
company_id: true,
first_name: true,
last_name: true,
email: true,
},
orderBy: [{ company_id: "asc" }, { id: "asc" }],
});
for (const admin of admins) {
if (map.has(admin.company_id)) {
continue;
}
const name = `${admin.first_name} ${admin.last_name}`.trim();
map.set(admin.company_id, { name, email: admin.email });
}
return map;
}
private toPlainRow(
row: Invoice & {
company: { id: number; name: string; email: string };
subscription: {
start_date: Date | null;
end_date: Date | null;
package: { name: string } | null;
} | null;
},
adminContact?: { name: string; email: string },
): Record<string, unknown> {
const total = decimalToNumber(row.total_amount) ?? 0;
const isRefund = invoiceRowIsRefund(row);
const fee = decimalToNumber(row.processing_fee);
const vat = decimalToNumber(row.vat_fee);
const subtotal = decimalToNumber(row.subtotal_amount) ?? 0;
const proration = decimalToNumber(row.proration_amount) ?? 0;
const netLicense = roundMoney(subtotal + proration);
const period = pickInvoicePeriodForPdf(row);
const periodLabel =
period.start || period.end
? `${formatIsoDate(period.start)} – ${formatIsoDate(period.end)}`
: "";
const packageName = row.subscription?.package?.name ?? "";
const cycleLabel = formatBillingCycle(row.billing_cycle);
const productParts = [packageName, cycleLabel, periodLabel].filter((p) => p.length > 0);
const product_service = productParts.join(" · ");
const paidDate = row.paid_date ?? row.created_at;
const customerName = adminContact?.name || row.company.name;
const customerEmail = adminContact?.email || row.company.email;
const customer_name_email =
customerName && customerEmail ? `${customerName} (${customerEmail})` : customerName || customerEmail;
return {
payment_date: paidDate.toISOString().slice(0, 10),
customer_name_email,
company_name: row.company.name,
product_service,
currency: "USD",
amount: isRefund ? null : optionalMoney(total),
amount_refunded: isRefund ? optionalMoney(Math.abs(total)) : null,
fee: optionalMoney(fee != null ? Math.abs(fee) : null),
vat: optionalMoney(vat != null ? Math.abs(vat) : null),
net_amount: optionalMoney(isRefund ? Math.abs(netLicense) : netLicense),
};
}
}