import { FastifyInstance } from 'fastify'; import { Prisma } from '@repo/database'; import { prisma } from '../services/prisma'; import { logger } from '../logger'; import { z } from 'zod'; import { redis } from '../lib/redis'; import { whatsappQueue } from '../services/queue'; export async function superAdminRoutes(fastify: FastifyInstance) { fastify.addHook('preHandler', async (request: any, reply) => { const role = request.user?.role; if (role !== 'SUPER_ADMIN') { return reply.code(403).send({ error: 'Super admin access required' }); } }); // ── Platform Stats ──────────────────────────────────────────────────────── fastify.get('/platform/stats', async (_req, reply) => { try { const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000); const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const [orgsCount, usersCount, messagesLast24h, revenueAgg, queueCounts] = await Promise.all([ prisma.organization.count(), prisma.user.count({ where: { deletedAt: null } }), prisma.message.count({ where: { createdAt: { gte: since24h } } }), prisma.walletTransaction.aggregate({ where: { type: 'TOP_UP_MANUAL', createdAt: { gte: since30d } }, _sum: { amount: true } }), Promise.all([ whatsappQueue.getWaitingCount(), whatsappQueue.getFailedCount(), whatsappQueue.getActiveCount(), ]).catch(() => [0, 0, 0]), ]); const activeOrgs = await prisma.organization.count({ where: { isHardStopped: false } }); return { orgsCount, activeOrgs, usersCount, messagesLast24h, queueDepth: Array.isArray(queueCounts) ? (queueCounts[0] + queueCounts[2]) : 0, queueFailed: Array.isArray(queueCounts) ? queueCounts[1] : 0, revenueThisMonth: revenueAgg._sum.amount ?? 0, }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] platform/stats failed'); return reply.code(500).send({ error: 'Failed to fetch platform stats' }); } }); // ── Organizations ───────────────────────────────────────────────────────── fastify.get('/organizations', async (req, reply) => { const QuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), search: z.string().optional(), plan: z.string().optional(), }); const parsed = QuerySchema.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' }); const { page, limit, search, plan } = parsed.data; const where: any = {}; if (search) where.name = { contains: search, mode: 'insensitive' }; if (plan) where.subscriptionPlan = plan; try { const [orgs, total] = await Promise.all([ prisma.organization.findMany({ where, select: { id: true, name: true, wabaId: true, walletBalance: true, subscriptionPlan: true, subscriptionStatus: true, isHardStopped: true, createdAt: true, mode: true, isCrmActive: true, isEdTechActive: true, _count: { select: { users: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.organization.count({ where }), ]); return { data: orgs.map(o => ({ ...o, userCount: o._count.users })), total, page, limit }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] organizations list failed'); return reply.code(500).send({ error: 'Failed to list organizations' }); } }); fastify.post('/organizations', async (req, reply) => { const Schema = z.object({ name: z.string().min(1), subscriptionPlan: z.enum(['STARTER', 'GROWTH', 'SCALE', 'ENTERPRISE']).default('STARTER'), aiCreditsLimit: z.number().int().positive().default(500), isCrmActive: z.boolean().default(false), isEdTechActive: z.boolean().default(true), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); try { const org = await prisma.organization.create({ data: body.data }); return reply.code(201).send(org); } catch (err: any) { logger.error({ err }, '[SUPER_ADMIN] org create failed'); return reply.code(500).send({ error: 'Failed to create organization' }); } }); fastify.patch<{ Params: { id: string } }>('/organizations/:id', async (req, reply) => { const Schema = z.object({ name: z.string().min(1).optional(), subscriptionPlan: z.enum(['STARTER', 'GROWTH', 'SCALE', 'ENTERPRISE']).optional(), subscriptionStatus: z.string().optional(), aiCreditsLimit: z.number().int().positive().optional(), isHardStopped: z.boolean().optional(), isCrmActive: z.boolean().optional(), isEdTechActive: z.boolean().optional(), walletBalance: z.number().int().optional(), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); try { const org = await prisma.organization.update({ where: { id: req.params.id }, data: body.data, }); return org; } catch (err: any) { logger.error({ err, orgId: req.params.id }, '[SUPER_ADMIN] org update failed'); if (err.code === 'P2025') return reply.code(404).send({ error: 'Organization not found' }); return reply.code(500).send({ error: 'Failed to update organization' }); } }); // Suspend / activate shortcut fastify.post<{ Params: { id: string } }>('/organizations/:id/suspend', async (req, reply) => { const Schema = z.object({ suspend: z.boolean() }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); try { await prisma.organization.update({ where: { id: req.params.id }, data: { isHardStopped: body.data.suspend, subscriptionStatus: body.data.suspend ? 'SUSPENDED' : 'ACTIVE' } }); return { ok: true }; } catch (err: any) { if (err.code === 'P2025') return reply.code(404).send({ error: 'Organization not found' }); return reply.code(500).send({ error: 'Failed to suspend organization' }); } }); // Soft-delete — sets subscriptionStatus to DELETED and hard-stops fastify.delete<{ Params: { id: string } }>('/organizations/:id', async (req, reply) => { try { const org = await prisma.organization.findUnique({ where: { id: req.params.id }, select: { id: true, name: true } }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); await prisma.organization.update({ where: { id: req.params.id }, data: { subscriptionStatus: 'DELETED', isHardStopped: true }, }); logger.info({ orgId: req.params.id, actor: (req as any).user?.id }, '[SUPER_ADMIN] Organization soft-deleted'); return { ok: true }; } catch (err) { return reply.code(500).send({ error: 'Delete failed' }); } }); // Impersonation — returns a short-lived JWT scoped to the target org fastify.post<{ Params: { id: string } }>('/organizations/:id/impersonate', async (req, reply) => { try { const org = await prisma.organization.findUnique({ where: { id: req.params.id }, select: { id: true, name: true } }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); const impersonateToken = (fastify as any).jwt.sign( { id: (req as any).user.id, role: 'ORG_ADMIN', organizationId: org.id, impersonating: true }, { expiresIn: '1h' } ); return { token: impersonateToken, orgName: org.name }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] impersonate failed'); return reply.code(500).send({ error: 'Impersonation failed' }); } }); // ── Users (cross-org) ───────────────────────────────────────────────────── fastify.get('/users', async (req, reply) => { const QuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), orgId: z.string().optional(), role: z.string().optional(), search: z.string().optional(), }); const parsed = QuerySchema.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' }); const { page, limit, orgId, role, search } = parsed.data; const where: any = { deletedAt: null }; if (orgId) where.organizationId = orgId; if (role) where.role = role; if (search) where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { email: { contains: search, mode: 'insensitive' } }, ]; try { const [users, total] = await Promise.all([ prisma.user.findMany({ where, select: { id: true, name: true, email: true, phone: true, role: true, organizationId: true, createdAt: true, lastActivityAt: true, organization: { select: { name: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.user.count({ where }), ]); return { data: users, total, page, limit }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] users list failed'); return reply.code(500).send({ error: 'Failed to list users' }); } }); fastify.patch<{ Params: { userId: string } }>('/users/:userId/role', async (req, reply) => { const Schema = z.object({ role: z.enum(['STUDENT', 'ORG_MEMBER', 'ORG_ADMIN', 'ADMIN', 'SUPER_ADMIN']) }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); try { const user = await prisma.user.update({ where: { id: req.params.userId }, data: { role: body.data.role } }); return { ok: true, role: user.role }; } catch (err: any) { if (err.code === 'P2025') return reply.code(404).send({ error: 'User not found' }); return reply.code(500).send({ error: 'Failed to update role' }); } }); // Reset password — generates a 1h JWT reset token and emails a link fastify.post<{ Params: { userId: string } }>('/users/:userId/reset-password', async (req, reply) => { try { const user = await prisma.user.findUnique({ where: { id: req.params.userId }, select: { id: true, email: true, name: true } }); if (!user) return reply.code(404).send({ error: 'User not found' }); if (!user.email) return reply.code(400).send({ error: 'User has no email address' }); const resetToken = (fastify as any).jwt.sign( { id: user.id, purpose: 'reset' }, { expiresIn: '1h' } ) as string; const adminUrl = process.env.ADMIN_URL || 'https://edtechadmin.netlify.app'; const resetLink = `${adminUrl}/reset-password?token=${resetToken}`; const { scheduleEmail } = await import('../services/queue'); await scheduleEmail({ to: user.email, subject: 'Réinitialisation de votre mot de passe — XAMLÉ (admin)', htmlContent: `
Bonjour ${user.name ?? ''},
Un administrateur a déclenché une réinitialisation de votre mot de passe. Cliquez ci-dessous pour définir un nouveau mot de passe. Ce lien expire dans 1 heure.
Réinitialiser mon mot de passe
Si vous n'attendiez pas cet email, contactez votre administrateur.
`, }); logger.info({ userId: user.id, actor: (req as any).user?.id }, '[SUPER_ADMIN] Password reset email sent'); return { ok: true, email: user.email }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] reset-password failed'); return reply.code(500).send({ error: 'Failed to send reset email' }); } }); // ── WhatsApp Numbers (cross-org) ────────────────────────────────────────── fastify.get('/whatsapp/numbers', async (_req, reply) => { try { const numbers = await prisma.whatsAppPhoneNumber.findMany({ include: { organization: { select: { id: true, name: true } } }, orderBy: { createdAt: 'desc' }, }); return { data: numbers }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] whatsapp numbers failed'); return reply.code(500).send({ error: 'Failed to list phone numbers' }); } }); // ── WhatsApp Number Registration (OTP flow) ─────────────────────────────── // Step 1 — Trigger Meta to send OTP to the phone number fastify.post('/whatsapp/numbers/register', async (req, reply) => { const Schema = z.object({ orgId: z.string(), phoneNumberId: z.string(), pin: z.string().length(6).regex(/^\d{6}$/).default('000000'), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); const { orgId, phoneNumberId, pin } = body.data; try { const { decryptSecrets } = await import('../services/organization'); const { whatsappService } = await import('../services/whatsapp'); const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { id: true, name: true, systemUserToken: true, wabaId: true, systemUserTokenIssuedAt: true, webhookSecret: true }, }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); if (!org.systemUserToken) return reply.code(400).send({ error: 'Organization has no systemUserToken configured' }); const decrypted = decryptSecrets(org as any); const result = await whatsappService.registerPhoneNumber({ accessToken: decrypted.systemUserToken! }, phoneNumberId, pin); logger.info({ orgId, phoneNumberId, actor: (req as any).user?.id }, '[SUPER_ADMIN] Phone number registration initiated'); return { ok: true, metaResponse: result }; } catch (err: any) { const detail = err.response?.data?.error?.message ?? err.message; return reply.code(502).send({ error: 'Meta API error', detail }); } }); // Step 2 — Verify OTP sent by Meta fastify.post('/whatsapp/numbers/verify', async (req, reply) => { const Schema = z.object({ orgId: z.string(), phoneNumberId: z.string(), code: z.string().min(4).max(8), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); const { orgId, phoneNumberId, code } = body.data; try { const { decryptSecrets } = await import('../services/organization'); const { whatsappService } = await import('../services/whatsapp'); const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { id: true, name: true, systemUserToken: true, webhookSecret: true, systemUserTokenIssuedAt: true }, }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); if (!org.systemUserToken) return reply.code(400).send({ error: 'Organization has no systemUserToken configured' }); const decrypted = decryptSecrets(org as any); const result = await whatsappService.verifyPhoneNumber({ accessToken: decrypted.systemUserToken! }, phoneNumberId, code); logger.info({ orgId, phoneNumberId, actor: (req as any).user?.id }, '[SUPER_ADMIN] Phone number verified'); return { ok: true, metaResponse: result }; } catch (err: any) { const detail = err.response?.data?.error?.message ?? err.message; return reply.code(502).send({ error: 'Meta verification failed', detail }); } }); // ── WhatsApp Template Creation (cross-org) ──────────────────────────────── fastify.post('/whatsapp/templates', async (req, reply) => { const Schema = z.object({ orgId: z.string(), name: z.string().min(1).regex(/^[a-z0-9_]+$/, 'Template name must be lowercase snake_case'), category: z.enum(['MARKETING', 'UTILITY', 'AUTHENTICATION']), language: z.string().min(2), body: z.string().min(1).max(1024), header: z.string().max(60).optional(), footer: z.string().max(60).optional(), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); const { orgId, name, category, language, body: bodyText, header, footer } = body.data; try { const { decryptSecrets } = await import('../services/organization'); const { whatsappService } = await import('../services/whatsapp'); const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { id: true, name: true, wabaId: true, systemUserToken: true, webhookSecret: true, systemUserTokenIssuedAt: true }, }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); if (!org.wabaId || !org.systemUserToken) return reply.code(400).send({ error: 'Organization WhatsApp not configured' }); const decrypted = decryptSecrets(org as any); const components: any[] = []; if (header) components.push({ type: 'HEADER', format: 'TEXT', text: header }); components.push({ type: 'BODY', text: bodyText }); if (footer) components.push({ type: 'FOOTER', text: footer }); const result = await whatsappService.createMetaTemplate( { accessToken: decrypted.systemUserToken!, wabaId: org.wabaId }, { name, category, language, components } ); await prisma.auditLog.create({ data: { action: 'SUPER_ADMIN_CREATE_TEMPLATE', actorId: (req as any).user?.id, resourceId: orgId, details: { templateName: name, category, language }, }, }); logger.info({ orgId, templateName: name, actor: (req as any).user?.id }, '[SUPER_ADMIN] Template created'); return { ok: true, template: result }; } catch (err: any) { const detail = err.response?.data?.error?.message ?? err.message; return reply.code(502).send({ error: 'Meta API error', detail }); } }); // ── Billing ─────────────────────────────────────────────────────────────── fastify.get('/billing/transactions', async (req, reply) => { const QuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), orgId: z.string().optional(), }); const parsed = QuerySchema.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' }); const { page, limit, orgId } = parsed.data; const where: any = {}; if (orgId) where.organizationId = orgId; try { const [txns, total] = await Promise.all([ prisma.walletTransaction.findMany({ where, include: { organization: { select: { id: true, name: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.walletTransaction.count({ where }), ]); return { data: txns, total, page, limit }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] billing transactions failed'); return reply.code(500).send({ error: 'Failed to list transactions' }); } }); fastify.post('/billing/credits', async (req, reply) => { const Schema = z.object({ orgId: z.string(), amount: z.number().int().positive(), description: z.string().optional(), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); try { const orgExists = await prisma.organization.findUnique({ where: { id: body.data.orgId }, select: { id: true } }); if (!orgExists) return reply.code(404).send({ error: 'Organization not found' }); const { orgId, amount, description } = body.data; const updated = await prisma.$transaction(async (tx) => { const org = await tx.organization.update({ where: { id: orgId }, data: { walletBalance: { increment: amount } }, select: { walletBalance: true, name: true }, }); const txn = await tx.walletTransaction.create({ data: { organizationId: orgId, amount, balanceAfter: org.walletBalance, type: 'TOP_UP_MANUAL', description: description || 'Manual credit by super-admin', actorId: (req as any).user?.id, }, }); return { org, txn }; }); return { ok: true, newBalance: updated.org.walletBalance, transactionId: updated.txn.id }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] add credits failed'); return reply.code(500).send({ error: 'Failed to add credits' }); } }); // ── Monitoring ──────────────────────────────────────────────────────────── fastify.get('/monitoring/health', async (_req, reply) => { try { const [dbPing, redisPing, queueWaiting, queueFailed, queueActive] = await Promise.all([ prisma.$queryRaw`SELECT 1`.then(() => true).catch(() => false), redis.ping().then(() => true).catch(() => false), whatsappQueue.getWaitingCount().catch(() => -1), whatsappQueue.getFailedCount().catch(() => -1), whatsappQueue.getActiveCount().catch(() => -1), ]); // Token expiry check — find orgs with systemUserTokenIssuedAt older than 50 days const tokenExpiryThreshold = new Date(Date.now() - 50 * 24 * 60 * 60 * 1000); const expiringTokenOrgs = await prisma.organization.findMany({ where: { systemUserTokenIssuedAt: { lte: tokenExpiryThreshold }, systemUserToken: { not: null } }, select: { id: true, name: true, systemUserTokenIssuedAt: true } }); // Low wallet balance const lowBalanceOrgs = await prisma.organization.findMany({ where: { walletBalance: { lt: 100 }, isHardStopped: false }, select: { id: true, name: true, walletBalance: true } }); return { db: { ok: dbPing }, redis: { ok: redisPing }, queue: { waiting: queueWaiting, failed: queueFailed, active: queueActive }, tokenExpiries: expiringTokenOrgs.map(o => ({ orgId: o.id, orgName: o.name, issuedAt: o.systemUserTokenIssuedAt, daysOld: o.systemUserTokenIssuedAt ? Math.floor((Date.now() - o.systemUserTokenIssuedAt.getTime()) / (24 * 60 * 60 * 1000)) : null })), lowBalanceOrgs, }; } catch (err) { logger.error({ err }, '[SUPER_ADMIN] monitoring failed'); return reply.code(500).send({ error: 'Failed to fetch monitoring data' }); } }); // ── AI Agentic Command ──────────────────────────────────────────────────── fastify.post('/ai/command', async (req, reply) => { const Schema = z.object({ command: z.string().min(1), confirm: z.boolean().default(false), pendingAction: z.object({ action: z.string(), params: z.record(z.unknown()), }).optional(), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); const { command, confirm, pendingAction } = body.data; // If confirming a pending action if (confirm && pendingAction) { return executeSuperAdminAction(pendingAction.action, pendingAction.params, req, reply); } // Parse command with AI const { aiService } = await import('../services/ai'); const { z: zod } = await import('zod'); const CommandSchema = zod.object({ action: zod.enum(['QUERY_STATS', 'LIST_ORGS', 'SUSPEND_ORG', 'ACTIVATE_ORG', 'ADD_CREDITS', 'LIST_LOW_BALANCE', 'LIST_ALERTS', 'LIST_USERS']), params: zod.record(zod.unknown()).default({}), confirmation_required: zod.boolean(), human_summary: zod.string(), }); try { const systemPrompt = `You are a super-admin AI assistant for the XAMLÉ platform. Parse the admin's command and return a structured action. Available actions: - QUERY_STATS: Get platform statistics (no params needed) - LIST_ORGS: List organizations (params: search?, plan?) - SUSPEND_ORG: Suspend an organization (params: orgName or orgId) — confirmation required - ACTIVATE_ORG: Reactivate a suspended org (params: orgName or orgId) — confirmation required - ADD_CREDITS: Add wallet credits to an org (params: orgName or orgId, amount) — confirmation required - LIST_LOW_BALANCE: List orgs with low wallet balance - LIST_ALERTS: List monitoring alerts - LIST_USERS: List users (params: orgName?, role?) Always extract organization names and amounts from the command. Set confirmation_required=true for destructive or financial actions.`; const { data: parsed } = await aiService.generateStructuredData( `${systemPrompt}\n\nAdmin command: "${command}"`, CommandSchema, 0.1 ); if (parsed.confirmation_required) { return { status: 'pending_confirmation', summary: parsed.human_summary, action: parsed.action, params: parsed.params, }; } return executeSuperAdminAction(parsed.action, parsed.params ?? {}, req, reply); } catch (err) { logger.error({ err }, '[SUPER_ADMIN] AI command failed'); return reply.code(503).send({ error: 'AI service unavailable' }); } }); // ── Audit Logs ──────────────────────────────────────────────────────────── fastify.get('/audit-logs', async (req, _reply) => { const q = req.query as { page?: string; limit?: string; orgId?: string; actorId?: string; action?: string; from?: string; to?: string }; const page = Math.max(1, parseInt(q.page ?? '1')); const limit = Math.min(100, parseInt(q.limit ?? '50')); const where: any = {}; if (q.orgId) where.resourceId = q.orgId; if (q.actorId) where.actorId = q.actorId; if (q.action) where.action = { contains: q.action, mode: 'insensitive' }; if (q.from || q.to) { where.createdAt = {}; if (q.from) where.createdAt.gte = new Date(q.from); if (q.to) where.createdAt.lte = new Date(q.to); } const [logs, total] = await Promise.all([ prisma.auditLog.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.auditLog.count({ where }), ]); return { logs, total, page, limit }; }); // ── WhatsApp Profiles ───────────────────────────────────────────────────── fastify.get('/whatsapp/profiles', async (_req, _reply) => { const orgs = await prisma.organization.findMany({ orderBy: { createdAt: 'desc' }, select: { id: true, name: true, wabaId: true, brandingData: true, subscriptionPlan: true, subscriptionStatus: true, systemUserTokenIssuedAt: true, createdAt: true, }, }); return { profiles: orgs }; }); fastify.patch<{ Params: { id: string } }>('/whatsapp/profiles/:id', async (req, reply) => { const Schema = z.object({ name: z.string().min(1).optional(), brandingData: z.record(z.unknown()).optional(), }); const body = Schema.safeParse(req.body); if (!body.success) return reply.code(400).send({ error: body.error.flatten() }); const org = await prisma.organization.findUnique({ where: { id: req.params.id } }); if (!org) return reply.code(404).send({ error: 'Organization not found' }); const updated = await prisma.organization.update({ where: { id: req.params.id }, data: { ...(body.data.name && { name: body.data.name }), ...(body.data.brandingData && { brandingData: body.data.brandingData as Prisma.InputJsonValue }), }, select: { id: true, name: true, brandingData: true }, }); return updated; }); // ── WhatsApp Templates (cross-org) ──────────────────────────────────────── fastify.get('/whatsapp/templates', async (_req, _reply) => { const orgs = await prisma.organization.findMany({ where: { wabaId: { not: null }, subscriptionStatus: 'ACTIVE' }, select: { id: true, name: true, wabaId: true, subscriptionPlan: true }, orderBy: { name: 'asc' }, }); return { orgs }; }); } async function executeSuperAdminAction(action: string, params: Record