File

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

Index

Methods

Constructor

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

Methods

Async listSubscriptionPayments
listSubscriptionPayments(dateFrom: string, dateTo: string)

Lists paid invoices and refunds whose Invoice.paid_date falls in [date_from, date_to] (inclusive, UTC).

Parameters :
Name Type Optional
dateFrom string No
dateTo string No
Private Async loadCompanyAdminContacts
loadCompanyAdminContacts(companyIds: number[])
Parameters :
Name Type Optional
companyIds number[] No
Returns : Promise<Map<number, literal type>>
Private toPlainRow
toPlainRow(row: unknown, adminContact?: literal type)
Parameters :
Name Type Optional
row unknown No
adminContact literal type Yes
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),
    };
  }
}

results matching ""

    No results matching ""