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);
}
}