import Plan from '#models/plan' import PlanStep from '#models/plan_step' import PlanTransformer from '#transformers/plan_transformer' import { updatePlanValidator } from '#validators/plan' import type { HttpContext } from '@adonisjs/core/http' import { Exception } from '@adonisjs/core/exceptions' import db from '@adonisjs/lucid/services/db' function requireOrgId(auth: HttpContext['auth']): string { const user = auth.getUserOrFail() if (!user.organizationId) { throw new Exception('Aucune organisation rattachée', { status: 404, code: 'not_found' }) } return user.organizationId } function serializePlan(p: Plan) { return new PlanTransformer(p).toObject() } /** * Compte combien de factures actives référencent chaque plan d'une org. * Utilisé pour enrichir la liste avec un badge d'usage. * * @todo Brancher sur Invoice quand le domaine arrive — pour l'instant 0 * partout (le contrat reste stable côté SPA). */ async function bulkComputePlanUsage( _organizationId: string, planIds: string[] ): Promise> { const map = new Map() for (const id of planIds) map.set(id, 0) return map } export default class PlansController { /** * GET /plans — liste enrichie avec compteurs d'usage. */ async index({ auth, response }: HttpContext) { const organizationId = requireOrgId(auth) const plans = await Plan.query() .where('organization_id', organizationId) .preload('steps') .orderBy('is_default', 'desc') .orderBy('name', 'asc') const usage = await bulkComputePlanUsage( organizationId, plans.map((p) => p.id) ) const data = plans.map((p) => ({ ...serializePlan(p), usageCount: usage.get(p.id) ?? 0, })) return response.json({ data }) } /** * GET /plans/:slug — détail. * Le SPA lookup par slug pour les plans pré-fournis (URL stable et * lisible : /parametres/plans/standard-30j). */ async show({ auth, params, response }: HttpContext) { const organizationId = requireOrgId(auth) const plan = await Plan.query() .where('organization_id', organizationId) .where('slug', params.slug) .preload('steps') .first() if (!plan) { throw new Exception('Plan introuvable', { status: 404, code: 'not_found' }) } const usage = await bulkComputePlanUsage(organizationId, [plan.id]) return response.json({ data: { ...serializePlan(plan), usageCount: usage.get(plan.id) ?? 0 }, }) } /** * PATCH /plans/:slug — édite nom, description et/ou recompose les étapes. * * Recomposition des steps : on ne fait pas de diff fin (id par id), on * remplace tout le set en transaction. Plus simple, plus prévisible, et * idiomatique côté UX (l'utilisateur a édité son plan dans son ensemble). */ async update({ auth, params, request, response }: HttpContext) { const organizationId = requireOrgId(auth) const payload = await request.validateUsing(updatePlanValidator) const plan = await Plan.query() .where('organization_id', organizationId) .where('slug', params.slug) .first() if (!plan) { throw new Exception('Plan introuvable', { status: 404, code: 'not_found' }) } await db.transaction(async (trx) => { plan.useTransaction(trx) if (payload.name !== undefined) plan.name = payload.name if (payload.description !== undefined) plan.description = payload.description await plan.save() if (payload.steps !== undefined) { // Remplace tout le set await PlanStep.query({ client: trx }).where('plan_id', plan.id).delete() await PlanStep.createMany( payload.steps.map((s) => ({ planId: plan.id, order: s.order, offsetDays: s.offsetDays, tone: s.tone, subject: s.subject, body: s.body, requiresManualValidation: s.requiresManualValidation, })), { client: trx } ) } }) await plan.load('steps') return response.json({ data: serializePlan(plan) }) } }