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:
@@ -243,25 +243,76 @@ export async function generatePaymentLink(tenantId: string) {
|
||||
const ownerEmail = await getTenantOwnerEmail(tenantId);
|
||||
if (!ownerEmail) throw new Error('No admin user found');
|
||||
|
||||
const subscription = await getActiveSubscription(tenantId);
|
||||
const plan = subscription?.plan || tenant.plan;
|
||||
const amount = subscription?.amount || 0;
|
||||
let subscription = await getActiveSubscription(tenantId);
|
||||
const plan = (subscription?.plan || tenant.plan) as Plan;
|
||||
if (plan === 'custom' || plan === 'trial') {
|
||||
throw new Error('No se puede generar link de pago para el plan actual');
|
||||
}
|
||||
|
||||
if (!amount) throw new Error('No se encontró monto de suscripción');
|
||||
const frequency = (subscription?.frequency as Frequency) || 'annual';
|
||||
let amount = subscription?.amount ? Number(subscription.amount) : 0;
|
||||
if (!amount) {
|
||||
amount = await getPlanPrice(plan, frequency, 'firstYear');
|
||||
}
|
||||
|
||||
// Los planes Business Control / Enterprise exceden el límite de cobro recurrente
|
||||
// de MercadoPago ($10k). Para esos montos usamos una Preference de pago único
|
||||
// anual; el webhook activa el período de 1 año al aprobarse.
|
||||
if (amount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
|
||||
if (!subscription) {
|
||||
subscription = await prisma.subscription.create({
|
||||
data: {
|
||||
tenantId,
|
||||
plan: plan as any,
|
||||
status: 'pending',
|
||||
amount,
|
||||
frequency,
|
||||
},
|
||||
});
|
||||
invalidateSubscriptionCache(tenantId);
|
||||
}
|
||||
|
||||
const mp = await mpService.createSubscriptionPreference({
|
||||
tenantId,
|
||||
subscriptionId: subscription.id,
|
||||
plan,
|
||||
amount,
|
||||
payerEmail: ownerEmail,
|
||||
});
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: { id: subscription.id },
|
||||
data: { mpPreferenceId: mp.preferenceId, status: 'pending', amount },
|
||||
});
|
||||
|
||||
return { paymentUrl: mp.checkoutUrl };
|
||||
}
|
||||
|
||||
const mp = await mpService.createPreapproval({
|
||||
tenantId,
|
||||
reason: `Horux360 - Plan ${plan} - ${tenant.nombre}`,
|
||||
amount,
|
||||
payerEmail: ownerEmail,
|
||||
frequency,
|
||||
});
|
||||
|
||||
// Update subscription with new MP preapproval ID
|
||||
if (subscription) {
|
||||
await prisma.subscription.update({
|
||||
where: { id: subscription.id },
|
||||
data: { mpPreapprovalId: mp.preapprovalId },
|
||||
data: { mpPreapprovalId: mp.preapprovalId, status: mp.status || 'pending' },
|
||||
});
|
||||
} else {
|
||||
await prisma.subscription.create({
|
||||
data: {
|
||||
tenantId,
|
||||
plan: plan as any,
|
||||
status: mp.status || 'pending',
|
||||
amount,
|
||||
frequency,
|
||||
mpPreapprovalId: mp.preapprovalId,
|
||||
},
|
||||
});
|
||||
invalidateSubscriptionCache(tenantId);
|
||||
}
|
||||
|
||||
return { paymentUrl: mp.initPoint };
|
||||
@@ -462,6 +513,54 @@ export async function subscribe(params: {
|
||||
? `${tenant.nombre} - Plan ${params.plan} - $${amount.toLocaleString('es-MX')} primer año, $${renewalAmount.toLocaleString('es-MX')} renovaciones`
|
||||
: `Horux360 - Plan ${params.plan} (${params.frequency}) - ${tenant.nombre}`;
|
||||
|
||||
// Planes Business Control / Enterprise superan el límite de cobro recurrente
|
||||
// de MercadoPago ($10k). Se cobra el año completo vía Preference one-off; el
|
||||
// webhook activa el período anual tras el primer pago aprobado.
|
||||
if (amount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
|
||||
const subscription = await prisma.subscription.create({
|
||||
data: {
|
||||
tenantId: params.tenantId,
|
||||
plan: params.plan,
|
||||
status: 'pending',
|
||||
amount,
|
||||
frequency: params.frequency,
|
||||
},
|
||||
});
|
||||
|
||||
const mp = await mpService.createSubscriptionPreference({
|
||||
tenantId: params.tenantId,
|
||||
subscriptionId: subscription.id,
|
||||
plan: params.plan,
|
||||
amount,
|
||||
payerEmail: params.payerEmail,
|
||||
});
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: { id: subscription.id },
|
||||
data: { mpPreferenceId: mp.preferenceId },
|
||||
});
|
||||
|
||||
await prisma.subscription.updateMany({
|
||||
where: { tenantId: params.tenantId, status: 'trial' },
|
||||
data: { status: 'trial_converted' },
|
||||
});
|
||||
|
||||
await prisma.tenant.update({
|
||||
where: { id: params.tenantId },
|
||||
data: { plan: params.plan },
|
||||
});
|
||||
|
||||
invalidateSubscriptionCache(params.tenantId);
|
||||
auditLog({
|
||||
tenantId: params.tenantId,
|
||||
action: 'subscription.created',
|
||||
entityType: 'Subscription',
|
||||
entityId: subscription.id,
|
||||
metadata: { plan: params.plan, frequency: params.frequency, amount, paymentMethod: 'preference' },
|
||||
});
|
||||
return { subscription, paymentUrl: mp.checkoutUrl };
|
||||
}
|
||||
|
||||
const mp = await mpService.createPreapproval({
|
||||
tenantId: params.tenantId,
|
||||
reason,
|
||||
@@ -637,13 +736,20 @@ export async function applyApprovedUpgrade(subscriptionId: string): Promise<void
|
||||
const newPlan = sub.upgradeTargetPlan as Plan;
|
||||
const newAmount = Number(sub.upgradeTargetAmount);
|
||||
|
||||
// Actualiza el monto del preapproval en MP (si existe)
|
||||
// Actualiza el monto del preapproval en MP (si existe). Si el nuevo monto
|
||||
// supera el límite de cobro recurrente de MP ($10k), cancelamos el preapproval
|
||||
// anterior: el plan alto se cobrará anualmente vía Preference one-off.
|
||||
if (sub.mpPreapprovalId) {
|
||||
try {
|
||||
await mpService.updatePreapprovalAmount(sub.mpPreapprovalId, newAmount);
|
||||
} catch (error: any) {
|
||||
console.error(`[Upgrade] Error actualizando preapproval ${sub.mpPreapprovalId}:`, error.message);
|
||||
throw error; // Re-lanza para que MP reintente el webhook
|
||||
if (newAmount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
|
||||
await mpService.cancelPreapproval(sub.mpPreapprovalId);
|
||||
console.log(`[Upgrade] Preapproval ${sub.mpPreapprovalId} cancelado porque el nuevo monto $${newAmount} supera el límite de MP`);
|
||||
} else {
|
||||
try {
|
||||
await mpService.updatePreapprovalAmount(sub.mpPreapprovalId, newAmount);
|
||||
} catch (error: any) {
|
||||
console.error(`[Upgrade] Error actualizando preapproval ${sub.mpPreapprovalId}:`, error.message);
|
||||
throw error; // Re-lanza para que MP reintente el webhook
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user