import bcrypt from 'bcryptjs' import { AppError } from '../../http/errors' import { prisma } from '../../lib/prisma' import { sendNotification } from '../../services/notificationService' import { coerceNotificationLocale, localizeBillingPeriod, localizePlanName, } from '../../services/notificationLocalizationService' import { presentCompanySignup } from './auth.presenter' import * as repo from './auth.company.repo' import { companySignupSchema } from './auth.company.schemas' import type { output } from 'zod' type CompanySignupInput = output const TRIAL_PERIOD_DAYS = 30 function slugify(value: string) { return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50) || 'company' } async function generateUniqueSlug(baseName: string) { const base = slugify(baseName) for (let attempt = 0; attempt < 25; attempt++) { const slug = attempt === 0 ? base : `${base}-${attempt + 1}` const existing = await repo.findCompanyBySlug(slug) if (!existing) return slug } return `${base}-${Date.now().toString(36)}` } export async function signup(body: CompanySignupInput) { if (await repo.findCompanyByEmail(body.companyEmail)) { throw new AppError('A company account with this contact email already exists', 409, 'company_email_taken') } if (await repo.findEmployeeByEmail(body.email)) { throw new AppError('An employee account with this owner email already exists', 409, 'owner_email_taken') } const slug = await generateUniqueSlug(body.companyName) const now = new Date() const trialEndAt = new Date(now.getTime() + TRIAL_PERIOD_DAYS * 24 * 60 * 60 * 1000) const passwordHash = await bcrypt.hash(body.password, 12) const result = await prisma.$transaction((tx: any) => repo.createCompanySignup(tx, { companyName: body.companyName, legalName: body.legalName, slug, ownerEmail: body.email, companyEmail: body.companyEmail, companyPhone: body.companyPhone, streetAddress: body.streetAddress, city: body.city, country: body.country, zipCode: body.zipCode, legalForm: body.legalForm, managerName: `${body.firstName} ${body.lastName}`.trim(), iceNumber: body.iceNumber, taxId: body.taxId, operatingLicenseNumber: body.operatingLicenseNumber, operatingLicenseIssuedAt: body.operatingLicenseIssuedAt, operatingLicenseIssuedBy: body.operatingLicenseIssuedBy, fax: body.fax, yearsActive: body.yearsActive, representativeName: body.representativeName, representativeTitle: body.representativeTitle, responsibleName: body.responsibleName, responsibleRole: body.responsibleRole, responsibleIdentityNumber: body.responsibleIdentityNumber, responsibleQualification: body.responsibleQualification, responsiblePhone: body.responsiblePhone, responsibleEmail: body.responsibleEmail, currency: body.currency, registrationNumber: body.registrationNumber, plan: body.plan, billingPeriod: body.billingPeriod, preferredLanguage: body.preferredLanguage, firstName: body.firstName, lastName: body.lastName, passwordHash, now, trialEndAt, })) const lang = coerceNotificationLocale(body.preferredLanguage) const emailResult = await sendNotification({ type: 'ACCOUNT_CREATED', companyId: result.company.id, employeeId: result.employee.id, email: body.email, channels: ['EMAIL', 'IN_APP'], locale: lang, templateKey: 'account.created', templateVariables: { firstName: body.firstName, companyName: body.companyName, planName: localizePlanName(body.plan, lang), billingPeriodLabel: localizeBillingPeriod(body.billingPeriod, lang), currency: body.currency, paymentProvider: body.paymentProvider, trialEndDate: trialEndAt, }, }).catch(() => []) const emailDelivery = (emailResult as Array<{ channel?: string }>).find((entry) => entry.channel === 'EMAIL') return presentCompanySignup({ company: result.company, trialEndAt, emailDelivery, }) } export function completeSignupDisabled() { throw new AppError('Clerk-based signup has been removed. Use /auth/company/signup instead.', 410, 'disabled') } export function verifyEmailDisabled() { throw new AppError('Email verification resend via Clerk has been removed from this project.', 410, 'disabled') }