# Cambio de negocio a portar — Picking diurno/nocturno Requerimiento de negocio **sin equivalente en este `.NET`**: las notificaciones de Picking durante el día ya no deben crear documentos transaccionales en SAP inmediatamente — eso causaba que el WMS reportara éxito sin que SAP reflejara nada. Especificación traducida 1:1 de la implementación ya validada en Python (`itca_api_sap_wms_python/app/services/picking_service.py`, `app/schemas/picking.py`, `app/api/routes/picking.py`). Ver también [`PORTE_FUNCIONALIDAD_NUEVA.md`](./PORTE_FUNCIONALIDAD_NUEVA.md) para el detalle de un bug de SQL injection real (`HelperProcesoPiciking.Set`) que hay que corregir como parte de este mismo trabajo, porque es exactamente el código que este cambio va a aislar y reusar. ## Flujo de negocio La hoja de ruta contiene varias facturas. El repartidor va notificando el estado de cada factura a medida que llega a cada punto de entrega (**preliminar**, sin crear nada en SAP). Al volver a la oficina, el tercero recorre esas mismas facturas con un `FOR` de su lado llamando otra vez al mismo endpoint pero en modo **cierre** (ahí sí se crean los documentos SAP), y al final cierra la hoja de ruta completa **una sola vez**. ## Regla Proceso + Notificacion (versión 2.1.2.0) `Notificacion` **solo se valida cuando `Proceso = "ULTIMA MILLA"`**. En los demás procesos el campo se ignora. | `Proceso` | `Notificacion` | Resultado | |---|---|---| | `ULTIMA MILLA` | `"cierre"` | Lógica completa: crea documentos en SAP (solicitud de devolución, estado 23). | | `ULTIMA MILLA` | ausente u otro valor | Solo actualiza estado en HANA; **no** crea nada en SAP. | | `DESPACHO` | cualquiera / ausente | Lógica completa siempre: NC y transferencias automáticas (estados 5 y 6). | | `PICKING` | cualquiera / ausente | Lógica completa siempre: NC y transferencias automáticas (estado 13). | Historial: en 2.1.1.0 el filtro se aplicaba a todos los procesos, por lo que DESPACHO y PICKING no generaban la NC si no llegaba `"cierre"`. Corregido en 2.1.2.0. ## Resumen del contrato | Método | Ruta | Cambia hoy | Qué hace | |---|---|---|---| | `POST` | `/api/picking` | **Sí** — agrega campo `Notificacion` | **Solo con `Proceso = "ULTIMA MILLA"`**: `"cierre"` → lógica completa (crea documento SAP); cualquier otro valor/ausente → solo actualiza estado en HANA, nunca toca SAP. Con `DESPACHO`, `PICKING` y demás procesos el campo se ignora y la lógica corre siempre (NC y transferencias). | | `POST` | `/api/picking/ruta` | No | Sin cambios — `CreateHojaRuta` ya existe y sigue igual. | | `POST` | `/api/picking/hojaruta/{docEntry}/cerrar` | **Nuevo** | Cierra el UDO `ARD_PHR` completo en SAP. Sin body. | ## 1. Campo `Notificacion` en `POST /api/Picking` ### DTO/Entidad — agregar una propiedad **Archivo**: `Net.Business.Entities/WMS/Picking/BE_PickingRequest.cs` — agregar: ```csharp public string Notificacion { get; set; } // solo se evalua con Proceso "ULTIMA MILLA": "cierre" | otro/ausente = preliminar ``` **Archivo**: `Net.Business.DTO/WMS/Picking/Dto_PickingRequest.cs` — agregar la misma propiedad **y** copiarla en `RetornaModelo()`: ```csharp public string Notificacion { get; set; } public BE_PickingRequest RetornaModelo() { return new BE_PickingRequest() { // ... resto de los campos existentes, sin tocar ... Notificacion = Notificacion, }; } ``` ### Dónde cortar la lógica — `PickingRepository.Create` **Archivo**: `Net.Data/WMS/Picking/PickingRepository.cs`, método `Create`. La buena noticia: el corte es **un único early-return**, justo después de la línea que ya actualiza el estado en HANA — no hace falta duplicar ninguna de las validaciones de arriba (a diferencia de la versión Python, que sí duplicó el bloque de validación en una función separada porque Python separa `actualizar_estado()`/`create()` en dos funciones independientes; acá alcanza con un `if` adentro del mismo método). ```csharp // ... validaciones existentes sin cambios (lineas 51-93: DocEntry, Proceso, // IdEstadoCubico, HelperProcesoPermitido.Get, desambiguacion Existe > 1) ... //=== REALIZA PROCESO COMPLETO DE ACTUALIZACION HelperProcesoPiciking.Set(entidad.Proceso, entidad.IdEstadoCubico, entidad.DocEntry, entidad.CodMotivo, Proceso.Code); // <-- linea 96 actual, sin tocar // === Solo ULTIMA MILLA: notificacion preliminar (diurna) - nunca toca SAP. // DESPACHO / PICKING no dependen de Notificacion. if (string.Equals(entidad.Proceso, "ULTIMA MILLA", StringComparison.OrdinalIgnoreCase) && !string.Equals(entidad.Notificacion, "cierre", StringComparison.OrdinalIgnoreCase)) { return resultadoTransaccion.GetInformacionResultado(entidad.DocEntry, entidad.NumOrden, "Estado actualizado correctamente", entidad); } #region PROCESO DE NOTA DE CREDITO // ... todo el resto del metodo (lineas 98-333 actuales) sin ninguna modificacion ... #endregion ``` Es decir: **todo lo que hoy hace `Create()` (líneas 45-341 completas)** sigue siendo exactamente la lógica de cierre — no se toca nada de ahí. Solo se agrega el `if` de corte después de la línea 96, y **únicamente para `ULTIMA MILLA`**. Para `DESPACHO` y `PICKING` la NC se genera sin importar `Notificacion`. > **Nota de contrato**: `detalle` ya es obligatorio hoy en este endpoint (la > validación de arriba lo necesita para desambiguar `Existe > 1`), así que no > hace falta agregar ninguna validación nueva ahí — la versión Python > solo lo documentó explícito porque separó la función, acá ya viene gratis > al reusar el mismo método. ### Bodies de ejemplo **Notificación preliminar** (solo `ULTIMA MILLA`, reparto en curso — `Notificacion` ausente o distinto de `"cierre"`): ```json { "numOrden": "OT-2026-001", "docEntry": 500, "proceso": "ULTIMA MILLA", "codMotivo": "M01", "idEstadoCubico": 23, "codigoAlmacen": "ALM01", "detalle": [ { "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" } ] } ``` **Cierre** (`ULTIMA MILLA`, mismo shape, agrega `notificacion: "cierre"` — crea el documento SAP real, misma respuesta que hoy): ```json { "numOrden": "OT-2026-001", "docEntry": 500, "proceso": "ULTIMA MILLA", "codMotivo": "M01", "idEstadoCubico": 23, "codigoAlmacen": "ALM01", "notificacion": "cierre", "detalle": [ { "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" } ] } ``` ### Respuesta — notificación preliminar (éxito) ```json { "docEntry": 500, "docNum": "OT-2026-001", "statusCode": 200, "message": "Estado actualizado correctamente", "error": null, "appName": "PickingRepository", "methodName": "Create", "listData": null, "data": { "...": "eco del PickingRequest enviado" }, "nombreEstacion": "SERVER-WMS" } ``` (La respuesta de cierre no cambia respecto a la que ya devuelve `Create()` hoy — ver el controller existente.) ## 2. `POST /api/Picking/HojaRuta/{docEntry}/Cerrar` (nuevo) Cierra la Hoja de Ruta completa en SAP — se llama **una sola vez**, después de que el tercero terminó el `FOR` de cierre por cada factura. - **Hoja de Ruta es un UDO en SAP**: `ARD_PHR`. - **SAP Service Layer**: `POST ARD_PHR({docEntry})/Close` — mismo patrón que `SolicitudTransferencia/{docEntry}/cerrar` (ver [`SOLICITUD_TRANSFERENCIA.md`](./SOLICITUD_TRANSFERENCIA.md) para el gap de infraestructura compartido: hace falta un método para `POST .../Close` con body vacío, no existe ninguno hoy en `ConnectionServiceLayer`). - **Body del request**: ninguno (solo `docEntry` en la ruta). - **Body enviado a SAP**: `{}`. - **Interfaz**: agregar a `IPickingRepository.cs`: ```csharp Task> CerrarHojaRuta(int docEntry); ``` ```csharp public async Task> CerrarHojaRuta(int docEntry) { _metodoName = "CerrarHojaRuta"; ResultadoTransaccion resultadoTransaccion = new ResultadoTransaccion(_metodoName, _aplicacionName); try { var connectServiceLayer = new ConnectionServiceLayer(_configuration, _clientFactory); await connectServiceLayer.PostAsync($"ARD_PHR({docEntry})/Close", new { }); return null; } catch (Exception ex) { return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), null); } } ``` Controller (`PickingController.cs`, agregar junto a `Ruta`): ```csharp [HttpPost] [Route("HojaRuta/{docEntry}/Cerrar")] public async Task CerrarHojaRuta(int docEntry) { var error = await _repository.Picking.CerrarHojaRuta(docEntry); var result = error ?? new BE_ResultadoTransaccion { DocEntry = docEntry, StatusCode = 200, Message = "Cerrado Correctamente", AppName = "PickingRepository", MethodName = "CerrarHojaRuta", }; B_Log.Add(/* mismo patron que el resto - proceso "Hoja Ruta" */); return error == null ? Ok(result) : BadRequest(result); } ``` > **Por qué `200` con body y no `204`**: mismo motivo que > `SolicitudTransferencia` — un `204` no puede llevar body (RFC 7231 §6.3.5) > y muchos clientes HTTP, incluido `HttpClient` de .NET, no lo leen. `201` se > descarta porque no se crea nada nuevo. ### Respuesta éxito (`200`) ```json { "docEntry": 77, "docNum": null, "statusCode": 200, "message": "Cerrado Correctamente", "error": null, "appName": "PickingRepository", "methodName": "CerrarHojaRuta", "listData": null, "data": null, "nombreEstacion": "SERVER-WMS" } ``` ### Respuesta error (`400`) ```json { "docEntry": null, "docNum": null, "statusCode": 500, "message": null, "error": "La hoja de ruta ya esta cerrada", "appName": "PickingRepository", "methodName": "CerrarHojaRuta", "listData": null, "data": null, "nombreEstacion": "SERVER-WMS" } ``` ## Checklist de integración para el WMS 1. **Nunca mandar un array a `POST /api/Picking`.** Un documento (una factura) por llamada — el `FOR` por factura es responsabilidad del WMS. 2. **`notificacion` ausente o distinto de `"cierre"` = preliminar** (default seguro, nunca crea nada en SAP). 3. **El orden importa**: primero el `FOR` de cierre por cada factura (`notificacion: "cierre"`), y **recién al final** —una sola vez— el `POST /api/Picking/HojaRuta/{docEntry}/Cerrar`. 4. **`docEntry` en `POST /api/Picking` es el de la Factura**, no el de la Hoja de Ruta. ## Pendiente de validar en QA - Confirmar en QA si el nuevo `Close` del UDO `ARD_PHR` efectivamente bloquea modificaciones futuras sobre el `DocEntry` una vez cerrado (no confirmado todavía ni en la versión Python). - Correr los mismos casos contra el endpoint `.NET` levantado antes de dar por válido este port — en Python tampoco se probó esto todavía end-to-end contra Service Layer real (ver `CLAUDE.md` del proyecto Python, sección "Pruebas contra HANA/SAP reales").