Manual del sistema: cómo funciona, guía técnica y de pruebas
Reescritura del e-commerce en Laravel 12
Generado el {{ now()->format('d/m/Y H:i') }}
Este proyecto es una reescritura completa, desde cero, del e-commerce que antes corría en PHP plano sin
framework (carpeta mi_tienda, servido por XAMPP). El sistema anterior tenía deuda técnica y de
seguridad real: sin llaves foráneas en la base de datos, credenciales de pago en texto plano, contraseñas con
salt compartido, y un carrito que vivía solo en el navegador. El proyecto nuevo (mi_tienda_v2)
corrige todo eso sobre una arquitectura moderna: Laravel 12, con panel de administración y tienda pública
completamente funcionales, pensado específicamente para operar un negocio en Perú (facturación electrónica
SUNAT, pasarelas de pago locales, WhatsApp) y para poder reutilizarse en clientes nuevos (carga masiva de
catálogo vía Excel y una herramienta para dejar la base de datos lista antes de poblarla).
Esta sección explica el funcionamiento completo en lenguaje simple, sin tecnicismos, siguiendo el recorrido real de un cliente y luego el de un administrador. Las secciones siguientes del manual retoman cada punto en detalle técnico.
| URL | http://127.0.0.1:8000 |
| Cliente de prueba | test@example.com / password |
| Registro | Cualquier visitante puede crear su propia cuenta desde "Crear cuenta" |
| URL | http://127.0.0.1:8000/admin/login |
| Super administrador | admin@mitienda.test / password |
El super administrador tiene acceso a todo. Existe también un rol editor con acceso restringido: Catálogo (categorías, productos, ofertas, cupones), Marketing (banners), Clientes y Reportes — sin acceso a Pedidos, Configuración de tienda, Perfiles de administrador, Reseñas, Libro de Reclamaciones ni a "Limpiar catálogo" (esta última reservada exclusivamente al super administrador por lo irreversible de la acción). Los roles se gestionan desde Perfiles en el panel.
La cuenta admin@mitienda.test tiene además dos permisos individuales (manual.view y
delivery.reset) que nunca se asignan a ningún rol y por lo tanto no los hereda ningún super
administrador nuevo que se cree después. Esos permisos son lo único que decide quién ve los enlaces "Manual
(PDF)" y "Preparar entrega" y puede usarlos. Por la misma razón, esta cuenta no aparece en el listado de
Perfiles ni puede editarse o eliminarse desde ahí, ni siquiera por otro super administrador —
se administra únicamente a sí misma desde Mi cuenta → Mi perfil, donde cualquier
administrador (de cualquier rol) puede cambiar su propio nombre, correo y contraseña confirmando la
contraseña actual.
Desde una terminal, dentro de la carpeta del proyecto:
cd C:\xampp\htdocs\mi_tienda_v2
php artisan serve — levanta la tienda en http://127.0.0.1:8000
php artisan queue:work — en otra terminal, obligatorio: sin esto, la
confirmación de pedido, el aviso de contra entrega, la emisión de comprobantes y notas de crédito SUNAT, el
aviso de envío, y el recordatorio de carrito abandonado se quedan encolados sin procesarse nunca (se guardan
en la tabla jobs, no se pierden, pero tampoco se envían hasta que corra este proceso).
npm run dev — (opcional, en otra terminal) recompila el CSS/JS automáticamente si vas a
editar vistas. Para dejar los assets listos de forma definitiva: npm run build.
| Componente | Tecnología |
|---|---|
| Framework | Laravel 12 (PHP 8.2) |
| Base de datos | MariaDB 10.4 (BD: mi_tienda_v2) |
| Frontend interactivo | Livewire 3 + Volt (componentes reactivos sin escribir una API aparte) |
| Estilos | Tailwind CSS, compilado con Vite; interactividad ligera puntual con Alpine.js |
| Autenticación | Laravel Breeze (clientes, en español) + guard separado admin a medida (administradores) |
| Roles y permisos | Spatie Laravel-Permission, granulares por módulo |
| Imágenes | Intervention Image (recorte manteniendo proporción, sin deformar) |
| Pagos - tarjeta/Yape (Culqi) | Culqi: cargo directo con tarjeta, Yape por teléfono+OTP, y Yape/Plin por QR (Órdenes de Culqi, confirmación asíncrona vía webhook/polling) |
| Pagos - Izipay | Pasarela independiente (motor Lyra/"Micuentaweb"), confirmación por firma HMAC-SHA256 |
| Pagos - internacional | PayPal vía srmklive/paypal (API Orders v2) |
| Pagos - contra entrega | Sin pasarela: pedido queda pendiente hasta que el repartidor cobra y un admin lo confirma |
| Facturación electrónica | Nubefact (OSE) — emite boleta/factura y nota de crédito (en reembolsos), y las declara ante SUNAT |
| Analítica | Google Analytics 4 + Meta Pixel (opcionales, se activan solo si se configuran): eventos de ver producto, iniciar checkout y compra (una sola vez por pedido) |
| Contacto | WhatsApp click-to-chat (botón flotante, pie de página, y enlace por producto) |
| Zona horaria | America/Lima (configurable vía APP_TIMEZONE) — todas las fechas se guardan en hora local de Perú |
| Carga masiva / Reportes | maatwebsite/excel (import de catálogo y export de reportes), CSV nativo, PDF vía barryvdh/laravel-dompdf |
| Verificación en dos pasos | pragmarx/google2fa + bacon/bacon-qr-code (TOTP, QR generado localmente, sin llamar a ningún servicio externo) |
| Backups automáticos | spatie/laravel-backup: dump de la base de datos + storage/app/public (no todo el código, ya vive en git), programado a diario vía routes/console.php, con limpieza y monitoreo de salud; avisa por correo solo si algo falla |
| Integración continua | GitHub Actions: corre Pest, análisis estático (Larastan) y formato de código (Pint) en cada push/PR a main |
| Testing | Pest (sobre PHPUnit) — 434 tests automatizados en verde a la fecha de este manual, más 9 pruebas E2E con Playwright (navegador real) |
| Calidad de código | Larastan/PHPStan (nivel 5, con baseline para deuda preexistente) y Laravel Pint, ambos como gate en el CI |
Controladores delgados que delegan la lógica de negocio a servicios reutilizables, en vez de mezclar todo en el controlador (como hacía el legacy):
PricingService — resuelve el precio efectivo de un producto respetando la prioridad
oferta de producto > subcategoría > categoría > precio base.CartService — agrega/actualiza/quita productos del carrito, valida stock, fusiona el
carrito de invitado al hacer login.ShippingCalculator / TaxCalculator — calculan envío e impuesto desde la
configuración de tienda.OrderService — crea el pedido reservando stock (30 min en pagos electrónicos, 3 días en
contra entrega), confirma el pago (descuenta stock definitivo, dispara la facturación electrónica),
confirma pagos asíncronos de Culqi QR/Izipay de forma idempotente (nunca confía en lo que diga un webhook
o el navegador sin re-verificar), procesa reembolsos totales (reversa el cargo real, repone stock,
dispara la nota de crédito), y libera la reserva si el pedido expira sin pagarse.NubefactService — arma y envía el comprobante electrónico (y la nota de crédito, en
reembolsos) a Nubefact; si falla, notifica por correo a los administradores con permiso sobre pedidos.CulqiService / IzipayService — encapsulan cada pasarela: cargos con
tarjeta/Yape, creación y consulta de Órdenes (Yape/Plin QR), generación de token de pago y verificación de
firma (Izipay), y reembolsos.ProductImportService / CategoryImportService — procesan la carga masiva
desde Excel, fila por fila, aislando errores por producto/categoría sin tumbar el archivo completo.CatalogResetService — borra productos/categorías/atributos/ofertas de forma controlada
para dejar la tienda lista antes de poblarla en un despliegue nuevo.ImageUploadService — redimensiona y guarda imágenes de forma consistente.StockAlertService — gestiona las suscripciones de "avísame cuando haya stock" por
variante, y dispara el correo (una sola vez) cuando el stock vuelve a estar disponible.WishlistNotifier — avisa a quien tiene un producto en su lista de deseos si su precio
efectivo bajó desde la última vez que se le avisó (nunca si sube).AdminActivityLogger — deja constancia en la bitácora de auditoría de las acciones
sensibles del panel (quién, cuándo, sobre qué).TwoFactorAuthService — genera el secreto TOTP y el QR de activación, verifica códigos,
y genera los códigos de recuperación de un solo uso.Las partes interactivas (carrito, selector de variantes, formulario de productos con variantes dinámicas, wishlist, checkout) usan Livewire o Alpine.js para pequeños estados de UI (deshabilitar un botón al enviar, confirmaciones de texto, polling del estado de un pago QR); el resto son vistas Blade tradicionales con controladores.
Los webhooks (avisos automáticos de Culqi e Izipay cuando se confirma un pago) son el único tipo de ruta que no pasa por la protección CSRF habitual de Laravel — por ser peticiones servidor-a-servidor que no pueden llevar el token de la aplicación. Para compensar, el código nunca confía ciegamente en lo que el webhook dice: para Culqi, siempre re-consulta el estado real de la orden con la propia API; para Izipay, valida la firma HMAC-SHA256 antes de procesar cualquier dato.
38 tablas (incluyendo autenticación, roles/permisos y cola de trabajos de Laravel), todas con llaves foráneas reales (el legacy no tenía ninguna). Las más relevantes:
| Tabla | Contenido |
|---|---|
categories, subcategories, products | Taxonomía del catálogo |
attributes, attribute_values, product_variants | Variantes de producto (Talla/Color) con stock real por combinación |
offers | Ofertas polimórficas: aplican a categoría, subcategoría o producto |
carts, cart_items | Carrito persistente en servidor (invitado o cliente) |
orders, order_items, payments, order_status_histories | Pedidos con líneas (con snapshot de precio/título), pagos (incluye intentos fallidos y reembolsos) y auditoría de cambios de estado; los pedidos también guardan courier y número/enlace de seguimiento del envío |
invoices | Comprobantes SUNAT por pedido: boleta, factura, o nota de crédito (en un reembolso) — un pedido puede tener como máximo un comprobante original y una nota de crédito |
coupons | Cupones de descuento (porcentaje o monto fijo, con tope de usos y vigencia) |
reviews, wishlists | Reseñas (solo compradores) y lista de deseos — cada fila de wishlists guarda además el último precio efectivo que se le avisó al cliente |
stock_alerts | Suscripciones de "avísame cuando haya stock" por variante y correo (invitado o cliente registrado) |
banners | Slides del carrusel de la página de inicio |
users / admins | Clientes y administradores en tablas y guards de autenticación separados; admins también guarda el secreto y los códigos de recuperación de la verificación en dos pasos, si el admin la activó |
roles, permissions | Roles granulares del panel (Spatie) |
admin_activity_logs | Bitácora de auditoría: quién hizo qué acción sensible del panel y cuándo |
store_settings | Impuesto, tarifas de envío, número de WhatsApp, textos de política de devoluciones/privacidad/términos, e IDs de Google Analytics 4/Meta Pixel (configuración única de la tienda) |
delivery.reset, igual de restringido que manual.view): borra todos los pedidos,
clientes, reclamos, reseñas, listas de deseos, carritos, alertas de restock y la bitácora anterior, para
entregar la instancia sin datos de prueba a un cliente nuevo. No toca catálogo, cupones, ofertas ni
configuración de tienda. Se elige qué administradores conservar (el resto se elimina); exige dejar al
menos un super administrador activo, y la propia acción queda como primera entrada de la bitácora ya
limpiamanual.view, nunca asignado por rol) no aparece en este listado ni puede editarse o
eliminarse desde aquíphp artisan activity-log:prune (programado una vez al
mes) archiva a CSV y borra de la base de datos lo más antiguo que
ACTIVITY_LOG_RETENTION_MONTHS (24 meses por defecto) — el CSV archivado queda en
storage/app/activity-log-archives/, incluido en el backup de archivos, así que nunca se
pierde el historialreviews.manage, no se sincroniza al rol editor) y registrado en
la bitácora de actividad/libro-de-reclamaciones, enlace en el pie de
página): conforme al Código de Protección y Defensa del Consumidor (D.S. N° 011-2011-PCM y su
modificatoria D.S. N° 101-2022-PCM), cualquier consumidor puede registrar un reclamo o una queja sin
necesidad de cuenta; recibe un código correlativo y una copia por correo. En el panel (reservado al super
administrador, permiso complaints.manage) se listan, filtran por estado/tipo, marcan el
plazo legal de 15 días hábiles (con aviso si está vencido), y se responden — la respuesta se envía por
correo al cliente y queda en la bitácora de actividad. La razón social, RUC y domicilio que aparecen en
el reclamo se configuran una vez en Configuración → Datos de la empresa. Al no exigir cuenta, el correo
de destino no se verifica — para que no se use como relay de spam/acoso hacia un tercero real, hay un
tope de 3 reclamos por día por correo destino (no por IP) más un campo trampa (honeypot) invisible en
el formularioDOCUMENT_LOOKUP_TOKEN configurado por defecto). Nunca sobrescribe un nombre ya escrito a
mano, y cualquier falla o falta de configuración degrada a que el cliente lo escriba él mismo, como
siempre — nunca bloquea el formulario. Para RUC, si el proveedor informa que la empresa no está
ACTIVO o está en condición NO HABIDO ante SUNAT, el checkout muestra un aviso
junto a la razón social antes de que el cliente intente facturar. Al ser un endpoint público, tiene un
tope diario global compartido entre todas las IPs (DOCUMENT_LOOKUP_DAILY_LIMIT, 500 por
defecto) para que nadie lo use como proxy gratuito hacia la cuota pagada del proveedor| Legacy | Ahora |
|---|---|
| Salt de contraseña compartido y hardcodeado | Bcrypt con salt único por usuario (Laravel Hash) |
| Recuperar contraseña la reenviaba en texto plano por correo | Link de reseteo firmado y con expiración |
Token de verificación de email = md5(email), sin expirar | Link de verificación firmado y expirable |
| Sin llaves foráneas en la base de datos | FKs reales en las 38 tablas |
| Credenciales de PayPal/PayU en la base de datos, en texto plano | Solo en .env, nunca en BD (igual para Culqi, Izipay y Nubefact) |
Roles como texto libre (if ($perfil == "administrador")) | Roles y permisos granulares (Spatie), incluyendo un permiso aparte para acciones irreversibles ("Limpiar catálogo") |
Carrito solo en localStorage | Carrito persistente en servidor, con validación de stock real |
| Precio/stock/cupón confiados al navegador en el checkout | Todo se recalcula server-side antes de crear el pedido |
| No existía el concepto de webhook | Los avisos de Culqi/Izipay nunca se procesan "a ciegas": siempre se re-verifican (re-consulta a la API o validación de firma HMAC) antes de tocar un pedido |
Además de la comparación con el legacy, el proyecto ya en marcha pasó por una auditoría propia, archivo por
archivo, agrupada por dimensión (autenticación, IDOR, inyección, pagos/webhooks, archivos, XSS, CSRF/carreras,
datos sensibles, panel admin), donde cada hallazgo se verificó de forma adversarial antes de aplicarlo, para
descartar falsos positivos. Esa auditoría corrigió 20 hallazgos; junto con una corrección aparte de la misma
naturaleza (formularios sin method="POST"), estos son los más relevantes:
| Severidad | Hallazgo | Corrección |
|---|---|---|
| Crítico | Un pedido caro propio podía marcarse como pagado reutilizando (replay) la confirmación firmada de un pedido barato propio en Izipay | Se compara el order_number y el monto de la respuesta firmada contra el pedido real de la ruta antes de aceptarla |
| Alto | Condición de carrera permitía ejecutar markAsPaid()/refund() dos veces sobre el mismo pedido (doble descuento de stock, doble comprobante/nota de crédito) | Se bloquea la fila del pedido y se re-valida su estado dentro de la misma transacción |
| Alto | XSS en el checkout: un valor dinámico se interpolaba sin escapar dentro de un atributo x-data de Alpine.js | Se usa @@js() para cualquier valor dentro de contexto JS de Alpine |
| Alto | Sin límite de intentos en /admin/login | Bloqueo de 5 intentos por correo+IP, igual que ya tenía el login de clientes |
| Alto | Desactivar un administrador no revocaba su sesión ni su cookie "recordarme" ya activa | Middleware que revisa el estado activo en cada petición, no solo al iniciar sesión |
| Alto | Ningún formulario Livewire declaraba method: si el JS de Livewire no llegaba a cargar, el navegador caía a un submit nativo por GET, exponiendo la contraseña en la URL | method="POST" explícito como respaldo en los 10 formularios Livewire |
| Medio | Enumeración de usuarios en "olvidé mi contraseña" (la respuesta variaba según si el correo existía) | Mismo mensaje de éxito exista o no la cuenta, salvo por límite de intentos |
| Medio | Sin límite de peticiones en webhooks/checkout/buscador; el webhook de Culqi logueaba el payload completo | Rate limiting agregado; el log ahora solo registra metadatos, no el cuerpo crudo |
| Medio | Condición de carrera en el tope de usos de un cupón | Incremento atómico condicionado al tope, en vez de leer-y-luego-escribir |
| Medio | El dashboard y el manual del panel admin no respetaban permisos granulares (el rol editor veía datos fuera de su alcance documentado) | Cada sección del dashboard y la ruta del manual ahora exigen el permiso correspondiente |
| Bajo | Se podía reasignar una variante de otro producto manipulando su id (IDOR) | La búsqueda de la variante se restringe siempre al producto actual |
| Bajo | Importación de Excel sin límite de tamaño de archivo | Límite de tamaño agregado a la validación de subida |
| Bajo | Cookie de sesión sin Secure forzado en producción | Secure activado automáticamente cuando APP_ENV=production |
Aparte de la auditoría, dos correcciones más de esta misma naturaleza: los enlaces de navegación y los
assets (CSS/JS/imágenes) usaban rutas absolutas armadas con el host interno, rompiéndose al compartir la
tienda por un túnel HTTPS (devtunnels/ngrok) — se corrige generando rutas relativas y configurando
trustProxies para reconocer el dominio público real vía cabeceras X-Forwarded-*.
Cada admin puede activar 2FA (TOTP) por su cuenta desde "Mi seguridad" — no es obligatoria para todos, cada
uno decide si la activa. El QR se genera de forma local (pragmarx/google2fa +
bacon/bacon-qr-code), sin llamar a ningún servicio externo que expondría el secreto por la red.
Al iniciar sesión con 2FA activado, la contraseña correcta no deja entrar directamente: el
admin queda en un estado intermedio (sin sesión de guard todavía) hasta que confirma un código de su app o
uno de sus códigos de recuperación de un solo uso, con el mismo límite de 5 intentos por admin+IP que ya
protege el login. Desactivarla exige volver a confirmar la contraseña actual.
La bitácora de actividad (admin_activity_logs) registra automáticamente quién hizo qué en las
acciones más sensibles del panel: reembolsos (con el motivo y si repuso stock), cambios de estado de pedido,
confirmaciones manuales de pago, alta/edición/baja de administradores (incluyendo cambios de rol y
activar/desactivar), acciones masivas sobre productos, y "Limpiar catálogo". Antes de esto, esas acciones no
dejaban ningún rastro de quién las había hecho.
Con el proyecto ya más avanzado (autoservicio de "Mi perfil", exportación de la bitácora, protección de la
cuenta del desarrollador), una segunda auditoría — 6 dimensiones en paralelo (autenticación, inyección,
pagos/webhooks, archivos, CSRF/configuración, datos sensibles) — confirmó que las correcciones de la
auditoría anterior seguían vigentes y buscó superficie nueva. Detalle completo, incluyendo lo evaluado y
descartado por no aplicar (un índice único en payments que habría roto un flujo legítimo de
Culqi QR), en docs/auditoria-seguridad.md. Hallazgos corregidos:
| Severidad | Hallazgo | Corrección |
|---|---|---|
| Alto | Inyección de fórmulas (CSV/Excel) en la exportación de la bitácora: cualquier admin podía sembrar el payload cambiando su propio nombre en "Mi perfil", sin permisos especiales | Se neutraliza cualquier celda que empiece con =+-@ antes de exportar |
| Alto | Host Header Injection: sin trustHosts(), un Host falso podía envenenar el enlace de "olvidé mi contraseña" | trustHosts() (Laravel ya lo desactiva solo en entorno local y en tests, sin afectar el desarrollo) |
| Alto | Sin límite de intentos sobre current_password en Mi perfil, 2FA, cambio de contraseña y borrado de cuenta: una sesión comprometida podía adivinarla por fuerza bruta y tomar la cuenta de forma permanente | Bloqueo de 5 intentos por cuenta, mismo patrón que el login |
| Medio-Alto | Sin límite de intentos en el registro de cuentas (creación masiva + spam de verificación de email hacia terceros) | Límite de 5 registros por minuto por IP |
| Medio-Alto | Sin límite de pedidos contra entrega pendientes por cliente (congelamiento de inventario / fraude de "no-show") | Máximo 2 pedidos contra entrega pendientes por cliente |
| Medio | Livewire no revalidaba el permiso ni el estado activo en las peticiones AJAX del formulario de productos: revocarle el permiso o desactivar a un admin a mitad de sesión no le impedía seguir guardando cambios | Chequeo explícito en cada acción que modifica datos, no solo al cargar la página |
| Medio | Sin headers de seguridad HTTP (X-Frame-Options, etc.) | Middleware global; el Content-Security-Policy queda pendiente de calibrar contra los widgets de Culqi/Izipay/GA4/Meta Pixel antes de activarlo |
| Medio | Pagos síncronos (Culqi tarjeta/Yape) sin timeout ni manejo de fallo de conexión: una llamada colgada terminaba en un error sin capturar, sin saber si el cargo sí se había ejecutado | Timeout explícito y mensaje cauteloso ("si se realizó un cargo, contáctanos") en vez de un error genérico |
| Medio | El backup podía correr sin cifrar si faltaba BACKUP_ARCHIVE_PASSWORD en producción | Se salta el backup en vez de correrlo inseguro; backup:monitor avisa por correo que falta uno reciente |
| Bajo | Un super-admin podía cambiar su propio rol/estado desde "Perfiles" | Bloqueado ahí; solo puede hacerlo desde "Mi perfil" |
Checklist completa de lanzamiento, paso a paso, en
docs/checklist-lanzamiento.md. Resumen de lo más importante:
PAYPAL_SANDBOX_CLIENT_ID,
CULQI_SECRET_KEY, IZIPAY_USERNAME/IZIPAY_PASSWORD y
NUBEFACT_TOKEN están vacíos (o ni existen) en .env. Sin esto, el pago con
PayPal/Culqi/Izipay y la emisión real de comprobantes no se pueden completar de punta a punta (se
puede probar todo el flujo hasta ese paso, o usar "Confirmar pago manual"/"Confirmar cobro en
efectivo" desde el panel para simularlo). El checklist detallado de lo que falta específicamente para
Yape/Plin por QR e Izipay está en docs/produccion-pagos-pendiente.md.php artisan queue:work en segundo plano de forma permanente (con Supervisor o
similar en el servidor real)..env.localhost). Para probar antes de desplegar, se puede usar un túnel
como ngrok.APP_DEBUG=false en el .env del servidor real — hoy está en
true para desarrollo, y expone detalles internos si algo falla.127.0.0.1:8000).TRUSTED_PROXIES: hoy vale * (confía en cualquier proxy),
razonable para desarrollo detrás de un túnel (devtunnels/ngrok) pero no para producción — ahí hay que
restringirlo a la IP real del balanceador/reverse proxy delante de la aplicación.BACKUP_ARCHIVE_PASSWORD: obligatoria en producción — sin ella,
backup:run se salta solo (no corre sin cifrar) y backup:monitor termina
avisando por correo que no hay un backup reciente.
DOCUMENT_LOOKUP_TOKEN: opcional, no bloqueante para lanzar. Autocompleta el
nombre/razón social del cliente a partir de su DNI/RUC contra un servicio externo (RENIEC/SUNAT no tienen
API pública gratuita para terceros). El código ya está construido y probado — sin este token, el sistema
funciona exactamente igual que antes (el cliente escribe su nombre a mano). Detalle completo, incluyendo
proveedores candidatos a contratar, en docs/diferenciadores-comercio-local.md.
Además de la suite de tests, el CI corre dos herramientas de calidad en cada push/PR a
main:
laravel-ide-helper (vía
@mixin, sin tocar la lógica real de los modelos) para poder resolver relaciones y columnas
de Eloquent. Los ~68 hallazgos preexistentes a la fecha de este setup —casi todos la misma limitación
conocida de PHPStan al perder el tipo genérico en relaciones de Eloquent encadenadas con
->with()/->get(), no bugs reales— quedaron congelados en
phpstan-baseline.neon: el código nuevo no puede sumar hallazgos nuevos sin arreglar de
golpe esa deuda preexistente no relacionada.
Aparte del CI, hay una suite E2E con Playwright (e2e/, se corre con
npm run test:e2e) que maneja un navegador real contra la app corriendo de verdad — a diferencia
de Pest, que solo prueba a nivel HTTP, esta sí ejecuta JS/Livewire/Alpine tal como lo haría un cliente real.
Cubre: catálogo y buscador, login de cliente (válido e inválido) a través del formulario Livewire real,
agregar al carrito con la actualización reactiva del contador del header (o el aviso de stock si el producto
está agotado), selección masiva de productos en el panel admin, y el flujo completo de 2FA —activar, cerrar
sesión, volver a entrar pasando por el desafío con un código TOTP real, y desactivarlo de nuevo—. No corre en
GitHub Actions (para no alentar cada push); usa la base de datos local y cuentas ya sembradas, con acciones
pensadas para no ensuciar el catálogo de desarrollo.
| ☐ | El home carga y muestra categorías y productos destacados |
| ☐ | Navegar categoría → subcategoría → producto |
| ☐ | El buscador devuelve resultados relevantes |
| ☐ | Registro de una cuenta nueva (en español) |
| ☐ | Iniciar y cerrar sesión |
| ☐ | Agregar un producto con variantes (elegir Talla/Color) al carrito |
| ☐ | Ver el carrito, cambiar cantidad, quitar un producto |
| ☐ | Aplicar un cupón de descuento válido en el checkout |
| ☐ | Completar el checkout con tarjeta/Yape (Culqi), Izipay o PayPal: totales correctos (subtotal + envío + impuesto) |
| ☐ | Elegir Yape/Plin (QR) en un pedido dentro de S/ 6–500, y confirmar que la opción se oculta fuera de ese rango |
| ☐ | Completar el checkout con pago contra entrega en un carrito ≤ S/ 500, y confirmar que no aparece esa opción si el carrito supera S/ 500 |
| ☐ | Confirmar que el pedido queda "Pendiente" y reserva el stock (no lo descuenta aún) |
| ☐ | Ver el pedido en "Mi cuenta → Mis pedidos" y descargar el comprobante una vez pagado |
| ☐ | Una vez que el admin registra courier/tracking, verlo en el detalle del pedido |
| ☐ | Dejar una reseña en un producto ya comprado |
| ☐ | Agregar y quitar un producto de la lista de deseos |
| ☐ | Agregar a la lista de deseos un producto agotado y confirmar que queda suscrito al aviso de restock; quitarlo y confirmar que se cancela |
| ☐ | En un producto agotado, dejar el correo en "avísame cuando haya stock" y confirmar que se registra la suscripción |
| ☐ | Ver "Productos relacionados" al final de una ficha de producto |
| ☐ | Ver el botón de WhatsApp flotante, el enlace del pie de página, y el enlace específico en una ficha de producto |
| ☐ | Abrir las páginas de política de devoluciones, privacidad y términos desde el pie de página |
| ☐ | Iniciar sesión como super administrador |
| ☐ | El dashboard muestra las métricas correctamente |
| ☐ | Crear, editar y eliminar una categoría (y verificar que bloquea el borrado si tiene subcategorías/productos) |
| ☐ | Crear un producto con variantes (Talla/Color) e imágenes |
| ☐ | Descargar la plantilla de importación de productos, llenarla (un producto simple y uno con variantes) y subirla; confirmar que se crean correctamente y que una fila con error no bloquea el resto |
| ☐ | Descargar la plantilla de importación de categorías/subcategorías y subirla; confirmar que no duplica una categoría que ya existía |
| ☐ | Crear una oferta y verificar que el precio tachado aparece en la tienda pública |
| ☐ | Crear un cupón y aplicarlo en un pedido de prueba |
| ☐ | Ver el listado de pedidos, filtrar por estado y por "solo con comprobante fallido" |
| ☐ | Confirmar manualmente el pago de un pedido pendiente, y "Confirmar cobro en efectivo" en uno contra entrega |
| ☐ | Registrar courier y número de seguimiento en un pedido pagado |
| ☐ | Reembolsar un pedido pagado: confirmar que repone stock (si corresponde), notifica al cliente, y genera la nota de crédito |
| ☐ | Emitir/reintentar el comprobante SUNAT de un pedido pagado y descargar el PDF/XML |
| ☐ | Cancelar un pedido pendiente y confirmar que libera el stock |
| ☐ | Cambiar la configuración de impuesto/envío/WhatsApp/políticas/analítica de la tienda |
| ☐ | Crear un administrador con rol "editor" y confirmar que NO accede a Pedidos/Configuración/Perfiles/Reseñas/Libro de Reclamaciones/"Limpiar catálogo" (debe dar 403) pero SÍ a Categorías/Productos/Ofertas/Cupones/Banners/Clientes/Reportes |
| ☐ | (Solo super-admin) Desde "Reseñas", aprobar una reseña oculta, ocultar una aprobada, y eliminar una — confirmar que cada acción queda en la bitácora de actividad |
| ☐ | Desde /libro-de-reclamaciones (sin iniciar sesión), registrar un reclamo y confirmar que llega el código y la copia al correo; desde el panel, responderlo y confirmar que el cliente recibe la respuesta y que queda en la bitácora de actividad |
| ☐ | Sin DOCUMENT_LOOKUP_TOKEN configurado (el estado por defecto), escribir un DNI/RUC en el checkout o en el Libro de Reclamaciones y confirmar que no pasa nada raro — el nombre se sigue escribiendo a mano, sin errores |
| ☐ | En el checkout, elegir Departamento/Provincia/Distrito y confirmar que el envío/total se recalculan solos; con una tarifa de zona configurada en Configuración → Envío por zona, confirmar que el pedido se crea con esa tarifa (no la nacional plana) |
| ☐ | Generar el reporte de ventas y el de crecimiento diario, y exportar ambos en CSV, Excel y PDF |
| ☐ | Revisar el reporte de productos más vendidos y de stock bajo |
| ☐ | Revisar el reporte de pedidos pendientes por antigüedad y el de carritos abandonados |
| ☐ | Seleccionar varios productos con las casillas y activar/desactivar/eliminar en masa |
| ☐ | Activar 2FA desde "Mi seguridad", cerrar sesión, y volver a entrar confirmando el código de la app; probar también un código de recuperación |
| ☐ | Revisar la "Bitácora de actividad" después de un reembolso o de editar un administrador, confirmar que quedó registrado, y probar la exportación a CSV/Excel |
| ☐ | Correr php artisan activity-log:prune a mano una vez y confirmar que no borra nada reciente (solo lo más antiguo que ACTIVITY_LOG_RETENTION_MONTHS) y que deja el CSV en storage/app/activity-log-archives/ |
| ☐ | Desde "Mi cuenta → Mi perfil", cambiar el propio nombre/correo/contraseña confirmando la contraseña actual |
| ☐ | Crear un segundo super administrador y confirmar que NO ve la cuenta del desarrollador en "Perfiles" ni puede editarla/eliminarla (ni por URL directa) |
| ☐ | (Solo super-admin) Entrar a "Limpiar catálogo", confirmar que el botón sigue deshabilitado hasta escribir la frase exacta, y que tras confirmarlo el catálogo queda vacío sin afectar admins/pedidos/configuración |
| ☐ | (Solo la cuenta del desarrollador) Entrar a "Preparar entrega", confirmar que un administrador sin el permiso delivery.reset recibe 403, que no deja confirmar sin elegir al menos un administrador activo con rol "super-admin" para conservar, y que tras confirmarlo se borran pedidos/clientes/reclamos/reseñas/bitácora sin afectar catálogo/cupones/configuración |
| ☐ | Escribir mal la contraseña actual 6 veces seguidas en "Mi perfil" y confirmar que la 6ª ya no deja intentar, ni con la contraseña correcta |
| ☐ | Crear 3 pedidos contra entrega seguidos con la misma cuenta y confirmar que el 3ro se rechaza (tope de 2 pendientes) |
| Qué | Dónde |
|---|---|
| Modelos | app/Models/ |
| Lógica de negocio (servicios) | app/Services/ (pasarelas de pago en app/Services/Payments/) |
| Carga masiva desde Excel | app/Imports/ (lectura) + app/Exports/ (plantillas y reportes) |
| Controladores de tienda pública | app/Http/Controllers/Catalog/, CheckoutController.php, Checkout/, ComplaintController.php (Libro de Reclamaciones), DocumentLookupController.php (autocompletado y validez de DNI/RUC), UbigeoController.php (combos en cascada de dirección) |
| Webhooks de pasarelas | app/Http/Controllers/Webhooks/ |
| Controladores del panel admin | app/Http/Controllers/Admin/ |
| Componentes interactivos (Livewire) | app/Livewire/ |
| Notificaciones por correo | app/Notifications/ (confirmación de pedido, contra entrega, reembolso, envío, falla de comprobante, carrito abandonado, restock, bajada de precio, reclamo/queja recibido y respondido) — plantilla con marca propia en resources/views/vendor/mail/ |
| Jobs en cola | app/Jobs/ (emisión de comprobante/nota de crédito, liberación de reserva, recordatorio de carrito, aviso de restock) |
| Comandos artisan propios | app/Console/Commands/ (activity-log:prune, programado en routes/console.php junto con los backups) |
| Vistas de la tienda | resources/views/catalog/, checkout/, account/, policies/ |
| Vistas del panel admin | resources/views/admin/ (bitácora en admin/activity-log/, seguridad/2FA en admin/security/) |
| Traducciones al español | lang/es/ y lang/es.json |
| Rutas | routes/web.php (tienda, incluye webhooks), routes/admin.php (panel) |
| Migraciones (esquema de BD) | database/migrations/ |
| Tests automatizados | tests/Feature/ (434 tests Pest en verde a la fecha de este manual) + e2e/ (9 pruebas Playwright) |
| Integración continua | .github/workflows/tests.yml (Pest, Larastan y Pint) |
| Análisis estático | phpstan.neon (config) + phpstan-baseline.neon (deuda congelada) + _ide_helper_models.php (docblocks de modelos) |
| Checklist de lanzamiento a producción | docs/checklist-lanzamiento.md (credenciales, pasarelas, dominio/HTTPS, correo, backups...) |
| Checklist de credenciales pendientes (Culqi QR / Izipay) | docs/produccion-pagos-pendiente.md |
| Diferenciadores locales para la línea de tesis (Libro de Reclamaciones, autocompletado DNI/RUC) | docs/diferenciadores-comercio-local.md |
| Análisis del sistema legacy | mi_tienda/_analisis-legacy/ (esquema de BD y reglas de negocio del sistema anterior) |
Manual generado automáticamente desde el panel de administración de Mi Tienda.