CognxSafeTrack
feat: backlog P0→P3 — toast system, payments, tenant isolation, feedback handler, i18n parity
6dd9bad | import { prisma } from './prisma'; | |
| import { encryptSecrets, decryptSecrets } from './organization'; | |
| import { AuthService } from './auth'; | |
| import { auditService } from './audit'; | |
| import { scheduleEmail } from './queue'; | |
| import { z } from 'zod'; | |
| import { OrganizationSchema } from '@repo/shared-types'; | |
| export const OrganizationCreationSchema = OrganizationSchema.extend({ | |
| slug: z.string().min(3).regex(/^[a-z0-9-]+$/), | |
| adminEmail: z.string().email(), | |
| adminName: z.string().min(2), | |
| password: z.string().min(6).optional(), | |
| mode: z.enum(['CRM_MARKETING', 'PEDAGOGY', 'CUSTOMER_SERVICE']).default('PEDAGOGY'), | |
| useCase: z.enum(['EDUCATION', 'CRM_WHATSAPP']).default('EDUCATION'), | |
| isCrmActive: z.boolean().optional(), | |
| isEdTechActive: z.boolean().optional() | |
| }); | |
| export type OrganizationCreationInput = z.infer<typeof OrganizationCreationSchema>; | |
| export interface CreateOrganizationResult { | |
| organization: ReturnType<typeof decryptSecrets>; | |
| admin: { id: string; email: string; tempPassword: string }; | |
| } | |
| export async function createOrganizationWithAdmin( | |
| input: OrganizationCreationInput, | |
| actorId?: string | |
| ): Promise<CreateOrganizationResult> { | |
| const { adminEmail, adminName, password, slug, mode, useCase, isCrmActive, isEdTechActive, ...orgData } = input; | |
| const existing = await prisma.organization.findUnique({ where: { slug } }); | |
| if (existing) throw Object.assign(new Error('Slug already taken'), { code: 'SLUG_TAKEN' }); | |
| const data = encryptSecrets(orgData); | |
| const finalIsCrmActive = isCrmActive ?? (useCase === 'CRM_WHATSAPP'); | |
| const finalIsEdTechActive = isEdTechActive ?? (useCase === 'EDUCATION'); | |
| const { org, user, tempPassword } = await prisma.$transaction(async (tx) => { | |
| const org = await tx.organization.create({ | |
| data: { ...data, slug, mode, useCase, isCrmActive: finalIsCrmActive, isEdTechActive: finalIsEdTechActive } | |
| }); | |
| const tempPassword = password || Math.random().toString(36).slice(-10); | |
| const passwordHash = await AuthService.hashPassword(tempPassword); | |
| const user = await tx.user.create({ | |
| data: { email: adminEmail, name: adminName, passwordHash, role: 'ORG_ADMIN', organizationId: org.id } | |
| }); | |
| return { org, user, tempPassword }; | |
| }); | |
| await scheduleEmail({ | |
| to: adminEmail, | |
| subject: `Bienvenue chez Xamlé Studio - ${org.name}`, | |
| params: { | |
| name: adminName, | |
| organizationName: org.name, | |
| loginUrl: `https://${slug}.xamle.studio/login`, | |
| resetUrl: `https://${slug}.xamle.studio/reset-password` | |
| }, | |
| templateId: 1 | |
| }); | |
| await auditService.log({ | |
| action: 'ORGANIZATION_CREATED', | |
| actorId, | |
| resourceId: org.id, | |
| details: { name: org.name, slug: org.slug } | |
| }); | |
| return { | |
| organization: decryptSecrets(org), | |
| admin: { id: user.id, email: user.email ?? adminEmail, tempPassword } | |
| }; | |
| } | |