# Endpoint nuevo — Solicitud de Transferencia (a construir en .NET) Módulo **100% nuevo** — no existe ningún `SolicitudTransferenciaController` ni repositorio para `InventoryTransferRequests` en este proyecto hoy (confirmado: no hay ninguna referencia a `InventoryTransferRequests` en `Net.Data/`). Esta es la especificación para construirlo, traducida 1:1 de la implementación ya validada en Python (`itca_api_sap_wms_python/app/services/solicitud_transferencia_service.py`, `app/schemas/solicitud_transferencia.py`, `app/api/routes/solicitud_transferencia.py`). Ver también [`PORTE_FUNCIONALIDAD_NUEVA.md`](./PORTE_FUNCIONALIDAD_NUEVA.md) para los gaps de infraestructura compartida (headers extra en PATCH, GET de un solo recurso, acción `/Close`) y los bugs encontrados en el código que este módulo va a reusar. ## Resumen del contrato | Método | Ruta | Respuesta éxito | Qué hace | |---|---|---|---| | `PATCH` | `/api/solicitudtransferencia/{docEntry}` | `200` + `BE_ResultadoTransaccion` | Actualiza cantidades de línea; `cantidad: 0` elimina la línea. Hay que reenviar todas las líneas a conservar. | | `POST` | `/api/solicitudtransferencia/{docEntry}/cerrar` | `200` + `BE_ResultadoTransaccion` | Cierra el documento completo. Sin body de request. | - **Ruta base**: `api/SolicitudTransferencia` (bajo el virtual path `/integracion` de IIS, igual que el resto de los controllers — no hace falta nada especial en el código para eso). - **Autenticación**: `[Authorize(AuthenticationSchemes = "Bearer")]`, igual que el resto de los controllers (ver `TransferenciaController`/`PickingController` como referencia). - **Objeto SAP asociado**: `InventoryTransferRequests` (Service Layer). ## Cómo funciona el `PATCH` (reglas de negocio, confirmadas contra SAP real en QA) El `PATCH` de una Solicitud de Traslado **solo actualiza cantidades de línea existentes**, y **elimina la línea** cuando la cantidad llega en `0`. No toca fechas, almacenes, comentarios ni ningún otro campo del documento — por eso el DTO de este endpoint es mucho más chico que el de `Transferencia` (solo `NumOrden` + `Detalle[{Linea, Cantidad}]`). Reglas confirmadas directo contra el Service Layer real de QA: 1. Alcanza con mandar `LineNum` + `Quantity` por línea — nada de `ItemCode`, `WarehouseCode`, fechas, etc. 2. SAP **rechaza** una línea si se manda explícita con `Quantity: 0` — por eso una línea con `Cantidad: 0` en el request **se omite del array** en vez de mandarse en cero. 3. Para que SAP realmente **elimine** la línea omitida (en vez de dejarla intacta), hace falta el header `B1S-ReplaceCollectionsOnPatch: true` — le indica a Service Layer que trate `StockTransferLines` como reemplazo **completo** del array, no un merge parcial. 4. **Hallazgo posterior, crítico** (no estaba cubierto en la primera tanda de pruebas, que usó un documento con `Rate=0` en todas las líneas): al ser un reemplazo completo, SAP **también exige el `Rate` de cada línea** — si el documento tiene líneas valorizadas (`Rate != 0`) y no se reenvía, el PATCH falla. Por eso hay que hacer un `GET InventoryTransferRequests({docEntry})` **antes** del PATCH, leer el `Rate` real de cada línea, y reenviarlo tal cual junto con la `Quantity` nueva — **nunca mandar `Rate: 0` fijo**, eso pisaría la valorización real del documento (en C# esto no tiene el problema de serialización que tuvo la versión Python con `decimal.Decimal` de `hdbcli` — `Newtonsoft.Json` serializa `decimal`/`double` sin problema — pero la regla de negocio de todas formas aplica igual: hay que leer el `Rate` real, no inventarlo). > ⚠️ **Consecuencia del header para el WMS**: como el array es reemplazo > completo, cualquier línea que el request **no incluya** en `Detalle` > también se elimina de SAP — igual que si viniera con `Cantidad: 0`. No es > un PATCH parcial tradicional. El cliente (WMS) tiene que mandar **siempre** > todas las líneas que el documento debe conservar. ## DTO/Entidad a crear ```csharp // Net.Business.Entities/WMS/SolicitudTransferencia/BE_SolicitudTransferenciaPatchDetalle.cs public class BE_SolicitudTransferenciaPatchDetalle { public int Linea { get; set; } public double Cantidad { get; set; } } // Net.Business.Entities/WMS/SolicitudTransferencia/BE_SolicitudTransferenciaPatchRequest.cs public class BE_SolicitudTransferenciaPatchRequest { public string NumOrden { get; set; } // opcional, solo para el log de auditoria - no se usa en el PATCH a SAP public List Detalle { get; set; } } ``` El `Dto_SolicitudTransferenciaPatchRequest` (en `Net.Business.DTO/WMS/SolicitudTransferencia/`) es el mismo shape, con su `RetornaModelo()`, siguiendo el patrón de `Dto_TransferenciaRequest`/`Dto_PickingRequest`. El binding de ASP.NET Core ya es case-insensitive por defecto (igual que en Python), así que no hace falta nada especial para aceptar `numOrden`/`NumOrden`/`detalle`/`Detalle`. ### Body de ejemplo (`PATCH /api/SolicitudTransferencia/{docEntry}`) ```json { "numOrden": "SOL-000123", "detalle": [ { "linea": 0, "cantidad": 8 }, { "linea": 1, "cantidad": 0 } ] } ``` ## Lógica del repositorio (`SolicitudTransferenciaRepository.Update`) Traducción directa de `solicitud_transferencia_service.py::update`: ```csharp public async Task> Update(int docEntry, BE_SolicitudTransferenciaPatchRequest entidad) { _metodoName = "Update"; ResultadoTransaccion resultadoTransaccion = new ResultadoTransaccion(_metodoName, _aplicacionName); try { // 1. GET del documento actual para leer el Rate real de cada linea. // Reusar ConnectionServiceLayer.GetAsync(..., dinamico: true) - // ver nota de "gap de infraestructura" en PORTE_FUNCIONALIDAD_NUEVA.md. var doc = (await _connectServiceLayer.GetAsync( $"InventoryTransferRequests({docEntry})", 1, dinamico: true)).FirstOrDefault(); var ratesByLine = doc.StockTransferLines.ToDictionary(l => l.LineNum, l => l.Rate); // 2. Armar las lineas: omitir cantidad=0, preservar Rate real. var lines = new List(); foreach (var detalle in entidad.Detalle) { if (detalle.Cantidad == 0) continue; if (!ratesByLine.TryGetValue(detalle.Linea, out var rate)) { return resultadoTransaccion.GetInformacionResultado( $"La linea {detalle.Linea} no existe en la Solicitud de Traslado {docEntry}", entidad); } lines.Add(new { LineNum = detalle.Linea, Quantity = detalle.Cantidad, Rate = rate }); } // 3. PATCH con el header B1S-ReplaceCollectionsOnPatch. // Requiere el overload de PatchAsyncSBA con headers extra - ver gap #1. await _connectServiceLayer.PatchAsyncSBA( $"InventoryTransferRequests({docEntry})", new { StockTransferLines = lines }, extraHeaders: new Dictionary { ["B1S-ReplaceCollectionsOnPatch"] = "true" }); return null; // exito real de SAP = 204/200 real (ver controller abajo) } catch (Exception ex) { return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), entidad); } } ``` (Pseudocódigo de referencia — ajustar nombres exactos de clases/métodos una vez que exista el overload de `PatchAsyncSBA` con headers y la entidad `BE_InventoryTransferRequest`/`BE_InventoryTransferRequestLine` para deserializar el `GET`.) ## `POST /api/SolicitudTransferencia/{docEntry}/cerrar` Cierra el documento completo vía `POST InventoryTransferRequests({docEntry})/Close` — acción fija de Service Layer (no se traduce). Sin body de request, body vacío (`{}`) hacia SAP. Confirmado contra SAP real que cierra el documento completo aunque alguna línea ya se haya convertido en una Transferencia. ```csharp public async Task> Cerrar(int docEntry) { _metodoName = "Cerrar"; ResultadoTransaccion resultadoTransaccion = new ResultadoTransaccion(_metodoName, _aplicacionName); try { await _connectServiceLayer.PostAsync($"InventoryTransferRequests({docEntry})/Close", new { }); return null; } catch (Exception ex) { return resultadoTransaccion.GetInformacionResultado(ex.Message.ToString(), null); } } ``` ## Controller Igual patrón que `TransferenciaController`, con una diferencia importante de status code (ver nota abajo): ```csharp [HttpPatch("{docEntry}")] public async Task Patch(int docEntry, [FromBody] Dto_SolicitudTransferenciaPatchRequest value) { var error = await _repository.SolicitudTransferencia.Update(docEntry, value.RetornaModelo()); var result = error ?? new BE_ResultadoTransaccion { DocEntry = docEntry, StatusCode = 200, Message = "Registrado Correctamente", AppName = "SolicitudTransferenciaRepository", MethodName = "Update", Data = value, }; B_Log.Add(/* ... mismo patron que el resto de los controllers ... */); return error == null ? Ok(result) : BadRequest(result); // 200 con body si OK, 400 si error } [HttpPost("{docEntry}/cerrar")] public async Task Cerrar(int docEntry) { var error = await _repository.SolicitudTransferencia.Cerrar(docEntry); var result = error ?? new BE_ResultadoTransaccion { DocEntry = docEntry, StatusCode = 200, Message = "Cerrado Correctamente", AppName = "SolicitudTransferenciaRepository", MethodName = "Cerrar", }; B_Log.Add(/* ... */); return error == null ? Ok(result) : BadRequest(result); } ``` > **Por qué `200` con body y no `204`**: decisión deliberada, igual que en > Python. Un `204 No Content` no puede llevar body por spec HTTP (RFC 7231 > §6.3.5), y muchos clientes HTTP — **incluido `HttpClient` de .NET**, que es > justo el que va a consumir esto el WMS si está en .NET también — ni > siquiera lo leen si el status es `204`. `201` se descartó porque ni el > `PATCH` ni el `cerrar` crean un recurso nuevo. El resto de los endpoints > `PATCH` del proyecto (`Transferencia`) sigue en `204` sin body — esta es > una excepción deliberada, solo para estos dos endpoints nuevos. ## Respuestas de ejemplo ### `PATCH` éxito (`200`) ```json { "docEntry": 4521, "docNum": null, "statusCode": 200, "message": "Registrado Correctamente", "error": null, "appName": "SolicitudTransferenciaRepository", "methodName": "Update", "listData": null, "data": { "numOrden": "SOL-000123", "detalle": [ { "linea": 0, "cantidad": 8 } ] }, "nombreEstacion": "SERVER-WMS" } ``` ### `PATCH` error (`400`) ```json { "docEntry": null, "docNum": null, "statusCode": 500, "message": null, "error": "DocEntry no existe", "appName": "SolicitudTransferenciaRepository", "methodName": "Update", "listData": null, "data": { "numOrden": "SOL-000123", "detalle": [ "..." ] }, "nombreEstacion": "SERVER-WMS" } ``` (El `statusCode: 500` interno + el `400` HTTP real del controller es el mismo patrón que usa `ResultadoTransaccion.GetInformacionResultado(string, T)` ya existente — ver `Net.CrossCotting/ResultadoTransaccion.cs`.) ## Checklist de integración para el WMS (igual que la versión Python) 1. **Mandar solo la línea que cambia y no las demás** → borra las no incluidas (header de reemplazo completo). Reconstruir siempre el `detalle` completo. 2. **Mandar `cantidad: 0` esperando que quede en cero** → no, `0` significa "eliminar esta línea". 3. **Usar `linea` con un número que no existe en el documento** → SAP devuelve error, no crea una línea nueva. 4. **Esperar un `204 No Content`** → este endpoint responde `200` con body. ## Pendiente de validar en QA antes de dar por cerrado el porte - Correr los mismos casos de `tools/solicitud-transferencia-patch.http` y `tools/solicitud-transferencia-post-close.http` (del proyecto Python) contra el nuevo endpoint `.NET` levantado, no solo contra Service Layer directo. - Confirmar el nombre exacto del método de binding de parámetros del driver `Sap.Data.Hana` instalado (si se toca `HelperProcesoPiciking` como parte de este trabajo — ver bug #6 en `PORTE_FUNCIONALIDAD_NUEVA.md`, no aplica directo a este módulo pero comparte el mismo cliente HANA).