fix(pagos): permitir pagar plan actual trial_expired y soportar planes >$10k via Preference

- Expone subscription trial_expired en /despachos/me/plan e incluye planPrice.
- Para Business Control/Enterprise (>$10k) genera pago anual único con MP Preference
  en lugar de preapproval recurrente; el webhook activa 1 año de suscripción.
- Muestra CTA de pago en UI cuando la suscripción está trial/trial_expired.
- Agrega campo mp_preference_id a subscriptions y mejora mensajes de error MP.
This commit is contained in:
Horux Dev
2026-06-16 22:37:11 +00:00
parent 63908f9e9d
commit 3f3253d41b
8 changed files with 290 additions and 22 deletions

View File

@@ -3,6 +3,7 @@ import { z } from 'zod';
import { signupDespacho } from '../services/despacho.service.js';
import { AppError } from '../middlewares/error.middleware.js';
import { prisma } from '../config/database.js';
import { getPlanPrice } from '../services/payment/subscription.service.js';
const signupSchema = z.object({
despacho: z.object({
@@ -47,7 +48,7 @@ export async function getMyPlan(req: Request, res: Response, next: NextFunction)
// business_control desde una TrialInvitation), respetamos ese plan
// para que el feature-gate y los límites funcionen correctamente.
const subscription = await prisma.subscription.findFirst({
where: { tenantId, status: { in: ['authorized', 'pending', 'paused', 'trial'] } },
where: { tenantId, status: { in: ['authorized', 'pending', 'paused', 'trial', 'trial_expired'] } },
orderBy: { createdAt: 'desc' },
select: {
status: true, amount: true, plan: true,
@@ -64,6 +65,18 @@ export async function getMyPlan(req: Request, res: Response, next: NextFunction)
currentPlan = String(tenant.plan);
}
// Precio de catálogo del plan actual (primer año, anual). La UI lo usa
// cuando la suscripción aún no tiene monto (trial/trial_expired) para
// mostrar el CTA de pago.
let planPrice: number | null = null;
if (currentPlan && currentPlan !== 'trial' && currentPlan !== 'custom') {
try {
planPrice = await getPlanPrice(currentPlan as any, 'annual', 'firstYear');
} catch {
planPrice = null;
}
}
// Estado de suscripción activa (si hay) — alimenta la UI con el monto
// recurrente actual, fecha de próxima renovación y si el primer pago
// (cuando aplica dualidad firstYear) ya fue completado.
@@ -72,6 +85,7 @@ export async function getMyPlan(req: Request, res: Response, next: NextFunction)
dbMode: tenant.dbMode,
trialEndsAt: tenant.trialEndsAt?.toISOString() ?? null,
isTrialActive,
planPrice,
subscription: subscription
? {
status: subscription.status,