Skip to content

Informe de auditoría de bugs — Abaco ​

Fecha: 2026-05-22
Alcance: Backend (API pública + ERP), Frontend (módulo Sitios), Storefront (tienda)
Método: Revisión estática de código y rutas (sin QA manual en navegador en esta sesión).

Actualización: Los fixes P0/P1/P2 listados abajo fueron implementados en código (ver commit en rama actual).


1. Regresión del sprint anterior (baseline) ​

MitigaciónEstado en códigoEvidencia
Stock en carrito/checkoutPresenteINSUFFICIENT_STOCK en publicSiteController.js
getOrderStatus protegidoPresentecanViewOrderStatus + JWT o viewToken
Link de pago storefrontPresentegenerateStorefrontPaymentLink en paymentLinkService.js
Captcha obligatorio en prodPresentecaptcha.js L6–8; validateCriticalEnv.js
Webhook rawPresenteserver.js /webhooks/storefront con express.raw
Preview cookiePresenteStorefront/middleware.ts abaco_preview_token
Switcher móvilPresenteheader.tsx block lg:hidden + CompanySwitcher compact
Toasts carritoPresenteCartLines.tsx / MiniCartDrawer.tsx validan null
409 creación sitioPresentesitios/page.tsx SiteHero + existingSiteId

2. Resumen ejecutivo ​

SeveridadConfirmadosParcialesDescartados / mitigados
P0300
P11021
P2912

Prioridad inmediata: B1 (tipo documento ERP), B2 (carrera de stock), S1 (carrito borrado antes del pago).


3. Backend — API y pagos ​

B1 — Tipo de documento por defecto incorrecto al pagar (P0) — CONFIRMADO ​

CampoDetalle
ArchivosstorefrontCheckoutService.js L136; documentInventoryService.js L51–58
ActualSITES_DEFAULT_DOC_TYPE por defecto '07'; comentario dice “cotización” pero 07 = nota de débito con reduce stock; 100 = cotización sin movimiento (type: 'none')
EsperadoPedido web pagado → cotización (100) o factura (01) según negocio, no ND que reduce inventario por error
ReproCompletar pago vía webhook; revisar Document.type e inventario en ERP
Fix propuestoCambiar default a '100' o SITES_DEFAULT_DOC_TYPE=100 en .env.example; alinear comentario y validar con contabilidad

B2 — Carrera de stock sin reserva (P0) — CONFIRMADO ​

CampoDetalle
ArchivospublicSiteController.js checkout; storefrontCheckoutService.js
ActualStock validado en transacción de checkout; entre commit y pago no hay reserva; segundo comprador puede pasar checkout con la misma unidad
ReproProducto con stock=1; dos sesiones checkout casi simultáneo; ambos pending_payment
Fix propuestoReserva temporal al pending_payment o revalidación atómica en confirmPayment antes de marcar paid

B3 — Webhook paid sin exigir pending_payment (P0) — CONFIRMADO ​

CampoDetalle
ArchivosstorefrontPaymentWebhookController.js L37–42
ActualAcepta siteOrderId numérico (secuencial) o cartToken; no valida estado previo
RiesgoCon HMAC filtrado: marcar pedidos en cart como pagados; enumeración de IDs
Fix propuestoRechazar si order.status !== 'pending_payment'; preferir lookup solo por cartToken + siteId

B4 — Documentos ERP duplicados en webhooks concurrentes (P1) — CONFIRMADO ​

CampoDetalle
ArchivosstorefrontCheckoutService.js confirmPayment L125–131
ActualSin transacción ni SELECT FOR UPDATE; dos webhooks paralelos pueden crear dos Document
Fix propuestoTransacción con lock en SiteOrder; idempotencia por paymentReference

B5 — Secreto webhook = bypass CSRF global (P1) — CONFIRMADO ​

