From c71bf2fcd86677bfcb6c71d187cb92c6b699a948 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 16 Jun 2026 21:45:49 +0200 Subject: [PATCH] =?UTF-8?q?EK=C3=81ER:=20add=20mapping=20table,=20group=20?= =?UTF-8?q?logic,=20atomic=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored EKÁER logic to use EkaerHistoryMapping junction table, decoupling EkaerHistory from direct foreign keys. Grouped inbound declarations by (ShippingId, PartnerId, PartnerDepotId) and enabled atomic creation of histories with mappings. Updated controller endpoints, DI registration, and documentation. Improved error handling, logging, and adjusted method signatures for the new mapping-based approach. --- .../Controllers/FruitBankDataController.cs | 99 +++++++++++-------- .../Domains/DataLayer/EkaerHistoryDbTable.cs | 19 ++-- .../DataLayer/EkaerHistoryMappingDbTable.cs | 26 +++++ .../Domains/DataLayer/FruitBankDbContext.cs | 23 ++++- .../Infrastructure/PluginNopStartup.cs | 1 + .../Mapping/NameCompatibility.cs | 1 + .../docs/EKAER/EKAER_TODO.md | 17 ++++ Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md | 8 +- 8 files changed, 140 insertions(+), 54 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryMappingDbTable.cs diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index c82fed4..a948e96 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -275,7 +275,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers public async Task> GetEkaerHistories(EkaerHistoryFilter ekaerHistoryFilter) { _logger.Detail($"GetEkaerHistories invoked; ekaerHistoryFilter: {ekaerHistoryFilter}"); - return await ctx.EkaerHistories.GetByFilter(ekaerHistoryFilter).ToListAsync(); + return await ctx.EkaerHistories.GetByFilter(ekaerHistoryFilter, true).ToListAsync(); } [SignalR(SignalRTags.GetEkaerHistoryCount)] @@ -289,14 +289,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers public async Task GetEkaerHistoryById(int id) { _logger.Detail($"GetEkaerHistoryById invoked; id: {id}"); - return await ctx.EkaerHistories.GetByIdAsync(id); + return await ctx.EkaerHistories.GetByIdAsync(id, true); } [SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)] public async Task> GetEkaerHistoriesByForeignKey(int foreignKey) { _logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}"); - return await ctx.EkaerHistories.GetByForeignKey(foreignKey).ToListAsync(); + + // 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)] @@ -307,7 +312,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers _logger.Detail($"AddEkaerHistory invoked; id: {ekaerHistory.Id}"); await ctx.EkaerHistories.InsertAsync(ekaerHistory); - return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id); + return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } [SignalR(SignalRTags.UpdateEkaerHistory)] @@ -318,48 +323,57 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers _logger.Detail($"UpdateEkaerHistory invoked; id: {ekaerHistory.Id}"); await ctx.EkaerHistories.UpdateAsync(ekaerHistory); - return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id); + return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true); } [SignalR(SignalRTags.GenerateEkaerXmlDocument)] - public async Task GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing) + public async Task GenerateEkaerXmlDocument(int ekaerHistoryId) { - _logger.Detail($"GenerateEkaerXmlDocument invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}"); + _logger.Detail($"GenerateEkaerXmlDocument invoked; ekaerHistoryId: {ekaerHistoryId}"); - // Upsert: (ForeignKey + IsOutgoing) párra EGY rekord — az újragenerálás nem duplikál. - var ekaerHistory = await ctx.EkaerHistories.GetByForeignKey(foreignKey).FirstOrDefaultAsync(eh => eh.IsOutgoing == isOutgoing); + var ekaerHistory = await ctx.EkaerHistories.GetByIdAsync(ekaerHistoryId, true) + ?? throw new ArgumentException($"EkaerHistory not found; id: {ekaerHistoryId}", nameof(ekaerHistoryId)); - if (!isOutgoing) + // 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 GetAll(true) a mapperhez kellő teljes gráfot tölti (Partner, Items+ProductDto, Shipping→járművek). - var shippingDocument = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, true) - ?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey)); - ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(shippingDocument, ekaerHistory); + // 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ő: a GetByIdAsync(true) betölti a Customer-t, tételeket, palettákat (GrossWeight) és a ProductDto-t. - var order = await ctx.OrderDtos.GetByIdAsync(foreignKey, true) - ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey)); + // 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); } - if (ekaerHistory.Id > 0) await ctx.EkaerHistories.UpdateAsync(ekaerHistory); - else await ctx.EkaerHistories.InsertAsync(ekaerHistory); - - return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? 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 (foreignKey, isOutgoing) párra már van EkaerHistory, azt adja vissza - /// érintetlenül; különben új Pending rekordot hoz létre. A generálás (XmlDoc) külön lépés: GenerateEkaerXmlDocument. + /// 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}"); - var existing = await ctx.EkaerHistories.GetByForeignKey(foreignKey).FirstOrDefaultAsync(eh => eh.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. @@ -372,10 +386,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers _ = await ctx.OrderDtos.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey)); } - var ekaerHistory = new EkaerHistory { ForeignKey = foreignKey, IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }; - await ctx.EkaerHistories.InsertAsync(ekaerHistory); + // EGY EkaerHistory + EGY mapping sor (egyelemű csoport), atomikusan. + var ekaerHistory = await ctx.AddEkaerHistoryWithMappingsAsync(new EkaerHistory { IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }, [foreignKey]); - return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory; + // 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); } /// @@ -388,21 +403,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers { _logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}"); - var toCreate = new List(); + 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) = feladó→címzett→jármű: a küszöb a csoport ÖSSZES dokumentumára aggregál. + // 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.EkaerHistories.Table.Any(eh => eh.ForeignKey == sd.Id && !eh.IsOutgoing)) + .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) 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))) + // 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 @@ -411,14 +426,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; } if (result.Obligation == EkaerObligation.NotRequired) continue; - // Kötelező → a csoport minden dokumentumára egy-egy Pending sor (a rekord-szerkezet per-dokumentum marad). - foreach (var doc in docs) - toCreate.Add(new EkaerHistory { ForeignKey = doc.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }); + // 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}", ex); - messages.Add($"Szállítólevél-csoport (Shipping #{group.Key.ShippingId}, Partner #{group.Key.PartnerId}): a kötelezettség nem dönthető el — {ex.Message}"); + _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}"); } } @@ -426,7 +440,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers // 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.EkaerHistories.Table.Any(eh => eh.ForeignKey == o.Id && eh.IsOutgoing)) + .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(); @@ -440,7 +454,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; } if (result.Obligation == EkaerObligation.NotRequired) continue; - toCreate.Add(new EkaerHistory { ForeignKey = order.Id, IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }); + toCreate.Add((new EkaerHistory { IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }, [order.Id])); } catch (Exception ex) { @@ -451,16 +465,17 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers // ── Beszúrás ── var createdCount = 0; - foreach (var ekaerHistory in toCreate) + foreach (var (history, foreignKeys) in toCreate) { try { - await ctx.EkaerHistories.InsertAsync(ekaerHistory); + // 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; ForeignKey: {ekaerHistory.ForeignKey}; IsOutgoing: {ekaerHistory.IsOutgoing}", ex); + _logger.Error($"CreateMissingEkaerHistories; insert failed; IsOutgoing: {history.IsOutgoing}; foreignKeys: [{string.Join(",", foreignKeys)}]", ex); } } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs index 5931959..ce7d739 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs @@ -15,11 +15,16 @@ public class EkaerHistoryDbTable : MgDbTableBase { } - // History tábla: legújabb elöl. Nincs asszociáció → nincs LoadWith / loadRelations. + // History tábla: legújabb elöl. public override IOrderedQueryable GetAll() => base.GetAll().OrderByDescending(p => p.Id); - public Task GetByIdAsync(int id) => GetAll().FirstOrDefaultAsync(p => p.Id == id); - public IQueryable GetByForeignKey(int foreignKey) => GetAll().Where(p => p.ForeignKey == foreignKey); + // loadRelations → a deklarációt lefedő forrás-mapping sorok (Mappings) is töltődnek (a kliensnek kellenek). + public IQueryable GetAll(bool loadRelations) + => loadRelations ? GetAll().LoadWith(eh => eh.Mappings) : GetAll(); + + // FirstOrDefaultAsync kétértelmű (System.Linq.Async + LinqToDB) az IQueryable-ön (a GetAll(loadRelations) LoadWith + // miatt már nem IOrderedQueryable) → explicit a LinqToDB-é, ugyanúgy mint a CountByFilterAsync-nél. + public Task GetByIdAsync(int id, bool loadRelations) => LinqToDB.AsyncExtensions.FirstOrDefaultAsync(GetAll(loadRelations), p => p.Id == id); // A NAV-nál lévő partíciók fix StatusId-k; minden más (a jövőbeni új státuszok is) „beküldésre váró". private static readonly int[] ToSubmitStatusIds = [.. Enum.GetValues().Select(s => (int)s).Except([(int)EkaerStatus.Sent, (int)EkaerStatus.SentWithMissingData])]; @@ -27,20 +32,20 @@ public class EkaerHistoryDbTable : MgDbTableBase /// Flag-alapú szűrés a tabokhoz/count-hoz. (= 0) → minden; /// egyébként a beállított flag-ek diszjunkt StatusId-partícióinak UNIÓJA (IN). A státusz-helperek (IsSent stb.) /// nem fordulnak SQL-re, ezért közvetlen StatusId-halmazzal szűrünk. - public IQueryable GetByFilter(EkaerHistoryFilter filter) + public IQueryable GetByFilter(EkaerHistoryFilter filter, bool loadRelations) { // HasFlag(All) mindig true (0 bit), ezért előbb az All-ágat zárjuk rövidre. - if (filter == EkaerHistoryFilter.All) return GetAll(); + if (filter == EkaerHistoryFilter.All) return GetAll(loadRelations); var statusIds = new List(); if (filter.HasFlag(EkaerHistoryFilter.ToSubmit)) statusIds.AddRange(ToSubmitStatusIds); if (filter.HasFlag(EkaerHistoryFilter.Sent)) statusIds.Add((int)EkaerStatus.Sent); if (filter.HasFlag(EkaerHistoryFilter.NeedsCompletion)) statusIds.Add((int)EkaerStatus.SentWithMissingData); - return GetAll().Where(eh => statusIds.Contains(eh.StatusId)); + return GetAll(loadRelations).Where(eh => statusIds.Contains(eh.StatusId)); } // A CountAsync kétértelmű (a System.Linq.Async ÉS a LinqToDB is hozza ugyanazt a nevet, no-arg hívásnál mindkettő illik) // → explicit a LinqToDB-é. (A ToListAsync nem ütközik, azt a hívó közvetlenül használhatja.) - public Task CountByFilterAsync(EkaerHistoryFilter filter) => LinqToDB.AsyncExtensions.CountAsync(GetByFilter(filter)); + public Task CountByFilterAsync(EkaerHistoryFilter filter) => LinqToDB.AsyncExtensions.CountAsync(GetByFilter(filter, false)); } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryMappingDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryMappingDbTable.cs new file mode 100644 index 0000000..8cd91f8 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryMappingDbTable.cs @@ -0,0 +1,26 @@ +using FruitBank.Common.Entities; +using LinqToDB; +using Mango.Nop.Data.Repositories; +using Nop.Core.Caching; +using Nop.Core.Configuration; +using Nop.Core.Events; +using Nop.Data; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; + +public class EkaerHistoryMappingDbTable : MgDbTableBase +{ + public EkaerHistoryMappingDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings) + : base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings) + { + } + + // loadRelations → a szülő EkaerHistory is töltődik (a junction → deklaráció navigáció). + public IQueryable GetAll(bool loadRelations) => loadRelations ? base.GetAll().LoadWith(m => m.EkaerHistory) : base.GetAll(); + + // Egy EKÁER-deklaráció forrás-rekordjai (bejövő: N ShippingDocument; kimenő: 1 Order). + public IQueryable GetByEkaerHistoryId(int ekaerHistoryId, bool loadRelations) => GetAll(loadRelations).Where(m => m.EkaerHistoryId == ekaerHistoryId); + + // Egy forrás-rekordhoz (ShippingDocument/Order Id) tartozó mapping sor(ok) — a dedup-hoz / a sorhoz tartozó EKÁER kereséséhez. + public IQueryable GetByForeignKey(int foreignKey, bool loadRelations) => GetAll(loadRelations).Where(m => m.ForeignKey == foreignKey); +} diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs index 211361c..e5c17c6 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs @@ -57,6 +57,7 @@ public class FruitBankDbContext : MgDbContextBase, public PartnerDbTable Partners { get; set; } public PartnerDepotDbTable PartnerDepots { get; set; } public EkaerHistoryDbTable EkaerHistories { get; set; } + public EkaerHistoryMappingDbTable EkaerHistoryMappings { get; set; } public CargoPartnerDbTable CargoPartners { get; set; } public CargoTruckDbTable CargoTrucks{ get; set; } @@ -83,7 +84,7 @@ public class FruitBankDbContext : MgDbContextBase, public IRepository StockQuantityHistoriesExt { get; set; } public FruitBankDbContext(INopDataProvider dataProvider, ILockService lockService, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext, - CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, PartnerDepotDbTable partnerDepotDbTable, EkaerHistoryDbTable ekaerHistoryDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, + CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, PartnerDepotDbTable partnerDepotDbTable, EkaerHistoryDbTable ekaerHistoryDbTable, EkaerHistoryMappingDbTable ekaerHistoryMappingDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable, ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable, StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos, CustomerCreditDbTable customerCreditDbTable, @@ -112,6 +113,7 @@ public class FruitBankDbContext : MgDbContextBase, Partners = partnerDbTable; PartnerDepots = partnerDepotDbTable; EkaerHistories = ekaerHistoryDbTable; + EkaerHistoryMappings = ekaerHistoryMappingDbTable; CargoPartners = cargoPartnerDbTable; CargoTrucks = cargoTruckDbTable; @@ -767,6 +769,25 @@ public class FruitBankDbContext : MgDbContextBase, return list; } + /// EKÁER-deklaráció (EkaerHistory) + a hozzá tartozó forrás-mapping sorok ATOMIKUS létrehozása. + /// A mapping tartja a kapcsolatot a forrás-rekordokhoz (bejövő: N ShippingDocument egy (Shipping, Partner, + /// PartnerDepot) csoportból; kimenő: 1 Order) — így a deklaráció a rendszer-tábláktól FÜGGETLENÜL törölhető + /// és újragenerálható. Az InsertAsync az identity Id-t visszaírja, így a mapping sorok már a friss Id-ra mutatnak. + public async Task AddEkaerHistoryWithMappingsAsync(EkaerHistory ekaerHistory, IEnumerable foreignKeys) + { + ArgumentNullException.ThrowIfNull(ekaerHistory); + + await TransactionSafeAsync(async _ => + { + await EkaerHistories.InsertAsync(ekaerHistory); + foreach (var foreignKey in foreignKeys) + await EkaerHistoryMappings.InsertAsync(new EkaerHistoryMapping { EkaerHistoryId = ekaerHistory.Id, ForeignKey = foreignKey }); + return true; + }); + + return ekaerHistory; + } + public async Task AddCustomerAddressMappingAsync(int customerId, int addressId) { var customerAddressMapping = new CustomerAddressMapping diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 967692d..5099de6 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -87,6 +87,7 @@ public class PluginNopStartup : INopStartup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs b/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs index e9d6ba0..7a11378 100644 --- a/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs +++ b/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs @@ -52,6 +52,7 @@ public partial class NameCompatibility : INameCompatibility { typeof(PartnerDepot), FruitBankConstClient.PartnerDepotDbTableName}, { typeof(EkaerHistory), FruitBankConstClient.EkaerHistoryDbTableName}, + { typeof(EkaerHistoryMapping), FruitBankConstClient.EkaerHistoryMappingDbTableName}, }; diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index 9e842ec..81502de 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -79,3 +79,20 @@ A NAV felé **egy VTSZ-re egy `tradeCardItem`** kell — több azonos-VTSZ-ű te - `FruitBank.Common.Server/Services/Ekaer/{IFruitBankEkaerService,FruitBankEkaerService}.cs` — a `vtszNames` átadása a generate-en - `FruitBankDataController.GenerateEkaerXmlDocument` (plugin) — `(productId, categoryId)` tuple + batch `Category`-load → `Dictionary` - nopCommerce `Category` (MetaKeywords = VTSZ, Name = productName) — adat/üzemeltetés + +## MGFBANKPLUG-EKAER-T-D8R4: Kapu-csoportosítás (Shipping, PartnerDepot)-re; a PartnerDepotId kötelező + +**Status:** Open · **Priority:** P2 · **Type:** T (kapu) · **2026-06** + +A `CreateMissingEkaerHistories` bejövő kapuja jelenleg `(ShippingId, PartnerId, PartnerDepotId)`-re csoportosít. Mivel a **`PartnerDepot` egyértelműen egy `Partner`-hez tartozik** (a depó implikálja a feladót), a `PartnerId` a kulcsban **redundáns** → elég a `(ShippingId, PartnerDepotId)` csoportosítás. + +**Cél:** +- A csoportosítási kulcs `(ShippingId, PartnerDepotId)` (a `PartnerId` kivehető). +- A **`PartnerDepotId` kötelező** az EKÁER-hez: ha egy szállítólevélen **nincs** `PartnerDepotId` → **hiba**, és **NE generálódjon** rá deklaráció. A kapu a hiányzó-telephelyű dokumentumokat kihagyja + üzenettel jelzi (mint a `DataError`-nál); a generate `ValidationError`-t ad (nem küldhető), nem üres/téves tradeCard-ot. + +**Megj.:** szigorítás a jelenlegihez képest, ahol a `null` `PartnerDepotId` még külön (null-telephelyű) csoportként kezelődik. Bevezetés előtt a meglévő szállítólevelek `PartnerDepotId`-ját pótolni kell, különben a kapu kihagyja őket. + +**Affected:** +- `FruitBankDataController.CreateMissingEkaerHistories` (plugin) — a `GroupBy` kulcs `(ShippingId, PartnerDepotId)`-re + a hiányzó-`PartnerDepotId` hiba-ág (kihagyás + üzenet) +- `FruitBankDataController.GenerateEkaerXmlDocument` / `FruitBankEkaerService` — hiányzó telephelynél `ValidationError`, nincs generálás +- `ShippingToEkaerMapper` / `EkaerReportability` (`FruitBank.Common/Services/Ekaer/`) — ha a feladó-cím a `PartnerDepot`-ból jön, a hiányzó telephely validációs hiba diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md index 0fd5ccb..52dbfaf 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md @@ -27,8 +27,8 @@ FruitBankEkaerService.SubmitShippingAsync(shipping) // FruitBank.Common.S Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki kérés. NAV-oldali hiba → `NavReportException` propagál. **További belépési pontok** (a kézi NAV-beadás munkafolyamathoz — `FruitBankDataController` SignalR-endpointok, kliens felől triggerelve): -- **`CreateMissingEkaerHistories(fromDate)`** — reconciliation-kapu: a dátumtól rekord nélküli szállítólevelekre/rendelésekre `Pending` `EkaerHistory` sorokat hoz létre, **csak ha kötelező** (lásd a kapu-szakaszt lentebb). `EkaerCreateResult`-ot ad vissza (létrehozott szám + a kihagyott tételek üzenetei). -- **`GenerateEkaerXmlDocument(foreignKey, isOutgoing)`** — egy sorra legenerálja + validálja a tradeCard XML-t (NEM küld NAV-ot); a `Status` / `ErrorText` / `ConversionRate` töltődik, az XML vágólapra másolható a kézi beadáshoz. +- **`CreateMissingEkaerHistories(fromDate)`** — reconciliation-kapu: a dátumtól még le nem fedett szállítólevelekre/rendelésekre `Pending` `EkaerHistory` sorokat hoz létre, **csak ha kötelező** (lásd a kapu-szakaszt lentebb). Bejövőnél a `(Shipping, Partner, PartnerDepot)` csoport **EGY** `EkaerHistory`-t kap, a csoport dokumentumai pedig `EkaerHistoryMapping` sorokként kapcsolódnak; kimenőnél egy rendelés = egy `EkaerHistory` + egy mapping. `EkaerCreateResult`-ot ad vissza (létrehozott szám + a kihagyott tételek üzenetei). +- **`GenerateEkaerXmlDocument(ekaerHistoryId)`** — egy `EkaerHistory`-deklarációra (a mappingben rögzített forrás-dokumentumok ÖSSZES tételét egy tradeCard-ba vonva) legenerálja + validálja a tradeCard XML-t (NEM küld NAV-ot); a `Status` / `ErrorText` / `ConversionRate` töltődik, az XML vágólapra másolható a kézi beadáshoz. ## Companion fájlok @@ -53,11 +53,11 @@ A nyitott pontok (tradeType, destination, value/deviza, granularitás, kimenő i A kézi NAV-beadás munkafolyamat: a **kapu** létrehozza a sorokat → a user **generál** + másol/beküld → kézzel rögzíti a NAV-választ. -**`EkaerHistory` életciklus** (egy sor = egy tradeCard; `ForeignKey` = bejövő `ShippingDocument.Id` / kimenő `Order.Id` + `IsOutgoing`; a `Status` int-oszlop + `[NotColumn]` enum-nézet): +**`EkaerHistory` életciklus** (egy sor = egy tradeCard; a forrás-rekordok az **`EkaerHistoryMapping`** junctionön át kapcsolódnak — bejövő: a `(Shipping, Partner, PartnerDepot)` csoport N `ShippingDocument`-je, kimenő: 1 `Order` —, az `EkaerHistory` maga **nem** hordoz forrás-kulcsot, az irány az `IsOutgoing`; a `Status` int-oszlop + `[NotColumn]` enum-nézet; a mapping a `GetAll(loadRelations: true)`-zal töltődik a `Mappings` asszociációba): `Pending` (a kapu hozta létre) → **Generate** → `Generated` / `GeneratedWithWarning` (küldhető, de pótlandó — pl. hiányzó rendszám) / `ValidationError` (blokkoló) → kézi NAV-beadás után `Sent` / `SentWithMissingData` (pótlásra vár). A grid 3 tabja ezekre szűr (szerver-oldali `EkaerHistoryFilter`), a warning ⚠️ / error ⛔ ikonnal megkülönböztetve. **Bejelentés-kötelezettség** — `EkaerReportability.Evaluate(consignment, settings)`, a kapu hívja egységenként: -- **egység:** bejövőnél a `(Shipping, Partner)` csoport (= feladó→címzett→jármű) — a tömeg/érték a csoport **ÖSSZES** dokumentumára **aggregálva** (13/2020 PM rendelet); kimenőnél egy rendelés; +- **egység:** bejövőnél a `(Shipping, Partner, PartnerDepot)` csoport (= feladó+telephely→címzett→jármű) — a tömeg/érték a csoport **ÖSSZES** dokumentumára **aggregálva** (13/2020 PM rendelet), és a csoport EGY `EkaerHistory`-t kap; kimenőnél egy rendelés; - **küszöb felett** → kötelező, az országkódtól **függetlenül** (az országkód-hibát a generate-validálás jelzi); - **küszöb alatt** → csak a **külföldi** reláció kötelező (a feladó és a címzett országkódja **eltér**); belföld → nem; hiányzó/érvénytelen (nem ISO-2) országkód → **`DataError`** (a sor kimarad + üzenet a popupban); - **mentesség:** `Partner.IsEkaer = false`, és a Partner nélküli (félkész) szállítólevél — csendben kimaradnak.