File

apps/recallassess/recallassess-api/src/jobs/subscription-expiry-schedule.service.ts

Description

Runs subscription expiry synchronization on a fixed interval while API is up.

Index

Properties
Methods

Constructor

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

Methods

Private Async expireSubscriptions
expireSubscriptions()
Returns : Promise<void>
Async runExpireSubscriptionsCron
runExpireSubscriptionsCron()
Decorators :
@Cron(CronExpression.EVERY_MINUTE)
Returns : Promise<void>
Private Async syncCompanySubscriptionExpiryToggleWithSubscriptionState
syncCompanySubscriptionExpiryToggleWithSubscriptionState()
Returns : Promise<void>

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(SubscriptionExpiryScheduleService.name)
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
import { SubscriptionStatus } from "@prisma/client";

/**
 * Runs subscription expiry synchronization on a fixed interval while API is up.
 */
@Injectable()
export class SubscriptionExpiryScheduleService {
  private readonly logger = new Logger(SubscriptionExpiryScheduleService.name);

  constructor(private readonly prisma: BNestPrismaService) {}

  @Cron(CronExpression.EVERY_MINUTE)
  async runExpireSubscriptionsCron(): Promise<void> {
    try {
      await this.expireSubscriptions();
    } catch (err) {
      this.logger.error("Scheduled expireSubscriptions failed", err instanceof Error ? err.stack : String(err));
    }
  }

  private async expireSubscriptions(): Promise<void> {
    this.logger.log("Running subscription expiry cron...");

    const now = new Date();
    const subscriptions = await this.prisma.client.subscription.findMany({
      where: {
        is_current: true,
        status: SubscriptionStatus.ACTIVE,
        next_billing_date: { lt: now },
      },
      select: { id: true, company_id: true },
    });

    if (subscriptions.length === 0) {
      this.logger.debug("No subscriptions eligible for past-due ACTIVE expiry.");
      return;
    }

    let expiredCount = 0;
    for (const subscription of subscriptions) {
      try {
        await this.prisma.client.$transaction(async (tx) => {
          await tx.subscription.update({
            where: { id: subscription.id },
            data: {
              status: SubscriptionStatus.EXPIRED,
              is_current: false,
            },
          });
          await tx.company.update({
            where: { id: subscription.company_id },
            data: { is_subscription_expiry: true },
          });
        });
        expiredCount += 1;
      } catch (error) {
        this.logger.error(
          `Failed to expire subscription ${subscription.id} for company ${subscription.company_id}`,
          error instanceof Error ? error.stack : String(error),
        );
      }
    }

    await this.syncCompanySubscriptionExpiryToggleWithSubscriptionState();
    this.logger.log(`Subscription expiry cron completed. Expired: ${expiredCount}`);
  }

  private async syncCompanySubscriptionExpiryToggleWithSubscriptionState(): Promise<void> {
    const [companies, activeByCompany] = await Promise.all([
      this.prisma.client.company.findMany({
        select: { id: true, is_subscription_expiry: true },
      }),
      this.prisma.client.subscription.findMany({
        where: { is_current: true, status: SubscriptionStatus.ACTIVE },
        select: { company_id: true },
        distinct: ["company_id"],
      }),
    ]);

    const hasCurrentActiveSub = new Set(activeByCompany.map((r) => r.company_id));

    for (const company of companies) {
      const desiredExpiry = !hasCurrentActiveSub.has(company.id);
      if (desiredExpiry !== company.is_subscription_expiry) {
        await this.prisma.client.company.update({
          where: { id: company.id },
          data: { is_subscription_expiry: desiredExpiry },
        });
      }
    }
  }
}

results matching ""

    No results matching ""