using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AyCode.Core.Extensions; using AyCode.Core.Loggers; using AyCode.Services.Nav.Ekaer; using AyCode.Services.SignalRs; using DocumentFormat.OpenXml.Office2010.Excel; using FruitBank.Common.Dtos; using FruitBank.Common.Entities; using FruitBank.Common.Interfaces; using FruitBank.Common.Loggers; using FruitBank.Common.Models; using FruitBank.Common.Server; using FruitBank.Common.Server.Interfaces; using FruitBank.Common.Server.Services.Ekaer; using FruitBank.Common.Services.Ekaer; using FruitBank.Common.SignalRs; using LinqToDB; using Mango.Nop.Core.Dtos; using Mango.Nop.Core.Loggers; using Mango.Nop.Core.Models; using Nop.Core; using Nop.Core.Domain.Customers; using Nop.Core.Domain.Orders; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Factories; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Customers; using Nop.Services.Localization; using Nop.Web.Framework.Controllers; using NUglify.Helpers; namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers { //https://linq2db.github.io/articles/sql/Join-Operators.html public class FruitBankDataController( FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, MeasurementService measurementService, IWorkContext workContext, ICustomerService customerService, ICustomerRegistrationService customerRegistrationService, ILocalizationService localizationService, PreOrderConversionService preorderConversionService, IFruitBankEkaerService fruitBankEkaerService, IEkaerSettings ekaerSettings, IEnumerable logWriters) : BasePluginController, IFruitBankDataControllerServer { private const int LastShippingDays = 15; private readonly ILogger _logger = new Logger(logWriters.ToArray()); [SignalR(SignalRTags.ProcessAndSaveFullShippingJson)] public async Task> ProcessAndSaveFullShippingJson(string fullShippingJson, int customerId) { return await measurementService.ProcessAndSaveFullShippingJson(fullShippingJson, customerId); } [SignalR(SignalRTags.GetGenericAttributeDtosByEntityIdAndKeyGroup)] public async Task> GetGenericAttributeDtosByEntityIdAndKeyGroup(int productId, string keyGroup, int storeId) { return await ctx.GetGenericAttributeDtosByEntityIdAndKeyGroup(productId, keyGroup, storeId); } [SignalR(SignalRTags.AddGenericAttributeDto)] public async Task AddGenericAttributeDto(GenericAttributeDto genericAttributeDto) { await ctx.GenericAttributeDtos.InsertAsync(genericAttributeDto); return await ctx.GenericAttributeDtos.GetByIdAsync(genericAttributeDto.Id); } [SignalR(SignalRTags.UpdateGenericAttributeDto)] public async Task UpdateGenericAttributeDto(GenericAttributeDto genericAttributeDto) { await ctx.GenericAttributeDtos.UpdateAsync(genericAttributeDto); return await ctx.GenericAttributeDtos.GetByIdAsync(genericAttributeDto.Id); } [SignalR(SignalRTags.GetStockQuantityHistoryDtos)] public async Task> GetStockQuantityHistoryDtos() { _logger.Detail($"GetStockQuantityHistoryDtos invoked; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.StockQuantityHistoryDtos.GetAll(true).Where(sqh => sqh.CreatedOnUtc >= fromDateUtc).ToListAsync(); } [SignalR(SignalRTags.GetStockQuantityHistoryDtosByProductId)] public async Task> GetStockQuantityHistoryDtosByProductId(int productId) { _logger.Detail($"GetStockQuantityHistoryDtosByProductId invoked; productId: {productId}; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.StockQuantityHistoryDtos.GetByProductIdAsync(productId, true).Where(sqh => sqh.CreatedOnUtc >= fromDateUtc).ToListAsync(); } [SignalR(SignalRTags.GetMeasuringModels)] public Task> GetMeasuringModels() { throw new NotImplementedException("GetMeasuringModels"); } [SignalR(SignalRTags.GetPartners)] public async Task> GetPartners() { _logger.Detail($"GetPartners invoked"); // A telephelyeket is behúzzuk (CSAK a depókat, NEM a teljes GetAll(true) láncot) — a partnerek // úgyis cache-eltek, így a ShippingDocument-grid telephely-legördülője kliens-oldalon kaszkádolhat. return await ctx.Partners.GetAll().LoadWith(p => p.PartnerDepots).ToListAsync(); } [SignalR(SignalRTags.GetPartnerById)] public async Task GetPartnerById(int id) { _logger.Detail($"GetPartnerById invoked; id: {id}"); //var customers = await ctx.GetCustormersBySystemRoleName("Measuring").ToListAsync(); //_logger.Error($"COUNT: {customers.Count}"); return await ctx.Partners.GetByIdAsync(id, false); } [SignalR(SignalRTags.AddPartner)] public async Task AddPartner(Partner partner) { ArgumentNullException.ThrowIfNull(partner); _logger.Detail($"AddPartner invoked; id: {partner.Id}"); await ctx.Partners.InsertAsync(partner); return await ctx.Partners.GetByIdAsync(partner.Id, partner.ShippingDocuments != null); } [SignalR(SignalRTags.UpdatePartner)] public async Task UpdatePartner(Partner partner) { ArgumentNullException.ThrowIfNull(partner); _logger.Detail($"UpdatePartner invoked; id: {partner.Id}"); await ctx.Partners.UpdateAsync(partner); return await ctx.Partners.GetByIdAsync(partner.Id, partner.ShippingDocuments != null); } [SignalR(SignalRTags.GetCargoPartners)] public async Task> GetCargoPartners() { _logger.Detail($"GetCargoPartners invoked"); return await ctx.CargoPartners.GetAll(true).ToListAsync(); } [SignalR(SignalRTags.GetCargoPartnerById)] public async Task GetCargoPartnerById(int id) { _logger.Detail($"GetCargoPartnerById invoked; id: {id}"); return await ctx.CargoPartners.GetByIdAsync(id, true); } [SignalR(SignalRTags.AddCargoPartner)] public async Task AddCargoPartner(CargoPartner cargoPartner) { ArgumentNullException.ThrowIfNull(cargoPartner); _logger.Detail($"AddCargoPartner invoked; id: {cargoPartner.Id}"); await ctx.CargoPartners.InsertAsync(cargoPartner); return await ctx.CargoPartners.GetByIdAsync(cargoPartner.Id, cargoPartner.CargoTrucks != null); } [SignalR(SignalRTags.UpdateCargoPartner)] public async Task UpdateCargoPartner(CargoPartner cargoPartner) { ArgumentNullException.ThrowIfNull(cargoPartner); _logger.Detail($"UpdateCargoPartner invoked; id: {cargoPartner.Id}"); await ctx.CargoPartners.UpdateAsync(cargoPartner); return await ctx.CargoPartners.GetByIdAsync(cargoPartner.Id, cargoPartner.CargoTrucks != null); } [SignalR(SignalRTags.GetCargoTrucks)] public async Task> GetCargoTrucks() { _logger.Detail($"GetCargoTrucks invoked"); return await ctx.CargoTrucks.GetAll(true).ToListAsync(); } [SignalR(SignalRTags.GetCargoTruckById)] public async Task GetCargoTruckById(int id) { _logger.Detail($"GetCargoTruckById invoked; id: {id}"); return await ctx.CargoTrucks.GetByIdAsync(id, true); } [SignalR(SignalRTags.GetCargoTrucksByCargoPartnerId)] public async Task> GetCargoTrucksByCargoPartnerId(int cargoPartnerId) { _logger.Detail($"GetCargoTrucksByCargoPartnerId invoked; cargoPartnerId: {cargoPartnerId}"); return await ctx.CargoTrucks.GetByCargoPartnerId(cargoPartnerId, true).ToListAsync(); } [SignalR(SignalRTags.AddCargoTruck)] public async Task AddCargoTruck(CargoTruck cargoTruck) { ArgumentNullException.ThrowIfNull(cargoTruck); _logger.Detail($"AddCargoTruck invoked; id: {cargoTruck.Id}"); await ctx.CargoTrucks.InsertAsync(cargoTruck); return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true); } [SignalR(SignalRTags.UpdateCargoTruck)] public async Task UpdateCargoTruck(CargoTruck cargoTruck) { ArgumentNullException.ThrowIfNull(cargoTruck); _logger.Detail($"UpdateCargoTruck invoked; id: {cargoTruck.Id}"); await ctx.CargoTrucks.UpdateAsync(cargoTruck); return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true); } [SignalR(SignalRTags.GetPartnerDepots)] public async Task> GetPartnerDepots() { _logger.Detail($"GetPartnerDepots invoked"); return await ctx.PartnerDepots.GetAll(true).ToListAsync(); } [SignalR(SignalRTags.GetPartnerDepotById)] public async Task GetPartnerDepotById(int id) { _logger.Detail($"GetPartnerDepotById invoked; id: {id}"); return await ctx.PartnerDepots.GetByIdAsync(id, true); } [SignalR(SignalRTags.GetPartnerDepotsByPartnerId)] public async Task> GetPartnerDepotsByPartnerId(int partnerId) { _logger.Detail($"GetPartnerDepotsByPartnerId invoked; partnerId: {partnerId}"); return await ctx.PartnerDepots.GetByPartnerId(partnerId, true).ToListAsync(); } [SignalR(SignalRTags.AddPartnerDepot)] public async Task AddPartnerDepot(PartnerDepot partnerDepot) { ArgumentNullException.ThrowIfNull(partnerDepot); _logger.Detail($"AddPartnerDepot invoked; id: {partnerDepot.Id}"); await ctx.PartnerDepots.InsertAsync(partnerDepot); return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true); } [SignalR(SignalRTags.UpdatePartnerDepot)] public async Task UpdatePartnerDepot(PartnerDepot partnerDepot) { ArgumentNullException.ThrowIfNull(partnerDepot); _logger.Detail($"UpdatePartnerDepot invoked; id: {partnerDepot.Id}"); await ctx.PartnerDepots.UpdateAsync(partnerDepot); return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true); } [SignalR(SignalRTags.GetEkaerHistories)] public async Task> GetEkaerHistories(EkaerHistoryFilter ekaerHistoryFilter) { _logger.Detail($"GetEkaerHistories invoked; ekaerHistoryFilter: {ekaerHistoryFilter}"); return await ctx.EkaerHistories.GetByFilter(ekaerHistoryFilter, true).ToListAsync(); } [SignalR(SignalRTags.GetEkaerHistoryCount)] public async Task GetEkaerHistoryCount(EkaerHistoryFilter ekaerHistoryFilter) { _logger.Detail($"GetEkaerHistoryCount invoked; ekaerHistoryFilter: {ekaerHistoryFilter}"); return await ctx.EkaerHistories.CountByFilterAsync(ekaerHistoryFilter); } [SignalR(SignalRTags.GetEkaerHistoryById)] public async Task GetEkaerHistoryById(int id) { _logger.Detail($"GetEkaerHistoryById invoked; id: {id}"); return await ctx.EkaerHistories.GetByIdAsync(id, true); } [SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)] public async Task> GetEkaerHistoriesByForeignKey(int foreignKey) { _logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}"); // A forrás-rekordot (ShippingDocument/Order Id) lefedő EKÁER-deklaráció(k), a Mappings-szel betöltve — EGY query // (EXISTS a mapping-táblára), nem kettő. return await ctx.EkaerHistories.GetAll(true) .Where(eh => ctx.EkaerHistoryMappings.Table.Any(m => m.EkaerHistoryId == eh.Id && m.ForeignKey == foreignKey)) .ToListAsync(); } [SignalR(SignalRTags.AddEkaerHistory)] public async Task AddEkaerHistory(EkaerHistory ekaerHistory) { ArgumentNullException.ThrowIfNull(ekaerHistory); _logger.Detail($"AddEkaerHistory invoked; id: {ekaerHistory.Id}"); await ctx.EkaerHistories.InsertAsync(ekaerHistory); return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } [SignalR(SignalRTags.UpdateEkaerHistory)] public async Task UpdateEkaerHistory(EkaerHistory ekaerHistory) { ArgumentNullException.ThrowIfNull(ekaerHistory); _logger.Detail($"UpdateEkaerHistory invoked; id: {ekaerHistory.Id}"); await ctx.EkaerHistories.UpdateAsync(ekaerHistory); return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } [SignalR(SignalRTags.GenerateEkaerXmlDocument)] public async Task GenerateEkaerXmlDocument(int ekaerHistoryId) { _logger.Detail($"GenerateEkaerXmlDocument invoked; ekaerHistoryId: {ekaerHistoryId}"); var ekaerHistory = await ctx.EkaerHistories.GetByIdAsync(ekaerHistoryId, true) ?? throw new ArgumentException($"EkaerHistory not found; id: {ekaerHistoryId}", nameof(ekaerHistoryId)); // A deklarációhoz tartozó forrás-rekordok a BETÖLTÖTT mappingből (bejövő: N szállítólevél; kimenő: 1 rendelés). var foreignKeys = ekaerHistory.Mappings?.Select(m => m.ForeignKey).ToList() ?? []; if (foreignKeys.Count == 0) throw new InvalidOperationException($"EkaerHistory #{ekaerHistoryId} has no mapped source records."); if (!ekaerHistory.IsOutgoing) { // Bejövő: a csoport ÖSSZES szállítólevele EGY tradeCard-dá (összevont tömeg/érték); GetAll(true) = teljes gráf. var documents = await ctx.ShippingDocuments.GetAll(true).Where(sd => foreignKeys.Contains(sd.Id)).ToListAsync(); if (documents.Count == 0) throw new InvalidOperationException($"EkaerHistory #{ekaerHistoryId}: no ShippingDocuments found for the mapped ids."); ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(documents, ekaerHistory); } else { // Kimenő: egy rendelés (a GetByIdAsync(true) tölti a Customer-t, tételeket, palettákat (GrossWeight), ProductDto-t). var order = await ctx.OrderDtos.GetByIdAsync(foreignKeys[0], true) ?? throw new ArgumentException($"Order not found; id: {foreignKeys[0]}"); ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(order, ekaerHistory); } await ctx.EkaerHistories.UpdateAsync(ekaerHistory); // A friss sort adjuk vissza (frissített Modified + a betöltött Mappings); a sor biztosan létezik (épp updateltük). return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } /// /// Idempotens rekord-létrehozás: ha a forrás-rekordra (foreignKey + isOutgoing) már van mapping → a hozzá tartozó /// EkaerHistory-t adja vissza érintetlenül; különben új Pending rekordot + EGY mapping sort hoz létre (atomikusan). /// A generálás (XmlDoc) külön lépés: GenerateEkaerXmlDocument(ekaerHistoryId). /// [SignalR(SignalRTags.CreateEkaerHistory)] public async Task CreateEkaerHistory(int foreignKey, bool isOutgoing) { _logger.Detail($"CreateEkaerHistory invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}"); // Idempotencia: a forrás-rekordra (azonos irányban) már létező mapping → a hozzá tartozó EkaerHistory, // a Mappings-szel betöltve (GetAll(true)) — EGY query (EXISTS a mappingre), nincs külön újraolvasás. var existing = await LinqToDB.AsyncExtensions.FirstOrDefaultAsync( ctx.EkaerHistories.GetAll(true).Where(eh => eh.IsOutgoing == isOutgoing && ctx.EkaerHistoryMappings.Table.Any(m => m.EkaerHistoryId == eh.Id && m.ForeignKey == foreignKey))); if (existing != null) return existing; // A forrás-entitás léte irányfüggő: bejövő → ShippingDocument, kimenő → Order. if (!isOutgoing) { _ = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey)); } else { _ = await ctx.OrderDtos.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey)); } // EGY EkaerHistory + EGY mapping sor (egyelemű csoport), atomikusan. var ekaerHistory = await ctx.AddEkaerHistoryWithMappingsAsync(new EkaerHistory { IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }, [foreignKey]); // A friss sort adjuk vissza, a betöltött Mappings-szel; a sor biztosan létezik (épp beszúrtuk). return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } /// /// A megadott dátumtól kezdődő, EKÁER-rekord nélküli szállítólevelekre Pending rekordot hoz létre. /// User-vezérelt (toolbar gomb) — szándékosan NINCS automata rekord-érzékelés: a létrehozás explicit, /// idempotens, és bármikor újrafuttatható (maga a gomb a "reconciliation"). /// [SignalR(SignalRTags.CreateMissingEkaerHistories)] public async Task CreateMissingEkaerHistories(DateTime fromDate) { _logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}"); var toCreate = new List<(EkaerHistory History, List ForeignKeys)>(); var messages = new HashSet(); // dedup: ugyanaz a partner (pl. hiányzó országkód) több csoportban is előjöhet // ── Bejövő ── Rekord nélküli szállítólevelek a dátumtól, Partnerrel + tételekkel betöltve a kötelezettség-kapuhoz. // A bejelentés-egység (Shipping, Partner, PartnerDepot) = feladó(+telephely)→címzett→jármű: EGY EkaerHistory/csoport, a küszöb a csoport ÖSSZES dokumentumára aggregál. var candidates = await ctx.ShippingDocuments.GetAll() .Where(sd => sd.ShippingDate >= fromDate) .Where(sd => !ctx.EkaerHistoryMappings.Table.Any(m => m.ForeignKey == sd.Id && ctx.EkaerHistories.Table.Any(eh => eh.Id == m.EkaerHistoryId && !eh.IsOutgoing))) .LoadWith(sd => sd.Partner) .LoadWith(sd => sd.ShippingItems) .ToListAsync(); // Partner nélküli (még nincs rendesen felvett) szállítólevél CSENDBEN kihagyva — nincs feladó, nem kész; // a mentes partner (IsEkaer=false) is. A maradék (Shipping, Partner, PartnerDepot) csoportban (a dokumentum mindig kap Shippinget). foreach (var group in candidates.Where(sd => sd.Partner != null && sd.Partner.IsEkaer != false).GroupBy(sd => (sd.ShippingId, sd.PartnerId, sd.PartnerDepotId))) { var docs = group.ToList(); try { var result = fruitBankEkaerService.EvaluateObligation(docs); if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; } if (result.Obligation == EkaerObligation.NotRequired) continue; // Kötelező → EGY EkaerHistory a csoportra + a csoport dokumentumai mapping-foreignKey-ként. toCreate.Add((new EkaerHistory { IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }, docs.Select(d => d.Id).ToList())); } catch (Exception ex) { _logger.Error($"CreateMissingEkaerHistories; inbound evaluate failed; ShippingId: {group.Key.ShippingId}; PartnerId: {group.Key.PartnerId}; PartnerDepotId: {group.Key.PartnerDepotId}", ex); messages.Add($"Szállítólevél-csoport (Shipping #{group.Key.ShippingId}, Partner #{group.Key.PartnerId}, Telephely #{group.Key.PartnerDepotId?.ToString() ?? "—"}): a kötelezettség nem dönthető el — {ex.Message}"); } } // ── Kimenő ── Rekord nélküli, lezárt (Complete) rendelések a dátumtól; rendelésenként vizsgáljuk (nincs Shipping). // Előbb csak az azonosítók (könnyű szűrés), majd EGY batch-betöltés a teljes gráffal (GetAllByIds) — nincs N+1. var missingOrderIds = await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false) .Where(o => o.GenericAttributes.Any(ga => ga.Key == nameof(OrderDto.DateOfReceipt) && DateTime.Parse(ga.Value) >= fromDate.Date)) .Where(o => !ctx.EkaerHistoryMappings.Table.Any(m => m.ForeignKey == o.Id && ctx.EkaerHistories.Table.Any(eh => eh.Id == m.EkaerHistoryId && eh.IsOutgoing))) .Select(o => o.Id) .ToListAsync(); var missingOrders = missingOrderIds.Count == 0 ? [] : await ctx.OrderDtos.GetAllByIds(missingOrderIds, true).ToListAsync(); foreach (var order in missingOrders) { try { var result = fruitBankEkaerService.EvaluateObligation(order); if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; } if (result.Obligation == EkaerObligation.NotRequired) continue; toCreate.Add((new EkaerHistory { IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }, [order.Id])); } catch (Exception ex) { _logger.Error($"CreateMissingEkaerHistories; outbound evaluate failed; OrderId: {order.Id}", ex); messages.Add($"Rendelés #{order.Id}: a kötelezettség nem dönthető el — {ex.Message}"); } } // ── Beszúrás ── var createdCount = 0; foreach (var (history, foreignKeys) in toCreate) { try { // EkaerHistory + a forrás-mapping sorok atomikusan (a junction tartja a kapcsolatot, nem a rendszer-tábla). await ctx.AddEkaerHistoryWithMappingsAsync(history, foreignKeys); createdCount++; } catch (Exception ex) { _logger.Error($"CreateMissingEkaerHistories; insert failed; IsOutgoing: {history.IsOutgoing}; foreignKeys: [{string.Join(",", foreignKeys)}]", ex); } } _logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; messages: {messages.Count}; fromDate: {fromDate:yyyy-MM-dd}"); return new EkaerCreateResult { CreatedCount = createdCount, Messages = [.. messages.OrderBy(m => m)] }; } [SignalR(SignalRTags.GetShippings)] public async Task> GetShippings() { _logger.Detail($"GetShippings invoked; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.Shippings.GetAll(true).Where(s => s.ShippingDate >= fromDateUtc).ToListAsync(); //return await ctx.Shippings.Table.LoadWith(sd => sd.ShippingDocuments).ThenLoad(si => si.ShippingItems).ToListAsync(); } [SignalR(SignalRTags.GetNotMeasuredShippings)] public async Task> GetNotMeasuredShippings() { _logger.Detail($"GetNotMeasuredShippings invoked; lastDaysCount: {LastShippingDays}"); var startTime = DateTime.Now; var shippings = await ctx.Shippings.GetAllNotMeasured(true, LastShippingDays).ToListAsync(); _logger.Detail($"GetNotMeasuredShippings; shippingsCount: {shippings.Count}; dbResponse: {(DateTime.Now - startTime).TotalSeconds} sec."); return shippings; } [SignalR(SignalRTags.GetShippingById)] public async Task GetShippingById(int id) { _logger.Detail($"GetShippingById invoked; id: {id}"); return await ctx.Shippings.GetByIdAsync(id, true); } [SignalR(SignalRTags.AddShipping)] public async Task AddShipping(Shipping shipping) { ArgumentNullException.ThrowIfNull(shipping); _logger.Detail($"AddShipping invoked; id: {shipping.Id}"); await ctx.Shippings.InsertAsync(shipping); return await ctx.Shippings.GetByIdAsync(shipping.Id, true); } [SignalR(SignalRTags.UpdateShipping)] public async Task UpdateShipping(Shipping shipping) { ArgumentNullException.ThrowIfNull(shipping); _logger.Detail($"UpdateShipping invoked; id: {shipping.Id}"); await ctx.Shippings.UpdateAsync(shipping); return await ctx.Shippings.GetByIdAsync(shipping.Id, true); } [SignalR(SignalRTags.GetShippingItems)] public async Task> GetShippingItems() { _logger.Detail($"GetShippingItems invoked; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.ShippingItems.GetAll(true).Where(si => si.ShippingDocument.Shipping == null || si.ShippingDocument.Shipping.ShippingDate >= fromDateUtc).ToListAsync(); } [SignalR(SignalRTags.GetShippingItemsByDocumentId)] public async Task> GetShippingItemsByDocumentId(int shippingDocumentId) { _logger.Detail($"GetShippingItemsByDocumentId invoked"); return await ctx.ShippingItems.GetAllByShippingDocumentIdAsync(shippingDocumentId, true).ToListAsync(); } [SignalR(SignalRTags.GetShippingItemsByShippingId)] public async Task> GetShippingItemsByShippingId(int shippingId) { _logger.Detail($"GetShippingItemsByShippingId invoked"); return await ctx.ShippingItems.GetAllByShippingIdAsync(shippingId, true).ToListAsync(); } [SignalR(SignalRTags.GetShippingItemsByPartnerId)] public async Task> GetShippingItemsByPartnerId(int partnerId) { _logger.Detail($"GetShippingItemsByPartnerId invoked"); return await ctx.ShippingItems.GetAllByPartnerIdAsync(partnerId, true).ToListAsync(); } [SignalR(SignalRTags.GetShippingItemById)] public async Task GetShippingItemById(int id) { _logger.Detail($"GetShippingItemById invoked; id: {id}"); var shippingItem = await ctx.ShippingItems.GetByIdAsync(id, true); return shippingItem; } [SignalR(SignalRTags.AddShippingItem)] public async Task AddShippingItem(ShippingItem shippingItem) { ArgumentNullException.ThrowIfNull(shippingItem); _logger.Detail($"AddShippingItem invoked; id: {shippingItem.Id}"); if (!await ctx.AddShippingItemAsync(shippingItem)) return null; // Update IncomingQuantity — EventConsumer handles conversion separately if (shippingItem.ProductId.HasValue && shippingItem.QuantityOnDocument > 0) await preorderConversionService.SyncIncomingQuantityAsync( shippingItem.ProductId.Value, 0, shippingItem.QuantityOnDocument); return await ctx.ShippingItems.GetByIdAsync(shippingItem.Id, true); } [SignalR(SignalRTags.UpdateShippingItem)] public async Task UpdateShippingItem(ShippingItem shippingItem) { ArgumentNullException.ThrowIfNull(shippingItem); _logger.Detail($"UpdateShippingItem invoked; id: {shippingItem.Id}"); // Load BEFORE the update to capture previous ProductId and QuantityOnDocument var oldItem = await ctx.ShippingItems.GetByIdAsync(shippingItem.Id, false); if (!await ctx.UpdateShippingItemSafeAsync(shippingItem)) return null; if (oldItem != null) { var productChanged = oldItem.ProductId != shippingItem.ProductId; var quantityChanged = oldItem.QuantityOnDocument != shippingItem.QuantityOnDocument; if (productChanged && shippingItem.ProductId.HasValue) { // Full replacement: swap stock, order items, preorder items await preorderConversionService.ReplaceShippingItemProductAsync( shippingItem.Id, shippingItem.ProductId.Value, oldItem); } else if (quantityChanged && shippingItem.ProductId.HasValue) { // Only quantity changed: sync IncomingQuantity delta await preorderConversionService.SyncIncomingQuantityAsync( shippingItem.ProductId.Value, oldItem.QuantityOnDocument, shippingItem.QuantityOnDocument); // If quantity increased, trigger conversion // (EventConsumer also fires this, double-call is idempotent) if (shippingItem.QuantityOnDocument > oldItem.QuantityOnDocument) _ = Task.Run(async () => await preorderConversionService .ConvertPreOrdersForProductsAsync( new List { shippingItem.ProductId.Value }, shippingItem.ShippingDocumentId)); } } return await ctx.ShippingItems.GetByIdAsync(shippingItem.Id, shippingItem.ShippingDocument != null); } [SignalR(SignalRTags.UpdateMeasuredShippingItem)] public async Task UpdateMeasuredShippingItem(ShippingItem shippingItem) { ArgumentNullException.ThrowIfNull(shippingItem); _logger.Detail($"UpdateMeasuredShippingItem invoked; id: {shippingItem.Id}"); if (!await ctx.UpdateMeasuredShippingItemSafeAsync(shippingItem)) return null; return await ctx.ShippingItems.GetByIdAsync(shippingItem.Id, shippingItem.ShippingDocument != null); } [SignalR(SignalRTags.GetShippingItemPallets)] public async Task> GetShippingItemPallets() { _logger.Detail($"GetShippingItemPallets invoked; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.ShippingItemPallets.GetAll(true).Where(sip => sip.ShippingItem.ShippingDocument.Shipping == null || sip.ShippingItem.ShippingDocument.Shipping.ShippingDate >= fromDateUtc).ToListAsync(); } [SignalR(SignalRTags.AddShippingItemPallet)] public async Task AddShippingItemPallet(ShippingItemPallet shippingItemPallet) { ArgumentNullException.ThrowIfNull(shippingItemPallet); _logger.Detail($"AddShippingItemPallet invoked; {shippingItemPallet}"); if (!await ctx.AddShippingItemPalletSafeAsync(shippingItemPallet)) return null; return await ctx.ShippingItemPallets.GetByIdAsync(shippingItemPallet.Id, false); } [SignalR(SignalRTags.UpdateShippingItemPallet)] public async Task UpdateShippingItemPallet(ShippingItemPallet shippingItemPallet) { ArgumentNullException.ThrowIfNull(shippingItemPallet); _logger.Detail($"UpdateShippingItemPallet invoked; {shippingItemPallet}"); if (!await ctx.UpdateShippingItemPalletSafeAsync(shippingItemPallet)) return null; return await ctx.ShippingItemPallets.GetByIdAsync(shippingItemPallet.Id, false); } [SignalR(SignalRTags.AddOrUpdateMeasuredShippingItemPallets)] public async Task AddOrUpdateMeasuredShippingItemPallets(List shippingItemPallets) { // ArgumentNullException.ThrowIfNull(shippingItemPallets); // _logger.Detail($"AddOrUpdateMeasuredShippingItemPallets invoked; count: {shippingItemPallets.Count}"); // if (shippingItemPallets.Count == 0) return null; // var shippingItemId = shippingItemPallets.FirstOrDefault()!.ShippingItemId; // if (shippingItemId <= 0 || shippingItemPallets.Any(x => x.ShippingItemId != shippingItemId)) return null; // var shippingItem = await ctx.ShippingItems.GetByIdAsync(shippingItemId, false); // shippingItem.ShippingItemPallets = shippingItemPallets.Where(sip => sip.IsValidMeasuringValues(shippingItem.IsMeasurable)).ToList(); // return await UpdateMeasuredShippingItem(shippingItem); return null; } [SignalR(SignalRTags.AddOrUpdateMeasuredShippingItemPallet)] public async Task AddOrUpdateMeasuredShippingItemPallet(ShippingItemPallet shippingItemPallet) { ArgumentNullException.ThrowIfNull(shippingItemPallet); _logger.Detail($"AddOrUpdateMeasuredShippingItemPallet invoked; {shippingItemPallet}"); if (!await ctx.AddOrUpdateShippingItemPalletSafeAsync(shippingItemPallet)) return null; var savedShippingItemPallet = await ctx.ShippingItemPallets.GetByIdAsync(shippingItemPallet.Id, true); //update average weight and quantity on Product await measurementService.CalculateAndSetAverageWeight(savedShippingItemPallet); //return await ctx.ShippingItemPallets.GetByIdAsync(shippingItemPallet.Id, false); return shippingItemPallet; } [SignalR(SignalRTags.GetShippingDocuments)] public async Task> GetShippingDocuments() { _logger.Detail($"GetShippingDocuments invoked; lastDaysCount: {LastShippingDays}"); var fromDateUtc = DateTime.UtcNow.Date.AddDays(-LastShippingDays); return await ctx.ShippingDocuments.GetAll(true).Where(sd => sd.Shipping == null || sd.Shipping.ShippingDate >= fromDateUtc).ToListAsync(); } [SignalR(SignalRTags.GetShippingDocumentById)] public async Task GetShippingDocumentById(int id) { _logger.Detail($"GetShippingDocumentById invoked; id: {id}"); return await ctx.ShippingDocuments.GetByIdAsync(id, true); } [SignalR(SignalRTags.GetShippingDocumentsByShippingId)] public async Task> GetShippingDocumentsByShippingId(int shippingId) { _logger.Detail($"GetShippingDocumentsByShippingId invoked; shippingId: {shippingId}"); return await ctx.ShippingDocuments.GetAllByShippingIdAsync(shippingId, true).ToListAsync(); } [SignalR(SignalRTags.GetShippingDocumentsByProductId)] public async Task> GetShippingDocumentsByProductId(int productId) { _logger.Detail($"GetShippingDocumentsByProductId invoked; productId: {productId}"); return await ctx.ShippingDocuments.GetAllByProductIdAsync(productId, true).ToListAsync(); } [SignalR(SignalRTags.GetShippingDocumentsByPartnerId)] public async Task> GetShippingDocumentsByPartnerId(int partnerId) { _logger.Detail($"GetShippingDocumentsByPartnerId invoked; partnerId: {partnerId}"); return await ctx.ShippingDocuments.GetAllByPartnerIdAsync(partnerId, true).ToListAsync(); } [SignalR(SignalRTags.AddShippingDocument)] public async Task AddShippingDocument(ShippingDocument shippingDocument) { ArgumentNullException.ThrowIfNull(shippingDocument); _logger.Detail($"AddShippingDocument invoked; id: {shippingDocument.Id}"); await ctx.ShippingDocuments.InsertAsync(shippingDocument); if(shippingDocument.ShippingItems != null) { foreach (var item in shippingDocument.ShippingItems) { var product = await ctx.Products.GetByIdAsync(item.ProductId); if(product != null) { product.ProductCost = Convert.ToDecimal(item.UnitPriceOnDocument); } } } return await ctx.ShippingDocuments.GetByIdAsync(shippingDocument.Id, shippingDocument.Shipping != null || shippingDocument.Partner != null); } [SignalR(SignalRTags.UpdateShippingDocument)] public async Task UpdateShippingDocument(ShippingDocument shippingDocument) { ArgumentNullException.ThrowIfNull(shippingDocument); _logger.Detail($"UpdateShippingDocument invoked; id: {shippingDocument.Id}"); await ctx.ShippingDocuments.UpdateAsync(shippingDocument); return await ctx.ShippingDocuments.GetByIdAsync(shippingDocument.Id, shippingDocument.Shipping != null || shippingDocument.Partner != null); } [SignalR(SignalRTags.GetMeasuringUsers)] public async Task> GetMeasuringUsers() { _logger.Detail($"GetMeasuringUsers invoked"); var customers = await ctx.GetCustomersBySystemRoleName(FruitBankConst.MeasuringRoleSystemName).Select(c => new CustomerDto(c)).ToListAsync(); return customers; //.ToModelDto(); } [SignalR(SignalRTags.GetCustomerRolesByCustomerId)] public async Task> GetCustomerRolesByCustomerId(int customerId) { _logger.Detail($"GetCustomerRolesByCustomerId invoked; customerId: {customerId}"); return await ctx.GetCustomerRolesByCustomerId(customerId).ToListAsync(); } [SignalR(SignalRTags.GetProductDtos)] public async Task> GetProductDtos() { _logger.Detail($"GetProductDtos invoked"); return await ctx.ProductDtos.GetAll(false).ToListAsync(); } //[SignalR(SignalRTags.GetAllMeasuringProductDtos)] //public async Task> GetAllMeasuringProductDtos() //{ // _logger.Detail($"GetAllMeasuringProductDtos invoked"); // return await ctx.GetAllMeasuringProductDtos(false).ToListAsync(); //} [SignalR(SignalRTags.GetMeasuringProductDtoById)] public async Task GetProductDtoById(int productId) { _logger.Detail($"GetProductDtoById invoked; productId: {productId}"); return await ctx.ProductDtos.GetByIdAsync(productId, true); } [SignalR(SignalRTags.AuthenticateUser)] public async Task LoginMeasuringUser(MgLoginModelRequest loginModelRequest) { var customerEmail = loginModelRequest?.Email; var customerPassword = loginModelRequest?.Password; _logger.Detail($"LoginMeasuringUser invoked; customerEmail; {customerEmail}"); var resultLoginModel = new MgLoginModelResponse(); if (!customerEmail.IsNullOrWhiteSpace() && !customerPassword.IsNullOrWhiteSpace()) { var loginResult = await customerRegistrationService.ValidateCustomerAsync(customerEmail, customerPassword); switch (loginResult) { case CustomerLoginResults.Successful: { var customer = await customerService.GetCustomerByEmailAsync(customerEmail); var isInMeasuringRole = await customerService.IsInCustomerRoleAsync(customer, FruitBankConst.MeasuringRoleSystemName); if (!isInMeasuringRole) { resultLoginModel.ErrorMessage = "Is not in MeauringRole!"; break; } //var actionResult = await customerRegistrationService.SignInCustomerAsync(customer, returnUrl, loginModel.RememberMe); //await _workContext.SetCurrentCustomerAsync(customer); //await _authenticationService.SignInAsync(customer, isPersist); ////raise event //await _eventPublisher.PublishAsync(new CustomerLoggedinEvent(customer)); resultLoginModel.CustomerDto = new CustomerDto(customer); //customer.ToModel(); break; } case CustomerLoginResults.CustomerNotExist: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials.CustomerNotExist"); break; case CustomerLoginResults.Deleted: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials.Deleted"); break; case CustomerLoginResults.NotActive: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials.NotActive"); break; case CustomerLoginResults.NotRegistered: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials.NotRegistered"); break; case CustomerLoginResults.LockedOut: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials.LockedOut"); break; case CustomerLoginResults.WrongPassword: default: resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials"); break; } } else resultLoginModel.ErrorMessage = await localizationService.GetResourceAsync("Account.Login.WrongCredentials"); if (!resultLoginModel.ErrorMessage.IsNullOrWhiteSpace()) _logger.Error($"{resultLoginModel.ErrorMessage}; email: {customerEmail}"); return resultLoginModel; } } }