edtech / apps /api /src /services /organization-service.ts
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 }
};
}