f22e0d45e1
Build & Deploy / Build & Push Docker Image (push) Successful in 7m57s
Test / Type Check (all packages) (push) Successful in 4m27s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 3m2s
Test / Homepage Unit Tests (push) Successful in 4m3s
Test / Storefront Unit Tests (push) Successful in 3m32s
Test / Admin Unit Tests (push) Successful in 3m27s
Test / Dashboard Unit Tests (push) Successful in 3m3s
Test / API Integration Tests (push) Failing after 3m53s
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
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<typeof companySignupSchema>
|
|
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')
|
|
}
|