// FLUJO DE PAGO — Checkout, Mercado Pago (real), Oxxo Pay (real), Pendiente, Confirmado
window.PaymentFlow = {};
// ── Utilidad compartida ───────────────────────────────────────
function authHeaders() {
const tok = localStorage.getItem('sd_token');
return tok
? { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' }
: { 'Content-Type': 'application/json' };
}
// ══════════════════════════════════════════════════════════════
// CHECKOUT — elige método y llama la API
// ══════════════════════════════════════════════════════════════
window.PaymentFlow.Checkout = function Checkout({ navigate, store }) {
if (!store.session) { navigate('/login'); return null; }
const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS;
const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean);
const subtotal = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0);
const descuento = items.length >= 3 ? Math.round(subtotal * 0.15) : 0;
const total = subtotal - descuento;
const [metodo, setMetodo] = React.useState('mercadopago');
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState('');
if (items.length === 0) {
return (
🛒
Tu carrito está vacío
Agrega materias desde tu panel.
navigate('/dashboard')}>← Volver al panel
);
}
const procesar = async () => {
setLoading(true);
setError('');
const ids = store.cart;
try {
if (metodo === 'mercadopago') {
const r = await fetch(`${window.API_BASE}/checkout/mercadopago`, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ prueba_ids: ids }),
});
const d = await r.json();
if (!r.ok) { setError(d.error || 'Error al procesar'); setLoading(false); return; }
if (d.sandbox) {
// API no configurada con credenciales reales → flujo demo
navigate('/pago/mercadopago');
} else {
// Redirigir al checkout real de Mercado Pago
// En sandbox usa sandbox_init_point, en producción usa init_point
const url = d.sandbox_init_point || d.init_point;
window.location.href = url;
}
} else {
// OXXO: crear pago y mostrar ficha
const r = await fetch(`${window.API_BASE}/checkout/oxxo`, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ prueba_ids: ids }),
});
const d = await r.json();
if (!r.ok) { setError(d.error || 'Error al procesar'); setLoading(false); return; }
// Guardar datos del pago en sessionStorage para que la pantalla OXXO los muestre
sessionStorage.setItem('sd_oxxo_pago', JSON.stringify(d));
navigate('/pago/oxxo');
}
} catch {
// Sin API (local sin servidor) → demo
if (metodo === 'mercadopago') navigate('/pago/mercadopago');
else { sessionStorage.removeItem('sd_oxxo_pago'); navigate('/pago/oxxo'); }
}
setLoading(false);
};
return (
Checkout
Confirma tu compra
Materias seleccionadas ({items.length})
{items.map(p => (
{p.icono || '📘'}
{p.nombre}
Simulador completo · acceso permanente
{window.formatPrice(p.precio || p.precio_mxn)}
store.removeFromCart(p.id)}>✕
))}
Método de pago
setMetodo('mercadopago')} />
Tarjeta, débito o billetera digital
Mercado Pago · acreditación inmediata
Mercado Pago
setMetodo('oxxo')} />
Pago en efectivo en tienda OXXO
Ficha con referencia · acreditación 1-24h
Oxxo Pay
Resumen
Subtotal ({items.length} {items.length === 1 ? 'materia' : 'materias'}) {window.formatPrice(subtotal)}
{descuento > 0 &&
Descuento 3+ materias (15%) -{window.formatPrice(descuento)}
}
Total {window.formatPrice(total)}
{error &&
{error}
}
{loading ? 'Procesando…' : `Pagar con ${metodo === 'mercadopago' ? 'Mercado Pago' : 'Oxxo Pay'} →`}
Pago único. Acceso permanente a las materias compradas.
);
};
// ══════════════════════════════════════════════════════════════
// MERCADO PAGO — pantalla de transición / demo fallback
// En producción el usuario ya fue redirigido a MP directamente.
// Esta pantalla se muestra solo si MP no está configurado (sandbox=true).
// ══════════════════════════════════════════════════════════════
window.PaymentFlow.MercadoPago = function MercadoPago({ navigate, store }) {
const [paying, setPaying] = React.useState(false);
const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS;
const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean);
const sub = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0);
const total = sub - (items.length >= 3 ? Math.round(sub * 0.15) : 0);
const confirm = () => {
setPaying(true);
setTimeout(() => {
store.grantPremium(store.cart);
store.clearCart();
navigate('/pago/confirmado');
}, 1600);
};
return (
Checkout · Mercado Pago (demo)
navigate('/checkout')}>← Cancelar
Total a pagar
{window.formatPrice(total)}
Número de tarjeta
Nombre del titular
{paying ? 'Procesando…' : `Pagar ${window.formatPrice(total)}`}
Modo demo · configura MP_ACCESS_TOKEN en api/config.php para pagos reales.
);
};
// ══════════════════════════════════════════════════════════════
// OXXO PAY — muestra la referencia real generada por MP
// ══════════════════════════════════════════════════════════════
window.PaymentFlow.Oxxo = function OxxoPay({ navigate, store }) {
// Leer datos del pago que guardó Checkout en sessionStorage
const pagoData = React.useMemo(() => {
try { return JSON.parse(sessionStorage.getItem('sd_oxxo_pago') || '{}'); } catch { return {}; }
}, []);
const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS;
const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean);
const sub = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0);
const total = pagoData.total || (sub - (items.length >= 3 ? Math.round(sub * 0.15) : 0));
// Referencia real de MP o demo
const ref = pagoData.referencia
|| React.useMemo(() => '93000' + Math.floor(Math.random() * 1e10).toString().padStart(10, '0'), []);
const expira = pagoData.expira_en
? new Date(pagoData.expira_en).toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' })
: null;
const simularPago = () => {
store.grantPremium(store.cart);
store.clearCart();
sessionStorage.removeItem('sd_oxxo_pago');
navigate('/pago/confirmado');
};
return (
OXXO PAY
Ficha de pago
navigate('/checkout')}>← Cancelar
Monto a pagar
{window.formatPrice(total)}
Referencia · presenta este número en caja
{/* Código de barras visual (decorativo) */}
{Array.from({ length: 60 }).map((_, i) => (
))}
{ref}
{expira &&
Vigencia: {expira}
}
Acude a cualquier tienda OXXO.
Indica en caja que harás un pago de OXXO Pay.
Dicta o muestra la referencia de esta ficha.
Realiza el pago en efectivo.
Tu acceso premium se activa automáticamente al acreditarse el pago (1-24h).
{pagoData.barcode_url
?
Ver ficha completa ↗
:
window.print()}>Imprimir ficha
}
navigate('/pago/pendiente')}>Entendido →
{pagoData.sandbox && (
⚡ Demo: simular acreditación inmediata
)}
);
};
// ══════════════════════════════════════════════════════════════
// PENDIENTE — OXXO no acreditado todavía
// ══════════════════════════════════════════════════════════════
window.PaymentFlow.Pendiente = function Pendiente({ navigate, store }) {
return (
Pago pendiente de acreditación
Estamos esperando que OXXO confirme tu pago. Suele tardar entre 1 y 24 horas .
En cuanto se acredite recibirás un correo y tu acceso premium se activará solo —
no necesitas hacer nada más.
Mientras tanto puedes:
Practicar con los simuladores muestra (gratis)
Revisar el contenido del examen USICAMM
Cerrar esta página — recibirás un correo al acreditarse
navigate('/dashboard')}>← Volver al panel
);
};
// ══════════════════════════════════════════════════════════════
// CONFIRMADO — pago exitoso (MP redirige aquí con ?payment_id=)
// ══════════════════════════════════════════════════════════════
window.PaymentFlow.Confirmado = function Confirmado({ navigate, store }) {
// MP agrega ?payment_id=xxx&status=approved al volver al sitio
const hashQuery = new URLSearchParams(window.location.hash.split('?')[1] || '');
const mpStatus = hashQuery.get('status');
const mpPayId = hashQuery.get('payment_id');
// Refrescar premium desde la API una vez llegamos aquí.
// El webhook de MP ya pudo haberlo acreditado; este fetch lo confirma en el cliente.
React.useEffect(() => {
const tok = localStorage.getItem('sd_token');
if (!tok) return;
fetch(`${window.API_BASE}/me/accesos`, { headers: { Authorization: `Bearer ${tok}` } })
.then(r => r.ok ? r.json() : null)
.then(d => { if (d?.accesos?.length) store.grantPremium(d.accesos); })
.catch(() => {});
store.clearCart();
sessionStorage.removeItem('sd_oxxo_pago');
}, []);
return (
✓
¡Pago confirmado!
Tu acceso premium ya está activo. Practica las veces que necesites.
{mpPayId && (
Referencia MP: {mpPayId}
)}
navigate('/dashboard')}>
Ir a mis simuladores →
);
};