From c574a48c4fc12d2160cdb08addaa27ddb63321c1 Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Mon, 18 Aug 2025 14:34:24 -0300 Subject: [PATCH] Fix ParseGS1 to Backend --- Domain/Dtos/Stock/StockScanRawRequest.cs | 2 + .../Stock/LSStockScanController.cs | 56 +++++++--- .../obj/Debug/net8.0/ApiEndpoints.json | 2 +- .../Services/Stock/StockScanService.cs | 101 ++---------------- 4 files changed, 57 insertions(+), 104 deletions(-) create mode 100644 Domain/Dtos/Stock/StockScanRawRequest.cs diff --git a/Domain/Dtos/Stock/StockScanRawRequest.cs b/Domain/Dtos/Stock/StockScanRawRequest.cs new file mode 100644 index 0000000..5f0b207 --- /dev/null +++ b/Domain/Dtos/Stock/StockScanRawRequest.cs @@ -0,0 +1,2 @@ +namespace Domain.Dtos.Stock; +public record StockScanRawRequest(string Raw, int LocationId, int Page = 1, int PageSize = 10); diff --git a/phronCare.API/Controllers/Stock/LSStockScanController.cs b/phronCare.API/Controllers/Stock/LSStockScanController.cs index 6d50698..00479a3 100644 --- a/phronCare.API/Controllers/Stock/LSStockScanController.cs +++ b/phronCare.API/Controllers/Stock/LSStockScanController.cs @@ -45,36 +45,68 @@ namespace API.Controllers.Stock return Ok(result); } - // DTO liviano para el request RAW - public record StockScanRawRequest(string Raw, int LocationId, int Page = 1, int PageSize = 10); - /// /// Recibe un escaneo RAW (GS1-128/DataMatrix), lo parsea y ejecuta la búsqueda paginada. /// [HttpPost("parse-and-search")] public async Task>> ParseAndSearch([FromBody] StockScanRawRequest req) { - if (string.IsNullOrWhiteSpace(req.Raw)) + if (req is null || string.IsNullOrWhiteSpace(req.Raw)) return BadRequest("Raw is required."); - // 1) Parseo GS1 en backend - var parsed = Gs1CodeParser.Parse(req.Raw.Trim()); + var raw = req.Raw.Trim(); - // 2) Armar parámetros "ya parseados" + // 1) Parseo GS1 + var parsed = Gs1CodeParser.Parse(raw); + + // 2) ¿Hay AIs útiles? + bool hasAis = + !string.IsNullOrWhiteSpace(parsed.Lot) + || parsed.ExpirationDate.HasValue + || !string.IsNullOrWhiteSpace(parsed.Serial) + || !string.IsNullOrWhiteSpace(parsed.Variant); + + // 3) Elegir "code" con prioridades + string? code = LooksLikeGtin(parsed.Gtin) ? parsed.Gtin : null; + + // Opcional: si NO hay serial y querés usar Variant como último intento: + // if (code is null && string.IsNullOrWhiteSpace(parsed.Serial)) + // code = parsed.Variant; + + // Fallback a "código plano" (ej. factory/regulatory tipeado) si no hay AIs ni GTIN + if (code is null && !hasAis && IsPlainCode(raw)) + code = raw; + + // 4) Armar parámetros de búsqueda var sp = new StockItemParsedSearchParams { - Gtin = string.IsNullOrWhiteSpace(parsed.Gtin) ? parsed.Variant : parsed.Gtin, // (22) como fallback + Gtin = code, // tratado como "code" genérico en el repo Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, - Expiration = parsed.ExpirationDate.HasValue ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) : null, + Expiration = parsed.ExpirationDate.HasValue ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) : (DateOnly?)null, Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, LocationId = req.LocationId, - Page = req.Page, - PageSize = req.PageSize + Page = req.Page <= 0 ? 1 : req.Page, + PageSize = req.PageSize <= 0 ? 10 : req.PageSize }; - // 3) Delegar a la misma lógica que ya tenés implementada + // 5) Delegar al servicio existente var result = await _service.SearchParsedAsync(sp); return Ok(result); + + // === Helpers locales === + static bool LooksLikeGtin(string? x) + => !string.IsNullOrWhiteSpace(x) + && x!.All(char.IsDigit) + && (x.Length == 8 || x.Length == 12 || x.Length == 13 || x.Length == 14); + + static bool IsPlainCode(string s) + { + if (string.IsNullOrWhiteSpace(s)) return false; + if (s.Contains('$') || s.Contains((char)29) || s.Contains(' ')) return false; // FNC1/espacios + var prefix = s.Length >= 2 ? s[..2] : s; + if (prefix is "01" or "10" or "11" or "17" or "21" or "22") return false; // evita confundir AIs + return s.All(c => char.IsLetterOrDigit(c) || c is '-' or '/' or '_'); // agrega '.' si lo necesitás + } } } diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index cbed662..58dba96 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -1236,7 +1236,7 @@ "Parameters": [ { "Name": "req", - "Type": "API.Controllers.Stock.LSStockScanController\u002BStockScanRawRequest", + "Type": "Domain.Dtos.Stock.StockScanRawRequest", "IsRequired": true } ], diff --git a/phronCare.UIBlazor/Services/Stock/StockScanService.cs b/phronCare.UIBlazor/Services/Stock/StockScanService.cs index fdab276..f2ae4dd 100644 --- a/phronCare.UIBlazor/Services/Stock/StockScanService.cs +++ b/phronCare.UIBlazor/Services/Stock/StockScanService.cs @@ -1,7 +1,6 @@ using System.Net.Http.Json; using Domain.Dtos.Stock; using Domain.Generics; -//using Transversal.Services; public class StockScanService : IStockScanService { @@ -11,13 +10,19 @@ public class StockScanService : IStockScanService { _http = http; } + /// + /// Envía el RAW al backend (parsea + busca) y devuelve el primer match mapeado a la UI. + /// public async Task ParseAndMatchAsync(string rawInput, int locationId) { - if (string.IsNullOrWhiteSpace(rawInput)) return null; + if (string.IsNullOrWhiteSpace(rawInput)) + return null; - var payload = new { Raw = rawInput, LocationId = locationId, Page = 1, PageSize = 10 }; - var resp = await _http.PostAsJsonAsync("/api/lsstockscan/parse-and-search", payload); - if (!resp.IsSuccessStatusCode) return null; + var payload = new StockScanRawRequest(rawInput, locationId, Page: 1, PageSize: 10); + + using var resp = await _http.PostAsJsonAsync("/api/lsstockscan/parse-and-search", payload); + if (!resp.IsSuccessStatusCode) + return null; var pr = await resp.Content.ReadFromJsonAsync>(); var first = pr?.Items?.FirstOrDefault(); @@ -35,90 +40,4 @@ public class StockScanService : IStockScanService Serial = first.Serial // si lo devolvés en el DTO de scan }; } - - //public async Task ParseAndMatchAsync(string rawInput, int locationId) - //{ - // if (string.IsNullOrWhiteSpace(rawInput)) - // return null; - - // try - // { - // var parsed = new Gs1ScanResult(); - // //var parsed = ""; - // var raw = rawInput.Trim(); - - // bool hasParsedAis = !string.IsNullOrWhiteSpace(parsed.Lot) - // || parsed.ExpirationDate.HasValue - // || !string.IsNullOrWhiteSpace(parsed.Serial) - // || !string.IsNullOrWhiteSpace(parsed.Variant); // incluir (22) - - // string? gtinToSend = parsed.Gtin ?? parsed.Variant; // (22) como fallback - // if (gtinToSend is null && !hasParsedAis && IsPlainCode(raw)) - // gtinToSend = raw; // código plano tipeado (factory/regulatory) - - - // // 3. Armar parámetros de búsqueda - // var sp = new StockItemParsedSearchParams - // { - // Gtin = gtinToSend, - // Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, - // Expiration = parsed.ExpirationDate.HasValue - // ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) - // : null, - // Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, - // LocationId = locationId, - // Page = 1, - // PageSize = 10 - // }; - - // // 4. Log para depuración (quitar en producción) - // Console.WriteLine($"[ParseAndMatchAsync] Gtin={sp.Gtin}, Batch={sp.Batch}, Exp={sp.Expiration}, Serial={sp.Serial}, Loc={sp.LocationId}"); - - // // 5. Llamar a la API - // var resp = await _http.PostAsJsonAsync("/api/lsstockscan/search-parsed", sp); - // if (!resp.IsSuccessStatusCode) - // { - // var err = await resp.Content.ReadAsStringAsync(); - // Console.WriteLine($"[ParseAndMatchAsync] API devolvió error {resp.StatusCode}: {err}"); - // return null; - // } - - // // 6. Leer resultado - // var pr = await resp.Content.ReadFromJsonAsync>(); - // var first = pr?.Items?.FirstOrDefault(); - // if (first == null) - // { - // Console.WriteLine("[ParseAndMatchAsync] No se encontró ningún ítem que coincida."); - // return null; - // } - - // // 7. Mapear a DTO de selección - // return new StockItemSelectionDto - // { - // StockItemId = first.StockItemId, - // ProductId = first.ProductId, - // ProductName = first.ProductName, - // Batch = first.Batch ?? string.Empty, - // Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue), - // Quantity = first.AvailableQty, - // LocationId = first.LocationId ?? 0 - // }; - // } - // catch (Exception ex) - // { - // Console.WriteLine($"[ParseAndMatchAsync] Error inesperado: {ex}"); - // throw; - // } - - //} - //private static bool IsPlainCode(string s) - //{ - // // sin FNC1 ($), sin espacios y sin prefijos AI típicos - // if (s.Contains('$') || s.Contains((char)29) || s.Contains(' ')) return false; - // // evita raws que empiezan como AIs "01","10","17","21","22" - // var prefix = s.Length >= 2 ? s[..2] : s; - // if (prefix is "01" or "10" or "11" or "17" or "21" or "22") return false; - // // permite letras/dígitos y algunos separadores comunes - // return s.All(c => char.IsLetterOrDigit(c) || c is '-' or '/' or '_'); - //} }