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:
commit
2b82744eb8
168 files changed
+11066
No files matched your search
+272
@@ -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").
|
||||
@@ -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.)
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user