File

apps/recallassess/recallassess-api/src/api/shared/stripe/services/stripe-account.service.ts

Index

Properties

Properties

company_id
company_id: number
Type : number
Optional
email_verification_token
email_verification_token: string
Type : string
Optional
error
error: string
Type : string
Optional
participant_id
participant_id: number
Type : number
Optional
password_setup_token
password_setup_token: string
Type : string
Optional
success
success: boolean
Type : boolean
temporary_password
temporary_password: string
Type : string
Optional
import * as crypto from "node:crypto";
import { BNestPrismaService } from "@bish-nest/core";
import { Injectable, Logger } from "@nestjs/common";
import {
  BillingCycle,
  InvoiceStatus,
  PackageType,
  ParticipantRole,
  Prisma,
  SubscriptionPlan,
  SubscriptionType,
} from "@prisma/client";
import * as argon from "argon2";
import {
  calculateNextBillingDateFromAnchor,
  calculateTrialNextBillingDate,
  getBillingCycleMultiplier,
} from "../../../../config/billing-cycle";
import {
  buildInvoiceBillingAmounts,
  invoiceBillingAmountsToDbFields,
  roundMoney,
} from "../../../../config/billing.config";
import { StripePaymentBillingHelpersService } from "./stripe-payment/stripe-payment-billing-helpers.service";
import { StripeService } from "./stripe.service";
import type Stripe from "stripe";

export interface AccountCreationData {
  // Company info
  company_name: string;
  company_email: string;
  company_phone?: string;

  // Admin user info
  admin_first_name: string;
  admin_last_name: string;
  admin_email: string;
  admin_password?: string;
  admin_phone?: string;
  admin_address?: string;
  admin_city?: string;
  admin_country?: string;

  // Package info
  package_id: number; // Foreign key to Package table
  package_type: PackageType; // For invoice/reference only
  license_count: number;
  unit_price: number;
  total_amount: number;
  billing_cycle?: string; // QUARTERLY, HALF_YEARLY, ANNUAL
  discount_amount?: number; // Discount amount from promo code
  coupon_code?: string; // Promo code used

  // VAT info (e.g. UAE 5% VAT)
  vat_fee_percentage?: number; // Stored as percentage (e.g. 5.00 for 5%)
  vat_fee?: number; // Calculated VAT amount

  // Stripe info
  stripe_customer_id?: string;
  stripe_subscription_id?: string;
  stripe_payment_intent_id?: string; // For paid amounts
  stripe_setup_intent_id?: string; // For $0 amounts (trial packages)
  stripe_invoice_id?: string; // Stripe Billing invoice id (when created)
}

