Commit inicial: ITCA_API_SAP_WMS (.NET Core 3.1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-05 17:53:41 -05:00
commit 2b82744eb8
168 files changed
+11066

No files matched your search

+272
View File
@@ -0,0 +1,272 @@
# 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**.
## Resumen del contrato
| Método | Ruta | Cambia hoy | Qué hace |
|---|---|---|---|
| `POST` | `/api/picking` | **Sí** — agrega campo `Notificacion` | `"cierre"` → lógica actual completa (crea documento SAP); cualquier otro valor/ausente → solo actualiza estado en HANA, nunca toca SAP. |
| `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; } // "preliminar" | "cierre"; cualquier otro valor o 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
// === NUEVO: notificacion preliminar (diurna) - nunca toca SAP
if (!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 `notificacion == "cierre"` — no se toca nada
de ahí. Solo se agrega el `if` de corte después de la línea 96.
> **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** (reparto en curso — `Notificacion` ausente o
distinto de `"cierre"`):
```json
{
"numOrden": "OT-2026-001",
"docEntry": 500,
"proceso": "DESPACHO",
"codMotivo": "M01",
"idEstadoCubico": 6,
"codigoAlmacen": "ALM01",
"detalle": [
{ "item": 0, "codigoArticulo": "ART-001", "cantidadPedida": 3, "estado": "C" }
]
}
```
**Cierre** (mismo shape, agrega `notificacion: "cierre"` — crea el documento
SAP real, misma respuesta que hoy):
```json
{
"numOrden": "OT-2026-001",
"docEntry": 500,
"proceso": "DESPACHO",
"codMotivo": "M01",
"idEstadoCubico": 6,
"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<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry);
```
```csharp
public async Task<BE_ResultadoTransaccion<object>> CerrarHojaRuta(int docEntry)
{
_metodoName = "CerrarHojaRuta";
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
try
{
var connectServiceLayer = new ConnectionServiceLayer(_configuration, _clientFactory);
await connectServiceLayer.PostAsync<object>($"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<IActionResult> CerrarHojaRuta(int docEntry)
{
var error = await _repository.Picking.CerrarHojaRuta(docEntry);
var result = error ?? new BE_ResultadoTransaccion<object>
{
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").
+231
View File
@@ -0,0 +1,231 @@
# Porte de funcionalidad nueva desde la reescritura Python
Este documento (y los dos que lo acompañan, [`SOLICITUD_TRANSFERENCIA.md`](./SOLICITUD_TRANSFERENCIA.md)
y [`PICKING.md`](./PICKING.md)) son la guía para agregar a **este proyecto
`.NET Core 3.1`** la funcionalidad de negocio nueva que se diseñó e
implementó primero en la reescritura Python
(`/Users/george/Documents/Desarrollo/ITCG/itca_api_sap_wms_python`), porque
este `.NET` va a seguir corriendo en producción (IIS, Windows) más tiempo
del previsto originalmente.
**La lógica de negocio ya está validada en Python — acá es 100% traducción
de lenguaje, no rediseño.** El contrato HTTP (rutas, shapes de request/response,
status codes) debe quedar idéntico al de la API Python, porque ambas corren
sobre el mismo SAP Business One y el mismo WMS las va a consumir.
> Alcance confirmado: **solo la funcionalidad nueva** (sin equivalente en
> este `.NET` hoy). No se portan acá las correcciones de bugs de contrato que
> sí se hicieron en la reescritura Python (ej. `201` real en los `POST` de
> creación, `400` explícito en Inventario/Recibo en vez de éxito falso) —
> quedan fuera de este trabajo salvo que se decida lo contrario más adelante.
## Qué hay que agregar
1. **Módulo SolicitudTransferencia completo** (no existe ningún controller
ni repositorio hoy) — ver [`SOLICITUD_TRANSFERENCIA.md`](./SOLICITUD_TRANSFERENCIA.md).
2. **Picking diurno/nocturno** — campo `Notificacion` en el `POST /api/Picking`
existente + nuevo `POST /api/Picking/HojaRuta/{docEntry}/Cerrar` — ver
[`PICKING.md`](./PICKING.md).
## Dónde vive cada pieza hoy (mapa de archivos .NET)
| Concepto | Archivo |
|---|---|
| Controllers existentes | `Net.Business.Services/Controllers/*.cs` |
| Repositorios de negocio (WMS) | `Net.Data/WMS/*/` |
| Repositorios de Service Layer (SAP) | `Net.Data/ServicesLayer/*/` |
| Cliente HTTP de Service Layer | `Net.Connection.ServiceLayer/ConnectionServiceLayer.cs` |
| DTOs (shape del request HTTP) | `Net.Business.DTO/**/Dto_*.cs` |
| Entidades de negocio | `Net.Business.Entities/**/BE_*.cs` |
| Wrapper de inyección de dependencias | `Net.Data/DependencyInjection/{IRepositoryWrapper,RepositoryWrapper}.cs` |
| Wrapper de resultado (`BE_ResultadoTransaccion<T>`) | `Net.Business.Entities/ResultadoTransaccion/BE_ResultadoTransaccion.cs` + `Net.CrossCotting/ResultadoTransaccion.cs` |
| Logging de auditoría | `Net.Business.Services/Log/B_Log.cs` (se llama desde cada controller, después del repositorio) |
| Handler global de excepciones | `Net.Business.Services/Extensions/ExceptionMiddlewareExtensions.cs` |
Patrón a seguir para cada pieza nueva (igual que `TransferenciaController`/
`TransferenciaRepository`/`StockTransferRepository`): DTO → `RetornaModelo()`
→ Entidad → Repositorio de negocio (usa `ConnectionServiceLayer` + helpers de
HANA) → Controller (valida, llama al repo, llama a `B_Log.Add`, decide el
status code).
## Gaps de infraestructura compartida a tener en cuenta
Estos dos módulos nuevos necesitan cosas que `ConnectionServiceLayer.cs` hoy
**no soporta**, y hay que agregarlas:
1. **Headers extra en PATCH.** `SolicitudTransferencia` necesita mandar
`B1S-ReplaceCollectionsOnPatch: true` en el PATCH a SAP. Ningún método de
`ConnectionServiceLayer` (`PostAsync`, `PatchAsyncSBA`, etc.) acepta headers
adicionales hoy — todos hardcodean `Accept`/`Content-Type`/`Cookie`
únicamente. Hay que agregar un parámetro opcional (ej.
`Dictionary<string, string> extraHeaders = null`) a `PatchAsyncSBA<T>`.
2. **GET de un solo recurso (no colección).** `SolicitudTransferencia`
necesita `GET InventoryTransferRequests({docEntry})` (un objeto, no una
lista `.value`). `ConnectionServiceLayer.GetAsync<T>` ya tiene un
parámetro `dinamico` que deserializa `T` directo sin esperar el wrapper
`{ "value": [...] }` (`ConnectionServiceLayer.cs:363-373`) — **reusar esa
ruta** (`dinamico: true`) en vez de escribir un método nuevo.
3. **Acción `/Close` sin body.** Ni `SolicitudTransferencia` ni
`Picking/HojaRuta` tienen hoy un método para `POST {Recurso}({docEntry})/Close`
con body vacío — el único análogo es `StockTransferRepository.StockTransfer`
(`Net.Data/ServicesLayer/StockTransfer/StockTransferRepository.cs`), que sí
manda body. Hay que agregar un método nuevo (ej. `CloseAsyncSBA`) o un
overload de `PostAsync<T>` que acepte `obj: null`/`{}`.
## Bugs e inconsistencias encontrados en el código `.NET` existente
Auditado puntualmente el código que estos dos módulos nuevos van a tocar o
reusar (`ConnectionServiceLayer.cs`, `HelperProcesoPiciking.cs`,
`BE_ResultadoTransaccion.cs`). No es una auditoría completa del proyecto —
solo lo relevante para este trabajo.
### 1. `BE_ResultadoTransaccion<T>.NombreEstacion` — setter con recursión infinita (bug serio, no relacionado al porte pero lo tocan ambos módulos nuevos)
**Archivo**: `Net.Business.Entities/ResultadoTransaccion/BE_ResultadoTransaccion.cs`,
línea 36 (propiedad `NombreEstacion`).
```csharp
public string NombreEstacion { get => System.Environment.MachineName; set => NombreEstacion = System.Environment.MachineName; }
```
El `set` asigna `NombreEstacion = ...`, lo que vuelve a invocar al propio
setter — recursión infinita. Si alguna vez se invoca ese setter (por ejemplo,
si algo deserializa un `BE_ResultadoTransaccion<T>` con `JsonConvert.DeserializeObject`
y el JSON trae la propiedad `NombreEstacion`/`nombreEstacion`, o si cualquier
código hace `objeto.NombreEstacion = "algo"` directamente), el proceso
revienta con `StackOverflowException` — una excepción que **.NET no puede
atrapar con try/catch**, tumba el proceso entero (el worker de IIS).
Hoy no se dispara porque nada construye `BE_ResultadoTransaccion<T>` por
deserialización ni asigna esa propiedad directamente (todo pasa por
`ResultadoTransaccion<T>.GetInformacionResultado(...)`, que no la toca). Pero
al escribir el código nuevo para SolicitudTransferencia/HojaRuta, **nunca
hacer `.NombreEstacion = ...` a mano ni deserializar un `BE_ResultadoTransaccion<T>`
desde JSON** — usar siempre `GetInformacionResultado(...)`.
**Solución propuesta**: la propiedad solo necesita ser de lectura.
```csharp
public string NombreEstacion => System.Environment.MachineName;
```
(o, si hace falta que el setter exista por compatibilidad con algo que no
vimos en esta auditoría puntual, que el setter no haga nada: `set { }`).
### 2. `ConnectionServiceLayer.PatchAsyncSBA<T>` nunca marca éxito en un `204`
**Archivo**: `Net.Connection.ServiceLayer/ConnectionServiceLayer.cs`,
método `PatchAsyncSBA<T>`, líneas 739-856 (el caso puntual, líneas 840-848).
Con un `204 No Content` real de SAP, el código no entra en la rama de éxito
(la que solo cubre `200`/`201`, líneas 788-796) ni en ninguna rama de error
— el bloque `if (response_.StatusCode == NoContent) { ... }` (línea 840) está
con el cuerpo comentado, así que cae directo a `return default(T);` (línea
848), o sea `null`. Cualquier caller que acceda a una propiedad del
resultado (`.Exito`, `.DocEntry`, etc.) revienta con `NullReferenceException`.
Ya es un bug conocido y documentado en el proyecto Python (`CLAUDE.md`,
regla 4) — ahí se corrigió tratando un `204` real como éxito explícito.
**Importante**: confirmado que **nadie llama a este método hoy**
(`grep` de `PatchAsyncSBA` en todo el proyecto no da otro resultado que su
propia definición) — es seguro modificarlo directamente, no hace falta
duplicarlo para no romper nada en producción.
**Solución propuesta**: al construir el repositorio de SolicitudTransferencia,
modificar `PatchAsyncSBA<T>` para que un `204` (o `200`) se trate como éxito
real, sin intentar deserializar un body que puede venir vacío — por ejemplo,
devolviendo un `bool`/un objeto liviano de éxito en vez de `T` deserializado
a la fuerza, o agregando un chequeo explícito antes del `return default(T)`
final.
### 3. `ConnectionServiceLayer.PatchAsyncSBA<T>` — un `401` con `error.code` distinto de `"301"` también devuelve `null` silenciosamente
**Archivo**: `Net.Connection.ServiceLayer/ConnectionServiceLayer.cs`,
método `PatchAsyncSBA<T>`, líneas 797-822.
Dentro de la rama `401`, si `NumError < 2` y el código de error de SAP no es
`"301"` (el que dispara relogin + reintento), no hay ningún `goto band` ni
`return` — el flujo simplemente sale del `if/else if` (porque ya entró a la
rama `401`, la siguiente condición es `else if` y no se evalúa) y cae en el
`return default(T);` del final del método. Mismo síntoma que el bug anterior
(`null` silencioso, no un error explícito) pero por una causa distinta.
**Solución propuesta**: agregar un `else` final dentro de la rama `401` que
construya y devuelva un resultado de error explícito, en vez de dejar que el
flujo caiga al `return default(T)` genérico.
### 4. `ConnectionServiceLayer` — `throw` muerto después de `return` en 3 métodos
**Archivo**: `Net.Connection.ServiceLayer/ConnectionServiceLayer.cs`,
métodos `CancelAsyncSBA<T>` (líneas 621-625), `DeleteAsyncSBA<T>` (líneas
733-737) y `PatchAsyncSBA<T>` (líneas 851-855):
```csharp
catch (Exception ex)
{
return default(T);
throw new Exception(ex.Message.ToString());
}
```
El `throw` es código inalcanzable (el compilador de C# debería marcarlo con
el warning `CS0162`) — la excepción original queda silenciada sin loguear ni
propagar, y el caller recibe `null` sin ninguna pista de qué pasó.
**Solución propuesta**: sacar el `throw` muerto y decidir explícitamente el
comportamiento — si se quiere tragar el error y devolver `null`/un resultado
de error (dejar solo el `return`), o repropagar la excepción real (`throw;`,
sin el `return` antes). Para los métodos nuevos de SolicitudTransferencia, no
repetir este patrón: loguear o envolver el error en el `BE_ResultadoTransaccion`
de salida, igual que hace el resto del proyecto (ver `TransferenciaRepository.Create`,
que atrapa la excepción y la mete en `GetInformacionResultado(ex.Message, entidad)`).
### 5. `ConnectionServiceLayer.Login()` y `.LoginPost()` — código duplicado
**Archivo**: `Net.Connection.ServiceLayer/ConnectionServiceLayer.cs`,
`Login()` (líneas 44-130) y `LoginPost()` (líneas 131-218).
Son copias casi idénticas línea por línea — la única diferencia real es un
parámetro `Language` que está comentado en `LoginPost`. No es un bug
funcional, es una inconsistencia de mantenimiento (dos lugares que hay que
tocar a mano si cambia el login). No bloquea el porte de la funcionalidad
nueva — se deja solo documentado por si se decide refactorizar en el futuro.
**Solución propuesta**: unificar en un solo método (`LoginPost` ya tiene el
parámetro `Language` listo para habilitarse si hiciera falta).
### 6. `HelperProcesoPiciking.Set` — SQL injection real por interpolación de string
**Archivo**: `Net.Data/HelperData/ProcesoPiciking/HelperProcesoPiciking.cs`,
ambas sobrecargas de `Set` (líneas 14 y 28):
```csharp
cmd.CommandText = $"CALL\"B1_ARDI_PROD\".\"ITCA_WMS_SAP_SetProcesoPicking\"('{Proceso}', {IdEstado}, {DocEntry}, '', '', '{CodMotivo}', '{Code}')";
```
`Proceso` y `CodMotivo` vienen directo del body del request HTTP
(`entidad.Proceso`, `entidad.CodMotivo` en `PickingRepository.cs:96` y
`:374`) e, interpolados así, son un vector de SQL injection real — mismo
patrón de vulnerabilidad ya documentado y confirmado en el módulo Chatbot del
`.NET` (`CLAUDE.md` del proyecto Python, sección "Pendiente"). Este método es
justo el que hay que tocar para aislar la actualización de estado (ver
[`PICKING.md`](./PICKING.md)), así que es un buen momento para corregirlo de
una vez en vez de solo copiarlo.
**Solución propuesta**: parametrizar con `HanaParameter`/placeholders en vez
de interpolar el string, por ejemplo:
```csharp
cmd.CommandText = "CALL \"B1_ARDI_PROD\".\"ITCA_WMS_SAP_SetProcesoPicking\"(?, ?, ?, ?, ?, ?, ?)";
cmd.Parameters.Add(new HanaParameter("1", Proceso));
cmd.Parameters.Add(new HanaParameter("2", IdEstado));
cmd.Parameters.Add(new HanaParameter("3", DocEntry));
cmd.Parameters.Add(new HanaParameter("4", HoraInicio?.ToString() ?? ""));
cmd.Parameters.Add(new HanaParameter("5", HoraFin?.ToString() ?? ""));
cmd.Parameters.Add(new HanaParameter("6", CodMotivo));
cmd.Parameters.Add(new HanaParameter("7", Code));
```
(Ajustar la sintaxis exacta de binding de parámetros al driver `Sap.Data.Hana`
realmente instalado — confirmar la forma correcta de parámetros posicionales
de ese driver ADO.NET antes de aplicar, no asumida en esta auditoría.)
+301
View File
@@ -0,0 +1,301 @@
# 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<BE_SolicitudTransferenciaPatchDetalle> 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<BE_ResultadoTransaccion<object>> Update(int docEntry, BE_SolicitudTransferenciaPatchRequest entidad)
{
_metodoName = "Update";
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
try
{
// 1. GET del documento actual para leer el Rate real de cada linea.
// Reusar ConnectionServiceLayer.GetAsync<T>(..., dinamico: true) -
// ver nota de "gap de infraestructura" en PORTE_FUNCIONALIDAD_NUEVA.md.
var doc = (await _connectServiceLayer.GetAsync<BE_InventoryTransferRequest>(
$"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<object>();
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<object>(
$"InventoryTransferRequests({docEntry})",
new { StockTransferLines = lines },
extraHeaders: new Dictionary<string, string> { ["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<BE_ResultadoTransaccion<object>> Cerrar(int docEntry)
{
_metodoName = "Cerrar";
ResultadoTransaccion<object> resultadoTransaccion = new ResultadoTransaccion<object>(_metodoName, _aplicacionName);
try
{
await _connectServiceLayer.PostAsync<object>($"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<IActionResult> Patch(int docEntry, [FromBody] Dto_SolicitudTransferenciaPatchRequest value)
{
var error = await _repository.SolicitudTransferencia.Update(docEntry, value.RetornaModelo());
var result = error ?? new BE_ResultadoTransaccion<object>
{
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<IActionResult> Cerrar(int docEntry)
{
var error = await _repository.SolicitudTransferencia.Cerrar(docEntry);
var result = error ?? new BE_ResultadoTransaccion<object>
{
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<T>.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).