QA · Cabizz · Auditoría de código

¿Están estos flujos contemplados en el código?

Cobertura de los dos planes pendientes de ejecutar — Flujo Operativo end-to-end y Escenarios de Excepción— verificada leyendo el código real de las 3 apps de cliente, la app de conductor, los 3 paneles admin y el backend app-api-v2 (la API en producción).

Backend auditado: app-api-v2 (Slim 4 / PHP 8.2) · 14-jul-2026 · evidencia con archivo:línea

106
Casos totales
67
Contemplado
31
Parcial
8
Ausente

El camino feliz está prácticamente todo implementado. Los 67 casos del Flujo Operativo se pueden ejecutar: home→carrito→checkout Stripe→panel admin→máquina de estados→despacho a conductores→pickup→entrega→notificaciones existen en código, con evidencia. Fallarán al ejecutar solo detalles puntuales (botón "Calificar", contador del dashboard del conductor, historial de estados en admin).

Los escenarios de excepción son el terreno flojo. De 39, hay 13 sólidos, 20 parciales y 6 completamente ausentes. El hueco es sistemático y está concentrado en dinero (no hay reembolsos de Stripe en ningún lado) y en fallos del conductor en ruta (sin timeout de pickup ni reasignación). Esto coincide con la propia autoevaluación de la v2: Fase 1 y 2 aplicadas, Fase 3 (dinero/despacho avanzado) y 4 pendientes.

Ojo con el vocabulario de estados. El plan de pruebas original usaba nombres en inglés que no coinciden con tu DB. El pipeline real (tal como aparece en la app) es Pendiente(1) → En proceso(2) → En camino al punto(3) → En ruta(4) → Confirma entrega(8) → Completado(5), con Cancelado(6) / Rechazado(7). Punto clave: no hay un estado "Confirmado"/"Preparando" separado — aceptar el pedido y prepararlo caen ambos en En proceso(2). Los archivos de prueba ya fueron reetiquetados a estos nombres.

Contemplado — existe en código, ejecutable Parcial — hay pieza base pero falta lógica/regla Ausente — no implementado

1 · Flujo Operativo 67 casos · Fork Discount · Bye Bye Junk · Cheers Go + conductor

Las 17 fases del camino feliz (hoja DESEMPEÑO). Las 3 apps de cliente comparten plantilla y los 3 admin también; se anota la divergencia relevante.

FaseActorEstadoEvidencia / matiz
1 · Cliente realiza pedido (home, carrito, checkout, Stripe → Pendiente)ClienteContempladoHome/carrito/checkout/Stripe en las 3 apps. Matiz: el pedido se crea antes del pago (createorder); en web Stripe es redirect en la misma pestaña, no nueva.
2 · Pedido aparece en panel Admin (tiempo real Pusher)SistemaContempladoEvento order-created + useOrdersRealtime (Pusher + poll 60s de respaldo) en los 3 admin.
3 · Admin acepta (Pendiente → En proceso)AdminContempladoSin botón "Accept" dedicado: cambio vía q-select de estado. No existe "Confirmado"/"Preparando" aparte → aceptar y preparar son el mismo estado En proceso(2).
3b · Admin rechaza pedidoAdminContempladoPOST /orders/{id}/reject con motivo obligatorio. Sin reembolso Stripe (ver excepciones).
4 · Notificación al cliente (confirmado)SistemaContempladoorder-status Pusher+FCM (estados 2–5, 8). Matiz: el handler FCM in-app solo loguea; la UI se actualiza por Pusher/poll, no por el tap del push.
5 · Admin marca preparación (sigue En proceso)AdminContempladoMáquina de estados real con guardas de transición. "Start Collection" de byebye viene de la API, no del front. Nota: no cambia de estado (ya está en En proceso(2)).
6 · Admin marca listo (→ En camino al punto, 3)AdminContempladoLa transición a En camino al punto(3) habilita el despacho a conductores.
7 · Conductor ACTIVO recibe order-offeredDriverContempladoModal AppEvents.vue + OrderDispatchService. Matiz: la oferta la emite el cron cli/events.php, no la transición; si el cron no corre, no hay oferta.
7b · Conductor INACTIVO NO recibeDriverContempladoFiltro en backend: vista orders_pending exige active=true AND approved=true. El front NO filtra — depende 100% del backend.
8 · Driver ACEPTA → En ruta (4), aparece en pestaña "En Proceso"DriverContempladoPOST /tracker/offerresponse + asignación atómica. Pestañas En Proceso/Completados en OrderSummary.vue.
8b · Cliente notificado (driver asignado)SistemaContempladoorder-status al pasar a status 4.
9 · Driver RECHAZA → vuelve al poolDriverParcialEl rechazo existe (Offerresponse accepted:false) pero la re-oferta a otro conductor no es activa: depende de que el cron vuelva a encontrar candidatos.
10 · Driver recoge (pickup, status 3 → 4)DriverContempladoBotón "Picked up" + navegación Google Maps a pickup y cliente.
11 · Driver marca entrega (→ Confirma entrega 8 / Completado 5)DriverParcialEl front envía Confirma entrega(8) (espera confirmación del cliente), no Completado(5) directo. La pestaña "Completados" cuenta 5 y 8. El contador del Dashboard del conductor está hardcodeado a 0 (Dashboard.vue es un stub).
11b · Cliente notificado (entregado) + botón CalificarSistemaParcialNotificación OK. El botón "Calificar" no existe: es stub (console.log) en byebye/cheers e inexistente en fork.
12 · Admin ve estado final Completado en panelAdminParcialBadge de estado con color OK. Sin historial/timeline de transiciones ni timestamp de entrega (solo createdAt).
13 · Cancelación por Admin en cualquier estadoAdminParcialCambio a Cancelado vía select. Cancelar por select no pide motivo (solo el flujo "reject") y no reembolsa.
12.1 · Geolocalización del conductor (GPS periódico)DriverContempladoPOST /tracker/Geolocation + BackgroundGeolocation nativo, filtro 30m, permisos Info.plist.
14.1 · Flujo completo en Web (PWA)ClienteContempladoFunciona. Matiz: en web Stripe usa window.location.assign (misma pestaña), no window.open.

