import { Router } from 'express' import { z } from 'zod' import bcrypt from 'bcryptjs' import { prisma } from '../lib/prisma' import { sendNotification } from '../services/notificationService' const router = Router() const signupSchema = z.object({ firstName: z.string().min(1).max(80), lastName: z.string().min(1).max(80), email: z.string().email(), password: z.string().min(8).max(128), companyName: z.string().min(2).max(120), legalForm: z.string().min(1).max(80), registrationNumber: z.string().min(1).max(120), streetAddress: z.string().min(1).max(200), city: z.string().min(1).max(120), country: z.string().min(1).max(120), zipCode: z.string().min(1).max(40), companyPhone: z.string().min(1).max(80), fax: z.string().max(80).optional(), yearsActive: z.string().min(1).max(80), preferredLanguage: z.enum(['en', 'fr', 'ar']).default('en'), plan: z.enum(['STARTER', 'GROWTH', 'PRO']), billingPeriod: z.enum(['MONTHLY', 'ANNUAL']), currency: z.enum(['MAD', 'USD', 'EUR']), paymentProvider: z.enum(['AMANPAY', 'PAYPAL']), }) type EmailDeliveryResult = { attempted: boolean success: boolean error: string | null } function slugifyCompanyName(value: string) { return value .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 50) || 'company' } async function generateUniqueSlug(baseName: string) { const base = slugifyCompanyName(baseName) for (let attempt = 0; attempt < 25; attempt += 1) { const slug = attempt === 0 ? base : `${base}-${attempt + 1}` const existing = await prisma.company.findUnique({ where: { slug } }) if (!existing) return slug } return `${base}-${Date.now().toString(36)}` } function addDays(date: Date, days: number) { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000) } function buildSignupConfirmationEmail(opts: { firstName: string companyName: string plan: 'STARTER' | 'GROWTH' | 'PRO' billingPeriod: 'MONTHLY' | 'ANNUAL' currency: 'MAD' | 'USD' | 'EUR' paymentProvider: 'AMANPAY' | 'PAYPAL' trialEndAt: Date }) { const greetingName = opts.firstName.trim() || 'there' return [ `Hi ${greetingName},`, '', `Your RentalDriveGo workspace for ${opts.companyName} has been created successfully.`, `Plan: ${opts.plan} (${opts.billingPeriod.toLowerCase()})`, `Currency: ${opts.currency}`, `Primary payment provider: ${opts.paymentProvider}`, `Free trial ends on ${opts.trialEndAt.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', })}.`, '', 'Your workspace is ready and you can sign in with the email address and password you chose during signup.', '', 'RentalDriveGo', ].join('\n') } async function sendSignupEmailNotification(opts: Parameters[0]): Promise { const result = await sendNotification(opts) const emailResult = result.find((entry) => entry.channel === 'EMAIL') if (!emailResult) { return { attempted: false, success: false, error: null } } return { attempted: true, success: emailResult.success, error: emailResult.error ?? null, } } router.post('/signup', async (req, res, next) => { try { const body = signupSchema.parse(req.body) const existingCompany = await prisma.company.findUnique({ where: { email: body.email } }) if (existingCompany) { return res.status(409).json({ error: 'email_taken', message: 'A company account with this email already exists', statusCode: 409, }) } const existingEmployee = await prisma.employee.findFirst({ where: { email: body.email } }) if (existingEmployee) { return res.status(409).json({ error: 'email_taken', message: 'An employee account with this email already exists', statusCode: 409, }) } const slug = await generateUniqueSlug(body.companyName) const now = new Date() const trialEndAt = addDays(now, 14) const passwordHash = await bcrypt.hash(body.password, 12) const result = await prisma.$transaction(async (tx) => { const company = await tx.company.create({ data: { name: body.companyName, slug, email: body.email, phone: body.companyPhone, address: { streetAddress: body.streetAddress, city: body.city, country: body.country, zipCode: body.zipCode, legalForm: body.legalForm, managerName: `${body.firstName} ${body.lastName}`.trim(), fax: body.fax || null, yearsActive: body.yearsActive, }, status: 'TRIALING', }, }) await tx.brandSettings.create({ data: { companyId: company.id, displayName: body.companyName, subdomain: slug, publicEmail: body.email, publicPhone: body.companyPhone, publicAddress: body.streetAddress, publicCity: body.city, publicCountry: body.country, defaultCurrency: body.currency, }, }) await tx.contractSettings.create({ data: { companyId: company.id, legalName: body.companyName, registrationNumber: body.registrationNumber, }, }) await tx.subscription.create({ data: { companyId: company.id, plan: body.plan, billingPeriod: body.billingPeriod, currency: body.currency, status: 'TRIALING', trialStartAt: now, trialEndAt, currentPeriodStart: now, currentPeriodEnd: trialEndAt, }, }) const employee = await tx.employee.create({ data: { companyId: company.id, clerkUserId: `local_owner_${company.id}`, firstName: body.firstName, lastName: body.lastName, email: body.email, phone: body.companyPhone, passwordHash, role: 'OWNER', preferredLanguage: body.preferredLanguage, isActive: true, }, }) return { company, employeeId: employee.id, } }) const emailDelivery = await sendSignupEmailNotification({ type: 'SUBSCRIPTION_TRIAL_ENDING', title: 'Your workspace is ready', body: buildSignupConfirmationEmail({ firstName: body.firstName, companyName: body.companyName, plan: body.plan, billingPeriod: body.billingPeriod, currency: body.currency, paymentProvider: body.paymentProvider, trialEndAt, }), companyId: result.company.id, employeeId: result.employeeId, email: body.email, channels: ['EMAIL', 'IN_APP'], }) res.status(201).json({ data: { companyId: result.company.id, companyName: result.company.name, slug: result.company.slug, invitationId: null, trialEndsAt: trialEndAt.toISOString(), nextStep: 'workspace_created', emailDelivery, }, }) } catch (err) { next(err) } }) router.post('/complete-signup', (_req, res) => { res.status(410).json({ error: 'disabled', message: 'Clerk-based signup has been removed. Use /auth/company/signup instead.', statusCode: 410, }) }) router.post('/verify-email', (_req, res) => { res.status(410).json({ error: 'disabled', message: 'Email verification resend via Clerk has been removed from this project.', statusCode: 410, }) }) export default router