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:
@@ -24,6 +24,7 @@ interface PlanInfo {
|
||||
dbMode: string;
|
||||
trialEndsAt: string | null;
|
||||
isTrialActive: boolean;
|
||||
planPrice: number | null;
|
||||
subscription: SubscriptionInfo | null;
|
||||
}
|
||||
|
||||
@@ -91,6 +92,12 @@ export default function PlanesDespachoPage() {
|
||||
const hasActiveSub = subStatus != null
|
||||
&& subStatus !== 'cancelled'
|
||||
&& subStatus !== 'trial_expired';
|
||||
// Estados en los que se puede generar un link de pago (incluye trial y vencido).
|
||||
const isPayableStatus = subStatus === 'trial'
|
||||
|| subStatus === 'trial_expired'
|
||||
|| hasActiveSub;
|
||||
const isCurrentPlanPaid = currentPlan === planInfo?.subscription?.plan
|
||||
&& (subStatus === 'authorized' || subStatus === 'pending');
|
||||
|
||||
/** Resuelve la frecuencia para un plan. Mi Empresa y Mi Empresa+ leen su
|
||||
* propio toggle; el resto (business_*) siempre annual. */
|
||||
@@ -225,9 +232,25 @@ export default function PlanesDespachoPage() {
|
||||
|
||||
function PlanActionButton({ plan }: { plan: PaidPlan }) {
|
||||
const isCurrent = currentPlan === plan;
|
||||
if (isCurrent) {
|
||||
if (isCurrent && isCurrentPlanPaid) {
|
||||
return <Button disabled className="w-full">Plan actual</Button>;
|
||||
}
|
||||
if (isCurrent) {
|
||||
return (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleContratar(plan)}
|
||||
disabled={busy === plan}
|
||||
>
|
||||
{busy === plan ? 'Procesando...' : (
|
||||
<>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Pagar este plan
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
const label = hasActiveSub ? 'Cambiar a este plan' : 'Contratar';
|
||||
return (
|
||||
<Button
|
||||
@@ -302,7 +325,7 @@ export default function PlanesDespachoPage() {
|
||||
)}
|
||||
|
||||
{/* Banner de suscripción activa */}
|
||||
{!loading && planInfo?.subscription && hasPaidPlan && (() => {
|
||||
{!loading && planInfo?.subscription && hasPaidPlan && (subStatus === 'authorized' || subStatus === 'pending') && (() => {
|
||||
const sub = planInfo.subscription;
|
||||
const periodEndDate = sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd) : null;
|
||||
const fechaFormato = periodEndDate
|
||||
@@ -329,18 +352,30 @@ export default function PlanesDespachoPage() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Botón "Pagar mi período actual" — visible cuando la sub corre y hay
|
||||
un monto > 0 que cobrar. Crea una MP Preference one-off por el monto
|
||||
actual (custom $10, paid plan, lo que sea). Útil para pre-pagar antes
|
||||
del cobro automático o cuando no hay preapproval recurrente activo. */}
|
||||
{!loading && hasActiveSub && planInfo?.subscription && Number(planInfo.subscription.amount) > 0 && (() => {
|
||||
{/* Banner de trial vencido */}
|
||||
{!loading && subStatus === 'trial_expired' && hasPaidPlan && (
|
||||
<div className="flex items-start gap-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
|
||||
<Clock className="h-5 w-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-red-800 dark:text-red-300">Tu período de prueba terminó</span>
|
||||
<span className="text-red-700 dark:text-red-400"> — elige un plan o paga el plan actual para recuperar el acceso.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botón "Pagar mi período actual" — visible cuando se puede pagar y hay
|
||||
un monto definido (subscription.amount > 0 o precio de catálogo).
|
||||
Crea una MP Preference one-off por el monto actual. */}
|
||||
{!loading && isPayableStatus && planInfo?.subscription && (() => {
|
||||
const sub = planInfo.subscription!;
|
||||
const effectiveAmount = Number(sub.amount) > 0 ? Number(sub.amount) : (planInfo.planPrice ?? 0);
|
||||
if (!effectiveAmount) return null;
|
||||
const periodEnd = sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd) : null;
|
||||
const fechaFmt = periodEnd
|
||||
? periodEnd.toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })
|
||||
: null;
|
||||
const dias = periodEnd ? Math.max(0, Math.ceil((periodEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : null;
|
||||
const montoFmt = Number(sub.amount).toLocaleString('es-MX', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||
const montoFmt = effectiveAmount.toLocaleString('es-MX', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg px-5 py-4 max-w-3xl mx-auto">
|
||||
<CreditCard className="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
|
||||
Reference in New Issue
Block a user