CampoDetalle
ArchivoscsrfMiddleware.js L4–11
Actualx-abaco-storefront-secret acepta STOREFRONT_REVALIDATE_SECRET y STOREFRONT_WEBHOOK_SECRET
Fix propuestoSeparar secretos: solo REVALIDATE para CSRF; webhook solo en ruta /webhooks/*

B6 — Bundles sin validación de stock de componentes (P1) — CONFIRMADO ​

CampoDetalle
ArchivospublicSiteController.js (sin referencias a bundle); ERP valida en documentController con parentLineId
ActualCarrito solo valida is_inventoriable del producto padre; líneas planas en confirmPayment
Fix propuestoExpandir bundle en checkout o rechazar bundles inventariables en storefront hasta soportarlos

B7 — Orden por price en lugar de webPrice (P1) — CONFIRMADO ​

CampoDetalle
ArchivospublicSiteController.js listProducts L211–212
Fix propuestoOrdenar por expresión SQL COALESCE(webPrice, price) o post-procesar con webPriceOf

B8 — Rate limit OTP en memoria (P2) — CONFIRMADO ​

CampoDetalle
ArchivosotpService.js rateBuckets = new Map()
Fix propuestoRedis o rate limit en proxy

B9 — JWT comprador sin revocación en logout (P2) — CONFIRMADO ​

CampoDetalle
ArchivosstorefrontAuthController.js logoutStorefront; storefrontCustomerToken.js
Fix propuestotokenVersion en BD o denylist

B10 — Carrito sin FOR UPDATE en add/update (P2) — CONFIRMADO ​

CampoDetalle
ArchivosaddCartItem / updateCartItem vs checkout con LOCK.UPDATE
Fix propuestoLock en SiteOrder en mutaciones de carrito

B11 — getCustomerTokenAction exportado (P1) — CONFIRMADO (bajo uso) ​

CampoDetalle
ArchivosStorefront/app/actions/auth-actions.ts L78–80
ActualExportado; no importado en otros .tsx del repo (solo definición)
Fix propuestoEliminar export o restringir a uso interno documentado

B12 — confirmPayment sin paid_pending_erp (P2) — CONFIRMADO ​

CampoDetalle
ActualSi falla createDocumentFromPayload, pedido queda paid sin documentId; solo logger.warn
Fix propuestoEstado intermedio + job de reintento

B13 — /internal/domains/check sin token (P2) — CONFIRMADO (parcial) ​

CampoDetalle
ArchivosinternalSiteController.js checkDomain — sin auth; reportCertIssued sí usa CADDY_INTERNAL_TOKEN
NotaPuede ser intencional para Caddy on_demand_tls; riesgo = enumeración de dominios registrados
Fix propuestoToken compartido Caddy↔API o restricción de red interna

B14 — Rate limit OTP incluye purpose en clave (P2) — CONFIRMADO ​

CampoDetalle
ArchivosotpService.js L64 bucketKey(..., purpose)
Fix propuestoFijar purpose en rutas públicas o excluir de clave si no aplica

B15 — Migración OTP: índices en catch vacío (P2) — CONFIRMADO ​

CampoDetalle
Archivos20260522140000-create-otp-challenge-and-storefront-customer.js L48–55
Fix propuestologger.warn en catch; script de reparación de índices

4. Frontend — ERP / Sitios ​

F1 — Sidebar Sitios visible sin permiso (P1) — CONFIRMADO ​

CampoDetalle
Archivossidebar.tsx L305 permission: null; rutas /sitios con requiredRoles={['owner','admin']}
Fix propuestopermission: 'owner' o filtrar por rol como en header.tsx

F2 — Cambio de empresa desmonta editor (P1) — CONFIRMADO ​

CampoDetalle
Archivossitios/page.tsx L111–112 fetchSiteForCompany() con showLoading default true
Fix propuestofetchSiteForCompany({ showLoading: false }) en CompanyChangeEvent o guardar borradores en sessionStorage

F3 — pageId de URL tras cambio de empresa (P1) — PARCIALMENTE MITIGADO ​

CampoDetalle
Archivospaginas/[pageId]/page.tsx resetea site/page y load()
ActualSi la página no existe en la nueva empresa → error y redirect /sitios (OK); breve flash o petición con ID ajeno
Fix propuestorouter.replace('/sitios') inmediato al CompanyChangeEvent antes de load()

F4 — SiteProductsPanel sin CompanyChangeEvent (P2) — CONFIRMADO ​

CampoDetalle
ArchivosSiteProductsPanel.tsx; solo useEffect([page])
Mitigación actualPadre muestra spinner al cambiar empresa
Fix propuestoSuscribir CompanyChangeEvent y recargar lista

F5 — Switcher no refetch si ya hay empresas (P2) — CONFIRMADO ​

CampoDetalle
Archivoscompany-switcher.tsx L164–168
Fix propuestoRefetch ligero en subscribe o invalidar cache al emit desde settings

F6 — Sidebar no activo en subrutas (P2) — CONFIRMADO ​

CampoDetalle
Archivossidebar.tsx L359 pathname === link.href
Fix propuestopathname.startsWith('/sitios') para ítem Sitios
CampoDetalle
ArchivosSiteSettingsForm.tsx — navigationJson validado con try/catch al guardar
PendienteVerificar si footerJson tiene la misma validación

5. Storefront — Tienda pública ​

S1 — Carrito borrado antes del pago en pasarela (P0) — CONFIRMADO ​

CampoDetalle
Archivoscart-actions.ts L63 clearCartTokenCookie tras checkout exitoso; CheckoutForm.tsx redirect a N1co
ReproCheckout con link de pago → cancelar en N1co → /checkout vacío; pedido sigue pending_payment
Fix propuestoDiferir clearCart hasta paid o conservar cartToken en sesión hasta gracias

S2 — /gracias sin view ni JWT en SSR (P1) — CONFIRMADO ​

CampoDetalle
Archivosgracias/page.tsx solo pasa viewToken; no readCustomerTokenCookie + customerToken en getOrderStatus
Fix propuestoEn server component, leer cookie de cliente y pasar opts.customerToken
CampoDetalle
Archivoslayout.tsx L47–53 catch sin clearCartTokenCookie; cart-actions.ts devuelve null
Fix propuestoclearCartTokenCookie en catch de layout y carrito

S4 — Sin STOREFRONT_REVALIDATE_SECRET → 403 (P1) — CONFIRMADO (config) ​

CampoDetalle
Archivosapi.ts L21–27; csrfMiddleware.js
Fix propuestoDocumentar en deploy; fallar build si falta en prod

S5 — listMyOrdersAction no limpia sesión en 401 (P2) — CONFIRMADO ​

CampoDetalle
Archivosauth-actions.ts L71–75 vs L28–38
Fix propuestoMisma lógica ApiError 401/403 → clearCustomerTokenCookie
CampoDetalle
Archivosmiddleware.ts L41–43 catch sin borrar cookie
Fix propuestoBorrar abaco_preview_token en catch si había cookie

S7 — Doble fetch preview (P2) — CONFIRMADO ​

CampoDetalle
Archivosmiddleware.ts + site-context.ts resolvePreview
Fix propuestoConfiar solo en headers del middleware en RSC

S8 — safeInternalPath bypass anidado (P2) — CONFIRMADO ​

CampoDetalle
Archivossafe-path.ts — permite /ruta?next=https://evil.com
Fix propuestoRechazar ? en path o sanitizar query next recursivamente

S9 — Toast éxito antes de pasarela (P2) — CONFIRMADO ​

CampoDetalle
ArchivosCheckoutForm.tsx toastPromise + redirect externo
Fix propuestoMensaje “Redirigiendo al pago…” sin “Pedido registrado” hasta confirmación

Ver S3.

S11 — serverActions.allowedOrigins: ['*'] (P2) — CONFIRMADO ​

CampoDetalle
ArchivosStorefront/next.config.js L12
Fix propuestoLista explícita de dominios storefront en prod

6. Sprint de corrección — estado de implementación ​

IDEstado
B1Corregido — default SITES_DEFAULT_DOC_TYPE=100
B2Mitigado — revalidación en confirmPayment + stockReservedAt en checkout
B3Corregido — solo cartToken + exige pending_payment
B4Corregido — transacción + lock en confirmPayment
B5Corregido — CSRF solo STOREFRONT_REVALIDATE_SECRET
B6Corregido — storefrontStockService valida bundles
B7Corregido — sort por COALESCE(webPrice, price)
B8Mitigado — rate limit OTP con Redis + fallback Map
B9Corregido — sessionTokenVersion + logout
B10Corregido — FOR UPDATE en carrito
B11–B15Corregido según informe
F1–F7, S1–S11Corregido según informe

7. Checklist QA manual (complemento) ​

  • [ ] Agregar al carrito > stock → error INSUFFICIENT_STOCK
  • [ ] Checkout N1co → paymentLinkUrl presente
  • [ ] Preview 3 páginas sin ?preview= en URL
  • [ ] Cambiar empresa en editor → sin datos ajenos persistentes
  • [ ] Móvil <640px → switcher visible
  • [ ] Cookie carrito inválida → error, no toast éxito
  • [ ] OTP + checkout con secrets alineados → sin 403 CSRF
  • [ ] Cancelar pago N1co → poder reintentar o ver pedido en cuenta
  • [ ] /gracias?token=&view= funciona sin sesión
  • [ ] Pago webhook → documento tipo correcto y stock coherente

Generado por auditoría estática. No modifica el plan de corrección en .cursor/plans/.

Documentación API abaco · Changelog