refill@store:~$ cat proyecto.md

Recargas de juegos con Pago Móvil verificado contra el banco y entrega automática en segundos
Desarrollador full-stack (arquitectura, frontend, API, integraciones y despliegue)
Una compra, línea a línea
Lo que ocurre entre que el jugador pulsa «pagar» y la recarga le llega, sin que nadie mire un teléfono.
# antes
Todo a mano, por WhatsApp
Las recargas de juegos en Venezuela se venden por WhatsApp: el cliente manda una captura del Pago Móvil, alguien la revisa a ojo y hace la recarga a mano. Eso limita las ventas al horario del vendedor, deja la puerta abierta a comprobantes falsos o referencias reutilizadas y convierte cada pedido en una conversación. Además el monto en bolívares se mueve con la tasa: entre que el cliente ve el precio y transfiere, la cifra ya no coincide y la verificación falla.
# después
Una tubería automática y auditable
Automatizar el ciclo completo en una sola pantalla. Al crear la orden se congela el monto en bolívares y la tasa; el cliente pega la referencia y la API consulta el movimiento real en el banco a través de Pabilo (el campo is_new descarta referencias ya consumidas). Confirmado el pago, un servicio de despacho encadena las llamadas al proveedor con un external_order_id estable que evita cobros dobles, y para los productos manuales genera un enlace de WhatsApp con el mensaje precargado. Las claves de los proveedores nunca llegan al navegador: viven en Secret Manager y sólo las lee la Cloud Function.
Seis piezas que sostienen la tienda
Verificación bancaria real, no capturas
La referencia se consulta contra el movimiento del banco vía Pabilo. Como su filtro de monto es exacto, si la consulta con importe no encuentra nada se repite sin él y es la tienda quien compara los montos redondeados a céntimos: pagar de menos se rechaza siempre, pagar de más se acepta y se abona al saldo.
Despacho automático con idempotencia
Cada llamada al proveedor lleva un external_order_id de la forma {código de orden}-{n.º de llamada}: estable entre reintentos y distinto por cada parte de un combo. Un timeout no compra dos veces, y el reintento del panel sólo repite las llamadas que quedaron en error.
Candado por referencia bancaria
Antes de llamar a Pabilo se toma un candado en paymentRefs/{referencia}. Sin él, dos peticiones simultáneas con la misma referencia verían ambas is_new: true y las dos órdenes se darían por pagadas. Si la verificación falla, el candado se libera.
Panel de administración completo
17 pantallas de gestión: resumen con gráficos de ingresos y utilidad, órdenes con reintento y reembolso, CRUD de catálogo con recálculo de precios por margen, usuarios con cartera y libro de movimientos, cupones, niveles editables en caliente, soporte y bitácora de auditoría.
Paginación por cursor en todas las listas
Firestore cobra por documento leído y offset(n) cobra los n que se salta. Todas las listas del panel usan startAfter con el ID del documento como último criterio de desempate, para que dos registros del mismo segundo no se repitan ni desaparezcan entre páginas.
Saldo, referidos y niveles de fidelidad
Reembolsos y recompensas van a la cartera del cliente y se pueden gastar: el saldo se descuenta del total dentro de una transacción al crear la orden. Ocho niveles calculados por total gastado aplican un descuento creciente en cada recarga.
Tres pantallas, un mismo recorrido
Del catálogo al pago y del pago al panel: la tienda que ve el jugador y la consola que ve el equipo.
Recarga tus juegos
en segundos
Elige tu juego
Portada — hero y selector de juegos
Monto exacto a transferir
Bs 1.953,44
$13,45 · Tasa Bs 145,20
⏱ Tiempo para pagar: 27:14Datos del Pago Móvil
Toca cualquier dato para copiarlo
Confirma tu pago
Checkout — pantalla de pago
Ingresos
$4.128
Utilidad
$1.204
Órdenes
317
Usuarios
182
Ingresos y utilidad
Panel — resumen del administrador
Las maquetas reproducen la interfaz real; los datos que muestran son de ejemplo.
32
pantallas enrutadas (15 tienda + 17 panel)
94
endpoints de la API Express
4
Cloud Functions desplegadas
23
colecciones de Firestore
20
servicios de dominio en el backend
5
juegos en el catálogo sembrado
53
productos en la siembra inicial
11
estados posibles de una orden
8
niveles de fidelidad
~30.800
líneas de TypeScript
Todo lo que ya está dentro
Con qué está hecho
Frontend
Backend
Firebase
Integraciones
Infraestructura y tooling
Arquitectura y lo que costó
Monorepo de dos mitades desplegadas por separado. El navegador carga la SPA de React desde Netlify y le habla siempre a rutas relativas /api/**; netlify.toml las reescribe con status 200 hacia la Cloud Function api, de modo que el navegador nunca ve la URL de Cloud Functions ni hace preflight de CORS (aunque la función sí valida el Origin, con patrones para los subdominios de vista previa). La función api es una única app Express que monta seis routers —public, orders, me, admin, setup y webhooks— sobre 94 endpoints, con middleware de autenticación por ID token de Firebase (con checkRevoked), rate limit respaldado en Firestore y un manejador central de errores. Debajo hay veinte servicios de dominio: orders, dispatch, pabilo, inefable, whatsapp, catalog, coupons, users, creators, rate, stats, audit, settings, notifications, email y compañía. Tres funciones programadas completan el sistema: expireOrders cada 5 minutos caduca órdenes impagas y limpia contadores, resolveDispatches cada 2 minutos es la red de seguridad para las recargas que el webhook del proveedor dejó sin cerrar, y refreshRate refresca la tasa Bs/USD cada hora. Los datos viven en 23 colecciones de Firestore (16 raíz y 7 subcolecciones) con reglas de cierre por defecto: el cliente lee sus propias órdenes en tiempo real y edita cuatro campos cosméticos de su perfil, pero precios, estados de pago, saldos y roles sólo los toca el backend. El rol viaja en los custom claims del token, no en el documento del usuario, para que no se pueda escalar privilegios escribiendo en Firestore. Las credenciales de Pabilo e Inefable están en Secret Manager y se montan por función.
error 01
El filtro de monto de Pabilo dejaba inservible la tolerancia: la API sólo devuelve el movimiento si el importe coincide exacto, así que un cliente que transfería 3.708,70 en vez de 3.708,60 recibía «no encontramos ese pago» antes de que la tienda pudiera comparar nada.
fix
Si la consulta con monto no encuentra el movimiento, se repite sin monto y es la tienda quien decide. Como sin ese filtro Pabilo devuelve el movimiento sea cual sea su importe, leer el monto real pasó a ser obligatorio en ese camino: si no se puede leer, se rechaza. La regla quedó de una sola dirección —pagar de menos se rechaza siempre, pagar de más se acepta sin límite y el excedente se registra— con los dos importes redondeados a céntimos para que 3708.5999999999995 no tumbe un pago exacto.
error 02
La documentación del proyecto describía mal la API de despacho: equivocaba la ruta y, sobre todo, el significado de product_id. Pedir un paquete sin él devolvía «Please insert Zone ID into input2», porque los package_id se repiten entre juegos y el proveedor emparejaba el paquete con otro juego.
fix
Se verificó contra la documentación oficial del proveedor: product_id es el game_id y package_id el paquete, sobre /api/v1/recharge. El éxito se decide por el booleano ok y nunca por la presencia de order_id, porque una recarga fallida también lo devuelve. La ruta quedó configurable por variable de entorno por si el proveedor la mueve.
error 03
Volver atrás con el botón del teléfono creaba órdenes duplicadas. location.state queda pegado a la entrada del historial, así que al regresar el checkout se montaba de nuevo, veía ahí los datos del jugador y creaba otra orden que se quedaba esperando pago hasta caducar. Llegaron a acumularse seis, de cinco clientes distintos.
fix
En cuanto se crea la orden, el checkout reemplaza su entrada del historial por la URL /comprar/{producto}?orden={id} y borra el state. Volver atrás retoma esa misma orden —mismo monto, misma tasa, mismos datos y el reloj original corriendo— o lleva a su detalle si ya se pagó, en lugar de crear nada.
error 04
«Un uso por usuario» en los cupones no protegía de nada: crear correos nuevos es gratis, así que quien estuviera dispuesto a registrarse varias veces podía recargar siempre el mismo personaje con el mismo cupón.
fix
El límite se aplica por dos caminos a la vez, la cuenta y el ID de jugador, con el conteo por ID acotado a su juego. Gastan uso las órdenes pagadas y las que todavía se pueden pagar; las canceladas y las caducadas no, porque antes abandonar un pago quemaba el cupón sin que nadie cobrara nada.
error 05
Cambiar Free Fire a la ruta del proveedor que sale entre un 3 % y un 5 % más barata tenía un coste oculto: ese juego es de tipo dynamic y deja de validar el ID del jugador —acepta cualquier número, responde «completada» y cobra igual—, y una recarga a un ID equivocado no se recupera.
fix
Cada juego lleva la bandera validatesPlayerId. Cuando está en false, la tienda muestra un aviso junto al formulario y exige confirmar los datos antes de crear la orden. Además hubo que renombrar el producto: el paquete de la ruta nueva entrega 2.160 diamantes donde el anterior daba 2.180, y prometer de más no es un detalle cosmético.
error 06
Tres cambios desplegados a Firebase Hosting y verificados «en producción» seguían sin verse: el cliente miraba Netlify, que es donde vive el frontend de verdad, y refill-e254f.web.app era un sitio sobrante del setup inicial que no usa nadie.
fix
Se documentó el reparto en el README y se fijó el flujo: cualquier cambio de web/ termina con git push origin main y se confirma el deploy con la API de Netlify; firebase deploy queda reservado para la API y las reglas. La verificación se hace siempre contra refill-store-ve.netlify.app.
error 07
Sin Java instalado no arranca el emulador de Firestore, y el puerto 5001 estaba ocupado por otro servidor de la máquina.
fix
Se separó el arranque en dos scripts: dev:api levanta sólo el emulador de Functions —que no necesita Java— y deja al Admin SDK escribir en el Firestore real con las credenciales del CLI, y dev:api:full levanta también Firestore y Auth para quien tenga un JDK. Los emuladores usan puertos poco comunes (5051, 8380, 9399, 5080, 4300) para no chocar con otros proyectos.

Tienda web de recargas para Free Fire, Blood Strike, Mobile Legends, Honor of Kings y Marvel Rivals, pensada mobile-first para el mercado venezolano.
El cliente elige el paquete, paga por Pago Móvil BDV o transferencia y pega la referencia: el backend la verifica contra el banco mediante la API de Pabilo y, si es válida, despacha la recarga con la API de Inefable sin intervención humana.
Monorepo con dos mitades: una SPA en React 18 + Vite + TypeScript + Tailwind desplegada en Netlify, y una API Express sobre Cloud Functions v2 que guarda los secretos y escribe en Firestore con el Admin SDK.
Incluye un panel de administración completo —órdenes, catálogo, usuarios, cupones, niveles de fidelidad, soporte, avisos y bitácora— además de cartera de saldo, referidos y códigos de creador.
Alrededor de 30.800 líneas de TypeScript repartidas entre 32 pantallas enrutadas, 94 endpoints y 4 Cloud Functions.
Refill Store
¿Quieres algo así para tu producto?
Una tienda que cobra, verifica contra el banco y despacha sola. Si necesitas algo parecido —pagos verificados, despacho idempotente, panel completo— es exactamente el terreno que conozco.
