// 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 total = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0); 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.

); } 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 { // init_point apunta a producción cuando el token es APP_USR- window.location.href = d.init_point; } } else { // OXXO: genera ficha directa con referencia let rawText = ''; try { const r = await fetch(`${window.API_BASE}/checkout/oxxo`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ prueba_ids: ids }), }); rawText = await r.text(); const d = JSON.parse(rawText); if (!r.ok) { setError(d.error || 'Error al procesar'); setLoading(false); return; } sessionStorage.setItem('sd_oxxo_pago', JSON.stringify(d)); store.clearCart(); navigate('/pago/oxxo'); } catch (err) { // Mostrar el error real para diagnóstico setError('Error OXXO: ' + (rawText ? rawText.slice(0, 200) : String(err))); setLoading(false); } return; } } catch { if (metodo === 'mercadopago') navigate('/pago/mercadopago'); } setLoading(false); }; return (
Checkout

Confirma tu compra

Procesos seleccionados ({items.length})

{items.map(p => (
{p.icono || '📘'}
{p.nombre}
Simulador completo · acceso permanente a este proceso
{window.formatPrice(p.precio || p.precio_mxn)}
))}

Método de pago

Resumen

{items.length} {items.length === 1 ? 'proceso' : 'procesos'}{window.formatPrice(total)}
Total{window.formatPrice(total)}
{error &&
{error}
}

Pago único. Acceso permanente al proceso comprado.

); }; // ══════════════════════════════════════════════════════════════ // 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)
Total a pagar
{window.formatPrice(total)}

Modo demo · configura MP_ACCESS_TOKEN en api/config.php para pagos reales.

); }; // ══════════════════════════════════════════════════════════════ // OXXO PAY — ficha con logo real, barcode JsBarcode y print // ══════════════════════════════════════════════════════════════ function OxxoLogoSVG({ width = 160 }) { const h = Math.round(width * 0.34); return ( {/* Rectángulo rojo */} {/* Franja amarilla superior */} {/* Franja amarilla inferior */} {/* OXXO texto */} OXXO {/* Separador */} {/* PAY texto */} PAY {/* ® */} ® ); } window.PaymentFlow.Oxxo = function OxxoPay({ navigate, store }) { const BASE = window.API_BASE || '/simuladordocente/api'; const pagoData = React.useMemo(() => { try { return JSON.parse(sessionStorage.getItem('sd_oxxo_pago') || '{}'); } catch { return {}; } }, []); const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const itemIds = pagoData.prueba_ids || store.cart; const items = itemIds.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean); const total = pagoData.total || items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0); const fallbackRef = React.useMemo(() => '93' + Math.floor(Math.random() * 1e16).toString().padStart(16, '0'), []); // Barcode mejorado desde MP (actualización silenciosa en background) const [mpBarcode, setMpBarcode] = React.useState(null); React.useEffect(() => { const pagoId = pagoData.pago_id; const tok = localStorage.getItem('sd_token'); if (!pagoId || !tok || pagoData.sandbox) return; // Pasamos también el barcode_url como fallback para extracción HTML const params = new URLSearchParams({ pago_id: pagoId }); if (pagoData.barcode_url) params.set('barcode_url', pagoData.barcode_url); fetch(`${BASE}/me/oxxo-barcode?${params}`, { headers: { Authorization: `Bearer ${tok}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (d && d.barcode_content) setMpBarcode(d); }) .catch(() => {}); }, []); // Usa el barcode_content de MP si llegó, si no usa la referencia de sessionStorage const ref = mpBarcode?.referencia || pagoData.referencia || fallbackRef; const barcodeValue = mpBarcode?.barcode_content || pagoData.barcode_content || ref; const barcodeUrl = mpBarcode?.barcode_url || pagoData.barcode_url || null; const expira = pagoData.expira_en ? new Date(pagoData.expira_en.replace(' ', 'T')).toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' }) : null; const barcodeRef = React.useRef(null); React.useEffect(() => { if (!barcodeRef.current || !window.JsBarcode || !barcodeValue) return; try { barcodeRef.current.innerHTML = ''; JsBarcode(barcodeRef.current, barcodeValue, { format: 'CODE128', width: 2.2, height: 68, displayValue: false, margin: 0, background: '#FFFFFF', lineColor: '#000000', }); } catch {} }, [barcodeValue]); const simularPago = () => { store.grantPremium(store.cart); store.clearCart(); sessionStorage.removeItem('sd_oxxo_pago'); navigate('/pago/confirmado'); }; const PASOS = [ 'Acude a cualquier tienda OXXO.', 'Indica en caja que harás un pago de OXXO Pay.', 'Muestra o dicta la referencia de esta ficha.', 'Paga el monto exacto en efectivo.', 'Tu acceso premium se activa automáticamente al acreditarse el pago (1–48 h).', ]; return (
Ficha de pago
DocentesMX · docentesmx.net
Monto a pagar {window.formatPrice(total)}
{/* ── Aviso de correo ── */}
📧 Revisa tu correo — también te llegó tu ficha de pago lista para usar. Si no puedes ver este código, usa el botón de descarga al final.
Referencia · presenta este número en caja
{pagoData.sandbox && (
⚠️ Referencia de prueba — no funciona en tiendas reales.
)} {mpBarcode?.barcode_content ? ( <>
{mpBarcode.barcode_content}
) : (

No se pudo cargar el código. Descarga tu ficha oficial de Mercado Pago para pagar en OXXO.

{barcodeUrl && ( Descargar ficha de pago ↗ )}
)} {expira && (
Válido hasta el {expira}
)}
{items.length > 0 && (
Detalle de compra
{items.map(p => (
{p.materiaNombre || p.nombre.replace(/^[^·]+·\s*/, '')} — Simulador completo ${p.precio ?? total}
))}
)}
    {PASOS.map((texto, i) => (
  1. {texto}
  2. ))}
{pagoData.barcode_url && ( Ficha oficial MP ↗ )}
{pagoData.sandbox && ( )}
); }; // ══════════════════════════════════════════════════════════════ // 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
); }; // ══════════════════════════════════════════════════════════════ // CONFIRMADO — pago exitoso (MP redirige aquí con ?payment_id=) // ══════════════════════════════════════════════════════════════ window.PaymentFlow.Confirmado = function Confirmado({ navigate, store }) { // MP puede poner los params antes del # (search) o dentro del hash — revisamos ambos const hashQuery = new URLSearchParams(window.location.hash.split('?')[1] || ''); const searchQuery = new URLSearchParams(window.location.search); const mpStatus = hashQuery.get('status') || searchQuery.get('status'); const mpPayId = hashQuery.get('payment_id') || searchQuery.get('payment_id'); const [verificando, setVerificando] = React.useState(true); // El webhook de MP suele llegar antes que el redirect, pero puede haber diferencia de segundos. // Intentamos 3 veces con 2 s de intervalo para dar tiempo al webhook. React.useEffect(() => { store.clearCart(); sessionStorage.removeItem('sd_oxxo_pago'); const tok = localStorage.getItem('sd_token'); if (!tok) { setVerificando(false); return; } let intentos = 0; const check = () => { 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); setVerificando(false); } else if (intentos < 3) { intentos++; setTimeout(check, 2000); } else { setVerificando(false); } }) .catch(() => setVerificando(false)); }; check(); }, []); return (
{verificando ? ( <>

Verificando tu pago…

Estamos confirmando con Mercado Pago. Toma solo unos segundos.

) : ( <>

¡Pago confirmado!

Tu acceso premium ya está activo. Practica las veces que necesites.

{mpPayId && (
Referencia MP: {mpPayId}
)} )}
); };