refill@store:~$ cat proyecto.md

Refill Store

Recargas de juegos con Pago Móvil verificado contra el banco y entrega automática en segundos

Plataforma web de comercio y pagos2026En producción

Desarrollador full-stack (arquitectura, frontend, API, integraciones y despliegue)

01 / El ciclo

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.

orders.log · producción
[12:04:31]POST /orders → RF-9K4BWDawaiting_payment
[12:05:12]paymentRefs.lock ref=004417832acquired
[12:05:58]pabilo.verify amount=3708.60 is_new=truepaid
[12:06:00]inefable.recharge pkg=171 ext=RF-9K4BWD-1dispatching
[12:06:01]inefable.recharge pkg=171 timeoutretry
[12:06:02]inefable.recharge pkg=171 ext=RF-9K4BWD-1ok=true
[12:06:03]orders.update statuscompleted
estados de una orden ███████████ 11/11

# 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.

02 / Lo que hace

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.

03 / La interfaz

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.

tienda · portada
⚡ Jose R. · recargó 520 + 52 Diamantes · hace 3 min⚡ Andrea M. · 1.060 Gold · hace 7 min
Entrega automática 24/7

Recarga tus juegos
en segundos

Recargar ahora →Tasa del díaBs 145,20

Elige tu juego

⚡ InstantáneoFree Fire6 paquetes · desde Bs 285,00
⚡ InstantáneoBlood Strike5 paquetes · desde Bs 310,00

Portada — hero y selector de juegos

tienda · pago

Monto exacto a transferir

Bs 1.953,44

$13,45 · Tasa Bs 145,20

⏱ Tiempo para pagar: 27:14
Pago MóvilAl teléfonoTransferenciaA la cuenta

Datos del Pago Móvil

Toca cualquier dato para copiarlo

Banco0102 · Banco de Venezuela
CédulaV-27.845.109
Teléfono0414-8624450

Confirma tu pago

004417832
Ya pagué, verificar

Checkout — pantalla de pago

panel · resumen
7 días30 días90 días

Ingresos

$4.128

Utilidad

$1.204

Órdenes

317

Usuarios

182

Ingresos y utilidad

3 orden(es) pagadas con fallo de entrega
2 orden(es) esperando confirmación

Panel — resumen del administrador

Las maquetas reproducen la interfaz real; los datos que muestran son de ejemplo.

Icono SVG del diamante de Free Fire, la moneda que pinta cada tarjeta de paqueteDiamantes · Free FireIcono SVG del Gold de Blood StrikeGold · Blood StrikeIcono SVG de uno de los productos manuales (pase élite) del catálogo de categoría BPase élite
stats --repo

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

04 / Funcionalidades

Todo lo que ya está dentro

01Compra en una sola pantalla: juego, datos del jugador, paquete y barra fija de resumen
02Pago Móvil BDV y transferencia, ambos verificados por referencia
03Monto en bolívares y tasa congelados al crear la orden, con reloj de caducidad de 30 minutos
04Despacho automático de combos como secuencia de llamadas encadenadas
05Productos manuales entregados por WhatsApp con mensaje precargado
06Campos por juego configurables (ID de jugador, Zone ID, campos sensibles)
07Bandera validatesPlayerId por juego con confirmación obligatoria cuando el proveedor no valida el ID
08Cupones por porcentaje o monto fijo, con límite por usuario contado también por ID de jugador
09Cartera de saldo a favor con libro de movimientos y pago total o parcial con saldo
10Programa de referidos y códigos de creador con comisiones y pagos
11Ocho niveles de fidelidad con descuento por total gastado, editables desde el panel
12Cinta de actividad con las últimas recargas completadas y el nombre enmascarado
13Correos al cliente en tres momentos: pago verificado, recarga entregada y entrega fallida
14Avisos al equipo por tres canales independientes: bandeja del panel, Telegram y webhook
15Tasa Bs/USD manual o automática con refresco horario e historial
16Modo mantenimiento y interruptor de despacho automático
17Soporte con tickets y conversación, tanto del lado del cliente como del panel
18Bitácora de auditoría con autor, fecha e IP de cada acción sensible
19Exportación de órdenes a CSV
20Subida de imágenes de catálogo con reescalado a 256 px y conversión a WebP en el navegador
21PWA instalable con manifiesto, iconos maskable y tema oscuro
05 / Stack

Con qué está hecho

Frontend

React 18TypeScriptVite 6Tailwind CSS 3React Router 6TanStack Query 5Framer Motion 11Recharts 2lucide-reactreact-hot-toastclsxtailwind-merge

Backend

Cloud Functions v2Express 4TypeScriptfirebase-admin 13firebase-functions 6ZodNodemailercors

Firebase

FirestoreFirebase AuthenticationCloud StorageSecret ManagerCloud SchedulerFirebase Hosting

Integraciones

API Pabilo (verificación de Pago Móvil BDV)API Inefable (despacho de recargas)SMTP de GmailTelegram Bot APIWebhook saliente en JSON

Infraestructura y tooling

Netlify (auto-deploy desde main con proxy /api/*)Firebase CLI y emuladoresESLintsharp (generación de la marca)Node 20
06 / Bajo el capó

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.

Refill Store

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.

Free FireBlood StrikeMobile LegendsHonor of KingsMarvel RivalsPago Móvil BDVEntrega automáticaFree FireBlood StrikeMobile LegendsHonor of KingsMarvel RivalsPago Móvil BDVEntrega automática

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.