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

@@ -25,6 +25,9 @@ const preApprovalClient = new PreApproval(config);
const paymentClient = new MPPayment(config);
const preferenceClient = new Preference(config);
/** Límite de la API legacy de preapproval de MercadoPago para MXN. */
export const MP_PREAPPROVAL_MAX_AMOUNT = 10000;
/**
* Fallback público para `back_url` cuando `FRONTEND_URL` apunta a localhost.
* MercadoPago rechaza URLs `http://localhost...` o cualquier dominio no
@@ -227,6 +230,51 @@ export async function createProrationPreference(params: {
};
}
/**
* Crea una Preference (checkout de pago único) para el pago anual de una
* suscripción. Se usa cuando el monto supera el límite de preapproval ($10k).
* external_reference = `subscription:{tenantId}:{subscriptionId}` para que el
* webhook active el período anual al aprobarse.
*/
export async function createSubscriptionPreference(params: {
tenantId: string;
subscriptionId: string;
plan: string;
amount: number;
payerEmail: string;
}): Promise<{ preferenceId: string; checkoutUrl: string }> {
if (!env.MP_ACCESS_TOKEN) {
throw new Error('MercadoPago no está configurado (falta MP_ACCESS_TOKEN en .env).');
}
const response = await preferenceClient.create({
body: {
items: [
{
id: `subscription-${params.subscriptionId}`,
title: `Horux360 - Plan ${params.plan} - Año completo`,
quantity: 1,
unit_price: params.amount,
currency_id: 'MXN',
},
],
payer: { email: resolvePayerEmail(params.payerEmail) },
external_reference: `subscription:${params.tenantId}:${params.subscriptionId}`,
back_urls: {
success: `${backUrlBase()}/configuracion/suscripcion?subscription=success`,
failure: `${backUrlBase()}/configuracion/suscripcion?subscription=failure`,
pending: `${backUrlBase()}/configuracion/suscripcion?subscription=pending`,
},
auto_return: 'approved',
},
});
return {
preferenceId: response.id!,
checkoutUrl: response.init_point!,
};
}
/**
* Crea una Preference (checkout de pago único) para comprar un paquete de
* timbres adicionales. external_reference = `timbres-pack:${paymentId}` para