export interface AccountCreationResult {
  success: boolean;
  company_id?: number;
  participant_id?: number;
  temporary_password?: string;
  password_setup_token?: string; // Token for password setup page
  email_verification_token?: string; // Token for email verification
  error?: string;
}

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

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

  private normalizeBillingCycle(raw: string | undefined | null): BillingCycle {
    const u = (raw ?? "QUARTERLY").trim().toUpperCase();
    if (u === "HALF_YEARLY") return BillingCycle.HALF_YEARLY;
    if (u === "ANNUAL") return BillingCycle.ANNUAL;
    return BillingCycle.QUARTERLY;
  }

  /**
   * Generate unique sequential invoice number
   */
  private async generateInvoiceNumber(): Promise<string> {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0");
    const day = String(today.getDate()).padStart(2, "0");
    const datePrefix = `${year}${month}${day}`;

    // Find the highest invoice number for today
    const todayInvoices = await this.prisma.client.invoice.findMany({
      where: {
        invoice_number: {
          startsWith: `INV-${datePrefix}-`,
        },
      },
      orderBy: {
        invoice_number: "desc",
      },
      take: 1,
    });

    let sequence = 1;
    if (todayInvoices.length > 0) {
      const lastInvoice = todayInvoices[0];
      const lastSequence = parseInt(lastInvoice.invoice_number.split("-").pop() ?? "0", 10);
      sequence = lastSequence + 1;
    }

    return `INV-${datePrefix}-${String(sequence).padStart(4, "0")}`;
  }

  private extractLatestInvoiceIdFromStripeSubscription(sub: Stripe.Subscription): string | null {
    const li = sub.latest_invoice;
    if (!li) {
      return null;
    }
    if (typeof li === "string") {
      return li;
    }
    if (typeof li === "object" && li !== null && "id" in li && typeof (li as Stripe.Invoice).id === "string") {
      return (li as Stripe.Invoice).id;
    }
    return null;
  }

  /**
   * Initial signup local invoice row — only persisted when we can set {@code stripe_invoice_id}.
   */
  private buildSignupInitialInvoiceUncheckedCreateInput(params: {
    companyId: number;
    subscriptionId: number;
    periodStart?: Date | null;
    periodEnd?: Date | null;
    data: AccountCreationData;
    invoiceNumber: string;
    stripeInvoiceId: string;
  }): Prisma.InvoiceUncheckedCreateInput {
    const { data, companyId, subscriptionId, invoiceNumber, stripeInvoiceId, periodStart, periodEnd } = params;
    const unitPrice = data.unit_price ?? 0;
    const subtotalBeforeDiscount = unitPrice * data.license_count;
    const discountAmount = data.discount_amount ?? 0;
    const billingAmounts = buildInvoiceBillingAmounts({
      grossLicenseAmount: subtotalBeforeDiscount,
      discountAmount,
      adminCountry: data.admin_country,
      processingFeeOnGrossLicense: true,
    });
    const calculatedTotal = billingAmounts.total_amount;
    const totalAmount =
      data.stripe_payment_intent_id && typeof data.total_amount === "number"
        ? data.total_amount
        : data.total_amount && Math.abs(data.total_amount - calculatedTotal) <= 0.01
          ? data.total_amount
          : calculatedTotal;
    const invoiceStatus: InvoiceStatus = "PAID";
    return {
      company_id: companyId,
      subscription_id: subscriptionId,
      invoice_number: invoiceNumber,
      unit_price_per_license: unitPrice,
      license_quantity: data.license_count,
      coupon_code: data.coupon_code || null,
      ...invoiceBillingAmountsToDbFields({
        ...billingAmounts,
        total_amount: roundMoney(totalAmount),
      }),
      package_type: data.package_type,
      status: invoiceStatus,
      invoice_type: "INITIAL_SUBSCRIPTION",
      stripe_invoice_id: stripeInvoiceId,
      stripe_payment_intent_id: data.stripe_payment_intent_id || null,
      stripe_checkout_session_id: data.stripe_setup_intent_id || null,
      paid_date: invoiceStatus === "PAID" ? new Date() : null,
      period_start: periodStart ?? undefined,
      period_end: periodEnd ?? undefined,
    } as Prisma.InvoiceUncheckedCreateInput;
  }

  /**
   * Link or create the signup local invoice once a Stripe Billing invoice id exists (subscription {@code latest_invoice}).
   */
  private async linkOrCreateSignupInvoiceFromStripeSubscription(params: {
    companyId: number;
    subscriptionId: number;
    stripeSubscriptionId: string;
    data: AccountCreationData;
  }): Promise<void> {
    if (!this.stripeService.isConfigured()) {
      return;
    }
    let expanded: Stripe.Subscription;
    try {
      expanded = await this.stripeService.retrieveSubscriptionWithLatestInvoice(params.stripeSubscriptionId);
    } catch (e) {
      this.logger.warn(
        `Signup invoice link: could not retrieve subscription ${params.stripeSubscriptionId}: ${
          e instanceof Error ? e.message : String(e)
        }`,
      );
      return;
    }
    const stripeInvoiceId = this.extractLatestInvoiceIdFromStripeSubscription(expanded);
    if (!stripeInvoiceId) {
      this.logger.warn(
        `Signup invoice link: no latest_invoice on Stripe subscription ${params.stripeSubscriptionId} (company ${params.companyId})`,
      );
      return;
    }

    const duplicate = await this.prisma.client.invoice.findFirst({
      where: { stripe_invoice_id: stripeInvoiceId },
      select: { id: true },
    });
    if (duplicate) {
      return;
    }

    const localSub = await this.prisma.client.subscription.findUnique({
      where: { id: params.subscriptionId },
      select: { start_date: true, end_date: true },
    });

    const localForSub = await this.prisma.client.invoice.findFirst({
      where: {
        company_id: params.companyId,
        subscription_id: params.subscriptionId,
      },
      orderBy: { id: "desc" },
      select: { id: true, stripe_invoice_id: true, period_start: true, period_end: true },
    });

    if (localForSub?.stripe_invoice_id) {
      return;
    }

    const isTrialSignupPackage =
      params.data.package_type === PackageType.FREE_TRIAL ||
      params.data.package_type === PackageType.PRIVATE_VIP_TRIAL;

    if (localForSub && !localForSub.stripe_invoice_id) {
      const updateData: Record<string, unknown> = { stripe_invoice_id: stripeInvoiceId };
      if (!localForSub.period_start && localSub?.start_date) {
        updateData["period_start"] = localSub.start_date;
      }
      if (!localForSub.period_end && localSub?.end_date) {
        updateData["period_end"] = localSub.end_date;
      }
      await this.prisma.client.invoice.update({ where: { id: localForSub.id }, data: updateData as any });
      this.logger.log(
        `Signup invoice linked: local invoice ${localForSub.id} -> stripe_invoice_id=${stripeInvoiceId} (company ${params.companyId})`,
      );
      return;
    }

    if (isTrialSignupPackage) {
      this.logger.log(
        `Signup invoice link skipped creating Stripe-mirror row for trial package (company ${params.companyId}); use local $0 INITIAL row only.`,
      );
      return;
    }

    const invoiceNumber = await this.generateInvoiceNumber();
    await this.prisma.client.invoice.create({
      data: this.buildSignupInitialInvoiceUncheckedCreateInput({
        companyId: params.companyId,
        subscriptionId: params.subscriptionId,
        periodStart: localSub?.start_date ?? null,
        periodEnd: localSub?.end_date ?? null,
        data: params.data,
        invoiceNumber,
        stripeInvoiceId,
      }),
    });
    this.logger.log(
      `Signup invoice created with stripe_invoice_id=${stripeInvoiceId} (company ${params.companyId}, subscription ${params.subscriptionId})`,
    );
  }

  /**
   * Create company account and admin participant after successful payment
   */
  async createAccountFromPayment(data: AccountCreationData): Promise<AccountCreationResult> {
    this.logger.log(`Creating account for company: ${data.company_name}`);

    try {
      // Use provided password if available, otherwise generate temporary password
      const passwordToUse = data.admin_password || this.generateTemporaryPassword();
      const passwordHash = await argon.hash(passwordToUse);

      // Generate password setup token (only if password was not provided)
      const setupToken = data.admin_password ? null : this.generatePasswordSetupToken();
      const tokenExpiry = data.admin_password ? null : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days

      // Generate email verification token
      const emailVerificationToken = this.generateEmailVerificationToken();
      const emailVerificationTokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours

      const trialPackageMeta = await this.prisma.client.package.findUnique({
        where: { id: data.package_id },
        select: {
          is_trial_package: true,
          package_type: true,
          trial_duration_days: true,
        },
      });
      const isTrialCompany =
        !!trialPackageMeta?.is_trial_package ||
        trialPackageMeta?.package_type === PackageType.FREE_TRIAL ||
        trialPackageMeta?.package_type === PackageType.PRIVATE_VIP_TRIAL;
      const trialStartDate = isTrialCompany ? new Date() : null;
      const trialEndDate = isTrialCompany
        ? calculateTrialNextBillingDate(trialPackageMeta?.trial_duration_days ?? null)
        : null;

      // Create company and admin participant in a transaction
      const result = await this.prisma.client.$transaction(async (tx: Prisma.TransactionClient) => {
        // 1. Create Company
        const company = await tx.company.create({
          data: {
            name: data.company_name,
            email: data.company_email,
            phone: data.company_phone,
            address: data.admin_address || null, // Save address from signup form
            city: data.admin_city || null, // Save city from signup form
            country: data.admin_country || null, // Save country from signup form
            plan: this.mapPackageTypeToSubscriptionPlan(data.package_type),
            stripe_customer_id: data.stripe_customer_id,
            stripe_subscription_id: data.stripe_subscription_id,
            trial_start_date: trialStartDate,
            trial_end_date: trialEndDate,
            is_active: true,
          },
        });

        this.logger.log(`Company created with ID: ${company.id}`);

        // 2. Fetch package to check if it's a trial package
        const packageData = await tx.package.findUnique({
          where: { id: data.package_id },
        });

        if (!packageData) {
          throw new Error(`Package with ID ${data.package_id} not found`);
        }

        // 3. Calculate next billing date and billing cycle
        const isTrialPackage = packageData.is_trial_package ||
                              packageData.package_type === "FREE_TRIAL" ||
                              packageData.package_type === "PRIVATE_VIP_TRIAL";

        let nextBillingDate: Date;
        let subscriptionBillingCycle: BillingCycle | null = null;

        if (isTrialPackage) {
          // Post-trial billing cadence is quarterly (30-day-month cycles); trial end still follows package / default.
          subscriptionBillingCycle = this.normalizeBillingCycle(data.billing_cycle);
          nextBillingDate = calculateTrialNextBillingDate(packageData.trial_duration_days ?? null);

          this.logger.log(
            `Trial package subscription: next_billing_date from trial_duration_days=${packageData.trial_duration_days}, billing_cycle=${subscriptionBillingCycle} (post-trial cadence)`,
          );
        } else {
          // First paid purchase: same 30-day-month term lengths as portal (e.g. annual = 360 days, priced as 10× monthly).
          const billingCycle = this.normalizeBillingCycle(data.billing_cycle);
          subscriptionBillingCycle = billingCycle;
          nextBillingDate = calculateNextBillingDateFromAnchor(billingCycle);
        }

        // 4. Create Subscription record
        const subscription = await tx.subscription.create({
          data: {
            company_id: company.id,
            package_id: data.package_id, // ✅ Use package_id (foreign key)
            subscription_type: "INITIAL" as SubscriptionType,
            license_count: data.license_count,
            // Convert null to undefined for Prisma (Prisma uses undefined for optional fields, not null)
            billing_cycle: subscriptionBillingCycle ?? undefined,
            start_date: new Date(),
            end_date: nextBillingDate,
            next_billing_date: nextBillingDate,
            // Billing details will be calculated based on Package
            status: "ACTIVE",
            is_current: true,
            licenses_available: data.license_count,
            licenses_consumed: 0,
            licenses_expired: 0,
          },
        });

        this.logger.log(`Subscription created with ID: ${subscription.id}`);

        // 5. Create Invoice — paid signup when stripe_invoice_id is known; trial/VIP get a $0 INITIAL row here
        // (Stripe latest_invoice id is linked later in linkOrCreateSignupInvoiceFromStripeSubscription).
        let existingInvoice = null;

        // Check if invoice already exists (by payment intent)
        if (data.stripe_payment_intent_id) {
          existingInvoice = await tx.invoice.findUnique({
            where: {
              stripe_payment_intent_id: data.stripe_payment_intent_id,
            },
          });
        }

        if (existingInvoice) {
          this.logger.warn(`Invoice already exists for payment intent: ${data.stripe_payment_intent_id}`);
          // Update the existing invoice to link to new company/subscription
          await tx.invoice.update({
            where: { id: existingInvoice.id },
            data: {
              company_id: company.id,
              subscription_id: subscription.id,
            },
          });
          if (data.stripe_invoice_id && !existingInvoice.stripe_invoice_id) {
            await tx.invoice.update({
              where: { id: existingInvoice.id },
              data: { stripe_invoice_id: data.stripe_invoice_id },
            });
          }
          this.logger.log(`Invoice ${existingInvoice.id} updated for company ${company.id}`);
        } else {
          const stripeInv = data.stripe_invoice_id?.trim();
          if (stripeInv && stripeInv.length > 0) {
            const invoiceNumber = await this.generateInvoiceNumber();
            await tx.invoice.create({
              data: this.buildSignupInitialInvoiceUncheckedCreateInput({
                companyId: company.id,
                subscriptionId: subscription.id,
                periodStart: subscription.start_date,
                periodEnd: subscription.end_date,
                data,
                invoiceNumber,
                stripeInvoiceId: stripeInv,
              }),
            });
            this.logger.log(
              `Invoice created for company ${company.id} with stripe_invoice_id=${stripeInv} (INITIAL_SUBSCRIPTION)`,
            );
          } else if (isTrialPackage) {
            const invoiceNumber = await this.generateInvoiceNumber();
            await tx.invoice.create({
              data: {
                company_id: company.id,
                subscription_id: subscription.id,
                invoice_number: invoiceNumber,
                unit_price_per_license: 0,
                license_quantity: data.license_count,
                package_type: data.package_type,
                billing_cycle: subscriptionBillingCycle ?? undefined,
                status: InvoiceStatus.PAID,
                invoice_type: "INITIAL_SUBSCRIPTION",
                paid_date: new Date(),
                period_start: subscription.start_date ?? undefined,
                period_end: subscription.end_date ?? undefined,
                ...invoiceBillingAmountsToDbFields(buildInvoiceBillingAmounts({ grossLicenseAmount: 0 })),
              },
            });
            this.logger.log(
              `Trial signup: created $0 INITIAL_SUBSCRIPTION invoice for company ${company.id} (subscription ${subscription.id})`,
            );
          } else {
            this.logger.log(
              `Signup local invoice deferred: no stripe_invoice_id yet (company ${company.id}; will link from subscription latest_invoice after sync)`,
            );
          }
        }

        // 4. Create Admin Participant
        const participant = await tx.participant.create({
          data: {
            company_id: company.id,
            first_name: data.admin_first_name,
            last_name: data.admin_last_name,
            email: data.admin_email,
            phone: data.admin_phone,
            password_hash: passwordHash,
            role: "PARTICIPANT_ADMIN" as ParticipantRole,
            is_active: true,
            email_verified: false, // Require email verification
            email_verified_at: null,
          },
        });

        this.logger.log(`Admin participant created with ID: ${participant.id}`);

        // 5. Create Password Setup Token (only if password was not provided)
        if (setupToken && tokenExpiry) {
          await tx.passwordSetupToken.create({
            data: {
              participant_id: participant.id,
              token: setupToken,
              expires_at: tokenExpiry,
            },
          });

          this.logger.log(`Password setup token created for participant ${participant.id}`);
        }

        // 6. Create Email Verification Token
        await (tx as any).emailVerificationToken.create({
          data: {
            participant_id: participant.id,
            token: emailVerificationToken,
            expires_at: emailVerificationTokenExpiry,
          },
        });

        this.logger.log(`Email verification token created for participant ${participant.id}`);

        return {
          company,
          subscription,
          participant,
          setupToken,
          emailVerificationToken,
        };
      });

      this.logger.log(`Account creation successful for: ${data.company_email}`);

      // Keep Stripe subscriptions in sync immediately after signup account creation.
      // For paid/trial plans with a Stripe customer, create (or replace) Stripe subscription now.
      try {
        if (data.stripe_customer_id) {
          const billingCycle = this.normalizeBillingCycle(data.billing_cycle);
          // For free-signup trial flows, unit_price may be 0 (amount charged now), but we still want
          // Stripe recurring subscription with the package's real monthly price after trial_end.
          const packageForSync = await this.prisma.client.package.findUnique({
            where: { id: result.subscription.package_id },
            select: { price_per_licence: true, is_trial_package: true, package_type: true },
          });
          const packageMonthlyPrice = Number(packageForSync?.price_per_licence ?? 0);
          const isTrialLike =
            !!packageForSync?.is_trial_package ||
            packageForSync?.package_type === "FREE_TRIAL" ||
            packageForSync?.package_type === "PRIVATE_VIP_TRIAL";

          // Free-signup rule:
          // create Stripe subscription using STARTUP plan pricing and at least 5 licenses,
          // then start billing at trial_end (local next_billing_date).
          let effectiveUnitPrice = data.unit_price && data.unit_price > 0 ? data.unit_price : packageMonthlyPrice;
          let effectiveLicenseCount = data.license_count || 0;
          if (isTrialLike) {
            const startupPackage = await this.prisma.client.package.findFirst({
              where: {
                package_type: PackageType.STARTUP,
                is_active: true,
              },
              select: {
                price_per_licence: true,
              },
            });
            const startupMonthlyPrice = Number(startupPackage?.price_per_licence ?? 0);
            if (startupMonthlyPrice > 0) {
              effectiveUnitPrice = startupMonthlyPrice;
            }
            effectiveLicenseCount = Math.max(5, effectiveLicenseCount);
          }
          const cycleMultiplier = getBillingCycleMultiplier(billingCycle);
          // {@link CLAccountService} already folds the billing-cycle multiplier into {@code data.unit_price}
          // for paid plans (e.g. annual = per-license price × 10). Do not multiply again or Stripe renewals ~10× PI.
          const useSignupQuotedCyclePricePerLicense =
            !isTrialLike && typeof data.unit_price === "number" && data.unit_price > 0;
          const cycleAmountCents = useSignupQuotedCyclePricePerLicense
            ? Math.round(data.unit_price * effectiveLicenseCount * 100)
            : Math.round(effectiveUnitPrice * effectiveLicenseCount * cycleMultiplier * 100);
          const renewalCharge = this.stripePaymentBillingHelpers.calculateRenewalChargeCents({
            baseAmountCents: cycleAmountCents,
            country: result.company.country,
          });
          const totalAmountCentsPerCycle = renewalCharge.totalCents;
          // Stripe `trial_end` puts the sub in `trialing` and shows "Free trial" — only for real trial/VIP offers.
          // Paid STARTUP/GROWTH/ENTERPRISE use `next_billing_date` as the renewal anchor, not a Stripe trial window.
          const trialEndUnix =
            isTrialLike && result.subscription.next_billing_date
              ? Math.floor(result.subscription.next_billing_date.getTime() / 1000)
              : undefined;
          const nowUnix = Math.floor(Date.now() / 1000);
          const isVipTrialPackage = packageForSync?.package_type === PackageType.PRIVATE_VIP_TRIAL;
          const firstInvoiceAmountOffCents =
            isVipTrialPackage &&
            typeof trialEndUnix === "number" &&
            trialEndUnix > nowUnix &&
            totalAmountCentsPerCycle > 0
              ? Math.max(1, Math.round(totalAmountCentsPerCycle / cycleMultiplier))
              : undefined;

          const cancelled = await this.stripeService.cancelCustomerBillableSubscriptions(data.stripe_customer_id);
          if (cancelled.length > 0) {
            this.logger.log(`Cancelled existing Stripe subscriptions on signup sync: ${cancelled.join(", ")}`);
          }

          if (cycleAmountCents > 0 || isTrialLike) {
            const stripeSub = await this.stripeService.createCustomerSubscription({
              customerId: data.stripe_customer_id,
              billingCycle: billingCycle as "QUARTERLY" | "HALF_YEARLY" | "ANNUAL",
              totalAmountCentsPerCycle: Math.max(0, totalAmountCentsPerCycle),
              autoPaymentEnabled: true,
              trialEndUnix,
              firstInvoiceAmountOffCents,
              metadata: {
                company_id: String(result.company.id),
                local_subscription_id: String(result.subscription.id),
                source: "signup_account_sync",
              },
            });

            // Paid signup: funds already captured on the signup PI + manual OOB invoice; close the
            // subscription's first invoice the same way so Stripe stops showing incomplete/requires_payment.
            if (data.stripe_payment_intent_id) {
              try {
                await this.stripeService.markSubscriptionLatestInvoicePaidOutOfBand(stripeSub.id);
              } catch (e) {
                this.logger.warn(
                  `Signup sync: subscription ${stripeSub.id} latest invoice could not be marked paid out-of-band after signup PI ${data.stripe_payment_intent_id}: ${
                    e instanceof Error ? e.message : String(e)
                  }`,
                );
              }
            }

            await this.prisma.client.company.update({
              where: { id: result.company.id },
              data: { stripe_subscription_id: stripeSub.id },
            });
            await this.prisma.client.subscription.update({
              where: { id: result.subscription.id },
              data: { stripe_subscription_id: stripeSub.id },
            });
            this.logger.log(
              `Signup sync created Stripe subscription ${stripeSub.id} for company ${result.company.id}`,
            );
            await this.linkOrCreateSignupInvoiceFromStripeSubscription({
              companyId: result.company.id,
              subscriptionId: result.subscription.id,
              stripeSubscriptionId: stripeSub.id,
              data,
            });
          } else {
            this.logger.warn(
              `Signup sync skipped Stripe subscription creation for company ${result.company.id}: non-positive cycle amount (unitPrice=${effectiveUnitPrice}, licenses=${effectiveLicenseCount}, billingCycle=${billingCycle})`,
            );
          }
        }
      } catch (syncError) {
        this.logger.warn(
          `Signup Stripe subscription sync failed for company ${result.company.id}: ${
            syncError instanceof Error ? syncError.message : String(syncError)
          }`,
        );
      }

      return {
        success: true,
        company_id: result.company.id,
        participant_id: result.participant.id,
        temporary_password: data.admin_password ? undefined : passwordToUse, // Only return if password was auto-generated
        password_setup_token: result.setupToken ?? undefined,
        email_verification_token: result.emailVerificationToken,
      };
    } catch (error) {
      this.logger.error(`Failed to create account for ${data.company_name}:`, error);
      return {
        success: false,
        error: error instanceof Error ? error.message : "Failed to create account",
      };
    }
  }

  /**
   * Map PackageType to SubscriptionPlan
   */
  private mapPackageTypeToSubscriptionPlan(packageType: PackageType): SubscriptionPlan {
    const mapping: Record<PackageType, SubscriptionPlan> = {
      FREE_TRIAL: SubscriptionPlan.FREE_TRIAL,
      STARTUP: SubscriptionPlan.STARTUP,
      GROWTH: SubscriptionPlan.GROWTH,
      ENTERPRISE: SubscriptionPlan.ENTERPRISE,
      PRIVATE_VIP_TRIAL: SubscriptionPlan.PRIVATE_VIP_TRIAL,
    };
    return mapping[packageType] || SubscriptionPlan.FREE_TRIAL;
  }

  /**
   * Generate a secure temporary password
   */
  private generateTemporaryPassword(): string {
    const length = 12;
    const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
    let password = "";

    // Ensure at least one of each type
    password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)]; // Uppercase
    password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)]; // Lowercase
    password += "0123456789"[Math.floor(Math.random() * 10)]; // Number
    password += "!@#$%^&*"[Math.floor(Math.random() * 8)]; // Special char

    // Fill the rest randomly
    for (let i = password.length; i < length; i++) {
      password += charset[Math.floor(Math.random() * charset.length)];
    }

    // Shuffle the password
    return password
      .split("")
      .sort(() => Math.random() - 0.5)
      .join("");
  }

  /**
   * Generate a secure password setup token
   */
  private generatePasswordSetupToken(): string {
    // Generate a secure random token (32 bytes = 64 hex characters)
    return crypto.randomBytes(32).toString("hex");
  }

  /**
   * Generate a secure email verification token
   */
  private generateEmailVerificationToken(): string {
    // Generate a secure random token (32 bytes = 64 hex characters)
    return crypto.randomBytes(32).toString("hex");
  }

  /**
   * Check if email is already registered
   */
  async isEmailRegistered(email: string): Promise<boolean> {
    const [participant, company] = await Promise.all([
      this.prisma.client.participant.findUnique({ where: { email } }),
      this.prisma.client.company.findUnique({ where: { email } }),
    ]);

    return !!(participant || company);
  }
}

results matching ""

    No results matching ""