2 · Escenarios de Excepción 39 escenarios · 13 sólidos / 20 parciales / 6 ausentes

El complemento al camino feliz. Aquí es donde el código tiene huecos reales — concentrados en dinero (reembolsos) y fallos del conductor en ruta.

1 · Rechazos y cancelaciones — 6

IDEscenarioEstadoEvidencia / qué falta
E1.1Negocio rechaza en Pendiente + reembolsoParcialReject con motivo OK; reembolso Stripe AUSENTE. El cliente queda cobrado.
E1.2Rechaza/cancela tras confirmar, con motivoParcialTransición controlada OK; reverso de cobro AUSENTE.
E1.3Cliente cancela antes de asignar driverParcialEndpoint /myorders/{id}/cancel + flag can_cancel OK; sin reembolso si estaba pagado.
E1.4Cliente cancela con conductor en caminoParcialDepende de que el backend ponga can_cancel=false; política de cancelación tardía AUSENTE.
E1.5Admin cancela con conductor en tránsitoParcialNotifica driver+cliente y lo desasigna (order_unassigned); sin reembolso.
E1.6Doble cancelación (idempotente)ContempladoEstados terminales no re-actualizan; fork filtra transiciones. Backend idempotente.

2 · Asignación y concurrencia — 6

IDEscenarioEstadoEvidencia / qué falta
E2.1Dos conductores aceptan el mismo pedido (race)ContempladoacceptOfferAtomically: SELECT … FOR UPDATE + WHERE driver IS NULL. Asignación atómica a uno solo.
E2.2Dos conductores en misma ubicaciónParcialVista con distancia existe; criterio de desempate no documentado ni probado.
E2.3Ningún conductor activo → pedido en colaParcialSeñal awaiting_driver_count + banner admin; sin escalamiento ni alerta por tiempo.
E2.4Todos rechazan (pool agotado)ParcialRechazo OK; sin re-oferta activa ni estado final cuando el pool se agota.
E2.5Aceptar oferta ya tomada / expiradaContempladoValidación hasPendingOffer → 404 + toast "ya tomado"; expiración cron a 60s.
E2.6Oferta con app en background (push abre modal)ParcialFCM en order-offered se envía; pero el front NO abre el modal desde el push (handler solo loguea, sin deep-link).

3 · Fallos del conductor en ruta — 7 · el bloque más débil

IDEscenarioEstadoEvidencia / qué falta
E3.1Conductor desaparece buscando el pedidoAusenteNo hay timeout de búsqueda ni reasignación (auto/manual).
E3.2Abandona con el pedido ya recogidoAusenteSe guarda última posición; sin workflow de escalamiento / re-entrega.
E3.3Pierde señal GPS en tránsitoParcialLa app no crashea y retoma al recuperar; falta "ubicación no disponible" explícito vs mapa congelado.
E3.4Cierra la app a mitad de la entregaContempladoAl reabrir, /order_summary restaura el pedido activo (estado server-side).
E3.5Se queda sin batería / se apagaParcialdriver_status conserva última posición; sin acción del admin ante dispositivo apagado.
E3.6Acepta pero nunca recoge (timeout pickup)AusenteNo existe deadline ni alerta de pickup.
E3.7Reasignación tras abandono del conductorAusenteNo hay UI ni endpoint de reasignar a otro conductor.

4 · Pagos y Stripe — 5

