SOP — Módulo Facturación / Estado de Cuenta de Almacenaje
Tipo: procedimiento operativo estándar. Estado: demo-ready (Fase 4 — gate primer cliente de pago). 🔴 Boundary: cobrar a un cliente real o emitir un CFDI fiscal son acciones outward-facing / CEO-gated. Este SOP describe el cómo; el CEO autoriza el cuándo para producción. Las palabras clave DEBE, NO DEBE, DEBERÍA y PUEDE se usan conforme a RFC-2119 (en español).
0. Propósito y alcance
Este SOP gobierna la operación del módulo Facturación (/facturacion) de Patio Abierto.
Cubre:
- Consulta del estado de cuenta de almacenaje: motor tiempo × espacio (dwell real × tarifa por tenant).
- Selección del método de cobro (Efectivo / Transferencia / CFDI / Propio). El cobro es opcional.
- Proceso de emisión de CFDI 4.0 (estructura parkeada; timbrado PAC pendiente de integración FacturAPI).
- Reglas de protección de datos de cobro y escalamiento en caso de error.
Quedan fuera de este SOP: la configuración del tarifario por tenant (pendiente F4), la
integración PAC/FacturAPI, y la carga real de inventario desde Supabase (ver db-activation-pr32.md).
1. Roles y permisos que aplican
| Rol | Permiso requerido | Accede a /facturacion |
|---|---|---|
admin | admin:gestionar | SÍ |
supervisor | — (no tiene admin:gestionar) | NO |
operador_garita | — | NO |
operador_patio | — | NO |
cliente_readonly | — | NO |
- Solo el rol
adminDEBE poder acceder al módulo de facturación. - El sidebar DEBE ocultar la entrada de navegación a usuarios sin
admin:gestionar. - La ruta DEBE redirigir a
/sin-accesoserver-side si el usuario no tiene el permiso, independientemente de la navegación.
⚠️ Los roles son PROPUESTOS / PROVISIONALES — deben confirmarse con el CEO antes de cablearse a cobros reales (
lib/auth/roles.ts).
2. Precondiciones
- El usuario DEBE estar autenticado (sesión Supabase Auth válida).
- El usuario DEBE tener rol
admin(verificado porrolTienePermiso(usuario.rol, PERMISO.ADMIN_GESTIONAR)). - DEBE existir al menos un contenedor con estado distinto a
SALIDOy con eventoINGRESO_PATIOregistrado en el historial de eventos del tenant. - La tarifa del tenant DEBE estar configurada. (En demo:
TARIFA_DEMO— ILUSTRATIVA; en producción: config por tenant en DB — pendiente F4.) export const dynamic = "force-dynamic"DEBE estar activo enapp/facturacion/page.tsxpara garantizar que el dwell se calcule en tiempo real, no desde caché de build.
3. Procedimiento paso a paso
3.1 Acceso y carga de la pantalla
- El operador autorizado navega a
/facturacion(o hace clic en "Facturación" en el sidebar). - El servidor verifica
rolTienePermiso(usuario.rol, PERMISO.ADMIN_GESTIONAR); si falla → redirige a/sin-acceso. El procedimiento termina aquí para usuarios sin permiso. - La página llama a
getEstadoCuenta(ahora, rfc), que a su vez: a. LlamagetInventario(rfc)ygetEventosPatio(rfc)en paralelo. b. Determina, por cada contenedor en inventario no-SALIDO, la fecha del último eventoINGRESO_PATIO(el último ingreso gana — regla anti-inflación de cargo por reingreso). c. CalculadiasEnPatio = floor((ahora − ingresoAt) / 86 400 000 ms), mínimo 0. d. Llama acalcularEstadoCuenta(facturables, tarifa)— motor puro y determinista. - La pantalla muestra el banner TARIFAS ILUSTRATIVAS (D-011). NUNCA DEBE omitirse mientras la tarifa sea
TARIFA_DEMO.
3.2 Lectura del estado de cuenta
-
El operador revisa el panel de resumen:
- Total a cobrar (IVA 16% incluido).
- Contenedores facturables (en patio, con ingreso registrado).
- Días-almacenaje (suma de días cobrables de todos los renglones).
- Maniobras (subtotal maniobra entrada + salida × número de contenedores).
-
El operador revisa la tabla de cargos por contenedor:
Columna Fuente Contenedor contenedorIsodel inventarioIngreso Fecha del último evento INGRESO_PATIODías diasCobrables = max(0, diasEnPatio − diasGracia)Tarifa / día tarifa.tarifaDiaMxn(MXN)Almacenaje diasCobrables × tarifaDiaMxn(redondeado a centavos)Maniobras maniobraEntradaMxn + maniobraSalidaMxn(por contenedor)Cargo almacenajeMxn + maniobrasMxn -
El operador DEBERÍA validar visualmente que los días en patio son coherentes con las fechas de ingreso reales antes de proceder al cobro.
3.3 Selección del método de cobro
-
El operador selecciona uno de los cuatro métodos disponibles en el selector
MetodoCobro:Método Acción que genera Efectivo Registro de pago; sin comprobante fiscal Transferencia Registro de referencia bancaria CFDI Abre panel de timbrado CFDI 4.0 (ver paso 9) Propio / sin cargo Solo seguimiento de costo interno; no se cobra ni se timbra -
Si el operador selecciona CFDI: a. La pantalla muestra los campos fiscales precargados: Uso CFDI
G03 · Gastos en general, Forma de pago03 · Transferencia, Clave ProdServ78121602 · Almacenaje, Método de pagoPUE. b. SiFACTURAPI_API_KEYestá configurada en el entorno → el botón de timbrado queda habilitado. c. SiFACTURAPI_API_KEYno está configurada → el botón de timbrado DEBE mostrarse deshabilitado con tooltip "Integración PAC pendiente". NO DEBE lanzar ninguna acción ni mostrar un error de red. d. (En demo y Fase 4 actual: el timbrado real está pendiente. El botón SIEMPRE aparece disabled. Ver sección 6.)
3.4 Cierre del flujo
- El operador registra el pago/cobro según el método elegido (efectivo: nota interna; transferencia: referencia bancaria). (En producción: acción de servidor que persiste el pago en BD — pendiente F4.)
- Tras el cobro, el operador DEBERÍA verificar en el módulo Inventario (
/) que el contenedor cuyo cargo fue cobrado refleja el estado correcto.
4. Reglas e invariantes
Lo que DEBE cumplirse
- El dwell DEBE derivarse exclusivamente del historial de eventos (
INGRESO_PATIO). NUNCA de campos manuales ni datos inventados. - El último ingreso DEBE usarse como fecha base (política anti-inflación de cargo).
- El motor
calcularRenglonycalcularEstadoCuentaDEBEN ser funciones puras; NUNCA DEBEN tener efectos secundarios ni leer datos externos. - Cada monto monetario DEBE redondearse a centavos (
Math.round(n * 100) / 100) para evitar errores de flotante. - Los
diasCobrablesNUNCA DEBEN ser negativos (Math.max(0, ...)). - El total calculado DEBE satisfacer:
totalMxn = subtotalMxn + ivaMxnysubtotalMxn = subtotalAlmacenajeMxn + subtotalManiobrasMxn. - El banner de TARIFAS ILUSTRATIVAS DEBE mostrarse mientras la tarifa sea
TARIFA_DEMO(gate D-011). - La ruta
/facturacionDEBE protegerse server-side conPERMISO.ADMIN_GESTIONAR, sin depender únicamente del sidebar.
Lo que NO DEBE ocurrir
- NO DEBE mostrarse ni comunicarse ningún monto como precio "real" del operador mientras la tarifa sea
TARIFA_DEMO. El banner es obligatorio. - NO DEBE emitirse un CFDI real sin
FACTURAPI_API_KEYconfigurada y sin autorización CEO. - NO DEBE calcularse el dwell como la diferencia entre la fecha de la pantalla y un campo de inventario estático; DEBE ser la diferencia respecto al evento
INGRESO_PATIO. - NO DEBE cachearse la respuesta del estado de cuenta (
dynamic = "force-dynamic"obligatorio). - NO DEBE inventarse el dato de tarifa para rellenar un vacío; si el operador no configuró tarifa, DEBE mostrarse
TARIFA_DEMOmarcada explícitamente como ILUSTRATIVA y reportarse el vacío. - Un usuario con rol distinto de
adminNO DEBE poder acceder a esta ruta, ni a través del sidebar ni navegando directamente a la URL.
5. Manejo de errores, excepciones y escalamiento
| Situación | Acción requerida |
|---|---|
Usuario sin permiso accede a /facturacion | Redirect automático a /sin-acceso. Sin escalamiento necesario. |
| Inventario vacío (0 contenedores facturables) | Pantalla DEBE mostrar estado vacío explícito. Revisar si hay contenedores en patio en el módulo Inventario (/). |
Contenedor en patio sin evento INGRESO_PATIO | El contenedor no aparece como facturable. DEBE reportarse al supervisor para registrar el evento faltante en Garita/Operaciones. |
| Error al leer inventario / eventos (demo-store o DB) | La página DEBE mostrar un error amigable. No mostrar datos parciales. Escalar al equipo técnico. |
FACTURAPI_API_KEY ausente con método CFDI seleccionado | Botón de timbrado deshabilitado; mensaje "Integración PAC pendiente". No es un error de usuario — es un gate de infraestructura. Escalar al CEO para habilitar la integración. |
| Discrepancia entre días calculados y evidencia física | Pausar el cobro. Revisar historial de eventos en /operaciones. Escalar al supervisor para corrección en fuente. |
6. Notas de demo vs. producción
| Aspecto | Estado actual (DEMO / Fase 4) | Producción (F4 completa) |
|---|---|---|
| Inventario y eventos | demo-store (in-memory, F3) | Supabase + Prisma (activar según db-activation-pr32.md) |
| Tarifa | TARIFA_DEMO — ILUSTRATIVA (85 MXN/día, 225 MXN maniobra entrada/salida, IVA 16%, 0 días de gracia) | Config por tenant en DB; el operador la configura vía panel admin (pendiente F4) |
| Timbrado CFDI | Estructura parkeada; botón disabled; FACTURAPI_API_KEY no conectada | FacturAPI + capa PAC (cfdi-suite); requiere FACTURAPI_API_KEY en Vercel + GO CEO |
| Persistencia de cobros | Sin server action de persistencia | Server action → tabla de pagos en Prisma (pendiente F4) |
| Gate de habilitación | Módulo visible solo para admin en demo | Igual; módulo se activa por tenant en tenant_config.modules al contratar F4 |
Los campos fiscales
78121602 · Almacenaje,G03,PUE, forma de pago03son reales y DEBEN mantenerse sin alteración — son catálogos del SAT (aduana/fiscal siempre real, D-011).
7. Qué NO hacer
- NO cambiar el motor de cálculo (
calculo.ts) sin correr los tests decalculo.test.ts(vitest). - NO eliminar el banner ILUSTRATIVO sin haber cargado la tarifa real del operador y haber obtenido GO CEO.
- NO habilitar el botón de timbrado sin que la integración PAC esté probada E2E en staging.
- NO renombrar
PERMISO.ADMIN_GESTIONARsin actualizar simultáneamente el gate de esta página. - NO omitir el
dynamic = "force-dynamic"en la page — quitarlo cachearía el dwell y el total sería incorrecto.