File

apps/recallassess/recallassess-api/src/api/shared/stripe/services/stripe-payment/stripe-payment-invoice-webhook.service.ts

Index

Properties
Methods

Constructor

constructor(prisma: BNestPrismaService, stripeService: StripeService, billing: StripePaymentBillingHelpersService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
stripeService StripeService No
billing StripePaymentBillingHelpersService No

Methods

Async handleInvoiceCreated
handleInvoiceCreated(invoice: Stripe.Invoice)
Parameters :
Name Type Optional
invoice Stripe.Invoice No
Returns : Promise<void>
Async handleInvoicePaid
handleInvoicePaid(invoice: Stripe.Invoice)
Parameters :
Name Type Optional
invoice Stripe.Invoice No
Returns : Promise<void>
Async handleInvoicePaymentFailed
handleInvoicePaymentFailed(stripeInvoice: Stripe.Invoice)
Parameters :
Name Type Optional
stripeInvoice Stripe.Invoice No
Returns : Promise<void>
Async handleInvoicePaymentPaid
handleInvoicePaymentPaid(eventObject: unknown)
Parameters :
Name Type Optional
eventObject unknown No
Returns : Promise<void>
Async handlePaymentMethodAttached
handlePaymentMethodAttached(paymentMethod: Stripe.PaymentMethod)
Parameters :
Name Type Optional
paymentMethod Stripe.PaymentMethod No
Returns : Promise<void>
Async handleSetupIntentSucceeded
handleSetupIntentSucceeded(setupIntent: Stripe.SetupIntent)
Parameters :
Name Type Optional
setupIntent Stripe.SetupIntent No
Returns : Promise<void>
Private Async sendPaymentFailedEmailFromStripeSubscriptionInvoice
sendPaymentFailedEmailFromStripeSubscriptionInvoice(stripeInvoice: Stripe.Invoice, failureMessage: string)
Parameters :
Name Type Optional
stripeInvoice Stripe.Invoice No
failureMessage string No
Returns : Promise<void>
Private shouldSendPaymentSucceededEmail
shouldSendPaymentSucceededEmail(amountUsd: number)
Parameters :
Name Type Optional
amountUsd number No
Returns : boolean

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(StripePaymentInvoiceWebhookService.name)
import { BNestPrismaService } from "@bish-nest/core";
import { Injectable, Logger } from "@nestjs/common";
import {
  InvoiceStatus,
  InvoiceType,
  PackageType,
  SubscriptionStatus,
  SubscriptionType,
} from "@prisma/client";
import Stripe from "stripe";
import {
  addBillingDays,
  getBillingCycleMultiplier,
  getBillingCyclePeriodDays,
} from "../../../../../config/billing-cycle";
import {
  invoiceBillingAmountsFromRenewalCents,
  invoiceBillingAmountsToDbFields,
} from "../../../../../config/billing.config";
import { localStripeCancelAtPeriodEndFromSubscription, StripeService } from "../stripe.service";
import {
  StripePaymentBillingHelpersService,
  StripeSubscriptionApi,
} from "./stripe-payment-billing-helpers.service";

@Injectable()
export class StripePaymentInvoiceWebhookService {
  private readonly logger = new Logger(StripePaymentInvoiceWebhookService.name);

  constructor(
    private readonly prisma: BNestPrismaService,
    private readonly stripeService: StripeService,
    private readonly billing: StripePaymentBillingHelpersService,
  ) {}

  private shouldSendPaymentSucceededEmail(amountUsd: number): boolean {
    return Number.isFinite(amountUsd) && amountUsd > 0;
  }

  private async sendPaymentFailedEmailFromStripeSubscriptionInvoice(
    stripeInvoice: Stripe.Invoice,
    failureMessage: string,
  ): Promise<void> {
    const sid = String(stripeInvoice.id);
    if (!sid || sid === "undefined") {
      return;
    }

    const cents =
      typeof stripeInvoice.amount_due === "number" && stripeInvoice.amount_due >= 0
        ? stripeInvoice.amount_due
        : typeof stripeInvoice.total === "number" && stripeInvoice.total >= 0
          ? stripeInvoice.total
          : 0;
    const invNo = stripeInvoice.number ? String(stripeInvoice.number) : sid;

    const sendFailed = async (companyId: number): Promise<void> => {
      await this.billing.sendCompanyTemplatedEmail({
        companyId,
        templateKey: "billing.payment.failed",
        dedupeKey: `billing.payment.failed:${sid}`,
        triggeredBy: "stripe_webhook_invoice_payment_failed",
        variables: {
          "invoice.number": invNo,
          "invoice.amount": this.billing.formatUsd(cents / 100),
          "invoice.failure_message": failureMessage,
        },
        metadata: { stripe_invoice_id: sid },
      });
    };

    const inv = stripeInvoice as Stripe.Invoice & {
      subscription?: string | Stripe.Subscription | null;
    };
    const subRef = inv.subscription;
    let subscriptionId = typeof subRef === "string" ? subRef : (subRef?.id ?? null);

    // Some Stripe webhook payloads may omit `invoice.subscription`.
    // If that happens, fetch expanded invoice from Stripe and retry resolution.
    if (!subscriptionId && this.stripeService.isConfigured()) {
      try {
        const expanded = await this.stripeService.getInvoiceExpanded(stripeInvoice.id);
        const expandedSub = (expanded as Stripe.Invoice & { subscription?: string | Stripe.Subscription | null })
          .subscription;
        subscriptionId = typeof expandedSub === "string" ? expandedSub : expandedSub?.id ?? null;
      } catch (e) {
        this.logger.warn(
          `[stripe-renewal-debug] could not resolve invoice subscription via expanded invoice. stripe_invoice_id=${sid}: ${
            e instanceof Error ? e.message : String(e)
          }`,
        );
      }
    }

    let companyId: number | null = null;

    if (subscriptionId && this.stripeService.isConfigured()) {
      try {
        const stripeSub = await this.stripeService.retrieveSubscription(subscriptionId);
        const companyIdStr = stripeSub.metadata?.["company_id"];
        if (companyIdStr) {
          const parsed = parseInt(companyIdStr, 10);
          if (!Number.isNaN(parsed)) companyId = parsed;
        }
        if (companyId === null) {
          const cust =
            this.billing.extractStripeCustomerId(stripeSub.customer) ?? this.billing.extractStripeCustomerId(inv.customer);
          if (cust) {
            companyId = await this.billing.resolveCompanyIdFromStripeCustomer(cust);
          }
        }
      } catch (e) {
        this.logger.warn(
          `invoice.payment_failed: retrieveSubscription ${subscriptionId} failed: ${
            e instanceof Error ? e.message : String(e)
          }`,
        );
      }
    }

    if (companyId === null) {
      const custOnly = this.billing.extractStripeCustomerId(inv.customer);
      if (custOnly) {
        companyId = await this.billing.resolveCompanyIdFromStripeCustomer(custOnly);
      }
    }

    if (companyId === null) {
      this.logger.warn(
        `invoice.payment_failed: could not resolve company for stripe_invoice=${sid} (subscription=${subscriptionId ?? "none"})`,
      );
      return;
    }

    await sendFailed(companyId);
  }
  async handleInvoicePaymentFailed(stripeInvoice: Stripe.Invoice): Promise<void> {
    const stripeSid = String(stripeInvoice.id);
    this.logger.warn(
      `invoice.payment_failed: stripe_invoice=${stripeSid}, subscription=${String(
        (stripeInvoice as Stripe.Invoice & { subscription?: string | Stripe.Subscription | null }).subscription ??
          "",
      )}`,
    );

    const { paymentIntentId } = this.billing.extractPaymentIntentAndChargeIds(stripeInvoice);
    const msg =
      typeof (stripeInvoice as { last_finalization_error?: { message?: string } }).last_finalization_error
        ?.message === "string"
        ? String(
            (stripeInvoice as { last_finalization_error?: { message?: string } }).last_finalization_error?.message,
          )
        : "Payment failed — update your payment method in Subscription & Billing or contact support.";

    let emailed = false;

    if (paymentIntentId) {
      const invoice = await this.prisma.client.invoice.findFirst({
        where: { stripe_payment_intent_id: paymentIntentId },
        select: { id: true, company_id: true, invoice_number: true, total_amount: true, stripe_invoice_id: true },
      });
      if (invoice) {
        await this.prisma.client.invoice.update({
          where: { id: invoice.id },
          data: {
            status: InvoiceStatus.FAILED,
            failure_code: "invoice_payment_failed",
            failure_message: msg.slice(0, 2000),
          },
        });
        this.logger.log(`Marked local invoice ${invoice.id} FAILED (payment_intent match)`);

        // Notification email (deduped)
        const total =
          typeof invoice.total_amount === "number" ? invoice.total_amount : Number(invoice.total_amount);
        const stripeInvoiceIdForDedupe = String(invoice.stripe_invoice_id ?? stripeSid);
        await this.billing.sendCompanyTemplatedEmail({
          companyId: invoice.company_id,
          templateKey: "billing.payment.failed",
          dedupeKey: `billing.payment.failed:${stripeInvoiceIdForDedupe}`,
          triggeredBy: "stripe_webhook_invoice_payment_failed",
          variables: {
            "invoice.number": invoice.invoice_number,
            "invoice.amount": this.billing.formatUsd(Number.isFinite(total) ? total : 0),
            "invoice.failure_message": msg,
          },
          metadata: {
            stripe_invoice_id: stripeInvoiceIdForDedupe,
            invoice_id: invoice.id,
          },
        });
        emailed = true;
      }
    }

    const sid = stripeSid && stripeSid !== "undefined" ? stripeSid : null;
    if (!emailed && sid) {
      const invoice = await this.prisma.client.invoice.findFirst({
        where: { stripe_invoice_id: sid },
        select: { id: true, company_id: true, invoice_number: true, total_amount: true, stripe_invoice_id: true },
      });
      if (invoice) {
        await this.prisma.client.invoice.update({
          where: { id: invoice.id },
          data: {
            status: InvoiceStatus.FAILED,
            failure_code: "invoice_payment_failed",
            failure_message: msg.slice(0, 2000),
          },
        });
        this.logger.log(`Marked local invoice ${invoice.id} FAILED (stripe_invoice_id match)`);

        const total =
          typeof invoice.total_amount === "number" ? invoice.total_amount : Number(invoice.total_amount);
        await this.billing.sendCompanyTemplatedEmail({
          companyId: invoice.company_id,
          templateKey: "billing.payment.failed",
          dedupeKey: `billing.payment.failed:${sid}`,
          triggeredBy: "stripe_webhook_invoice_payment_failed",
          variables: {
            "invoice.number": invoice.invoice_number,
            "invoice.amount": this.billing.formatUsd(Number.isFinite(total) ? total : 0),
            "invoice.failure_message": msg,
          },
          metadata: {
            stripe_invoice_id: sid,
            invoice_id: invoice.id,
          },
        });
        emailed = true;
      }
    }

    if (!emailed) {
      await this.sendPaymentFailedEmailFromStripeSubscriptionInvoice(stripeInvoice, msg);
    }
  }
  async handleInvoicePaid(invoice: Stripe.Invoice): Promise<void> {
    const inv = invoice as Stripe.Invoice & {
      subscription?: string | Stripe.Subscription | null;
    };
    const stripeInvoiceId = String(invoice.id);
    this.logger.log(
      `[stripe-renewal-debug] handleInvoicePaid start stripe_invoice_id=${stripeInvoiceId} amount_paid=${invoice.amount_paid ?? "null"} billing_reason=${inv.billing_reason ?? "null"}`,
    );
    const paidCents = typeof invoice.amount_paid === "number" && invoice.amount_paid >= 0 ? invoice.amount_paid : 0;
    if (paidCents <= 0) {
      const billingReason = inv.billing_reason ?? null;
      if (!this.billing.isStripeFirstSubscriptionInvoiceBillingReason(billingReason)) {
        // Renewal invoices can legitimately be $0 (discounts/credits) but we still must
        // update subscription rotation + next billing date.
        this.logger.log(
          `handleInvoicePaid: allow zero-amount non-initial invoice ${stripeInvoiceId} (amount_paid=${invoice.amount_paid ?? "null"}, billing_reason=${billingReason ?? "null"})`,
        );
      }
      this.logger.log(
        `handleInvoicePaid: allow zero-amount initial invoice ${stripeInvoiceId} (billing_reason=${billingReason ?? "null"})`,
      );
    }
    const subRef = inv.subscription;
    let subscriptionId = typeof subRef === "string" ? subRef : (subRef?.id ?? null);
    if (!subscriptionId) {
      // Some webhook payloads (and occasionally Stripe invoice objects) omit `invoice.subscription`.
      // Fallback to line items where Stripe often includes `line.subscription`.
      const invAny = invoice as unknown as Record<string, unknown>;
      const linesObj = invAny["lines"];
      const linesData =
        linesObj && typeof linesObj === "object" ? (linesObj as Record<string, unknown>)["data"] : null;
      const lines = Array.isArray(linesData) ? (linesData as unknown[]) : [];
      const lineSub = lines
        .map((l) => {
          if (!l || typeof l !== "object") return null;
          const rec = l as Record<string, unknown>;
          const sub = rec["subscription"];
          if (typeof sub === "string") return sub;
          if (sub && typeof sub === "object" && typeof (sub as Record<string, unknown>)["id"] === "string") {
            return String((sub as Record<string, unknown>)["id"]);
          }
          return null;
        })
        .find((v): v is string => typeof v === "string");
      subscriptionId = lineSub ?? null;
      if (subscriptionId) {
        this.logger.warn(
          `[stripe-link-debug] handleInvoicePaid derived subscription from invoice.lines stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId}`,
        );
      }
    }
    if (!subscriptionId && this.stripeService.isConfigured()) {
      const customerId = this.billing.extractStripeCustomerId(inv.customer);
      if (customerId) {
        try {
          const subs = await this.stripeService.listSubscriptionsForCustomer(customerId, [
            "active",
            "trialing",
            "past_due",
            "unpaid",
          ]);
          const matchByLatestInvoice = subs.find((s) => {
            const li = (s as any).latest_invoice;
            const liId = typeof li === "string" ? li : li && typeof li === "object" && "id" in li ? String(li.id) : null;
            return liId === stripeInvoiceId;
          });
          if (matchByLatestInvoice) {
            subscriptionId = matchByLatestInvoice.id;
          } else if (subs.length === 1) {
            subscriptionId = subs[0].id;
          }
          if (subscriptionId) {
            this.logger.warn(
              `[stripe-link-debug] handleInvoicePaid derived subscription from customer subscriptions stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId} customer=${customerId} subs=${subs.length}`,
            );
          }
        } catch (e) {
          this.logger.warn(
            `[stripe-link-debug] handleInvoicePaid failed deriving subscription via customer subscriptions stripe_invoice_id=${stripeInvoiceId} customer=${customerId}: ${
              e instanceof Error ? e.message : String(e)
            }`,
          );
        }
      }
    }

    if (!subscriptionId) {
      if (!this.stripeService.isConfigured()) {
        return;
      }
      const customerId = this.billing.extractStripeCustomerId(inv.customer);
      const companyIdNoSub = customerId ? await this.billing.resolveCompanyIdFromStripeCustomer(customerId) : null;
      if (companyIdNoSub === null) {
        this.logger.debug(
          `handleInvoicePaid: invoice ${stripeInvoiceId} has no subscription and no resolvable company (customer=${customerId ?? "none"})`,
        );
        return;
      }
      const invNo = invoice.number ? String(invoice.number) : stripeInvoiceId;
      const amountUsd = paidCents / 100;
      if (this.shouldSendPaymentSucceededEmail(amountUsd)) {
        await this.billing.sendCompanyTemplatedEmail({
          companyId: companyIdNoSub,
          templateKey: "billing.payment.succeeded",
          dedupeKey: `billing.payment.succeeded:${stripeInvoiceId}`,
          triggeredBy: "stripe_webhook_invoice_paid",
          variables: {
            "invoice.number": invNo,
            "invoice.amount": this.billing.formatUsd(amountUsd),
            "subscription.next_billing_date": "",
          },
          metadata: { stripe_invoice_id: stripeInvoiceId },
        });
      } else {
        this.logger.log(
          `handleInvoicePaid: skip billing.payment.succeeded email for zero-amount invoice stripe_invoice_id=${stripeInvoiceId} company=${companyIdNoSub}`,
        );
      }
      return;
    }

    if (!this.stripeService.isConfigured()) {
      this.logger.warn(
        `[stripe-link-debug] handleInvoicePaid skip: Stripe not configured stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId}`,
      );
      return;
    }

    let stripeSub: Stripe.Subscription;
    try {
      stripeSub = await this.stripeService.retrieveSubscription(subscriptionId);
    } catch {
      this.logger.warn(`handleInvoicePaid: could not retrieve subscription ${subscriptionId}`);
      return;
    }

    let companyId: number | null = null;
    const companyIdStr = stripeSub.metadata?.["company_id"];
    if (companyIdStr) {
      const parsed = parseInt(companyIdStr, 10);
      if (!Number.isNaN(parsed)) companyId = parsed;
    }
    if (companyId === null) {
      const customerId =
        this.billing.extractStripeCustomerId(stripeSub.customer) ?? this.billing.extractStripeCustomerId(inv.customer);
      if (customerId) {
        companyId = await this.billing.resolveCompanyIdFromStripeCustomer(customerId);
      }
    }
    if (companyId === null) {
      this.logger.warn(
        `[stripe-renewal-debug] handleInvoicePaid cannot resolve company for stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId}`,
      );
      this.logger.warn(
        `handleInvoicePaid: subscription ${subscriptionId} — could not resolve company (metadata + stripe_customer_id lookup)`,
      );
      return;
    }

    let localSub = await this.prisma.client.subscription.findFirst({
      where: {
        company_id: companyId,
        stripe_subscription_id: subscriptionId,
        is_current: true,
      },
    });
    if (!localSub) {
      localSub = await this.prisma.client.subscription.findFirst({
        where: { company_id: companyId, stripe_subscription_id: subscriptionId },
        orderBy: { id: "desc" },
      });
    }

    if (!localSub) {
      const metaLocalIdStr = stripeSub.metadata?.["local_subscription_id"];
      if (metaLocalIdStr) {
        const metaLocalId = parseInt(metaLocalIdStr, 10);
        if (!Number.isNaN(metaLocalId)) {
          const byMeta = await this.prisma.client.subscription.findFirst({
            where: { id: metaLocalId, company_id: companyId },
          });
          if (byMeta) {
            await this.prisma.client.subscription.update({
              where: { id: byMeta.id },
              data: { stripe_subscription_id: subscriptionId },
            });
            await this.prisma.client.company.update({
              where: { id: companyId },
              data: { stripe_subscription_id: subscriptionId },
            });
            localSub = await this.prisma.client.subscription.findUnique({ where: { id: byMeta.id } });
          }
        }
      }
    }

    if (!localSub) {
      const activeUnlinked = await this.prisma.client.subscription.findFirst({
        where: {
          company_id: companyId,
          is_current: true,
          status: SubscriptionStatus.ACTIVE,
          OR: [{ stripe_subscription_id: null }, { stripe_subscription_id: subscriptionId }],
        },
        orderBy: { id: "desc" },
      });
      if (activeUnlinked) {
        await this.prisma.client.subscription.update({
          where: { id: activeUnlinked.id },
          data: { stripe_subscription_id: subscriptionId },
        });
        await this.prisma.client.company.update({
          where: { id: companyId },
          data: { stripe_subscription_id: subscriptionId },
        });
        localSub = await this.prisma.client.subscription.findUnique({ where: { id: activeUnlinked.id } });
      }
    }

    if (!localSub) {
      const expiredUnlinked = await this.prisma.client.subscription.findFirst({
        where: {
          company_id: companyId,
          status: SubscriptionStatus.EXPIRED,
          OR: [{ stripe_subscription_id: null }, { stripe_subscription_id: subscriptionId }],
        },
        orderBy: { id: "desc" },
      });
      if (expiredUnlinked) {
        await this.prisma.client.subscription.update({
          where: { id: expiredUnlinked.id },
          data: { stripe_subscription_id: subscriptionId },
        });
        await this.prisma.client.company.update({
          where: { id: companyId },
          data: { stripe_subscription_id: subscriptionId },
        });
        localSub = await this.prisma.client.subscription.findUnique({ where: { id: expiredUnlinked.id } });
      }
    }

    if (!localSub) {
      // Rescue fallback: local row may exist but still not match the Stripe sub id.
      const mostRecentExpired = await this.prisma.client.subscription.findFirst({
        where: { company_id: companyId, status: SubscriptionStatus.EXPIRED },
        orderBy: { id: "desc" },
      });
      if (mostRecentExpired) {
        await this.prisma.client.subscription.update({
          where: { id: mostRecentExpired.id },
          data: { stripe_subscription_id: subscriptionId },
        });
        await this.prisma.client.company.update({
          where: { id: companyId },
          data: { stripe_subscription_id: subscriptionId },
        });
        localSub = await this.prisma.client.subscription.findUnique({ where: { id: mostRecentExpired.id } });
        this.logger.warn(
          `[stripe-renewal-debug] handleInvoicePaid rescue relinked local expired subscription id=${mostRecentExpired.id} to stripe_subscription_id=${subscriptionId} for company=${companyId}`,
        );
      }
    }

    if (!localSub) {
      // Hard fallback: the DB row might exist but not be linked/matching `stripe_subscription_id`.
      // We still want to create the local invoice + next renewal subscription record.
      this.logger.warn(
        `[stripe-renewal-debug] handleInvoicePaid cannot resolve local subscription match; attempting fallback (stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId} company_id=${companyId})`,
      );

      const fallbackCurrent = await this.prisma.client.subscription.findFirst({
        where: { company_id: companyId, is_current: true },
        orderBy: { id: "desc" },
        include: { package: true },
      });
      const fallbackAny = await this.prisma.client.subscription.findFirst({
        where: { company_id: companyId },
        orderBy: { id: "desc" },
        include: { package: true },
      });

      const picked = fallbackCurrent?.package ? fallbackCurrent : fallbackAny?.package ? fallbackAny : null;
      if (!picked) {
        this.logger.warn(
          `handleInvoicePaid: fallback also failed to load any local subscription/package for company ${companyId}; sending billing.payment.succeeded only`,
        );
        const centsPaid =
          typeof invoice.amount_paid === "number" && invoice.amount_paid >= 0 ? invoice.amount_paid : 0;
        const invNo = invoice.number ? String(invoice.number) : stripeInvoiceId;
        const periodEndUnix = (stripeSub as StripeSubscriptionApi).current_period_end;
        const nextBill = typeof periodEndUnix === "number" ? new Date(periodEndUnix * 1000) : null;
        const amountUsd = centsPaid / 100;
        if (this.shouldSendPaymentSucceededEmail(amountUsd)) {
          await this.billing.sendCompanyTemplatedEmail({
            companyId,
            templateKey: "billing.payment.succeeded",
            dedupeKey: `billing.payment.succeeded:${stripeInvoiceId}`,
            triggeredBy: "stripe_webhook_invoice_paid",
            variables: {
              "invoice.number": invNo,
              "invoice.amount": this.billing.formatUsd(amountUsd),
              "subscription.next_billing_date": nextBill ? nextBill.toISOString().split("T")[0] : "",
            },
            metadata: {
              stripe_invoice_id: stripeInvoiceId,
              stripe_subscription_id: subscriptionId,
            },
          });
        } else {
          this.logger.log(
            `handleInvoicePaid: skip billing.payment.succeeded email for zero-amount fallback invoice stripe_invoice_id=${stripeInvoiceId} company=${companyId}`,
          );
        }
        return;
      }

      // Relink chosen local row so subsequent calculations + invoice rotation have everything needed.
      await this.prisma.client.subscription.update({
        where: { id: picked.id },
        data: { stripe_subscription_id: subscriptionId },
      });
      await this.prisma.client.company.update({
        where: { id: companyId },
        data: { stripe_subscription_id: subscriptionId },
      });
      localSub = picked;
    }

    // Ensure package is loaded for invoice creation calculations
    let localSubWithPackage = await this.prisma.client.subscription.findUnique({
      where: { id: localSub.id },
      include: { package: true },
    });
    if (!localSubWithPackage?.package) {
      // Sometimes the Prisma relation is null while `package_id` is still populated.
      // Try loading by ID so renewals can still rotate subscriptions.
      const packageId = (localSub as any)?.package_id as number | null | undefined;
      if (packageId) {
        const pkg = await this.prisma.client.package.findUnique({ where: { id: packageId } });
        if (pkg) {
          localSubWithPackage = { ...localSubWithPackage, package: pkg } as any;
        }
      }
      if (!localSubWithPackage?.package) {
        // Hard fallback: use the company's current subscription row which should
        // have a valid package relation.
        const current = await this.prisma.client.subscription.findFirst({
          where: { company_id: companyId, is_current: true },
          orderBy: { id: "desc" },
          include: { package: true },
        });
        if (current?.package) {
          localSubWithPackage = current as any;
        }
        // If relation is still null but package_id exists, fetch package by ID.
        if (!localSubWithPackage?.package) {
          const currentPackageId = (current as any)?.package_id as number | null | undefined;
          if (currentPackageId) {
            const pkg = await this.prisma.client.package.findUnique({ where: { id: currentPackageId } });
            if (pkg) localSubWithPackage = { ...current, package: pkg } as any;
          }
        }
      }
      if (!localSubWithPackage?.package) {
        this.logger.warn(
          `[stripe-renewal-debug] handleInvoicePaid missing local subscription package for stripe_invoice_id=${stripeInvoiceId} company_id=${companyId} local_subscription_id=${localSub.id}`,
        );
        this.logger.warn(`handleInvoicePaid: missing local subscription package for company ${companyId}`);
        return;
      }
    }

    // Prefer the "old" subscription row that is marked current=true.
    // This ensures renewal rotation uses the expected old record even when stripe_subscription_id linking is off.
    const companyCurrent = await this.prisma.client.subscription.findFirst({
      where: { company_id: companyId, is_current: true },
      orderBy: { id: "desc" },
      include: { package: true },
    });
    if (companyCurrent?.package) {
      localSub = companyCurrent;
      localSubWithPackage = companyCurrent as any;
    }

    // Create local Invoice row for Stripe subscription invoice payments.
    // This is required because the renewal sync job bootstraps Stripe directly without creating local invoices.
    // If the row already exists but is incomplete (e.g. after earlier code versions), we update it.
    const existingInvoice = await this.prisma.client.invoice.findFirst({
      where: { stripe_invoice_id: invoice.id },
      select: {
        id: true,
        status: true,
        invoice_number: true,
        total_amount: true,
        pre_vat_total_amount: true,
        processing_fee_percentage: true,
        vat_fee_percentage: true,
        stripe_payment_intent_id: true,
        stripe_charge_id: true,
        stripe_checkout_session_id: true,
        period_start: true,
        period_end: true,
        invoice_type: true,
      },
    });
    const couldCreateLocalInvoiceRow = !existingInvoice;
    const wasAlreadyPaid =
      existingInvoice?.status === InvoiceStatus.PAID ||
      // backward compat if prisma returns raw string
      (typeof (existingInvoice as any)?.status === "string" && (existingInvoice as any).status === "PAID");
    this.logger.warn(
      `[stripe-link-debug] handleInvoicePaid invoiceRow stripe_invoice_id=${stripeInvoiceId} local_invoice_id=${
        existingInvoice?.id ?? "(none)"
      } local_status=${existingInvoice?.status ?? "(none)"} couldCreateLocalInvoiceRow=${couldCreateLocalInvoiceRow} wasAlreadyPaid=${wasAlreadyPaid}`,
    );

    const decimalToNumber = (v: unknown): number | null => {
      if (v === null || v === undefined) return null;
      if (typeof v === "number") return v;
      if (typeof v === "string") return Number(v);
      if (typeof v === "bigint") return Number(v);
      if (typeof (v as any)?.toNumber === "function") return (v as any).toNumber();
      return Number(v);
    };

    const company = await this.prisma.client.company.findUnique({
      where: { id: companyId },
      select: { country: true },
    });

    let stripeInvoiceForDb: Stripe.Invoice = invoice;
    try {
      stripeInvoiceForDb = await this.stripeService.getInvoiceExpanded(invoice.id);
    } catch (e) {
      this.logger.warn(
        `handleInvoicePaid: getInvoiceExpanded failed for ${invoice.id}: ${
          e instanceof Error ? e.message : String(e)
        }`,
      );
    }

    const signupPaidSubscriptionCreate =
      stripeInvoiceForDb.billing_reason === "subscription_create" &&
      stripeSub.metadata?.["source"] === "signup_account_sync";

    // Trial-like packages can have zero catalog price. For the first paid cycle
    // after trial, use STARTUP price as the billing baseline for local fee/tax
    // snapshot so invoice VAT/processing fields are not zeroed incorrectly.
    let pricePerLicense = Number(localSubWithPackage!.package.price_per_licence ?? 0);
    const isTrialLikePackageForBilling =
      localSubWithPackage!.package.is_trial_package ||
      localSubWithPackage!.package.package_type === PackageType.FREE_TRIAL ||
      localSubWithPackage!.package.package_type === PackageType.PRIVATE_VIP_TRIAL;
    if (isTrialLikePackageForBilling && (!Number.isFinite(pricePerLicense) || pricePerLicense <= 0)) {
      const startupPackage = await this.prisma.client.package.findFirst({
        where: { package_type: PackageType.STARTUP, is_active: true },
        select: { price_per_licence: true },
      });
      const startupPricePerLicense = Number(startupPackage?.price_per_licence ?? 0);
      if (Number.isFinite(startupPricePerLicense) && startupPricePerLicense > 0) {
        pricePerLicense = startupPricePerLicense;
      }
    }
    const cycleMultiplier = getBillingCycleMultiplier(localSubWithPackage!.billing_cycle || "QUARTERLY");
    const baseAmountCentsPerCycle = Math.round(
      pricePerLicense * localSubWithPackage!.license_count * cycleMultiplier * 100,
    );

    const charge = this.billing.calculateRenewalChargeCents({
      baseAmountCents: baseAmountCentsPerCycle,
      country: company?.country,
    });
    const feePcts = this.billing.renewalFeePercentagesForDb(company?.country);
    const stripePaidAmountCents =
      typeof stripeInvoiceForDb.amount_paid === "number" && stripeInvoiceForDb.amount_paid >= 0
        ? stripeInvoiceForDb.amount_paid
        : null;
    const finalTotalCents = stripePaidAmountCents !== null ? stripePaidAmountCents : charge.totalCents;
    const skipLocalInvoiceForZeroFirstSubscription =
      this.billing.isStripeFirstSubscriptionInvoiceBillingReason(stripeInvoiceForDb.billing_reason) &&
      finalTotalCents === 0;
    // Paid signup already has a local row + Stripe manual invoice (-0001); the subscription_create invoice (-0002)
    // is only for Billing state — duplicating it inflates admin totals (and confused amounts before Stripe price fix).
    const skipDuplicateSignupSubscriptionMirrorInvoice =
      this.billing.isStripeFirstSubscriptionInvoiceBillingReason(stripeInvoiceForDb.billing_reason) &&
      stripeSub.metadata?.["source"] === "signup_account_sync";
    const skipLocalInvoiceRow =
      skipLocalInvoiceForZeroFirstSubscription || skipDuplicateSignupSubscriptionMirrorInvoice;
    const createdInvoiceRow = couldCreateLocalInvoiceRow && !skipLocalInvoiceRow;

    const invoiceNumber = stripeInvoiceForDb.number ? String(stripeInvoiceForDb.number) : stripeInvoiceForDb.id;
    const { paymentIntentId, chargeId, checkoutSessionId } =
      this.billing.extractPaymentIntentAndChargeIds(stripeInvoiceForDb);

    const stripePaidAtUnix =
      stripeInvoiceForDb.status_transitions && typeof stripeInvoiceForDb.status_transitions.paid_at === "number"
        ? stripeInvoiceForDb.status_transitions.paid_at
        : null;

    const paidDate = stripePaidAtUnix ? new Date(stripePaidAtUnix * 1000) : new Date();
    const derived = this.billing.derivePeriodsFromStripeInvoice(stripeInvoiceForDb);
    // Prefer Stripe/invoice period boundaries over local DB values.
    // Local values can reflect an earlier cycle and would skew renewal rotation dates.
    let periodStart = derived.periodStart ?? localSubWithPackage!.start_date;
    let periodEndLocal = derived.periodEnd ?? localSubWithPackage!.end_date;
    const subPeriod = stripeSub as StripeSubscriptionApi;
    const cps = subPeriod.current_period_start;
    const cpe = subPeriod.current_period_end;
    if (typeof cps === "number") {
      periodStart = new Date(cps * 1000);
    }
    if (typeof cpe === "number") {
      periodEndLocal = new Date(cpe * 1000);
    }
    // Stripe `interval=year` uses calendar anchors; portal access uses fixed 30-day months (annual = 360d access).
    if (signupPaidSubscriptionCreate && periodStart && !isTrialLikePackageForBilling) {
      periodEndLocal = addBillingDays(
        periodStart,
        getBillingCyclePeriodDays(localSubWithPackage!.billing_cycle ?? "QUARTERLY"),
      );
    }
    // Portal billing uses fixed 30-day "months" (quarterly = 90×86400s days). Do not use calendar setDate(+90),
    // which follows month lengths and matches Stripe's calendar quarter, not product rules.
    if (stripeInvoiceForDb.billing_reason === "subscription_cycle" && periodStart) {
      periodEndLocal = addBillingDays(
        periodStart,
        getBillingCyclePeriodDays(localSubWithPackage!.billing_cycle ?? "QUARTERLY"),
      );
    }

    const effectiveSubscriptionType = await this.billing.normalizeLocalSubscriptionTypeAfterFirstStripeInvoice({
      subscriptionRowId: localSubWithPackage!.id,
      currentType: localSubWithPackage!.subscription_type,
      previousSubscriptionId: localSubWithPackage!.previous_subscription_id,
      billingReason: stripeInvoiceForDb.billing_reason,
    });

    const invoiceTypeResolved = this.billing.resolveInvoiceTypeForStripeSubscriptionInvoice({
      billingReason: stripeInvoiceForDb.billing_reason,
      localSubscriptionType: effectiveSubscriptionType,
    });

    let localInvoiceId: number | null = null;
    let localInvoiceNumber: string | null = null;
    let localInvoiceTotalAmount: number | null = null;

    if (couldCreateLocalInvoiceRow && !skipLocalInvoiceRow) {
      await this.prisma.client.invoice.create({
        data: {
          company_id: companyId,
          subscription_id: localSub.id,
          invoice_number: invoiceNumber,
          unit_price_per_license: Number(localSubWithPackage!.package.price_per_licence ?? 0),
          license_quantity: localSubWithPackage!.license_count,
          package_type: localSubWithPackage!.package.package_type,
          // Snapshot the live subscription cycle onto the invoice row — this
          // is the value the shared PDF builder prints, so later plan or
          // cycle changes do not silently rewrite history on this invoice.
          billing_cycle: localSubWithPackage!.billing_cycle ?? undefined,
          status: InvoiceStatus.PAID,
          invoice_type: invoiceTypeResolved,
          stripe_invoice_id: invoice.id,
          stripe_payment_intent_id: paymentIntentId ?? undefined,
          stripe_charge_id: chargeId ?? undefined,
          stripe_checkout_session_id: checkoutSessionId ?? undefined,
          paid_date: paidDate,
          period_start: periodStart ?? undefined,
          period_end: periodEndLocal ?? undefined,
          ...invoiceBillingAmountsToDbFields(
            invoiceBillingAmountsFromRenewalCents({
              baseAmountCents: baseAmountCentsPerCycle,
              processingFeeCents: charge.processingFeeCents,
              vatCents: charge.vatCents,
              totalCents: finalTotalCents,
              processingFeePct: feePcts.processingFeePct,
              vatPct: feePcts.vatPct,
            }),
          ),
        } as any,
      });
      if (stripePaidAmountCents !== null && stripePaidAmountCents !== charge.totalCents) {
        this.logger.warn(
          `handleInvoicePaid: Stripe paid amount mismatch for invoice ${invoice.id}. local_calc=${charge.totalCents}c stripe_paid=${stripePaidAmountCents}c`,
        );
      }
      this.logger.log(
        `handleInvoicePaid: created local Invoice row for stripe_invoice_id=${invoice.id} (company=${companyId}, subscription=${localSub.id}).`,
      );
      const createdInvoice = await this.prisma.client.invoice.findFirst({
        where: { stripe_invoice_id: invoice.id },
        select: { id: true, invoice_number: true, total_amount: true },
      });
      localInvoiceId = createdInvoice?.id ?? null;
      localInvoiceNumber = createdInvoice?.invoice_number ?? null;
      localInvoiceTotalAmount =
        createdInvoice?.total_amount !== null && createdInvoice?.total_amount !== undefined
          ? Number(createdInvoice.total_amount)
          : null;
    } else if (existingInvoice) {
      const existingTotal = decimalToNumber(existingInvoice["total_amount"]);

      const updateData: Record<string, unknown> = {};
      // If the invoice row already exists (often created earlier as PENDING from `invoice.created`),
      // ensure we mark it as PAID on the `invoice.paid` webhook.
      updateData["status"] = InvoiceStatus.PAID;
      updateData["paid_date"] = paidDate;
      if (!existingInvoice["invoice_number"] && invoiceNumber) {
        updateData["invoice_number"] = invoiceNumber;
      }
      const renewalBillingFields = invoiceBillingAmountsToDbFields(
        invoiceBillingAmountsFromRenewalCents({
          baseAmountCents: baseAmountCentsPerCycle,
          processingFeeCents: charge.processingFeeCents,
          vatCents: charge.vatCents,
          totalCents: finalTotalCents,
          processingFeePct: feePcts.processingFeePct,
          vatPct: feePcts.vatPct,
        }),
      );
      const breakdownTotal = renewalBillingFields.total_amount;
      const existingPreVat = decimalToNumber(existingInvoice.pre_vat_total_amount);
      const needsBillingFieldSync =
        existingTotal === null ||
        Math.abs(existingTotal - breakdownTotal) > 0.01 ||
        existingPreVat === null ||
        Math.abs(existingPreVat - renewalBillingFields.pre_vat_total_amount) > 0.01;
      // Trial signup creates a $0 INITIAL row, then links Stripe's $0 subscription_create invoice.
      // Do not replace that row with STARTUP catalog pricing (used only for post-trial billing estimates).
      const preserveTrialSignupZeroAmounts =
        skipLocalInvoiceRow ||
        (finalTotalCents === 0 && (existingTotal === null || existingTotal === 0));
      if (needsBillingFieldSync && !preserveTrialSignupZeroAmounts) {
        Object.assign(updateData, renewalBillingFields);
      } else if (needsBillingFieldSync && preserveTrialSignupZeroAmounts) {
        this.logger.log(
          `handleInvoicePaid: kept $0 billing amounts on local invoice ${existingInvoice.id} (stripe_invoice_id=${invoice.id}; trial/signup mirror).`,
        );
      }
      if (stripePaidAmountCents !== null && Math.abs(stripePaidAmountCents / 100 - breakdownTotal) > 0.01) {
        this.logger.warn(
          `handleInvoicePaid: Stripe paid amount differs from stored breakdown total for invoice ${invoice.id}. breakdown_total=${breakdownTotal} stripe_paid=${(stripePaidAmountCents / 100).toFixed(2)}`,
        );
      }

      if (paymentIntentId && existingInvoice["stripe_payment_intent_id"] !== paymentIntentId) {
        updateData["stripe_payment_intent_id"] = paymentIntentId;
      }
      if (chargeId && existingInvoice["stripe_charge_id"] !== chargeId) {
        updateData["stripe_charge_id"] = chargeId;
      }
      if (checkoutSessionId && existingInvoice["stripe_checkout_session_id"] !== checkoutSessionId) {
        updateData["stripe_checkout_session_id"] = checkoutSessionId;
      }

      if (periodStart && !existingInvoice["period_start"]) {
        updateData["period_start"] = periodStart;
      }
      if (periodEndLocal && !existingInvoice["period_end"]) {
        updateData["period_end"] = periodEndLocal;
      }

      if (existingInvoice["invoice_type"] !== invoiceTypeResolved) {
        updateData["invoice_type"] = invoiceTypeResolved;
      }

      if (Object.keys(updateData).length > 0) {
        await this.prisma.client.invoice.update({
          where: { id: existingInvoice.id },
          data: updateData as any,
        });
        this.logger.log(
          `handleInvoicePaid: updated local Invoice row for stripe_invoice_id=${invoice.id} (company=${companyId}, subscription=${localSub.id}).`,
        );
      }
      localInvoiceId = existingInvoice.id;
      localInvoiceNumber = existingInvoice.invoice_number ?? null;
      localInvoiceTotalAmount = needsBillingFieldSync
        ? breakdownTotal
        : (existingTotal ?? breakdownTotal);
    } else if (skipLocalInvoiceRow) {
      this.logger.log(
        `handleInvoicePaid: no local invoice row for stripe_invoice_id=${invoice.id} (${
          skipDuplicateSignupSubscriptionMirrorInvoice
            ? "skipped signup subscription mirror invoice"
            : "skipped zero first-subscription invoice"
        }); subscription dates still sync below.`,
      );
    }

    const periodEndUnix = (stripeSub as Stripe.Subscription & { current_period_end?: number }).current_period_end;
    const stripeInvoiceBr = stripeInvoiceForDb.billing_reason ?? "";
    const isSubscriptionCycleInvoice = stripeInvoiceBr === "subscription_cycle";
    const periodEnd =
      isSubscriptionCycleInvoice && periodEndLocal
        ? periodEndLocal
        : signupPaidSubscriptionCreate && periodEndLocal
          ? periodEndLocal
          : typeof periodEndUnix === "number"
            ? new Date(periodEndUnix * 1000)
            : periodEndLocal ?? null;
    // ── TRIAL-AWARE NEXT BILLING DATE ────────────────────────────────────────
    // For trialing Stripe subscriptions, current_period_end represents the END
    // of the FIRST POST-TRIAL CYCLE (= trial_end + interval), NOT the trial
    // cutoff. Using it as next_billing_date is wrong because the customer's
    // next CHARGE happens at trial_end, not at trial_end + interval.
    //
    // Bug this fixes: FREE_TRIAL/VIP_TRIAL accounts had next_billing_date
    // overwritten to "signup + 90 days" (Stripe's QUARTERLY current_period_end)
    // when the $0 trial invoice fired the invoice.paid webhook, even though
    // the initial create logic correctly set it to "signup + trial_duration_days".
    //
    // Fix: when the sub is trialing AND trial_end is in the future, prefer
    // trial_end as the next_billing_date — that's the actual next charge date.
    const isStripeTrialing = stripeSub.status === "trialing";
    const stripeTrialEndUnix = (stripeSub as Stripe.Subscription & { trial_end?: number | null }).trial_end;
    const trialEndDate =
      typeof stripeTrialEndUnix === "number" && stripeTrialEndUnix > 0
        ? new Date(stripeTrialEndUnix * 1000)
        : null;
    const trialEndIsInFuture = !!(trialEndDate && trialEndDate.getTime() > Date.now());
    // Expired recovery writes the intended next renewal here (already = one cycle from charge day). It is not
    // a period-start anchor — do not add another cycle (previously done only for batch_expired_recovery).
    const localNextBillingIso = stripeSub.metadata?.["local_next_billing_anchor"];
    let computedNextBillingDate: Date | null = null;
    if (isStripeTrialing && trialEndDate && trialEndIsInFuture) {
      // Highest priority: trial_end is the actual next charge date for trialing subs.
      computedNextBillingDate = trialEndDate;
      this.logger.log(
        `[stripe-renewal-debug] handleInvoicePaid: trialing sub ${stripeSub.id} — using trial_end=${trialEndDate.toISOString()} as next_billing_date (NOT current_period_end=${periodEnd?.toISOString() ?? "null"})`,
      );
    } else if (localNextBillingIso) {
      const parsed = new Date(localNextBillingIso);
      if (!Number.isNaN(parsed.getTime())) {
        computedNextBillingDate = parsed;
      }
    }
    if (isSubscriptionCycleInvoice) {
      // Product rules use fixed billing-cycle days for renewals.
      // Never let metadata/Stripe calendar anchors shrink this to 30-day-style periods.
      computedNextBillingDate =
        periodEndLocal ??
        (periodStart
          ? addBillingDays(periodStart, getBillingCyclePeriodDays(localSubWithPackage?.billing_cycle ?? "QUARTERLY"))
          : null);
    }
    if (!computedNextBillingDate) {
      computedNextBillingDate = periodEnd ?? localSub.next_billing_date ?? null;
    }
    if (!localSub) {
      return;
    }
    const resolvedLocalSub = localSub;
    const stripeCancelAtPeriodEnd = localStripeCancelAtPeriodEndFromSubscription(stripeSub);
    // Stripe is the source of truth for the next billing date.
    // Prefer Stripe subscription current_period_end; fall back to invoice-derived periodEnd/local DB.
    let nextBillingForRenewal = computedNextBillingDate ?? periodEnd ?? resolvedLocalSub.next_billing_date ?? null;
    // Ensure we always have a usable date for renewal rotation.
    if (!nextBillingForRenewal || Number.isNaN(nextBillingForRenewal.getTime())) {
      const anchor = periodEndLocal ?? resolvedLocalSub.next_billing_date ?? periodEnd ?? null;
      if (anchor && anchor instanceof Date && !Number.isNaN(anchor.getTime())) {
        nextBillingForRenewal = anchor;
      } else {
        // Final fallback: add one billing cycle from "now" based on local subscription billing_cycle.
        nextBillingForRenewal = addBillingDays(
          new Date(),
          getBillingCyclePeriodDays(resolvedLocalSub.billing_cycle ?? undefined),
        );
      }
    }
    // Keep these aligned since downstream uses computedNextBillingDate for emails and reactivation.
    computedNextBillingDate = nextBillingForRenewal;
    this.logger.log(
      `[stripe-renewal-debug] handleInvoicePaid timing stripe_invoice_id=${stripeInvoiceId} periodEnd=${
        periodEnd ? periodEnd.toISOString().slice(0, 10) : "null"
      } computedNextBillingDate=${computedNextBillingDate ? computedNextBillingDate.toISOString().slice(0, 10) : "null"} nextBillingForRenewal=${
        nextBillingForRenewal ? nextBillingForRenewal.toISOString().slice(0, 10) : "null"
      } localSub=${resolvedLocalSub.id} localStatus=${resolvedLocalSub.status} is_current=${resolvedLocalSub.is_current}`,
    );

    // Never "reactivate" CANCELLED rows (e.g. VIP superseded by renewal): that was incorrectly rewriting
    // next_billing_date on the archived subscription. EXPIRED recovery + stray non-current ACTIVE only.
    if (
      resolvedLocalSub.status === SubscriptionStatus.EXPIRED ||
      (resolvedLocalSub.status === SubscriptionStatus.ACTIVE && !resolvedLocalSub.is_current)
    ) {
      await this.prisma.client.$transaction(async (tx) => {
        await tx.subscription.updateMany({
          where: { company_id: companyId, id: { not: resolvedLocalSub.id } },
          data: { is_current: false },
        });
        await tx.subscription.updateMany({
          where: {
            company_id: companyId,
            id: { not: resolvedLocalSub.id },
            status: SubscriptionStatus.ACTIVE,
          },
          data: { status: SubscriptionStatus.CANCELLED },
        });
        await tx.subscription.update({
          where: { id: resolvedLocalSub.id },
          data: {
            status: SubscriptionStatus.ACTIVE,
            is_current: true,
            next_billing_date: computedNextBillingDate ?? periodEnd ?? resolvedLocalSub.next_billing_date,
            stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
          },
        });
        await tx.company.update({
          where: { id: companyId },
          data: { is_subscription_expiry: false },
        });
      });
      this.logger.log(
        `handleInvoicePaid: reactivated local subscription ${resolvedLocalSub.id} for company ${companyId} (Stripe ${subscriptionId})`,
      );
      // Do not return — the success email below must run for expired recovery and other reactivation flows.
    }

    // Used for the success email + webhook metadata even if we skip rotation.
    // Also prevents nullability issues when concurrent webhook deliveries reuse an existing renewal row.
    let subscriptionIdForEmail = localSub.id;

    if (nextBillingForRenewal) {
      const oldSubId = localSub.id;
      const oldSubNextBillingDate = localSub.next_billing_date;
      if (!nextBillingForRenewal || Number.isNaN(nextBillingForRenewal.getTime())) {
        this.logger.warn(
          `handleInvoicePaid: renewal subscription rotation skipped for ${companyId} (invalid next_billing_date)`,
        );
        // Fallback: keep existing behavior so next renewal date isn't left stale.
        await this.prisma.client.subscription.update({
          where: { id: localSub.id },
          data: {
            next_billing_date: computedNextBillingDate ?? periodEnd ?? localSub.next_billing_date,
            stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
          },
        });
      } else if (stripeInvoiceForDb.billing_reason !== "subscription_cycle") {
        await this.prisma.client.subscription.update({
          where: { id: resolvedLocalSub.id },
          data: {
            next_billing_date: nextBillingForRenewal,
            stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
            ...(signupPaidSubscriptionCreate && periodStart && periodEndLocal
              ? { start_date: periodStart, end_date: periodEndLocal }
              : {}),
          },
        });
      } else {
        const currentNext = resolvedLocalSub.next_billing_date;
        const alreadyAligned =
          currentNext &&
          nextBillingForRenewal &&
          !Number.isNaN(currentNext.getTime()) &&
          !Number.isNaN(nextBillingForRenewal.getTime()) &&
          currentNext.toISOString().slice(0, 10) === nextBillingForRenewal.toISOString().slice(0, 10);

        const shouldSkipRotation =
          !createdInvoiceRow &&
          wasAlreadyPaid &&
          alreadyAligned &&
          resolvedLocalSub.status === SubscriptionStatus.ACTIVE &&
          resolvedLocalSub.is_current;

        if (shouldSkipRotation) {
          await this.prisma.client.subscription.update({
            where: { id: resolvedLocalSub.id },
            data: {
              next_billing_date: nextBillingForRenewal,
              stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
            },
          });
          this.logger.warn(
            `[renewal-debug] subscription-rotation skipped (already aligned) stripe_invoice_id=${stripeInvoiceId} company_id=${companyId} local_subscription_id=${resolvedLocalSub.id}`,
          );
          this.logger.warn(
            `[stripe-link-debug] handleInvoicePaid skip-rotation reason=already-paid-and-aligned nextBilling=${nextBillingForRenewal.toISOString().slice(0, 10)}`,
          );
        } else {
          if (!localSubWithPackage?.package) {
            this.logger.warn(
              `[stripe-renewal-debug] rotation aborted: missing localSubWithPackage.package (stripe_invoice_id=${stripeInvoiceId} company_id=${companyId} local_subscription_id=${resolvedLocalSub.id})`,
            );
            await this.prisma.client.subscription.update({
              where: { id: localSub.id },
              data: {
                next_billing_date: nextBillingForRenewal,
                stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
              },
            });
            return;
          }
          const rotationSource = localSubWithPackage;
          const pkgType = rotationSource.package?.package_type;
          const shouldGraduateTrialToStartup =
            pkgType === PackageType.FREE_TRIAL || pkgType === PackageType.PRIVATE_VIP_TRIAL;
          const startupPackage = shouldGraduateTrialToStartup
            ? await this.prisma.client.package.findFirst({
                where: { package_type: PackageType.STARTUP, is_active: true },
                select: { id: true, minimum_license_required: true, price_per_licence: true },
              })
            : null;
          if (shouldGraduateTrialToStartup && !startupPackage) {
            this.logger.warn(
              `handleInvoicePaid: trial graduation (FREE_TRIAL/VIP) requires an active STARTUP package, but none found (company=${companyId}). Keeping package_id=${rotationSource.package_id}.`,
            );
          }
          const renewalPackageId =
            shouldGraduateTrialToStartup && startupPackage ? startupPackage.id : rotationSource.package_id;
          const renewalLicenseCount =
            shouldGraduateTrialToStartup &&
            startupPackage &&
            typeof startupPackage.minimum_license_required === "number"
              ? Math.max(rotationSource.license_count, startupPackage.minimum_license_required)
              : rotationSource.license_count;
          const shouldRewriteInvoiceToStartup = shouldGraduateTrialToStartup && !!startupPackage;
          const startupInvoiceSnapshot =
            shouldRewriteInvoiceToStartup && startupPackage
              ? (() => {
                  const pricePerLicense = Number(startupPackage.price_per_licence ?? 0);
                  const cycleMultiplier = getBillingCycleMultiplier(rotationSource.billing_cycle || "QUARTERLY");
                  const baseAmountCentsPerCycle = Math.round(pricePerLicense * renewalLicenseCount * cycleMultiplier * 100);
                  const charge = this.billing.calculateRenewalChargeCents({
                    baseAmountCents: baseAmountCentsPerCycle,
                    country: company?.country,
                  });
                  const feePcts = this.billing.renewalFeePercentagesForDb(company?.country);
                  return {
                    package_type: PackageType.STARTUP,
                    unit_price_per_license: pricePerLicense,
                    license_quantity: renewalLicenseCount,
                    ...invoiceBillingAmountsToDbFields(
                      invoiceBillingAmountsFromRenewalCents({
                        baseAmountCents: baseAmountCentsPerCycle,
                        processingFeeCents: charge.processingFeeCents,
                        vatCents: charge.vatCents,
                        totalCents: charge.totalCents,
                        processingFeePct: feePcts.processingFeePct,
                        vatPct: feePcts.vatPct,
                      }),
                    ),
                  };
                })()
              : null;

          // Concurrency/idempotency guard:
          // Stripe may deliver `invoice.paid` and its aliases nearly simultaneously, causing two rotations.
          // If a renewal row for this exact oldSubId + nextBilling already exists, reuse it and relink invoice.
          const existingRenewal = await this.prisma.client.subscription.findFirst({
            where: {
              company_id: companyId,
              previous_subscription_id: oldSubId,
              stripe_subscription_id: subscriptionId,
              subscription_type: SubscriptionType.RENEWAL,
              is_current: true,
              next_billing_date: nextBillingForRenewal,
            },
            orderBy: { id: "desc" },
            select: { id: true },
          });
          if (existingRenewal) {
            if (localInvoiceId) {
              await this.prisma.client.invoice.update({
                where: { id: localInvoiceId },
                data: {
                  subscription_id: existingRenewal.id,
                  ...(startupInvoiceSnapshot ? startupInvoiceSnapshot : {}),
                } as any,
              });
            }
            this.logger.warn(
              `[renewal-debug] subscription-rotation skipped (existing renewal) stripe_invoice_id=${stripeInvoiceId} company_id=${companyId} old_subscription_id=${oldSubId} existing_subscription_id=${existingRenewal.id}`,
            );
            subscriptionIdForEmail = existingRenewal.id;
            // Continue with email + expiry clear below.
          } else {
            const newSub = await this.prisma.client.$transaction(async (tx) => {
              await tx.subscription.updateMany({
                where: { company_id: companyId, id: { not: oldSubId } },
                data: { is_current: false },
              });
              await tx.subscription.updateMany({
                where: {
                  company_id: companyId,
                  id: { not: oldSubId },
                  status: SubscriptionStatus.ACTIVE,
                },
                data: { status: SubscriptionStatus.CANCELLED },
              });
              // 1) Archive previous current row (historical next_billing_date unchanged; clear Stripe link so lookups by sub id prefer the renewal row)
              await tx.subscription.update({
                where: { id: oldSubId },
                data: {
                  status: SubscriptionStatus.CANCELLED,
                  is_current: false,
                  stripe_subscription_id: null,
                  // Preserve historical next_billing_date on archived rows.
                  // Only the newly created renewal row should get the new next billing date.
                  next_billing_date: oldSubNextBillingDate,
                  end_date: periodStart ?? paidDate ?? new Date(),
                } as any,
              });

              // 2) Create the renewal "current" row
              const created = await tx.subscription.create({
                data: {
                  company_id: companyId,
                  package_id: renewalPackageId,
                  previous_subscription_id: oldSubId,
                  license_count: renewalLicenseCount,

                  status: SubscriptionStatus.ACTIVE,
                  billing_cycle: rotationSource.billing_cycle ?? undefined,
                  // Use Stripe period start, not webhook timestamp
                  start_date: periodStart ?? paidDate ?? new Date(),
                  end_date: nextBillingForRenewal,
                  next_billing_date: nextBillingForRenewal,

                  stripe_subscription_id: subscriptionId,
                  stripe_cancel_at_period_end: stripeCancelAtPeriodEnd,
                  is_current: true,
                  subscription_type: SubscriptionType.RENEWAL,

                  licenses_available: rotationSource.licenses_available,
                  licenses_consumed: rotationSource.licenses_consumed,
                  licenses_expired: rotationSource.licenses_expired,
                  last_license_assignment: rotationSource.last_license_assignment ?? undefined,
                  last_license_release: rotationSource.last_license_release ?? undefined,

                  user_id_created_by: rotationSource.user_id_created_by ?? undefined,
                  user_id_updated_by: rotationSource.user_id_updated_by ?? undefined,
                } as any,
              });

              return created;
            });

            localSub = newSub;
            subscriptionIdForEmail = newSub.id;
            if (localInvoiceId) {
              await this.prisma.client.invoice.update({
                where: { id: localInvoiceId },
                data: {
                  subscription_id: newSub.id,
                  ...(startupInvoiceSnapshot ? startupInvoiceSnapshot : {}),
                } as any,
              });
              this.logger.warn(
                `[renewal-debug] invoice-relinked-to-rotated-subscription stripe_invoice_id=${stripeInvoiceId} local_invoice_id=${localInvoiceId} old_subscription_id=${oldSubId} new_subscription_id=${newSub.id}`,
              );
            }
            this.logger.log(
              `handleInvoicePaid: rotated local subscription on renewal (company=${companyId}, old=${oldSubId}, new=${newSub.id})`,
            );
            this.logger.warn(
              `[renewal-debug] subscription-rotated stripe_invoice_id=${stripeInvoiceId} company_id=${companyId} old_subscription_id=${oldSubId} new_subscription_id=${newSub.id}`,
            );
          }
        }
      }
    } else {
      this.logger.warn(
        `[renewal-debug] subscription-update-skipped-invalid-next-billing stripe_invoice_id=${stripeInvoiceId} local_subscription_id=${localSub.id} company_id=${companyId} (nextBillingForRenewal=null)`,
      );
    }
    await this.billing.clearCompanySubscriptionExpiryWhenRenewalIsAhead(companyId);

    // Paid signup: email from manual OOB invoice; skip duplicate on mirror subscription_create (-0002) / $0 first invoice.
    if (!skipLocalInvoiceRow) {
      const nextBill = computedNextBillingDate ?? null;
      const amountUsd = localInvoiceTotalAmount ?? finalTotalCents / 100;
      if (this.shouldSendPaymentSucceededEmail(amountUsd)) {
        await this.billing.sendCompanyTemplatedEmail({
          companyId,
          templateKey: "billing.payment.succeeded",
          dedupeKey: `billing.payment.succeeded:${stripeInvoiceId}`,
          triggeredBy: "stripe_webhook_invoice_paid",
          variables: {
            "invoice.number": localInvoiceNumber ?? stripeInvoiceId,
            "invoice.amount": this.billing.formatUsd(amountUsd),
            "subscription.next_billing_date": nextBill ? nextBill.toISOString().split("T")[0] : "",
          },
          metadata: {
            stripe_invoice_id: stripeInvoiceId,
            invoice_id: localInvoiceId ?? undefined,
            subscription_id: subscriptionIdForEmail,
          },
        });
      } else {
        this.logger.log(
          `handleInvoicePaid: skip billing.payment.succeeded email for zero-amount invoice stripe_invoice_id=${stripeInvoiceId} company=${companyId}`,
        );
      }
    } else {
      this.logger.log(
        `handleInvoicePaid: skip billing.payment.succeeded email stripe_invoice_id=${stripeInvoiceId} company=${companyId} (${
          skipDuplicateSignupSubscriptionMirrorInvoice
            ? "signup subscription mirror"
            : skipLocalInvoiceForZeroFirstSubscription
              ? "zero first-subscription invoice"
              : "skipped"
        })`,
      );
    }
  }
  async handleInvoicePaymentPaid(eventObject: unknown): Promise<void> {
    if (!this.stripeService.isConfigured()) return;

    try {
      const obj = eventObject as any;
      let invoiceId: string | null = null;

      // Common shapes:
      // - { invoice: "in_...", ... }
      // - { invoice: { id: "in_..." }, ... }
      // - { invoice_id: "in_..." }
      // - invoice itself ({ id: "...", object: "invoice" })
      if (obj && typeof obj === "object") {
        if (typeof obj.id === "string" && obj.object === "invoice") {
          invoiceId = obj.id;
        }
        if (!invoiceId && typeof obj.invoice === "string") {
          invoiceId = obj.invoice;
        }
        if (!invoiceId && obj.invoice && typeof obj.invoice === "object" && typeof obj.invoice.id === "string") {
          invoiceId = obj.invoice.id;
        }
        if (!invoiceId && typeof obj.invoice_id === "string") {
          invoiceId = obj.invoice_id;
        }
      }

      if (!invoiceId) {
        this.logger.warn(
          `[stripe-renewal-debug] handleInvoicePaymentPaid: could not resolve invoice id from payload`,
        );
        return;
      }

      const invoice = await this.stripeService.getInvoiceExpanded(invoiceId);
      await this.handleInvoicePaid(invoice);
    } catch (e) {
      this.logger.warn(
        `[stripe-renewal-debug] handleInvoicePaymentPaid failed: ${e instanceof Error ? e.message : String(e)}`,
      );
    }
  }
  async handlePaymentMethodAttached(paymentMethod: Stripe.PaymentMethod): Promise<void> {
    if (!this.stripeService.isConfigured()) return;

    const customerId = this.billing.extractStripeCustomerId(paymentMethod.customer as any);
    if (!customerId) return;

    const companyId = await this.billing.resolveCompanyIdFromStripeCustomer(customerId);
    if (!companyId) return;

    const company = await this.prisma.client.company.findUnique({
      where: { id: companyId },
      select: { stripe_subscription_id: true },
    });
    const stripeSubId = company?.stripe_subscription_id ?? null;
    if (!stripeSubId) return;

    try {
      const payNow = await this.stripeService.attemptPayLatestSubscriptionInvoice(stripeSubId);
      if (payNow.paid && payNow.invoiceId) {
        const inv = await this.stripeService.getInvoiceExpanded(payNow.invoiceId);
        await this.handleInvoicePaid(inv);
      }
    } catch (e) {
      this.logger.warn(
        `handlePaymentMethodAttached: attemptPayLatestSubscriptionInvoice failed for ${stripeSubId}: ${
          e instanceof Error ? e.message : String(e)
        }`,
      );
    }
  }
  async handleSetupIntentSucceeded(setupIntent: Stripe.SetupIntent): Promise<void> {
    if (!this.stripeService.isConfigured()) return;
    const customerId = this.billing.extractStripeCustomerId(setupIntent.customer as any);
    if (!customerId) return;

    const companyId = await this.billing.resolveCompanyIdFromStripeCustomer(customerId);
    if (!companyId) return;

    const company = await this.prisma.client.company.findUnique({
      where: { id: companyId },
      select: { stripe_subscription_id: true },
    });
    const stripeSubId = company?.stripe_subscription_id ?? null;
    if (!stripeSubId) return;

    try {
      // Guard: only attempt pay if there's an open invoice with amount_due > 0.
      // Free signups/trials can produce setup_intent events but no payable invoice.
      const subWithInvoice = await this.stripeService.retrieveSubscriptionWithLatestInvoice(stripeSubId);
      const latest = (subWithInvoice as any).latest_invoice as unknown;
      const latestInvoiceId =
        typeof latest === "string"
          ? latest
          : latest && typeof latest === "object" && "id" in (latest as any)
            ? String((latest as any).id)
            : null;
      const latestInvoiceStatus =
        latest && typeof latest === "object" && "status" in (latest as any) ? String((latest as any).status) : null;
      const latestAmountDue =
        latest && typeof latest === "object" && "amount_due" in (latest as any) ? Number((latest as any).amount_due) : null;

      if (!latestInvoiceId) {
        this.logger.warn(`handleSetupIntentSucceeded: no latest_invoice for sub ${stripeSubId}; skip auto-pay`);
        return;
      }
      if (latestInvoiceStatus && latestInvoiceStatus !== "open" && latestInvoiceStatus !== "draft") {
        this.logger.warn(
          `handleSetupIntentSucceeded: latest invoice ${latestInvoiceId} status=${latestInvoiceStatus}; skip auto-pay`,
        );
        return;
      }
      if (typeof latestAmountDue === "number" && latestAmountDue <= 0) {
        this.logger.warn(
          `handleSetupIntentSucceeded: latest invoice ${latestInvoiceId} amount_due=${latestAmountDue}; skip auto-pay`,
        );
        return;
      }

      const payNow = await this.stripeService.attemptPayLatestSubscriptionInvoice(stripeSubId);
      if (payNow.paid && payNow.invoiceId) {
        const inv = await this.stripeService.getInvoiceExpanded(payNow.invoiceId);
        await this.handleInvoicePaid(inv);
      }
    } catch (e) {
      this.logger.warn(
        `handleSetupIntentSucceeded: attemptPayLatestSubscriptionInvoice failed for ${stripeSubId}: ${
          e instanceof Error ? e.message : String(e)
        }`,
      );
    }
  }
  async handleInvoiceCreated(invoice: Stripe.Invoice): Promise<void> {
    const inv = invoice as Stripe.Invoice & {
      subscription?: string | Stripe.Subscription | null;
    };
    const stripeInvoiceId = String(invoice.id);

    const paidStatus =
      invoice.status === "paid" ||
      (typeof invoice.amount_paid === "number" && invoice.amount_paid > 0) ||
      (invoice.status_transitions && typeof invoice.status_transitions.paid_at === "number");

    // Stripe sends both `invoice.created` and `invoice.paid` for the same invoice (often back-to-back).
    // Calling `handleInvoicePaid` from both races soft email dedupe and sends duplicate billing.payment.succeeded.
    // `invoice.paid` is the canonical handler (same idea as ignoring `invoice.payment_succeeded`).
    if (paidStatus) {
      this.logger.log(
        `handleInvoiceCreated: stripe_invoice_id=${stripeInvoiceId} already paid; deferring to invoice.paid (no-op here)`,
      );
      return;
    }

    const subRef = inv.subscription;
    let subscriptionId = typeof subRef === "string" ? subRef : subRef?.id ?? null;
    if (!subscriptionId) {
      const invAny = invoice as unknown as Record<string, unknown>;
      const linesObj = invAny["lines"];
      const linesData =
        linesObj && typeof linesObj === "object" ? (linesObj as Record<string, unknown>)["data"] : null;
      const lines = Array.isArray(linesData) ? (linesData as unknown[]) : [];
      const lineSub = lines
        .map((l) => {
          if (!l || typeof l !== "object") return null;
          const rec = l as Record<string, unknown>;
          const sub = rec["subscription"];
          if (typeof sub === "string") return sub;
          if (sub && typeof sub === "object" && typeof (sub as Record<string, unknown>)["id"] === "string") {
            return String((sub as Record<string, unknown>)["id"]);
          }
          return null;
        })
        .find((v): v is string => typeof v === "string");
      subscriptionId = lineSub ?? null;
      if (subscriptionId) {
        this.logger.warn(
          `[stripe-link-debug] handleInvoiceCreated derived subscription from invoice.lines stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId}`,
        );
      }
    }
    if (!subscriptionId && this.stripeService.isConfigured()) {
      const customerId = this.billing.extractStripeCustomerId(inv.customer);
      if (customerId) {
        try {
          const subs = await this.stripeService.listSubscriptionsForCustomer(customerId, [
            "active",
            "trialing",
            "past_due",
            "unpaid",
          ]);
          const matchByLatestInvoice = subs.find((s) => {
            const li = (s as any).latest_invoice;
            const liId = typeof li === "string" ? li : li && typeof li === "object" && "id" in li ? String(li.id) : null;
            return liId === stripeInvoiceId;
          });
          if (matchByLatestInvoice) {
            subscriptionId = matchByLatestInvoice.id;
          } else if (subs.length === 1) {
            subscriptionId = subs[0].id;
          }
          if (subscriptionId) {
            this.logger.warn(
              `[stripe-link-debug] handleInvoiceCreated derived subscription from customer subscriptions stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId} customer=${customerId} subs=${subs.length}`,
            );
          }
        } catch (e) {
          this.logger.warn(
            `[stripe-link-debug] handleInvoiceCreated failed deriving subscription via customer subscriptions stripe_invoice_id=${stripeInvoiceId} customer=${customerId}: ${
              e instanceof Error ? e.message : String(e)
            }`,
          );
        }
      }
    }
    if (!subscriptionId) {
      this.logger.warn(
        `[stripe-link-debug] handleInvoiceCreated skip: missing subscription id stripe_invoice_id=${stripeInvoiceId}`,
      );
      return;
    }

    if (!this.stripeService.isConfigured()) {
      this.logger.warn(
        `[stripe-link-debug] handleInvoiceCreated skip: Stripe not configured stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId}`,
      );
      return;
    }

    // If Stripe keeps the invoice in `draft`, it will not be charged.
    // Auto-finalize + attempt payment using the customer's default payment method.
    // This addresses the "invoice is draft even though default PM is set" scenario.
    if (invoice.status === "draft") {
      const amountDue = typeof invoice.amount_due === "number" ? invoice.amount_due : null;
      if (amountDue === null || amountDue > 0) {
        this.logger.warn(
          `[stripe-link-debug] handleInvoiceCreated: invoice draft -> attemptPayLatestSubscriptionInvoice stripe_invoice_id=${stripeInvoiceId} stripe_subscription_id=${subscriptionId} amount_due=${amountDue ?? "n/a"}`,
        );
        const payNow = await this.stripeService.attemptPayLatestSubscriptionInvoice(subscriptionId);
        if (payNow.paid && payNow.invoiceId) {
          const paidInv = await this.stripeService.getInvoiceExpanded(payNow.invoiceId);
          await this.handleInvoicePaid(paidInv);
          return;
        }
      }
    }

    // Prefer the current row so post-rotation we do not attach pending invoices to an archived trial row.
    let resolvedLocalSub = await this.prisma.client.subscription.findFirst({
      where: { stripe_subscription_id: subscriptionId, is_current: true },
      orderBy: { id: "desc" },
      include: { package: true },
    });
    if (!resolvedLocalSub) {
      resolvedLocalSub = await this.prisma.client.subscription.findFirst({
        where: { stripe_subscription_id: subscriptionId },
        orderBy: { id: "desc" },
        include: { package: true },
      });
    }

    if (!resolvedLocalSub) {
      // Fallback: Stripe may carry our mapping in metadata even if the local row hasn't
      // been linked with `stripe_subscription_id` yet.
      try {
        const stripeSub = await this.stripeService.retrieveSubscription(subscriptionId);
        const companyIdStr = stripeSub.metadata?.["company_id"];
        const localSubIdStr = stripeSub.metadata?.["local_subscription_id"];
        if (companyIdStr && localSubIdStr) {
          const companyId = parseInt(companyIdStr, 10);
          const localSubId = parseInt(localSubIdStr, 10);
          if (!Number.isNaN(companyId) && !Number.isNaN(localSubId)) {
            const byMeta = await this.prisma.client.subscription.findFirst({
              where: { id: localSubId, company_id: companyId },
              include: { package: true },
            });
            if (byMeta) {
              await this.prisma.client.subscription.update({
                where: { id: byMeta.id },
                data: { stripe_subscription_id: subscriptionId },
              });
              await this.prisma.client.company.update({
                where: { id: companyId },
                data: { stripe_subscription_id: subscriptionId },
              });
              resolvedLocalSub = await this.prisma.client.subscription.findUnique({
                where: { id: byMeta.id },
                include: { package: true },
              });
            }
          }
        }
      } catch {
        // ignore and handle unresolved below
      }
    }

    if (!resolvedLocalSub?.package) {
      const packageId = (resolvedLocalSub as any)?.package_id as number | null | undefined;
      if (packageId) {
        const pkg = await this.prisma.client.package.findUnique({ where: { id: packageId } });
        if (pkg) {
          resolvedLocalSub = { ...resolvedLocalSub, package: pkg } as any;
        }
      }
    }

    if (!resolvedLocalSub?.package) {
      this.logger.warn(
        `handleInvoiceCreated: could not resolve local subscription package for Stripe sub ${subscriptionId}; skipping pending invoice (stripe_invoice_id=${stripeInvoiceId})`,
      );
      return;
    }

    const companyId = resolvedLocalSub.company_id;
    const company = await this.prisma.client.company.findUnique({
      where: { id: companyId },
      select: { country: true },
    });
    if (!company) return;

    // Idempotency: do not create duplicate rows for the same Stripe invoice.
    const existing = await this.prisma.client.invoice.findFirst({
      where: { stripe_invoice_id: invoice.id },
      select: { id: true },
    });
    if (existing) return;

    const amountDueCents =
      typeof invoice.amount_due === "number" && invoice.amount_due >= 0 ? invoice.amount_due : null;

    const isTrialLikePackageForBilling = resolvedLocalSub.package.is_trial_package
      ? true
      : resolvedLocalSub.package.package_type === PackageType.FREE_TRIAL ||
        resolvedLocalSub.package.package_type === PackageType.PRIVATE_VIP_TRIAL;

    let pricePerLicense = Number(resolvedLocalSub.package.price_per_licence ?? 0);
    if (isTrialLikePackageForBilling && (!Number.isFinite(pricePerLicense) || pricePerLicense <= 0)) {
      const startupPackage = await this.prisma.client.package.findFirst({
        where: { package_type: PackageType.STARTUP, is_active: true },
        select: { price_per_licence: true },
      });
      const startupPricePerLicense = Number(startupPackage?.price_per_licence ?? 0);
      if (Number.isFinite(startupPricePerLicense) && startupPricePerLicense > 0) {
        pricePerLicense = startupPricePerLicense;
      }
    }

    const cycleMultiplier = getBillingCycleMultiplier(resolvedLocalSub.billing_cycle || "QUARTERLY");
    const baseAmountCentsPerCycle = Math.round(
      pricePerLicense * resolvedLocalSub.license_count * cycleMultiplier * 100,
    );
    const charge = this.billing.calculateRenewalChargeCents({
      baseAmountCents: baseAmountCentsPerCycle,
      country: company.country,
    });
    const feePcts = this.billing.renewalFeePercentagesForDb(company.country);

    const derived = this.billing.derivePeriodsFromStripeInvoice(invoice);
    const periodStart = derived.periodStart ?? resolvedLocalSub.start_date ?? null;
    let periodEndLocal = derived.periodEnd ?? resolvedLocalSub.end_date ?? null;
    if (invoice.billing_reason === "subscription_cycle" && periodStart) {
      periodEndLocal = addBillingDays(
        periodStart,
        getBillingCyclePeriodDays(resolvedLocalSub.billing_cycle ?? "QUARTERLY"),
      );
    }

    const invoiceNumber = invoice.number ? String(invoice.number) : stripeInvoiceId;
    const { paymentIntentId, chargeId, checkoutSessionId } = this.billing.extractPaymentIntentAndChargeIds(invoice);

    const finalTotalCents = amountDueCents !== null ? amountDueCents : charge.totalCents;

    const invoiceTypeResolved = this.billing.resolveInvoiceTypeForStripeSubscriptionInvoice({
      billingReason: invoice.billing_reason,
      localSubscriptionType: resolvedLocalSub.subscription_type,
    });

    // Paid signup already creates/links its own local initial invoice row.
    // The Stripe subscription_create mirror invoice is only a Stripe Billing state artifact
    // and should not create a second local invoice row for the same initial signup.
    let isSignupAccountSyncSource = false;
    if (this.billing.isStripeFirstSubscriptionInvoiceBillingReason(invoice.billing_reason)) {
      try {
        const stripeSub = await this.stripeService.retrieveSubscription(subscriptionId);
        isSignupAccountSyncSource = stripeSub.metadata?.["source"] === "signup_account_sync";
      } catch {
        isSignupAccountSyncSource = false;
      }
    }
    const skipDuplicateSignupSubscriptionMirrorInvoice =
      this.billing.isStripeFirstSubscriptionInvoiceBillingReason(invoice.billing_reason) &&
      isSignupAccountSyncSource;
    if (skipDuplicateSignupSubscriptionMirrorInvoice) {
      this.logger.log(
        `handleInvoiceCreated: skipped signup subscription mirror invoice stripe_invoice_id=${stripeInvoiceId} (company=${companyId}, subscription=${resolvedLocalSub.id}).`,
      );
      return;
    }

    await this.prisma.client.invoice.create({
      data: {
        company_id: companyId,
        subscription_id: resolvedLocalSub.id,
        invoice_number: invoiceNumber,
        unit_price_per_license: Number(resolvedLocalSub.package.price_per_licence ?? 0),
        license_quantity: resolvedLocalSub.license_count,
        package_type: resolvedLocalSub.package.package_type,
        billing_cycle: resolvedLocalSub.billing_cycle ?? undefined,
        status: InvoiceStatus.PENDING,
        invoice_type: invoiceTypeResolved,
        stripe_invoice_id: invoice.id,
        stripe_payment_intent_id: paymentIntentId ?? undefined,
        stripe_charge_id: chargeId ?? undefined,
        stripe_checkout_session_id: checkoutSessionId ?? undefined,
        period_start: periodStart ?? undefined,
        period_end: periodEndLocal ?? undefined,
        ...invoiceBillingAmountsToDbFields(
          invoiceBillingAmountsFromRenewalCents({
            baseAmountCents: baseAmountCentsPerCycle,
            processingFeeCents: charge.processingFeeCents,
            vatCents: charge.vatCents,
            totalCents: finalTotalCents,
            processingFeePct: feePcts.processingFeePct,
            vatPct: feePcts.vatPct,
          }),
        ),
      } as any,
    });

    this.logger.log(
      `handleInvoiceCreated: created local pending Invoice row for stripe_invoice_id=${stripeInvoiceId} (company=${companyId}, subscription=${resolvedLocalSub.id}).`,
    );
  }
}

results matching ""

    No results matching ""