IDEscenarioEstadoEvidencia / qué falta
E4.1Pago rechazado → no crea pedidoParcialEl pedido se crea antes del pago; un decline lo deja vivo hasta el cleanup a 24h. Sin estado awaiting_payment.
E4.2Callback /success no llega, pedido igual se registraContempladoWebhook checkout.session.completed con firma HMAC; independiente del redirect.
E4.3Doble submit del checkout (doble cobro)ParcialBloqueo de doble-click en UI OK; idempotencia server-side AUSENTE (sin Idempotency-Key).
E4.4Reembolso tras cancelación / rechazoAusenteCero referencias a refund en el backend. Cancelar/rechazar nunca devuelve la plata.
E4.5Cierra la app con pago "en proceso"ParcialCleanup de no-pagados a 24h; sin estado propio de "en proceso".

5 · Conectividad y tiempo real — 5

IDEscenarioEstadoEvidencia / qué falta
E5.1Caída de Pusher/SoketiContempladoBanner "desconectado" + poll 60s + refresh manual en admin/user/driver.
E5.2Push FCM no llega → estado se actualiza al abrirContempladoonMounted → Start() recarga desde API + poll. Falta refresh de token expirado.
E5.3Cliente pierde internet en el seguimientoParcialNo rompe; sin banner explícito "sin conexión" ni navigator.onLine.
E5.4Reconexión: recuperar eventos perdidosContempladovalidatePendingOffer + /tracker/offerstatus descarta ofertas viejas al reconectar.
E5.5Evento duplicado (llega dos veces)ParcialDedupe en cliente (pusherDedup.js) OK; webhook Stripe procesa aunque el evento esté duplicado (sin guard).

6 · Estados y datos inconsistentes — 6

IDEscenarioEstadoEvidencia / qué falta
E6.1Cambio de estado fuera de orden (Pendiente→En camino al punto)ContempladoOrderStatusPolicy::assertTransition → 422. El front de fork solo ofrece la transición válida (byebye/cheers dependen del backend).
E6.2Pedido huérfano En camino al punto sin conductorParcialConteo awaiting_driver; sin escalamiento por antigüedad.
E6.3Marcado ENTREGADO pero cliente no recibió (disputa)ParcialStatus 8 "confirmación del cliente" + Confirm/Dispute en fork-user + tabla order_delivery_disputes; falta canal completo con evidencia.
E6.4Geolocalización con permisos denegadosContempladoPermisos nativos + mensajes i18n; degrada sin crashear.
E6.5Sesión expira a mitad del flujoParcialRefresh de token transparente OK; device_id obligatorio / 409 AUSENTE (es opcional en el backend).
E6.6Múltiples pedidos simultáneos del mismo clienteContempladoPedidos por order_id independientes; sin estado global compartido.

7 · Casos límite de negocio — 4

IDEscenarioEstadoEvidencia / qué falta
E7.1Producto agotado tras el pagoParcialChequeo de stock en checkout (no vende agotado); sin sustitución/reembolso tras pago. (El stock además no se decrementa — bug conocido.)
E7.2Dirección fuera de zona de coberturaContempladoassertDeliveryWithinUserDistanceLimit + validación en el front antes de cobrar.
E7.3Negocio cierra con un pedido activoAusenteSin política al desactivar el negocio con pedidos en curso.
E7.4Pedido de monto cero / cupón 100%ContempladoTotal $0 crea payment.received status=2 sin pasar por Stripe.

3 · Lo que fallará al ejecutar los planes

Priorizado. Todo lo demás debería pasar (con los matices de nomenclatura de estados y dependencia del cron).

Ausentes — no hay código, el caso fallará seguro

  • Reembolsos de Stripe (E1.1, E4.4, E7.1) — cero refund en el backend. Todo caso que espere "se devuelve la plata" falla. Es el hueco de dinero más grande.
  • Timeout de pickup y reasignación de conductor (E3.1, E3.2, E3.6, E3.7) — el bloque completo de fallos en ruta. No hay job de timeout ni forma de reasignar.
  • Negocio cierra con pedido activo (E7.3) — sin política.

Detalles del camino feliz que fallan aunque el flujo funcione

  • Botón "Calificar" tras entrega (caso 11b) — es un console.log, no hay flujo de rating.
  • Contador de completados del Dashboard del conductor (caso 11) — Dashboard.vue está hardcodeado a 0.
  • Historial/timeline de estados en el panel admin (caso 12) — solo hay fecha de creación.
  • Push que abre el modal / navega (casos 4, 7.3, E2.6) — los handlers FCM solo loguean; la UI se actualiza por Pusher, no por el tap del push. Verificar en dispositivo real.

Antes de ejecutar — dos condiciones de entorno

  • El cron de despacho tiene que estar corriendo. La oferta a conductores (order-offered) la emite php app-api-v2/cli/events.php, no la transición de estado. Sin cron activo, ningún conductor recibe pedidos y toda la fase 7–11 queda bloqueada.
  • Etiquetas de estado = las de tu DB. Pipeline: Pendiente(1) → En proceso(2) → En camino al punto(3) → En ruta(4) → Confirma entrega(8) → Completado(5). Aceptar y preparar caen ambos en En proceso(2) (no hay un estado "Preparando" aparte). Los archivos de prueba ya usan estos nombres.