11. Lógica Algorítmica y Procesos: MDX-Cloud
Esta sección describe las especificaciones lógicas y el comportamiento algorítmico de las cuatro (4) reglas de negocio de alta complejidad dentro de MDX-Cloud. Estas directivas garantizan la consistencia transaccional y automatizan las decisiones críticas del backend.
11.1 Algoritmo de Surtido FEFO y Validación de Rotación Inversa
Este algoritmo gobierna el módulo WMS durante dos eventos: la generación de rutas de picking (surtido) y la alerta de reacomodo físico en andén cuando ingresa mercancía con caducidad corta.
A. Lógica de Surtido (Picking FEFO)
Cuando se autoriza un pedido comercial, el sistema calcula los lotes específicos que el auxiliar debe extraer del almacén:
ALGORITMO GenerarRutaPicking(PedidoId)
ENTRADA: PedidoId con una lista de {ProductoId, CantidadSolicitada}
SALIDA: ListaOrdenada de {Pasillo, Estante, LoteId, CantidadAExtraer}
PARA CADA Item EN Pedido.Items HACER
cantidadPendiente = Item.CantidadSolicitada
// Consultar lotes disponibles ordenados por la caducidad más próxima
LotesDisponibles = OBTENER Lotes DONDE Lote.ProductoId == Item.ProductoId
Y Lote.StockDisponible > 0
Y Lote.Estado == "Activo"
ORDENAR POR Lote.FechaCaducidad ASC
PARA CADA Lote EN LotesDisponibles HACER
SI cantidadPendiente <= 0 ENTONCES ROMPER BUCLE
cantidadExtraida = MINIMO(Lote.StockDisponible, cantidadPendiente)
cantidadPendiente = cantidadPendiente - cantidadExtraida
AGREGAR A ListaOrdenada -> {
Pasillo: Lote.Ubicacion.Pasillo,
Estante: Lote.Ubicacion.Estante,
LoteId: Lote.Id,
CantidadAExtraer: cantidadExtraida
}
FIN PARA
SI cantidadPendiente > 0 ENTONCES
DISPARAR ALERTA("Stock insuficiente para el Producto: " + Item.ProductoId)
FIN SI
FIN PARA
// Optimizar la ruta física del almacenista
ORDENAR ListaOrdenada POR Pasillo ASC, Estante ASC
RETORNAR ListaOrdenada
FIN ALGORITMO
B. Lógica de Entrada (Rotación Inversa de Anaquel)
Al escanear un lote nuevo en andén, el backend valida si se requiere un reacomodo físico preventivo en el rack de surtido:
ALGORITMO ValidarRotacionInversaAnaquel(CedisId, ProductoId, LoteNuevo)
ENTRADA: CedisId, ProductoId, LoteNuevo conteniendo {Id, NumeroLote, FechaCaducidad}
SALIDA: TareaReacomodo creada (SI aplica) y Estado de disponibilidad del inventario
// 1. Obtener los lotes del mismo producto en el mismo CEDIS con existencia disponible
LotesExistentes = OBTENER Lotes DONDE Lote.ProductoId == ProductoId
Y Lote.CedisId == CedisId
Y Lote.StockDisponible > 0
Y Lote.Id != LoteNuevo.Id
Y Lote.Estado == "Activo"
SI LotesExistentes.Count == 0 ENTONCES
// No hay stock previo; el lote nuevo es comercialmente disponible de inmediato
LoteNuevo.Estado = "Activo"
GUARDAR LoteNuevo
RETORNAR { EstadoLote: "Activo", RequiereReacomodo: FALSE }
FIN SI
// 2. Encontrar el lote en anaquel con la fecha de caducidad más lejana (máxima)
LoteCaducidadMasLejana = LotesExistentes.OBTENER_MAXIMO(Lote.FechaCaducidad)
// 3. Validar si la caducidad del lote nuevo es más corta/próxima que la existente (Inversa)
SI LoteNuevo.FechaCaducidad < LoteCaducidadMasLejana.FechaCaducidad ENTONCES
// Rotación Inversa detectada: el producto nuevo vencería antes que el que ya está exhibido.
// Bloquear disponibilidad automática del nuevo lote para obligar a rotar el anaquel físico.
LoteNuevo.Estado = "PendienteReacomodo"
GUARDAR LoteNuevo
// Generar una tarea de reacomodo prioritaria para el auxiliar
CREAR RegistroTareaReacomodo -> {
CedisId: CedisId,
ProductoId: ProductoId,
LoteNuevoId: LoteNuevo.Id,
LoteExistenteId: LoteCaducidadMasLejana.Id,
Ubicacion: LoteCaducidadMasLejana.Ubicacion, // Pasillo y Estante
Descripcion: "Alerta de Rotación Inversa: Colocar el Lote Nuevo '" + LoteNuevo.NumeroLote + "' (Cad: " + LoteNuevo.FechaCaducidad + ") al frente del anaquel, y desplazar el Lote Existente '" + LoteCaducidadMasLejana.NumeroLote + "' (Cad: " + LoteCaducidadMasLejana.FechaCaducidad + ") hacia atrás."
}
RETORNAR { EstadoLote: "PendienteReacomodo", RequiereReacomodo: TRUE }
SINO
// El lote nuevo tiene caducidad más larga que lo exhibido; entra activo normal
LoteNuevo.Estado = "Activo"
GUARDAR LoteNuevo
RETORNAR { EstadoLote: "Activo", RequiereReacomodo: FALSE }
FIN SI
FIN ALGORITMO
11.2 Algoritmo de División de Folios de Facturación
Este algoritmo se ejecuta en la interfaz y backend del Punto de Venta (Mostrador). Permite dividir una venta global de alto valor en múltiples folios de facturas independientes, ajustándose a topes monetarios específicos solicitados por el cliente para optimización fiscal.
ALGORITMO DividirVentaEnFolios(ListaProductosVenta, MontoMaximoPorFolio)
ENTRADA: Lista de {ProductoId, PrecioUnitario, Cantidad}, LimiteMonetario ($Y)
SALIDA: ColeccionFacturas conteniendo sub-listas de ítems timbrables
FacturaActual = NUEVA Factura()
MontoAcumuladoFactura = 0
PARA CADA Item EN ListaProductosVenta HACER
unidadesPendientes = Item.Cantidad
MIENTRAS unidadesPendientes > 0 HACER
montoUnidad = Item.PrecioUnitario
// Validar si agregar una sola pieza excede el tope del folio actual
SI (MontoAcumuladoFactura + montoUnidad) > MontoMaximoPorFolio ENTONCES
// Cerrar factura actual y preparar una nueva
AGREGAR FacturaActual A ColeccionFacturas
FacturaActual = NUEVA Factura()
MontoAcumuladoFactura = 0
FIN SI
// Calcular cuántas unidades de este producto caben en el espacio restante del tope
espacioDisponibleDinero = MontoMaximoPorFolio - MontoAcumuladoFactura
unidadesQueCaben = PARTE_ENTERA(espacioDisponibleDinero / montoUnidad)
SI unidadesQueCaben == 0 ENTONCES unidadesQueCaben = 1
unidadesAInsertar = MINIMO(unidadesQueCaben, unidadesPendientes)
// Insertar la partida en el folio actual
AGREGAR A FacturaActual.Partidas -> {
ProductoId: Item.ProductoId,
Cantidad: unidadesAInsertar,
PrecioUnitario: Item.PrecioUnitario
}
MontoAcumuladoFactura = MontoAcumuladoFactura + (unidadesAInsertar * montoUnidad)
unidadesPendientes = unidadesPendientes - unidadesAInsertar
FIN MIENTRAS
FIN PARA
// Agregar el último folio restante si contiene partidas
SI FacturaActual.Partidas.Count > 0 ENTONCES
AGREGAR FacturaActual A ColeccionFacturas
FIN SI
RETORNAR ColeccionFacturas
FIN ALGORITMO
11.3 Algoritmo de Dispersión Condicional de Abonos (FIFO de Cartera)
Este algoritmo se ejecuta en el backend al procesar un pago de cliente. Distribuye el dinero de forma automatizada protegiendo las facturas más viejas del vencimiento, a menos que se fuerce una asignación manual específica.
ALGORITMO DispersarAbonoCartera(ClienteId, MontoAbono, EsManual, FacturaEspecificaId)
ENTRADA: Identificadores de negocio, Monto monetario del abono ($), Bandera de control
SALIDA: Confirmación de afectación de saldos en Ledger contable
SI EsManual == TRUE ENTONCES
// Flujo de Anulación Manual: directo a un folio específico
FacturaDestino = OBTENER Factura DONDE Factura.Id == FacturaEspecificaId
MontoAplicado = MINIMO(FacturaDestino.SaldoPendiente, MontoAbono)
FacturaDestino.SaldoPendiente = FacturaDestino.SaldoPendiente - MontoAplicado
REGISTRAR AplicacionPago(AbonoId, FacturaDestino.Id, MontoAplicado)
// Si sobra dinero, el remanente queda como saldo a favor global del cliente
SI MontoAbono > MontoAplicado ENTONCES
Cliente.SaldoAFavor = Cliente.SaldoAFavor + (MontoAbono - MontoAplicado)
FIN SI
SINO
// Flujo por Defecto: Algoritmo FIFO (First In, First Out)
saldoDisponible = MontoAbono
FacturasDeudoras = OBTENER Facturas DONDE Factura.ClienteId == ClienteId Y Factura.SaldoPendiente > 0
ORDENAR POR Factura.FechaEmision ASC
PARA CADA Factura EN FacturasDeudoras HACER
SI saldoDisponible <= 0 ENTONCES ROMPER BUCLE
montoAAplicar = MINIMO(Factura.SaldoPendiente, saldoDisponible)
Factura.SaldoPendiente = Factura.SaldoPendiente - montoAAplicar
saldoDisponible = saldoDisponible - montoAAplicar
REGISTRAR AplicacionPago(AbonoId, Factura.Id, montoAAplicar)
FIN PARA
// Si el abono liquida todo y sobra dinero
SI saldoDisponible > 0 ENTONCES
Cliente.SaldoAFavor = Cliente.SaldoAFavor + saldoDisponible
FIN SI
FIN SI
FIN ALGORITMO
11.4 Algoritmo de Sugerencia Automática de Compra (Reabastecimiento)
Calculador determinista que corre de forma asíncrona en el host .Worker de .NET 10 para notificar al departamento de Compras sobre la escasez crítica de inventario.
ALGORITMO CalcularSugerenciaReabastecimiento(ProductoId)
// Parámetros de control preestablecidos en el catálogo del producto
StockMinimo = Producto.StockMinimoConfigurado
StockMaximo = Producto.StockMaximoConfigurado
// Sumar el stock físico de todas las canastillas y anaqueles de la sucursal
StockFisicoActual = SUMAR(Lotes.StockDisponible) DONDE Lote.ProductoId == ProductoId Y Lote.Estado == "Activo"
// Considerar compras que ya vienen en camino para no duplicar órdenes
StockEnTransito = SUMAR(ItemsOC.Cantidad) DONDE ItemsOC.ProductoId == ProductoId Y ItemsOC.OrdenCompra.Estado == "Transito"
StockVirtualTotal = StockFisicoActual + StockEnTransito
SI StockVirtualTotal <= StockMinimo ENTONCES
CantidadASugerir = StockMaximo - StockVirtualTotal
GENERAR RegistroSugerenciaCompra -> {
ProductoId: ProductoId,
CantidadSugerida: CantidadASugerir,
Razon: "Inventario Virtual por debajo del mínimo crítico (" + StockVirtualTotal + " / " + StockMinimo + ")"
}
FIN SI
FIN ALGORITMO
11.5 Mecanismo de Procesamiento Asíncrono (Task Queue Architecture)
Para garantizar que el tiempo de respuesta en el punto de venta y almacén permanezca por debajo de los 200 ms, MDX-Cloud delega los procesos de alta latencia o dependientes de terceros (APIs de facturación, envío de correos, generación de PDFs) a un esquema de colas en segundo plano gestionado por Hangfire respaldado en PostgreSQL.
ALGORITMO EncolarTareaFacturacion(VentaId, ClienteId)
ENTRADA: Identificadores únicos de la transacción
SALIDA: ID de la tarea en cola y liberación del hilo HTTP
// 1. Validar que la venta exista en la base de datos local
Venta = OBTENER Venta DONDE Venta.Id == VentaId
SI Venta NO EXISTE ENTONCES RETORNAR ERROR
// 2. Crear el Payload estructurado para el Job de fondo
MetaTarea = NUEVA TareaBackground() -> {
Tipo: "Timbrado_CFDI_4.0",
Payload: { VentaId: VentaId, ClienteId: ClienteId },
MaximosReintentos: 3,
TiempoEsperaEntreReintentos: "60 segundos"
}
// 3. Inyectar la tarea en el motor persistente de Hangfire
JobId = HANGFIRE.Encolar(Instancia -> Instancia.ProcesarTimbradoFiscalAsincrono(MetaTarea))
// 4. Responder de inmediato al cliente web (React)
RETORNAR exitoso -> { TareaId: JobId, Mensaje: "Transacción en cola de timbrado." }
FIN ALGORITMO
Política de Resiliencia ante Fallas de APIs Externas
Si la API de factura.com se cae o experimenta latencia:
-
El motor de la cola interceptará la excepción de red sin tumbar el sistema principal.
-
Aplicará una estrategia de Reintentos Exponenciales (Intentará de nuevo a los 2 minutos, luego a los 4 minutos, luego a los 8 minutos).
-
Si tras los 3 reintentos el documento sigue fallando, la tarea se moverá al estado de
Failed, detonando una notificación en el panel de soporte para auditoría manual.
11.6 Orquestación en Tiempo Real (SignalR)
Queda estrictamente prohibido el uso del método Clients.All (Broadcast) para la emisión de eventos de negocio en el backend de .NET, debido al riesgo de saturación de red en el modelo multisucursal y posibles fugas de privacidad (ej. enviar eventos de nómina a cajeros).
Para segmentar la memoria y ahorrar ancho de banda, todos los Hubs de SignalR utilizarán la siguiente estrategia de agrupamiento jerárquico basada en los claims del Token JWT:
- Envío por Sucursal (El más frecuente): Cuando el Frontend inicia conexión, el backend lee el
SucursalIddel token y suscribe la conexión a un grupo aislado (ej.Groups.AddToGroupAsync(connectionId, "Sucursal_MTY")).- Ejemplo de uso: Reflejar ventas en tiempo real o disminuir el stock del catálogo local.
- Código:
await _hubContext.Clients.Group("Sucursal_MTY").SendAsync("StockUpdated", payload);
- Envío por Rol (Autoridad): Si un proceso requiere autorización gerencial (ej. merma de almacén), el evento solo se dispara a los dispositivos que ostenten dicho rol.
- Ejemplo de uso: Autorizaciones de WMS, Notificaciones de límite de crédito.
- Código:
await _hubContext.Clients.Group("Role_Gerentes").SendAsync("AuthRequested", payload);
- Envío por Usuario (Unicast 1-a-1): SignalR mapea nativamente el
ClaimTypes.NameIdentifier. Si un proceso asíncrono (ej. la generación de un reporte Excel muy pesado en Hangfire) termina en segundo plano, se le avisa exclusivamente al usuario que lo solicitó.- Ejemplo de uso: Exportaciones de PDF/Excel, alertas de error personal.
- Código:
await _hubContext.Clients.User(usuarioId).SendAsync("ReporteListo", fileUrl);
11.7 Máquina de Estados: Ciclo de Vida del Pedido
Esta sección documenta todos los estados posibles de un pedido comercial dentro de MDX-Cloud, desde su creación hasta su cierre fiscal. Establece las transiciones válidas, los actores responsables de cada cambio de estado y las reglas de negocio que los gobiernan.
Diagrama de Estados
Especificación de Estados
| Estado | Descripción | Actor Responsable |
|---|---|---|
| Borrador | Pedido capturado pero no enviado al sistema. Solo visible para el Agente. | Agente de Ventas |
| Enviado | Pedido recibido por el backend. En proceso de validación automática de crédito y stock. | Sistema |
| Bloqueado | El sistema detectó un impedimento: límite de crédito excedido, saldo vencido o stock insuficiente crítico. | Sistema |
| Autorizado | Pedido aprobado (automáticamente o por excepción gerencial). El WMS recibe la Orden de Surtido. | Sistema / Gerencia |
| En Surtido | El Auxiliar tiene asignada la ruta de picking FEFO y está armando la canastilla. | Auxiliar de Almacén |
| Surtido Parcial | El picking se completó pero uno o más ítems tuvieron faltante de stock. | Auxiliar / Encargado |
| Surtido | Picking 100% validado por código de barras. Canastilla cerrada y en espera de despacho. | Auxiliar de Almacén |
| Despachado | Mercancía entregada al área de despacho. Pendiente de documento fiscal. | Facturador / Recepcionista |
| Remisionado | Se emitió remisión interna. El CFDI queda pendiente (máximo 30 días o dentro del mes calendario). | Facturador |
| Bloqueado Fiscal | La remisión superó el periodo permitido (30 días o mes concluido). Candado contable activo. | Sistema |
| Facturado | CFDI 4.0 timbrado exitosamente por el PAC. Ciclo completo cerrado. | Sistema (PAC) |
| Cancelado Fiscal | CFDI cancelado ante el SAT. Stock reintegrado al WMS automáticamente. | Facturador / Contabilidad |
| Cancelado | Pedido cancelado antes de despacho. Sin afectación fiscal. | Gerencia / Agente |
Reglas de Negocio Críticas del Flujo
- Transición
Surtido → Despachado: Una vez que el Auxiliar valida el surtido por código de barras, queda prohibida la modificación de cantidades o ítems desde cualquier estación del ERP. - Candado de Remisión (
Remisionado → Bloqueado Fiscal): El sistema evaluará diariamente las remisiones pendientes. Si el mes calendario de emisión ha concluido o han transcurrido más de 30 días desde la fecha de la remisión, el sistema aplicará el bloqueo fiscal automático y notificará al área de Crédito y Cobranza. Cancelado Fiscal(CFDI cancelado): La cancelación de un CFDI ejecuta en un solo flujo atómico: (a) notificación al SAT vía PAC, (b) incremento del stock del lote específico en WMS, y (c) ajuste en el ledger contable.Bloqueado → Autorizado(Excepción Gerencial): La aprobación deja registro enauditoria.logscon elempleado_iddel gerente, fecha, hora y el motivo de la excepción.