File

apps/recallassess/recallassess-api/src/api/admin/company/services/company.service.ts

Extends

BNestBaseModuleService

Index

Methods

Constructor

constructor(prisma: BNestPrismaService, systemLogService: SystemLogService, companyTimezoneService: CompanyTimezoneService, clSubscriptionService: CLSubscriptionService, stripeService: StripeService, stripePaymentService: StripePaymentService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
systemLogService SystemLogService No
companyTimezoneService CompanyTimezoneService No
clSubscriptionService CLSubscriptionService No
stripeService StripeService No
stripePaymentService StripePaymentService No

Methods

Async add
add(data: any)

Override add method to log creation

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

Override delete method to log deletion

Parameters :
Name Type Optional
id number No
Returns : Promise<void>
Async getDetail
getDetail(id: number)
Parameters :
Name Type Optional
id number No
Returns : Promise<DetailResponseDataInterface<any>>
Async getList
getList(paginationOptions: any)
Parameters :
Name Type Optional
paginationOptions any No
Returns : Promise<ListResponseDataInterface<any>>
getStripeConnectionDiagnostics
getStripeConnectionDiagnostics()

Whether the API process has Stripe env vars (UAT ECS / local docker). Does not call Stripe.

Async getStripeSubscriptionSnapshot
getStripeSubscriptionSnapshot(companyId: number)

Live Stripe subscription snapshot for admin UI (not from local subscription table).

Parameters :
Name Type Optional
companyId number No
Private Async hydrateCurrentSubscriptionFieldsForListRows
hydrateCurrentSubscriptionFieldsForListRows(rows: any[])
Parameters :
Name Type Optional
rows any[] No
Returns : Promise<any[]>
Async previewAdminReactivateStripeSubscription
previewAdminReactivateStripeSubscription(companyId: number, fromDate: string)
Parameters :
Name Type Optional
companyId number No
fromDate string No
Async runAdminCreateStripeSubscription
runAdminCreateStripeSubscription(companyId: number)

Admin: one-shot create/recreate Stripe subscription for this company. This cancels/overwrites existing billable subscriptions in Stripe (as required by ops).

Parameters :
Name Type Optional
companyId number No
Returns : Promise<literal type>
Async runAdminReactivateStripeSubscription
runAdminReactivateStripeSubscription(companyId: number, fromDate: string)
Parameters :
Name Type Optional
companyId number No
fromDate string No
Returns : Promise<literal type>
Async save
save(id: number, data: any)

Override save method to prevent updating email, plan, and name fields

Parameters :
Name Type Optional
id number No
data any No
Returns : Promise<any>
Async syncStripeInvoicesAndRefunds
syncStripeInvoicesAndRefunds(companyId: number)
Parameters :
Name Type Optional
companyId number No
Returns : Promise<literal type>
Async updateAdminStripeNextBillingDate
updateAdminStripeNextBillingDate(companyId: number, nextBillingDate: string)
Parameters :
Name Type Optional
companyId number No
nextBillingDate string No
Returns : Promise<literal type>
import { CompanyStripeConnectionDto } from "@api/admin/company/dto/company-stripe-connection.dto";
import { CompanyStripeSubscriptionDto } from "@api/admin/company/dto/company-stripe-subscription.dto";
import { CLSubscriptionService } from "@api/client/subscription/subscription.service";
import { SystemLogService } from "@api/shared/services";
import { StripeService } from "@api/shared/stripe/services/stripe.service";
import {
  ImmediateRecoveryQuote,
  StripePaymentService,
} from "@api/shared/stripe/services/stripe-payment.service";
import { CompanyTimezoneService } from "@api/shared/timezone/company-timezone.service";
import { buildListResponseData } from "@bish-nest/core";
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
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 { Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
import { SystemLogEntityType } from "@prisma/client";
import { plainToInstance } from "class-transformer";
import Stripe from "stripe";

/** End of current billing period (Stripe API 2025+ exposes this on subscription items, not the parent). */
function stripeSubscriptionPeriodEndUnix(sub: Stripe.Subscription): number {
  const ends = sub.items?.data?.map((item) => item.current_period_end) ?? [];
  return ends.length > 0 ? Math.max(...ends) : 0;
}

/** Never leak secrets (Stripe keys, webhook secrets) in API responses. */
function sanitizeExternalErrorMessage(input: unknown): string {
  const raw = input instanceof Error ? input.message : typeof input === "string" ? input : String(input);
  // Stripe sometimes echoes the provided key (e.g. "Expired API Key provided: sk_test_...").
  // Mask any recognizable secret-like tokens.
  return raw
    .replace(/\bsk_(test|live)_[A-Za-z0-9]+\b/g, "sk_$1_[REDACTED]")
    .replace(/\brk_(test|live)_[A-Za-z0-9]+\b/g, "rk_$1_[REDACTED]")
    .replace(/\bwhsec_[A-Za-z0-9]+\b/g, "whsec_[REDACTED]");
}

@Injectable()
export class CompanyService extends BNestBaseModuleService {
  constructor(
    protected prisma: BNestPrismaService,
    private readonly systemLogService: SystemLogService,
    private readonly companyTimezoneService: CompanyTimezoneService,
    private readonly clSubscriptionService: CLSubscriptionService,
    private readonly stripeService: StripeService,
    private readonly stripePaymentService: StripePaymentService,
  ) {
    super();
  }

  override async getList(paginationOptions: any): Promise<ListResponseDataInterface<any>> {
    const { orderBy, sortOrder } = paginationOptions ?? {};
    let normalizedOrderBy = typeof orderBy === "string" ? orderBy.trim() : "";
    let normalizedSortOrder = typeof sortOrder === "string" ? sortOrder.trim().toLowerCase() : "";

    // Defensive normalization for mixed query formats.
    // Accept:
    // - orderBy=expired_date&sortOrder=asc
    // - sortOrder=expired_date:asc
    // - orderBy=expired_date:asc&sortOrder=asc
    if (normalizedOrderBy.includes(":")) {
      const [field, direction] = normalizedOrderBy.split(":");
      normalizedOrderBy = field?.trim() || normalizedOrderBy;
      if (direction?.trim()) {
        normalizedSortOrder = direction.trim().toLowerCase();
      }
    }
    if (normalizedSortOrder.includes(":")) {
      const [field, direction] = normalizedSortOrder.split(":");
      if (field?.trim()) {
        normalizedOrderBy = field.trim();
      }
      normalizedSortOrder = direction?.trim().toLowerCase() || normalizedSortOrder;
    }

    // Some list columns are derived from latest subscription, so Prisma cannot orderBy them directly.
    const needsDerivedSort =
      normalizedOrderBy === "next_billing_date" ||
      normalizedOrderBy === "expired_date" ||
      normalizedOrderBy === "stripe_cancel_at_period_end" ||
      normalizedOrderBy === "current_plan";
    if (!needsDerivedSort) {
      const baseList = await super.getList(paginationOptions);
      baseList.data = await this.hydrateCurrentSubscriptionFieldsForListRows(baseList.data ?? []);
      return baseList;
    }

    const page = Number(paginationOptions?.page) || 1;
    const limit = Number(paginationOptions?.limit) || 10;
    const direction = normalizedSortOrder === "asc" ? "asc" : "desc";
    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;

    const whereCondition = this.moduleMethods.getWhereCondition(paginationOptions);

    const rawRows = await this.prisma.client.company.findMany({
      where: whereCondition,
      include: moduleCurrentCfg.relationObjToIncludeForList,
    });
    const hydratedRows = await this.hydrateCurrentSubscriptionFieldsForListRows(rawRows as any[]);

    const rowsForSort =
      normalizedOrderBy === "expired_date"
        ? hydratedRows.filter((row: any) => row.is_subscription_expiry === true && !!row.next_billing_date)
        : hydratedRows;

    const sortedRows = [...rowsForSort].sort((a: any, b: any) => {
      if (normalizedOrderBy === "next_billing_date" || normalizedOrderBy === "expired_date") {
        const isExpiredSort = normalizedOrderBy === "expired_date";
        const aDate =
          isExpiredSort && a.is_subscription_expiry !== true
            ? null
            : a.next_billing_date
              ? new Date(a.next_billing_date).getTime()
              : null;
        const bDate =
          isExpiredSort && b.is_subscription_expiry !== true
            ? null
            : b.next_billing_date
              ? new Date(b.next_billing_date).getTime()
              : null;

        // Keep empty dates at the end for both directions.
        if (aDate === null && bDate === null) return 0;
        if (aDate === null) return 1;
        if (bDate === null) return -1;

        return direction === "asc" ? aDate - bDate : bDate - aDate;
      }

      if (normalizedOrderBy === "current_plan") {
        const aPlan = (a.current_plan ?? null) as string | null;
        const bPlan = (b.current_plan ?? null) as string | null;

        // Keep empty values at the end for both directions.
        if (aPlan === null && bPlan === null) return 0;
        if (aPlan === null) return 1;
        if (bPlan === null) return -1;

        const cmp = aPlan.localeCompare(bPlan, undefined, { sensitivity: "base" });
        if (cmp !== 0) return direction === "asc" ? cmp : -cmp;

        // Stable-ish tie-breaker.
        return direction === "asc" ? (a.id ?? 0) - (b.id ?? 0) : (b.id ?? 0) - (a.id ?? 0);
      }

      // normalizedOrderBy === "stripe_cancel_at_period_end"
      // Auto-renew (display): Yes when cancel_at_period_end=false, No otherwise.
      // This intentionally does not depend on company.stripe_subscription_id.
      const aCancel = a.stripe_cancel_at_period_end ?? null;
      const bCancel = b.stripe_cancel_at_period_end ?? null;

      const aAutoRenew = aCancel === false ? 1 : 0; // 1=Yes, 0=No
      const bAutoRenew = bCancel === false ? 1 : 0;

      if (aAutoRenew !== bAutoRenew) {
        return direction === "asc" ? aAutoRenew - bAutoRenew : bAutoRenew - aAutoRenew;
      }

      // Stable-ish tie-breaker.
      return direction === "asc" ? (a.id ?? 0) - (b.id ?? 0) : (b.id ?? 0) - (a.id ?? 0);
    });

    const start = (page - 1) * limit;
    const pagedRows = sortedRows.slice(start, start + limit);

    let data: any[] = pagedRows;
    if (paginationOptions?.vl) {
      if (moduleCurrentCfg.simpleDto) {
        data = pagedRows.map((row: any) =>
          plainToInstance(moduleCurrentCfg.simpleDto, row, { excludeExtraneousValues: true }),
        );
      } else if (moduleCurrentCfg.listDto) {
        // Company module has no simpleDto; keep transformed list fields (current_plan, auto-renew flag) for vl requests.
        data = pagedRows.map((row: any) =>
          plainToInstance(moduleCurrentCfg.listDto, row, { excludeExtraneousValues: true }),
        );
      }
    } else if (moduleCurrentCfg.listDto) {
      data = pagedRows.map((row: any) =>
        plainToInstance(moduleCurrentCfg.listDto, row, { excludeExtraneousValues: true }),
      );
    }

    return buildListResponseData(data, page, limit, sortedRows.length);
  }

  private async hydrateCurrentSubscriptionFieldsForListRows(rows: any[]): Promise<any[]> {
    if (!Array.isArray(rows) || rows.length === 0) {
      return rows;
    }
    const companyIds = rows
      .map((row) => Number(row?.id))
      .filter((id) => Number.isInteger(id) && id > 0);
    if (companyIds.length === 0) {
      return rows;
    }

    const currentSubs = await this.prisma.client.subscription.findMany({
      where: {
        company_id: { in: companyIds },
        is_current: true,
      },
      orderBy: { id: "desc" },
      select: {
        company_id: true,
        next_billing_date: true,
        end_date: true,
        stripe_cancel_at_period_end: true,
        package: { select: { name: true } },
      },
    });

    const byCompanyId = new Map<
      number,
      {
        current_plan: string | null;
        next_billing_date: Date | null;
        stripe_cancel_at_period_end: boolean | null;
      }
    >();
    for (const sub of currentSubs) {
      if (!byCompanyId.has(sub.company_id)) {
        byCompanyId.set(sub.company_id, {
          current_plan: sub.package?.name ?? null,
          next_billing_date: sub.next_billing_date ?? sub.end_date ?? null,
          stripe_cancel_at_period_end: sub.stripe_cancel_at_period_end ?? null,
        });
      }
    }

    // After expiry jobs, a company may have no `is_current=true` row.
    // Fallback to the latest subscription row so list/detail still show plan + renewal context.
    const missingCompanyIds = companyIds.filter((id) => !byCompanyId.has(id));
    if (missingCompanyIds.length > 0) {
      const fallbackSubs = await this.prisma.client.subscription.findMany({
        where: {
          company_id: { in: missingCompanyIds },
        },
        orderBy: [{ company_id: "asc" }, { id: "desc" }],
        select: {
          company_id: true,
          next_billing_date: true,
          end_date: true,
          stripe_cancel_at_period_end: true,
          package: { select: { name: true } },
        },
      });

      for (const sub of fallbackSubs) {
        if (!byCompanyId.has(sub.company_id)) {
          byCompanyId.set(sub.company_id, {
            current_plan: sub.package?.name ?? null,
            next_billing_date: sub.next_billing_date ?? sub.end_date ?? null,
            stripe_cancel_at_period_end: sub.stripe_cancel_at_period_end ?? null,
          });
        }
      }
    }

    return rows.map((row) => {
      const id = Number(row?.id);
      const sub = byCompanyId.get(id);
      if (!sub) {
        return row;
      }
      const renewalOrExpiryDate = sub.next_billing_date;
      const isExpired = row["is_subscription_expiry"] === true;
      return {
        ...row,
        current_plan: sub.current_plan,
        next_billing_date: renewalOrExpiryDate,
        expired_date: isExpired && renewalOrExpiryDate ? renewalOrExpiryDate : null,
        stripe_cancel_at_period_end: sub.stripe_cancel_at_period_end,
      };
    });
  }

  override async getDetail(id: number): Promise<DetailResponseDataInterface<any>> {
    const result = await super.getDetail(id);
    const company = result.data[0] as Record<string, any>;
    company["timezone"] = this.companyTimezoneService.resolve({
      country: company["country"],
      preferred_timezone: company["preferred_timezone"],
    });
    const currentSub = await this.prisma.client.subscription.findFirst({
      where: { company_id: id, is_current: true },
      orderBy: { id: "desc" },
      select: {
        next_billing_date: true,
        end_date: true,
        stripe_cancel_at_period_end: true,
        package: { select: { name: true } },
      },
    });
    const subForDisplay = currentSub
      ?? (await this.prisma.client.subscription.findFirst({
        where: { company_id: id },
        orderBy: { id: "desc" },
        select: {
          next_billing_date: true,
          end_date: true,
          stripe_cancel_at_period_end: true,
          package: { select: { name: true } },
        },
      }));

    company["current_plan"] = subForDisplay?.package?.name ?? null;
    const renewalOrExpiryDate = subForDisplay?.next_billing_date ?? subForDisplay?.end_date ?? null;
    company["next_billing_date"] = renewalOrExpiryDate;
    company["stripe_cancel_at_period_end"] = subForDisplay?.stripe_cancel_at_period_end ?? null;
    // Virtual list/detail field: mirrors next_billing_date when subscription is expired.
    company["expired_date"] =
      company["is_subscription_expiry"] === true && renewalOrExpiryDate ? renewalOrExpiryDate : null;
    return result;
  }

  /**
   * Override add method to log creation
   */
  async add(data: any): Promise<any> {
    // Set only by subscription expiry job / system — not editable via admin API
    const { is_subscription_expiry: _ignoredSubscriptionExpiry, ...addData } = data;
    const addResponse = await super.add(addData);
    const company = addResponse.data;

    // Log the creation
    await this.systemLogService.logInsert(
      SystemLogEntityType.COMPANY,
      company.id,
      company as Record<string, unknown>,
    );

    return addResponse;
  }

  /**
   * Override save method to prevent updating email, plan, and name fields
   */
  async save(id: number, data: any): Promise<any> {
    // Get the existing company record (full record for logging)
    const oldCompany = await this.prisma.client.company.findUnique({
      where: { id },
    });

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

    // Get specific fields for validation
    const existingCompany = {
      name: oldCompany.name,
      email: oldCompany.email,
      plan: oldCompany.plan,
    };

    // Check if name is being changed
    if (data.name !== undefined && data.name !== existingCompany.name) {
      throw new UnprocessableEntityException({
        message: [
          {
            colName: "name",
            errorMessage: "Company name cannot be changed after creation.",
          },
        ],
        code: "FORM_VALIDATION_ERROR",
      });
    }

    // Check if email is being changed
    if (data.email !== undefined && data.email !== existingCompany.email) {
      throw new UnprocessableEntityException({
        message: [
          {
            colName: "email",
            errorMessage: "Company email cannot be changed after creation.",
          },
        ],
        code: "FORM_VALIDATION_ERROR",
      });
    }

    // Check if plan is being changed
    if (data.plan !== undefined && data.plan !== existingCompany.plan) {
      throw new UnprocessableEntityException({
        message: [
          {
            colName: "plan",
            errorMessage: "Company plan cannot be changed after creation.",
          },
        ],
        code: "FORM_VALIDATION_ERROR",
      });
    }

    const {
      name,
      email,
      plan,
      timezone,
      is_subscription_expiry: _ignoredSubscriptionExpiry,
      ...updateData
    } = data;

    // Call parent save method with cleaned data (timezone is derived in getDetail, not a company column;
    // is_subscription_expiry is driven by the subscription expiry job and cleared when upgrade/checkout/auto-payment leaves a current ACTIVE sub with renewal ahead.
    const saveResponse = await super.save(id, updateData);
    const returnPayload = saveResponse;
    const updatedCompany = returnPayload.data[0] as Record<string, unknown>;

    // Calculate changed fields and log the update (use latest company row for logging)
    const changedFields = SystemLogService.calculateChangedFields(
      oldCompany as Record<string, unknown>,
      updatedCompany,
    );

    await this.systemLogService.logUpdate(
      SystemLogEntityType.COMPANY,
      id,
      oldCompany as Record<string, unknown>,
      updatedCompany,
      changedFields,
    );

    return returnPayload;
  }

  /**
   * Whether the API process has Stripe env vars (UAT ECS / local docker). Does not call Stripe.
   */
  getStripeConnectionDiagnostics(): CompanyStripeConnectionDto {
    const secretConfigured = this.stripeService.isConfigured();
    return plainToInstance(CompanyStripeConnectionDto, {
      secret_key_configured: secretConfigured,
      key_mode: secretConfigured ? this.stripeService.getDashboardAccountMode() : null,
      publishable_key_configured: !!this.stripeService.getPublishableKey(),
    });
  }

  /**
   * Live Stripe subscription snapshot for admin UI (not from local `subscription` table).
   */
  async getStripeSubscriptionSnapshot(companyId: number): Promise<CompanyStripeSubscriptionDto> {
    const company = await this.prisma.client.company.findUnique({
      where: { id: companyId },
      select: {
        stripe_customer_id: true,
        stripe_subscription_id: true,
      },
    });
    if (!company) {
      throw new NotFoundException(`Company with ID ${companyId} not found`);
    }

    if (!this.stripeService.isConfigured()) {
      return plainToInstance(CompanyStripeSubscriptionDto, {
        loaded: false,
        stripe_subscription_id: null,
        status: null,
        current_period_end: null,
        cancel_at_period_end: null,
        collection_method: null,
        currency: null,
        quantity: null,
        stripe_api_mode: null,
        card_brand: null,
        card_last4: null,
        reason: "STRIPE_NOT_CONFIGURED",
        message: "Stripe is not configured",
      });
    }

    const stripeApiMode = this.stripeService.getDashboardAccountMode();

    let cardBrand: string | null = null;
    let cardLast4: string | null = null;
    if (company.stripe_customer_id) {
      try {
        const pm = await this.stripeService.getCustomerDefaultPaymentMethod(company.stripe_customer_id);
        cardBrand = pm.brand;
        cardLast4 = pm.last4;
      } catch {
        // Card summary is optional; subscription snapshot still useful
      }
    }

    const base = (): Record<string, unknown> => ({
      loaded: false,
      stripe_subscription_id: null,
      status: null,
      current_period_end: null,
      cancel_at_period_end: null,
      collection_method: null,
      currency: null,
      quantity: null,
      recurring_total_amount: null,
      stripe_api_mode: stripeApiMode,
      card_brand: cardBrand,
      card_last4: cardLast4,
    });

    let subscriptionId = company.stripe_subscription_id;
    if (!subscriptionId && company.stripe_customer_id) {
      try {
        const activeLike = await this.stripeService.listSubscriptionsForCustomer(company.stripe_customer_id, [
          "active",
          "trialing",
          "past_due",
          "unpaid",
        ]);
        const sortedActive = [...activeLike].sort(
          (a, b) => stripeSubscriptionPeriodEndUnix(b) - stripeSubscriptionPeriodEndUnix(a),
        );
        subscriptionId = sortedActive[0]?.id ?? null;

        // If nothing billable is open, still show the most recent ended subscription (e.g. canceled)
        // so admin sees why there is "no charge" and can open Stripe or ask the customer to resubscribe.
        if (!subscriptionId) {
          const endedLike = await this.stripeService.listSubscriptionsForCustomer(company.stripe_customer_id, [
            "canceled",
            "incomplete_expired",
            "incomplete",
          ]);
          const sortedEnded = [...endedLike].sort(
            (a, b) => stripeSubscriptionPeriodEndUnix(b) - stripeSubscriptionPeriodEndUnix(a),
          );
          subscriptionId = sortedEnded[0]?.id ?? null;
        }
      } catch (e) {
        return plainToInstance(CompanyStripeSubscriptionDto, {
          ...base(),
          reason: "STRIPE_ERROR",
          message: sanitizeExternalErrorMessage(e),
        });
      }
    }

    if (!subscriptionId) {
      return plainToInstance(CompanyStripeSubscriptionDto, {
        ...base(),
        reason: "NO_STRIPE_SUBSCRIPTION",
        message: "No Stripe subscription found for this customer",
      });
    }

    try {
      const sub = await this.stripeService.retrieveSubscription(subscriptionId);
      const quantity = sub.items?.data?.reduce((sum, item) => sum + (item.quantity ?? 0), 0) ?? null;
      const upcoming = await this.stripeService.retrieveUpcomingInvoiceAmountForSubscription(sub.id);
      const periodEndUnix = stripeSubscriptionPeriodEndUnix(sub);
      const periodEnd = periodEndUnix ? new Date(periodEndUnix * 1000).toISOString() : null;
      return plainToInstance(CompanyStripeSubscriptionDto, {
        loaded: true,
        stripe_subscription_id: sub.id,
        status: sub.status,
        current_period_end: periodEnd,
        cancel_at_period_end: sub.cancel_at_period_end ?? null,
        collection_method: sub.collection_method ?? null,
        currency: sub.currency ?? null,
        quantity,
        recurring_total_amount: upcoming.amount,
        stripe_api_mode: stripeApiMode,
        card_brand: cardBrand,
        card_last4: cardLast4,
      });
    } catch (e) {
      return plainToInstance(CompanyStripeSubscriptionDto, {
        ...base(),
        reason: "STRIPE_ERROR",
        message: sanitizeExternalErrorMessage(e),
        stripe_subscription_id: subscriptionId,
      });
    }
  }

  /**
   * Admin: one-shot create/recreate Stripe subscription for this company.
   * This cancels/overwrites existing billable subscriptions in Stripe (as required by ops).
   */
  async runAdminCreateStripeSubscription(companyId: number): Promise<{
    success: boolean;
    message: string;
    next_period_end_iso?: string | null;
  }> {
    return this.stripePaymentService.runAdminCreateStripeSubscription(companyId);
  }

  async runAdminReactivateStripeSubscription(
    companyId: number,
    fromDate: string,
  ): Promise<{ success: boolean; message: string }> {
    const oldCompany = await this.prisma.client.company.findUnique({ where: { id: companyId } });
    const result = await this.stripePaymentService.runImmediateExpiredRecoveryCharge(companyId, {
      forceRecreateStripeSubscription: true,
      recoveryFromDate: fromDate,
    });
    const newCompany = await this.prisma.client.company.findUnique({ where: { id: companyId } });
    await this.systemLogService.logUpdate(
      SystemLogEntityType.COMPANY,
      companyId,
      (oldCompany as unknown as Record<string, unknown>) ?? { id: companyId },
      (newCompany as unknown as Record<string, unknown>) ?? { id: companyId },
      {
        action: "admin_reactivate_subscription_request",
        requested_from_date: fromDate,
        success: result.success,
        response_message: result.message,
      },
    );
    return {
      success: result.success,
      message: result.message,
    };
  }

  async previewAdminReactivateStripeSubscription(
    companyId: number,
    fromDate: string,
  ): Promise<ImmediateRecoveryQuote> {
    return this.stripePaymentService.previewImmediateExpiredRecoveryCharge(companyId, fromDate);
  }

  async updateAdminStripeNextBillingDate(
    companyId: number,
    nextBillingDate: string,
  ): Promise<{ success: boolean; message: string; next_billing_date: string }> {
    return this.stripePaymentService.updateAdminStripeNextBillingDate(companyId, nextBillingDate);
  }

  async syncStripeInvoicesAndRefunds(companyId: number): Promise<{
    success: boolean;
    message: string;
    invoices_created: number;
    refunds_created: number;
  }> {
    return this.stripePaymentService.syncCompanyInvoicesAndRefundsFromStripe(companyId);
  }

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

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

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

    // Log the deletion
    await this.systemLogService.logDelete(SystemLogEntityType.COMPANY, id, company);
  }
}

results matching ""

    No results matching ""