12 KiB
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
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/integracionde 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 (verTransferenciaController/PickingControllercomo 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:
- Alcanza con mandar
LineNum+Quantitypor línea — nada deItemCode,WarehouseCode, fechas, etc. - SAP rechaza una línea si se manda explícita con
Quantity: 0— por eso una línea conCantidad: 0en el request se omite del array en vez de mandarse en cero. - 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 trateStockTransferLinescomo reemplazo completo del array, no un merge parcial. - Hallazgo posterior, crítico (no estaba cubierto en la primera tanda de
pruebas, que usó un documento con
Rate=0en todas las líneas): al ser un reemplazo completo, SAP también exige elRatede 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 unGET InventoryTransferRequests({docEntry})antes del PATCH, leer elRatereal de cada línea, y reenviarlo tal cual junto con laQuantitynueva — nunca mandarRate: 0fijo, 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 condecimal.Decimaldehdbcli—Newtonsoft.Jsonserializadecimal/doublesin problema — pero la regla de negocio de todas formas aplica igual: hay que leer elRatereal, no inventarlo).
⚠️ Consecuencia del header para el WMS: como el array es reemplazo completo, cualquier línea que el request no incluya en
Detalletambién se elimina de SAP — igual que si viniera conCantidad: 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
// 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})
{
"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:
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.
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):
[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é
200con body y no204: decisión deliberada, igual que en Python. Un204 No Contentno puede llevar body por spec HTTP (RFC 7231 §6.3.5), y muchos clientes HTTP — incluidoHttpClientde .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 es204.201se descartó porque ni elPATCHni elcerrarcrean un recurso nuevo. El resto de los endpointsPATCHdel proyecto (Transferencia) sigue en204sin body — esta es una excepción deliberada, solo para estos dos endpoints nuevos.
Respuestas de ejemplo
PATCH éxito (200)
{
"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)
{
"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)
- Mandar solo la línea que cambia y no las demás → borra las no
incluidas (header de reemplazo completo). Reconstruir siempre el
detallecompleto. - Mandar
cantidad: 0esperando que quede en cero → no,0significa "eliminar esta línea". - Usar
lineacon un número que no existe en el documento → SAP devuelve error, no crea una línea nueva. - Esperar un
204 No Content→ este endpoint responde200con body.
Pendiente de validar en QA antes de dar por cerrado el porte
- Correr los mismos casos de
tools/solicitud-transferencia-patch.httpytools/solicitud-transferencia-post-close.http(del proyecto Python) contra el nuevo endpoint.NETlevantado, no solo contra Service Layer directo. - Confirmar el nombre exacto del método de binding de parámetros del driver
Sap.Data.Hanainstalado (si se tocaHelperProcesoPicikingcomo parte de este trabajo — ver bug #6 enPORTE_FUNCIONALIDAD_NUEVA.md, no aplica directo a este módulo pero comparte el mismo cliente HANA).