import { AppError, ErrorCodes } from '@api/utils/errors'; import type { TProfile, TUser } from '@cio/db/types'; import { checkSiteNameExists, createOrganization, createOrganizationMember, createOrganizationPlan, getOrganizationByProfileId, getOrganizationCount } from '@cio/db/queries'; import { getProfileById, updateProfile } from '@cio/db/queries/auth'; import { env } from '@api/config/env'; import { ROLE } from '@cio/utils/constants'; import { PLAN } from '@cio/utils/plans'; import { db } from '@cio/db/drizzle'; import { enqueueTransactionalEmail } from '@api/services/jobs'; export async function createOrganizationWithOwner( profileId: string, input: { fullname?: string; orgName: string; siteName: string; } ) { // Self-hosted: block org creation when an org already exists if (env.PUBLIC_IS_SELFHOSTED === 'true') { const count = await getOrganizationCount(); if (count > 0) { throw new AppError('Self-hosted instances support only one organization', ErrorCodes.VALIDATION_ERROR, 403); } } // Business Logic: Check sitename availability const exists = await checkSiteNameExists(input.siteName); if (exists) { console.error('Site name already exists:', input.siteName); throw new AppError(`Site name '${input.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409); } // Business Logic: Create org and member in a transaction try { const result = await db.transaction(async (tx) => { const organization = await createOrganization( { name: input.orgName, siteName: input.siteName }, tx ); const member = await createOrganizationMember( { organizationId: organization.id, profileId, roleId: ROLE.ADMIN, verified: true }, tx ); // Self-hosted: assign Enterprise plan to the new org if (env.PUBLIC_IS_SELFHOSTED === 'true') { await createOrganizationPlan( { orgId: organization.id, planName: PLAN.ENTERPRISE as 'ENTERPRISE', subscriptionId: `selfhosted-${organization.id}`, triggeredBy: member.id, payload: {}, isActive: true, provider: 'selfhosted' }, tx ); } return { organization, member }; }); // Fetch updated organizations list const organizations = await getOrganizationByProfileId(profileId); return { organization: result.organization, member: result.member, organizations }; } catch (error) { console.error('Error creating organization:', error); // Handle database constraint violations if (error && typeof error === 'object' && 'code' in error && error.code === '23505') { throw new AppError(`Site name '${input.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409); } throw new AppError(error instanceof Error ? error : new Error('Unknown error'), ErrorCodes.ORG_CREATE_FAILED, 500); } } export async function updateUserOnboarding(userId: string, data: Partial) { try { const updatedProfile = await updateProfile(userId, data); if (!updatedProfile) { throw new AppError('Failed to update profile - profile not found', ErrorCodes.PROFILE_NOT_FOUND, 404); } return updatedProfile; } catch (error) { if (error instanceof AppError) { throw error; } throw new AppError( error instanceof Error ? error : new Error('Unknown error'), ErrorCodes.PROFILE_UPDATE_FAILED, 500 ); } } export async function completeOnboarding(user: Partial & { id: string; email: string }) { try { const profile = await getProfileById(user.id); if (!profile) { throw new AppError('Profile not found', ErrorCodes.PROFILE_NOT_FOUND, 404); } await enqueueTransactionalEmail('welcome', { to: user.email, fields: { name: profile.fullname }, idempotencyKey: `onboarding:welcome:${user.id}` }); } catch (error) { if (error instanceof AppError) { throw error; } throw new AppError(error instanceof Error ? error : new Error('Unknown error'), ErrorCodes.INTERNAL_ERROR, 500); } }