import { DateTime } from 'luxon' import db from '@adonisjs/lucid/services/db' import Organization from '#models/organization' /** * Politique de plans Rubis V1 : * * - Free : 5 factures actives en relance, 1 user * - Pro : factures illimitées, 1 user * - Business : factures illimitées, 5 users (V2 multi-users), réponses * par l'adresse mail user (V2 enhancement) * * Période de grâce : à l'inscription, l'org démarre en `free` avec un * `gracePeriodEndsAt = createdAt + 3 mois`. Pendant cette fenêtre, AUCUNE * limite n'est appliquée — l'user peut tester full power. Au-delà des 3 * mois, si `activeInvoicesCount > 5` → import bloqué jusqu'à upgrade. */ export type PlanKey = 'free' | 'pro' | 'business' export type PlanCaps = { /** Nombre max de factures dans un état "actif" (pending / awaiting / in_relance / litigation). null = illimité. */ activeInvoicesLimit: number | null /** Nombre max d'utilisateurs par org. null = illimité. */ seatsLimit: number | null /** Multi-users autorisé ? V1 : seulement Business (mais pas implémenté). */ multiUsers: boolean /** Réponse via l'email du user au lieu d'un from-Rubis générique ? V2. */ replyFromUserEmail: boolean /** SMS V2. */ smsEnabled: boolean } export const PLAN_CAPS: Record = { free: { activeInvoicesLimit: 5, seatsLimit: 1, multiUsers: false, replyFromUserEmail: false, smsEnabled: false, }, pro: { activeInvoicesLimit: null, seatsLimit: 1, multiUsers: false, replyFromUserEmail: false, smsEnabled: false, }, business: { activeInvoicesLimit: null, seatsLimit: 5, multiUsers: true, replyFromUserEmail: true, smsEnabled: false, // V2 }, } const ACTIVE_STATUSES = ['pending', 'awaiting_user_confirmation', 'in_relance', 'litigation'] /** * Compte les factures considérées "actives" pour la limite Free : * statut ∈ {pending, awaiting_user_confirmation, in_relance, litigation}. * paid / cancelled n'occupent pas de slot. */ export async function countActiveInvoices(organizationId: string): Promise { const row = await db .from('invoices') .where('organization_id', organizationId) .whereIn('status', ACTIVE_STATUSES) .count('* as n') .first() return Number(row?.n ?? 0) } export type EnforcementResult = | { allowed: true } | { allowed: false reason: 'free_limit_active_invoices' limit: number current: number gracePeriodEndsAt: string | null } /** * Vérifie si l'org peut créer N nouvelles factures actives. * * Règle : * - Plans payants → toujours autorisé * - Free pendant la période de grâce → autorisé sans limite * - Free après période de grâce → bloque si `current + delta > limit` * * `delta` = nombre de factures qu'on s'apprête à créer (typiquement 1 * pour saisie manuelle, N pour upload OCR multi-fichiers). */ export async function canCreateInvoices( organizationId: string, delta = 1 ): Promise { const org = await Organization.find(organizationId) if (!org) return { allowed: true } // org introuvable → pas notre rôle de bloquer ici const plan = (org.plan ?? 'free') as PlanKey const caps = PLAN_CAPS[plan] if (caps.activeInvoicesLimit === null) return { allowed: true } // Free + dans la période de grâce → unlimited const now = DateTime.utc() if (org.gracePeriodEndsAt && org.gracePeriodEndsAt > now) { return { allowed: true } } const current = await countActiveInvoices(organizationId) if (current + delta <= caps.activeInvoicesLimit) { return { allowed: true } } return { allowed: false, reason: 'free_limit_active_invoices', limit: caps.activeInvoicesLimit, current, gracePeriodEndsAt: org.gracePeriodEndsAt?.toISO() ?? null, } } /** * État subscription de l'org pour exposition côté SPA — utilisé par la * page /parametres/abonnement. */ export type OrgSubscriptionState = { plan: PlanKey caps: PlanCaps /** Compteur courant de factures actives. */ activeInvoicesCount: number /** True tant que l'org bénéficie de la fenêtre 3 mois post-signup. */ inGracePeriod: boolean gracePeriodEndsAt: string | null /** Status Stripe (`active`, `trialing`, `past_due`, `canceled`...). null pour les Free. */ subscriptionStatus: string | null /** 'monthly' | 'yearly' | null pour les Free. */ billingCycle: 'monthly' | 'yearly' | null /** ISO date de fin de période courante (= prochaine facture Stripe). */ currentPeriodEnd: string | null /** True si l'org a un Stripe customer ID (= a déjà payé une fois). */ hasStripeCustomer: boolean /** * True si l'user a annulé sa souscription côté Stripe et qu'elle s'éteindra * à `currentPeriodEnd`. Pendant cette fenêtre l'org reste sur son plan * payant (status `active`), mais l'UI affiche "annulé, accès jusqu'au DD/MM" * et propose un bouton "Réactiver". */ cancelAtPeriodEnd: boolean } export async function getOrgSubscriptionState( organizationId: string ): Promise { const org = await Organization.findOrFail(organizationId) const plan = (org.plan ?? 'free') as PlanKey const now = DateTime.utc() const inGracePeriod = plan === 'free' && !!org.gracePeriodEndsAt && org.gracePeriodEndsAt > now return { plan, caps: PLAN_CAPS[plan], activeInvoicesCount: await countActiveInvoices(organizationId), inGracePeriod, gracePeriodEndsAt: org.gracePeriodEndsAt?.toISO() ?? null, subscriptionStatus: org.subscriptionStatus ?? null, billingCycle: org.billingCycle === 'monthly' || org.billingCycle === 'yearly' ? org.billingCycle : null, currentPeriodEnd: org.currentPeriodEnd?.toISO() ?? null, hasStripeCustomer: !!org.stripeCustomerId, cancelAtPeriodEnd: !!org.cancelAtPeriodEnd, } }