From b782d6a88d5cedb2b29d63fe8f8276ca1119bf48 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 9 Jun 2026 15:28:57 +0200 Subject: [PATCH 01/13] Add PartnerDepot support: data table, DI, SignalR endpoints Introduced PartnerDepotDbTable for managing PartnerDepot entities, registered it in DI, and integrated it into FruitBankDbContext. Updated NameCompatibility and PartnerDbTable for proper mapping and relation loading. Added SignalR controller endpoints for PartnerDepot CRUD and queries. --- .../Controllers/FruitBankDataController.cs | 44 +++++++++++++++++++ .../Domains/DataLayer/FruitBankDbContext.cs | 4 +- .../Domains/DataLayer/PartnerDbTable.cs | 7 +-- .../Domains/DataLayer/PartnerDepotDbTable.cs | 27 ++++++++++++ .../Infrastructure/PluginNopStartup.cs | 1 + .../Mapping/NameCompatibility.cs | 3 ++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDepotDbTable.cs diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index d7dcf78..78e9b2d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -219,6 +219,50 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers 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.GetShippings)] public async Task> GetShippings() { diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs index 4186529..f3b9ffc 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs @@ -55,6 +55,7 @@ public class FruitBankDbContext : MgDbContextBase, public OrderItemDtoDbTable OrderItemDtos { get; set; } public PartnerDbTable Partners { get; set; } + public PartnerDepotDbTable PartnerDepots { get; set; } public CargoPartnerDbTable CargoPartners { get; set; } public CargoTruckDbTable CargoTrucks{ get; set; } @@ -81,7 +82,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, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, + CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, PartnerDepotDbTable partnerDepotDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable, ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable, StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos, CustomerCreditDbTable customerCreditDbTable, @@ -108,6 +109,7 @@ public class FruitBankDbContext : MgDbContextBase, Files = filesDbTable; Partners = partnerDbTable; + PartnerDepots = partnerDepotDbTable; CargoPartners = cargoPartnerDbTable; CargoTrucks = cargoTruckDbTable; diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs index 15444e4..b768bc1 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs @@ -22,9 +22,10 @@ public class PartnerDbTable : MgDbTableBase { return loadRelations ? GetAll() - .LoadWith(sd => sd.ShippingDocuments).ThenLoad(s => s.Shipping) - .LoadWith(sd => sd.ShippingDocuments).ThenLoad(si => si.ShippingItems).ThenLoad(sip => sip.ShippingItemPallets) - .LoadWith(sd => sd.ShippingDocuments).ThenLoad(sdtof => sdtof.ShippingDocumentToFiles).ThenLoad(f => f.ShippingDocumentFile) + .LoadWith(p => p.PartnerDepots) + .LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.Shipping) + .LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.ShippingItems).ThenLoad(si => si.ShippingItemPallets) + .LoadWith(p => p.ShippingDocuments).ThenLoad(sd => sd.ShippingDocumentToFiles).ThenLoad(sdtof => sdtof.ShippingDocumentFile) : GetAll(); } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDepotDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDepotDbTable.cs new file mode 100644 index 0000000..83d0c63 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDepotDbTable.cs @@ -0,0 +1,27 @@ +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 PartnerDepotDbTable : MgDbTableBase +{ + public PartnerDepotDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings) + : base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings) + { + } + + public override IOrderedQueryable GetAll() => base.GetAll().OrderBy(p => p.PartnerId); + + public IQueryable GetAll(bool loadRelations) + { + return loadRelations ? GetAll().LoadWith(sd => sd.Partner) : GetAll(); + } + + public Task GetByIdAsync(int id, bool loadRelations) => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id); + public IQueryable GetByPartnerId(int partnerId, bool loadRelations) => GetAll(loadRelations).Where(p => p.PartnerId == partnerId); +} diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 9c11b76..20a7476 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -83,6 +83,7 @@ public class PluginNopStartup : INopStartup 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 ee7dc2f..e9d6ba0 100644 --- a/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs +++ b/Nop.Plugin.Misc.AIPlugin/Mapping/NameCompatibility.cs @@ -49,6 +49,9 @@ public partial class NameCompatibility : INameCompatibility { typeof(CargoPartner), FruitBankConstClient.CargoPartnerDbTableName}, { typeof(CargoTruck), FruitBankConstClient.CargoTruckDbTableName}, + + { typeof(PartnerDepot), FruitBankConstClient.PartnerDepotDbTableName}, + { typeof(EkaerHistory), FruitBankConstClient.EkaerHistoryDbTableName}, }; From 9cdd65eebda11ce1afd28913870aa20019215a6e Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 10 Jun 2026 16:24:59 +0200 Subject: [PATCH 02/13] Add EkaerHistory support: controller, DI, and DbTable - Added SignalR endpoints for EkaerHistory CRUD in FruitBankDataController. - Introduced EkaerHistoryDbTable with ordering and query helpers. - Registered EkaerHistoryDbTable in DI and DbContext. - Updated appsettings.json to comment out NopLogWriter config with explanation. --- .../Controllers/FruitBankDataController.cs | 44 +++++++++++++++++++ .../Domains/DataLayer/EkaerHistoryDbTable.cs | 23 ++++++++++ .../Domains/DataLayer/FruitBankDbContext.cs | 4 +- .../Infrastructure/PluginNopStartup.cs | 1 + 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index 78e9b2d..e343c73 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -263,6 +263,50 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true); } + [SignalR(SignalRTags.GetEkaerHistories)] + public async Task> GetEkaerHistories() + { + _logger.Detail($"GetEkaerHistories invoked"); + + return await ctx.EkaerHistories.GetAll().ToListAsync(); + } + + [SignalR(SignalRTags.GetEkaerHistoryById)] + public async Task GetEkaerHistoryById(int id) + { + _logger.Detail($"GetEkaerHistoryById invoked; id: {id}"); + return await ctx.EkaerHistories.GetByIdAsync(id); + } + + [SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)] + public async Task> GetEkaerHistoriesByForeignKey(int foreignKey) + { + _logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}"); + return await ctx.EkaerHistories.GetByForeignKey(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); + } + + [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); + } + [SignalR(SignalRTags.GetShippings)] public async Task> GetShippings() { diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs new file mode 100644 index 0000000..3e4923f --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs @@ -0,0 +1,23 @@ +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 EkaerHistoryDbTable : MgDbTableBase +{ + public EkaerHistoryDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings) + : base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings) + { + } + + // History tábla: legújabb elöl. Nincs asszociáció → nincs LoadWith / loadRelations. + 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); +} diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs index f3b9ffc..211361c 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs @@ -56,6 +56,7 @@ public class FruitBankDbContext : MgDbContextBase, public PartnerDbTable Partners { get; set; } public PartnerDepotDbTable PartnerDepots { get; set; } + public EkaerHistoryDbTable EkaerHistories { get; set; } public CargoPartnerDbTable CargoPartners { get; set; } public CargoTruckDbTable CargoTrucks{ get; set; } @@ -82,7 +83,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, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, + CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, PartnerDepotDbTable partnerDepotDbTable, EkaerHistoryDbTable ekaerHistoryDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable, ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable, ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable, StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos, CustomerCreditDbTable customerCreditDbTable, @@ -110,6 +111,7 @@ public class FruitBankDbContext : MgDbContextBase, Files = filesDbTable; Partners = partnerDbTable; PartnerDepots = partnerDepotDbTable; + EkaerHistories = ekaerHistoryDbTable; CargoPartners = cargoPartnerDbTable; CargoTrucks = cargoTruckDbTable; diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 20a7476..6dc44bd 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -84,6 +84,7 @@ public class PluginNopStartup : INopStartup services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); From c8ecbc94a259b22fe6d325b78c1ce0338109ad44 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 10 Jun 2026 18:09:26 +0200 Subject: [PATCH 03/13] Add EkaerHistory SignalR endpoints and service injection Added IFruitBankEkaerService to FruitBankDataController and introduced two new SignalR methods: GenerateEkaerXmlDocument (generates/updates EkaerHistory for a shipping document) and CreateEkaerHistory (idempotently creates or returns EkaerHistory for a given key and direction). Both methods include detailed logging and entity validation. --- .../Controllers/FruitBankDataController.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index e343c73..9a213bd 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -13,6 +13,7 @@ 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.SignalRs; using LinqToDB; using Mango.Nop.Core.Dtos; @@ -40,6 +41,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers ICustomerRegistrationService customerRegistrationService, ILocalizationService localizationService, PreOrderConversionService preorderConversionService, + IFruitBankEkaerService fruitBankEkaerService, IEnumerable logWriters) : BasePluginController, IFruitBankDataControllerServer { @@ -307,6 +309,55 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id); } + [SignalR(SignalRTags.GenerateEkaerXmlDocument)] + public async Task GenerateEkaerXmlDocument(int shippingDocumentId) + { + _logger.Detail($"GenerateEkaerXmlDocument invoked; shippingDocumentId: {shippingDocumentId}"); + + // A GetAll(true) a mapperhez kellő teljes gráfot tölti: Partner, Items+ProductDto, Shipping→járművek/fuvarozó. + var shippingDocument = await ctx.ShippingDocuments.GetByIdAsync(shippingDocumentId, true) + ?? throw new ArgumentException($"ShippingDocument not found; id: {shippingDocumentId}", nameof(shippingDocumentId)); + + // Upsert: dokumentumonként EGY bejövő rekord (ForeignKey + IsOutgoing) — az újragenerálás nem duplikál. + var ekaerHistory = await ctx.EkaerHistories.GetByForeignKey(shippingDocumentId).FirstOrDefaultAsync(eh => !eh.IsOutgoing); + ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(shippingDocument, ekaerHistory); + + if (ekaerHistory.Id > 0) await ctx.EkaerHistories.UpdateAsync(ekaerHistory); + else await ctx.EkaerHistories.InsertAsync(ekaerHistory); + + return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory; + } + + /// + /// 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. + /// + [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); + 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)); + } + + var ekaerHistory = new EkaerHistory { ForeignKey = foreignKey, IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }; + await ctx.EkaerHistories.InsertAsync(ekaerHistory); + + return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory; + } + [SignalR(SignalRTags.GetShippings)] public async Task> GetShippings() { From 75ce276f64a8737562691899b48546de7122143a Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Jun 2026 08:52:15 +0200 Subject: [PATCH 04/13] =?UTF-8?q?Add=20method=20to=20create=20missing=20EK?= =?UTF-8?q?=C3=81ER=20history=20records?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced CreateMissingEkaerHistories in FruitBankDataController to generate Pending EKÁER records for inbound shipping documents and completed outbound orders lacking such records from a specified date. The method is user-invoked, idempotent, logs actions and errors, and supports SignalR notifications. --- .../Controllers/FruitBankDataController.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index 9a213bd..e485604 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -21,6 +21,7 @@ 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; @@ -358,6 +359,49 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory; } + /// + /// 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}"); + + // Bejövő: rekord nélküli szállítólevelek a dátumtól. + var missingInbound = await ctx.ShippingDocuments.GetAll(false) + .Where(sd => sd.ShippingDate >= fromDate) + .Where(sd => !ctx.EkaerHistories.Table.Any(eh => eh.ForeignKey == sd.Id && !eh.IsOutgoing)) + .Select(sd => new EkaerHistory { ForeignKey = sd.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }) + .ToListAsync(); + + // Kimenő: rekord nélküli, lezárt (Complete) rendelések, DateOfReceipt a dátumtól (jövőbeli is — előre-bejelentés). + var missingOutgoing = 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)) + .Select(o => new EkaerHistory { ForeignKey = o.Id, IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }) + .ToListAsync(); + + var createdCount = 0; + + foreach (var ekaerHistory in missingInbound.Concat(missingOutgoing)) + { + try + { + await ctx.EkaerHistories.InsertAsync(ekaerHistory); + createdCount++; + } + catch (Exception ex) + { + _logger.Error($"CreateMissingEkaerHistories; insert failed; ForeignKey: {ekaerHistory.ForeignKey}; IsOutgoing: {ekaerHistory.IsOutgoing}", ex); + } + } + + _logger.Info($"CreateMissingEkaerHistories; created: {createdCount} (inbound: {missingInbound.Count}, outgoing: {missingOutgoing.Count}); fromDate: {fromDate:yyyy-MM-dd}"); + return createdCount; + } + [SignalR(SignalRTags.GetShippings)] public async Task> GetShippings() { From f1db5a9a99f8fb621ef2b0e6aa2adb4fdf46a5cc Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Jun 2026 17:40:42 +0200 Subject: [PATCH 05/13] =?UTF-8?q?Update=20EK=C3=81ER=20config,=20threshold?= =?UTF-8?q?s,=20and=20value=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switched DB connection to FruitBank_PROD in appsettings.json - Added Ekaer config section: company info, thresholds, exchange rate - Refactored CreateMissingEkaerHistories to use new settings, load related data, and apply exemption/threshold logic - Updated controller DI to use EkaerSettings - Centralized EKÁER config binding in PluginNopStartup - Documented currency source issue and new value/category logic in EKAER_ISSUES.md and EKAER_TODO.md --- .../Controllers/FruitBankDataController.cs | 27 +++++++++++++++-- .../Infrastructure/PluginNopStartup.cs | 30 +++++++++++++------ .../docs/EKAER/EKAER_ISSUES.md | 15 ++++++++++ .../docs/EKAER/EKAER_TODO.md | 25 ++++++++++++++-- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index e485604..961128f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -14,6 +14,7 @@ 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; @@ -43,6 +44,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers ILocalizationService localizationService, PreOrderConversionService preorderConversionService, IFruitBankEkaerService fruitBankEkaerService, + EkaerSettings ekaerSettings, IEnumerable logWriters) : BasePluginController, IFruitBankDataControllerServer { @@ -369,13 +371,32 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers { _logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}"); - // Bejövő: rekord nélküli szállítólevelek a dátumtól. - var missingInbound = await ctx.ShippingDocuments.GetAll(false) + // Bejövő: rekord nélküli szállítólevelek a dátumtól — Partnerrel és tételekkel betöltve a kapuhoz + // (IsEkaer-mentesség + tömeg/érték küszöb). A fájl-blobokat NEM töltjük (csak Partner + Items). + var candidates = await ctx.ShippingDocuments.GetAll() .Where(sd => sd.ShippingDate >= fromDate) .Where(sd => !ctx.EkaerHistories.Table.Any(eh => eh.ForeignKey == sd.Id && !eh.IsOutgoing)) - .Select(sd => new EkaerHistory { ForeignKey = sd.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }) + .LoadWith(sd => sd.Partner) + .LoadWith(sd => sd.ShippingItems) .ToListAsync(); + var missingInbound = new List(); + foreach (var doc in candidates) + { + // Explicit mentesítés (pl. nagybani piac, azonos cím — nincs közúti fuvar a partnerek között). + if (doc.Partner?.IsEkaer == false) continue; + + var items = doc.ShippingItems ?? []; + var totalWeight = items.Sum(i => i.MeasuredGrossWeight); + var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(doc.Partner?.Currency, ekaerSettings.EurHufRate); + var totalValueHuf = items.Sum(EkaerValueCalculator.ItemLineValue) * rateToHuf; + + // Küszöb alatt (tömeg ÉS érték is) → nem kötelező EKÁER → nem hozunk létre sort. + if (totalWeight < ekaerSettings.ThresholdWeightKg && totalValueHuf < ekaerSettings.ThresholdValueHuf) continue; + + missingInbound.Add(new EkaerHistory { ForeignKey = doc.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }); + } + // Kimenő: rekord nélküli, lezárt (Complete) rendelések, DateOfReceipt a dátumtól (jövőbeli is — előre-bejelentés). var missingOutgoing = await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false) .Where(o => o.GenericAttributes.Any(ga => ga.Key == nameof(OrderDto.DateOfReceipt) && DateTime.Parse(ga.Value) >= fromDate.Date)) diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 6dc44bd..6a09561 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection.Extensions; +using Nop.Core; using Nop.Core.Domain.Orders; using Nop.Core.Infrastructure; using Nop.Data; @@ -192,18 +193,29 @@ public class PluginNopStartup : INopStartup BaseUrl = c["BaseUrl"] ?? "https://import-test-b.ekaer.nav.gov.hu", // TEST; PROD: https://import.ekaer.nav.gov.hu }; }); + // EKÁER konfiguráció — egyetlen objektum (cégadat + küszöb + árfolyam), configból (appsettings "Ekaer"). + // A bejelentő cégadata: a NAV-helyes 11-jegyű adószám + telefon/e-mail itt adható meg, a webshop Store-tól függetlenül. services.AddSingleton(sp => { - var c = sp.GetRequiredService().GetSection("Ekaer:Company"); - return new EkaerCompanyInfo + var c = sp.GetRequiredService().GetSection("Ekaer"); + var co = c.GetSection("Company"); + return new EkaerSettings { - Name = c["Name"], - TaxId = c["TaxId"], - CountryCode = c["CountryCode"] ?? "HU", // a NAV EKÁER magyar - PostalCode = c["PostalCode"], - City = c["City"], - Street = c["Street"], - // UnloadLocation (saját telephely): magyar címnél a NAV Name/VatNumber/Phone/Email-t is kér — TODO configból összeállítani. + Company = new EkaerCompanyInfo + { + Name = co["Name"], + TaxId = co["TaxId"], + CountryCode = co["CountryCode"] ?? "HU", // a NAV EKÁER magyar + PostalCode = co["PostalCode"], + City = co["City"], + Street = co["Street"], + // UnloadLocation (saját telephely): magyar címnél a NAV Name/VatNumber/Phone/Email-t is kér — TODO összeállítani. + }, + // Default ÉRTÉK szándékosan nincs: be nem töltött config → 0. A 0 árfolyam a számításkor hibát dob, + // a 0 küszöb „mindent jelentünk" — egyik sem számol elavult, kódba égetett értékkel. + EurHufRate = c.GetValue("ExchangeRate:EurHuf"), + ThresholdWeightKg = c.GetValue("Thresholds:WeightKg"), + ThresholdValueHuf = c.GetValue("Thresholds:ValueHuf"), }; }); diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md index 760715e..a01943f 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md @@ -27,3 +27,18 @@ Egy termékhez a kettő nem azonos; a `Gtin` oszlop VTSZ-ként való használata **Affected:** - `FruitBank.Common/Dtos/ProductDto.cs` → `Gtin` property (a `[Column(nameof(Product.Gtin))]` jelöléssel, summary-ban megjelölve) - jövőbeli: `ShippingToEkaerMapper` (`FruitBank.Common/Services/Ekaer/`) — a `productVtsz`-t a külön `Vtsz` mezőből olvassa + +## MGFBANKPLUG-EKAER-I-M6Y2: A számla-pénznem a Partnerből jön — jobb lenne a ShippingDocument-en + +**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell) + +Az EKÁER tétel-érték HUF-ra számításához a forrás-pénznem jelenleg a **`Partner.Currency`**-ből jön (a beszállító alap-devizája). **Most 100%-ban megbízható** — a partner egyértelműen meghatározza a devizát. + +**Hatás:** működik; de a pénznem **konceptuálisan a szállítmányhoz / dokumentumhoz tartozik**, nem a partnerhez (egy partner elvileg többféle devizában is számlázhat). + +**Javítási irány:** külön **`Currency` oszlop a `ShippingDocument`-en**, és az `EkaerValueCalculator` / `ShippingToEkaerMapper` onnan olvasson; a `Partner.Currency` maradjon fallback. **Nem prioritás** — a jelenlegi partner-alapú forrás elegendő. + +**Affected:** +- `FruitBank.Common/Entities/ShippingDocument.cs` — jövőbeli `Currency` oszlop +- `FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs` + `ShippingToEkaerMapper.cs` — a deviza-forrás +- jelenlegi forrás: `PartnerBase.Currency` diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index e78a6ca..4bf7412 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -17,17 +17,38 @@ A `ShippingToEkaerMapper` megírása előtt tisztázandó pontok. Minden sor a j |---|------|------------------------|----------------| | 1 | `tradeType` (E/I/D irány) | Bejövő = beszerzés. HU feladó → **D** (belföldi), egyébként → **I** (import). A feladó országát a `ShippingDocument.Partner` / `Country` adja. | Automatikusan az országból dőljön el, vagy kézzel (UI) állítható? A kimenő (**E**, export) egyelőre hatókörön kívül (lásd #7). | | 2 | Célhely (destination) | Hardcode-olt FruitBank-telephely (cím). | Ne hardcode legyen — konfigból/beállításból jöjjön. Honnan (plugin settings / nopCommerce warehouse)? | -| 3 | Érték (`value`, HUF) | `UnitPriceOnDocument × MeasuredQuantity`. | A bejövő tételek tipikusan **EUR**-ban — deviza + árfolyam (FX) konverzió kell HUF-ra. Árfolyam forrása/időpontja? Vevőnként egyeztetni. | +| 3 | Érték (`value`, HUF) | ✅ **Megoldva:** `UnitPriceOnDocument × QuantityOnDocument × árfolyam` (`EkaerValueCalculator`). Pénznem a `Partner.Currency`-ből; EUR-HUF árfolyam configból (`Ekaer:ExchangeRate:EurHuf`, MNB középárfolyam). | Árfolyam dinamizálása (jelenleg „drót" config-érték) — későbbi. | | 4 | Tömeg (`weight`) | `ShippingItem.MeasuredGrossWeight` (bruttó). | Az EKÁER **bruttó** tömeget vár — megerősíteni, hogy ez bruttó és nem nettó. | | 5 | Granularitás | 1 `ShippingDocument` → 1 `tradeCard`; 1 `Shipping` → több művelet (több dokumentum). | Megerősíteni: tényleg dokumentumonként egy tradeCard? | | 6 | Eladó (`seller*`) mezők | `ShippingDocument.Partner`-ből: `Name`, `TaxId` (első 8 jegy = adószám-törzs), `CountryCode`, `City`, `Street`, `PostalCode`. | A `Partner` (`PartnerBase`) pontos mezőnevei/forrásai — megerősíteni. | | 7 | Kimenő irány (`Order` → EKÁER) | Egyelőre nincs. A vevő gyakran maga viszi el (saját fuvar) → nem mi jelentünk. | Mikor kell mégis nekünk export/kimenő (E) tradeCard-ot küldeni? | | 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md). Hosszú távon külön `Vtsz` mező. | | 9 | tétel `tradeReason` | `A` (beszerzés) — a mapper ezt használja bejövő árura. | Korábban tévesen `S`-nek feltételezve; az enum valójában `S`=értékesítés, `A`=beszerzés. Üzletileg megerősítendő. | -| 10 | `value` mező | Egyelőre **üresen hagyva** (opcionális mező) a deviza tisztázásáig (#3). | Megerősíteni, hogy a NAV elfogadja érték nélkül bejövő relációban, vagy kötelező-e. | +| 10 | `value` mező | ✅ **Megoldva:** a value most töltve (lásd #3); 0 / ismeretlen ár esetén `null` marad. | A NAV 2021-től `value > 0`-t vár — a validátorban a `value > 0` üzleti szabály még hozzáadandó. | **Affected:** - `ShippingToEkaerMapper` (`FruitBank.Common/Services/Ekaer/`) — a leképezés - `EkaerTradeCardValidator` / `EkaerSubmitService` / `EkaerManageService` (`AyCode.Services/Nav/Ekaer/`) — validáció + beküldés + HTTP (általános NAV-réteg) - `FruitBankEkaerService` (`FruitBank.Common.Server/Services/Ekaer/`) — a szerver-oldali fogyasztó (map → submit) - Forrás-entitások (`FruitBank.Common`): `Shipping`, `ShippingDocument`, `ShippingItem`, `ProductDto.Gtin` + +## MGFBANKPLUG-EKAER-T-W3R8: EKÁER küszöb-kategória a terméknél (enum, DB-ben tárolva) + +**Status:** Open · **Priority:** P2 · **Type:** T (adatmodell + mapping) + +Az EKÁER bejelentés-kötelezettség küszöbe **termékfüggő** — a VTSZ kockázati besorolása dönti el, melyik értékhatár vonatkozik rá: + +| Kategória | Tömeg | Érték (nettó) | +|---|---|---| +| Kockázatos élelmiszer | 200 kg | 250 000 Ft | +| Kockázatos egyéb (nem élelmiszer) | 500 kg | 1 000 000 Ft | +| Nem kockázatos | 2500 kg | 5 000 000 Ft | + +**Jelenlegi (átmeneti):** a `CreateMissingEkaerHistories` egyetlen, globális küszöböt használ az `EkaerSettings`-ből (config: 200 kg / 250 000 Ft — a legszigorúbb, hogy ne legyen aluljelentés). + +**Cél:** a **terméknél (Product) kézzel megadható** küszöb-**KATEGÓRIA** (nem a konkrét érték!) — egy **enum** (pl. `EkaerThresholdCategory { RiskyFood, RiskyOther, NonRisky }`) —, **DB-ben tárolva** (Product oszlop / GenericAttribute). A konkrét kg/Ft értékeket a **kód** rendeli a kategóriához → értékhatár-változáskor egy helyen módosul. A küszöb-kapu dokumentumonként a tételek kategóriái szerint alkalmazza a megfelelő határt. (Későbbi lépés: a kategória automatikus levezetése a NAV kockázatos-VTSZ-listákból.) + +**Affected:** +- `FruitBank.Common/Dtos/ProductDto.cs` — küszöb-kategória mező (enum, DB-ben) +- `EkaerSettings` / `EkaerValueCalculator` (`FruitBank.Common/Services/Ekaer/`) — kategória → küszöbérték leképezés +- `FruitBankDataController.CreateMissingEkaerHistories` — a kapu a tételek kategóriái szerint From cd49b572adcd82a75192d58af038009517ffa57b Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 12 Jun 2026 07:54:33 +0200 Subject: [PATCH 06/13] EKAER: add site config, outgoing support, refactor settings - Added "Site" section to appsettings.json for company location. - Increased EKAER threshold values; updated phone format. - Refactored to use IEkaerSettings DI and registered in startup. - GenerateEkaerXmlDocument now supports both incoming and outgoing (order) documents. - Updated EKAER_TODO.md to reflect config and outgoing mapping changes. --- .../Controllers/FruitBankDataController.cs | 29 +++++++++++++------ .../Infrastructure/PluginNopStartup.cs | 26 +++++++++++++---- .../docs/EKAER/EKAER_TODO.md | 4 +-- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index 961128f..d07fb60 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -4,6 +4,7 @@ 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; @@ -44,7 +45,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers ILocalizationService localizationService, PreOrderConversionService preorderConversionService, IFruitBankEkaerService fruitBankEkaerService, - EkaerSettings ekaerSettings, + IEkaerSettings ekaerSettings, IEnumerable logWriters) : BasePluginController, IFruitBankDataControllerServer { @@ -313,17 +314,27 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers } [SignalR(SignalRTags.GenerateEkaerXmlDocument)] - public async Task GenerateEkaerXmlDocument(int shippingDocumentId) + public async Task GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing) { - _logger.Detail($"GenerateEkaerXmlDocument invoked; shippingDocumentId: {shippingDocumentId}"); + _logger.Detail($"GenerateEkaerXmlDocument invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}"); - // A GetAll(true) a mapperhez kellő teljes gráfot tölti: Partner, Items+ProductDto, Shipping→járművek/fuvarozó. - var shippingDocument = await ctx.ShippingDocuments.GetByIdAsync(shippingDocumentId, true) - ?? throw new ArgumentException($"ShippingDocument not found; id: {shippingDocumentId}", nameof(shippingDocumentId)); + // 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); - // Upsert: dokumentumonként EGY bejövő rekord (ForeignKey + IsOutgoing) — az újragenerálás nem duplikál. - var ekaerHistory = await ctx.EkaerHistories.GetByForeignKey(shippingDocumentId).FirstOrDefaultAsync(eh => !eh.IsOutgoing); - ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(shippingDocument, ekaerHistory); + if (!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); + } + 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)); + ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(order, ekaerHistory); + } if (ekaerHistory.Id > 0) await ctx.EkaerHistories.UpdateAsync(ekaerHistory); else await ctx.EkaerHistories.InsertAsync(ekaerHistory); diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 6a09561..6f6059d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -44,6 +44,7 @@ using System.Net.Http.Headers; using Mango.Nop.Core.Loggers; using AyCode.Services.Nav; using AyCode.Services.Nav.Ekaer; +using AyCode.Services.Nav.Ekaer.Models; using FruitBank.Common.Services.Ekaer; using FruitBank.Common.Server.Services.Ekaer; using System.Security.Authentication; @@ -193,12 +194,14 @@ public class PluginNopStartup : INopStartup BaseUrl = c["BaseUrl"] ?? "https://import-test-b.ekaer.nav.gov.hu", // TEST; PROD: https://import.ekaer.nav.gov.hu }; }); - // EKÁER konfiguráció — egyetlen objektum (cégadat + küszöb + árfolyam), configból (appsettings "Ekaer"). - // A bejelentő cégadata: a NAV-helyes 11-jegyű adószám + telefon/e-mail itt adható meg, a webshop Store-tól függetlenül. - services.AddSingleton(sp => + + // EKÁER konfiguráció (cégadat + telephely + küszöb + árfolyam), configból (appsettings "Ekaer"). + // IEkaerSettings-ként kötjük be (AyCode-kontraktus) — a fogyasztók az interfészt kérik, mint az INavCredentials-t. + services.AddSingleton(sp => { var c = sp.GetRequiredService().GetSection("Ekaer"); var co = c.GetSection("Company"); + var site = co.GetSection("Site"); return new EkaerSettings { Company = new EkaerCompanyInfo @@ -209,13 +212,26 @@ public class PluginNopStartup : INopStartup PostalCode = co["PostalCode"], City = co["City"], Street = co["Street"], - // UnloadLocation (saját telephely): magyar címnél a NAV Name/VatNumber/Phone/Email-t is kér — TODO összeállítani. + // Telephely = fizikai fel-/lerakodási hely (bejövőnél lerakodás, kimenőnél felrakodás). + // Magyar címnél a NAV Name/VatNumber/Phone/Email-t is megköveteli. Ha = székhely, ugyanaz a cím. + Site = new LocationType + { + Name = co["Name"], + VatNumber = co["TaxId"], + Country = co["CountryCode"] ?? "HU", + ZipCode = site["PostalCode"], + City = site["City"], + Street = site["Street"], + Phone = co["Phone"], + Email = co["Email"], + }, }, + // Default ÉRTÉK szándékosan nincs: be nem töltött config → 0. A 0 árfolyam a számításkor hibát dob, // a 0 küszöb „mindent jelentünk" — egyik sem számol elavult, kódba égetett értékkel. EurHufRate = c.GetValue("ExchangeRate:EurHuf"), ThresholdWeightKg = c.GetValue("Thresholds:WeightKg"), - ThresholdValueHuf = c.GetValue("Thresholds:ValueHuf"), + ThresholdValueHuf = c.GetValue("Thresholds:ValueHuf"), }; }); diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index 4bf7412..67127b0 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -16,12 +16,12 @@ A `ShippingToEkaerMapper` megírása előtt tisztázandó pontok. Minden sor a j | # | Téma | Jelenlegi feltételezés | Nyitott kérdés | |---|------|------------------------|----------------| | 1 | `tradeType` (E/I/D irány) | Bejövő = beszerzés. HU feladó → **D** (belföldi), egyébként → **I** (import). A feladó országát a `ShippingDocument.Partner` / `Country` adja. | Automatikusan az országból dőljön el, vagy kézzel (UI) állítható? A kimenő (**E**, export) egyelőre hatókörön kívül (lásd #7). | -| 2 | Célhely (destination) | Hardcode-olt FruitBank-telephely (cím). | Ne hardcode legyen — konfigból/beállításból jöjjön. Honnan (plugin settings / nopCommerce warehouse)? | +| 2 | Célhely / telephely | ✅ **Megoldva:** configból (`Ekaer:Company` + `Site` al-szekció) → `IEkaerSettings.Company` + `EkaerCompanyInfo.Site` (`LocationType`, fel-/lerakodás). Nincs hardcode. | Több telephely (mint a `PartnerDepot`) — későbbi, ha kell. | | 3 | Érték (`value`, HUF) | ✅ **Megoldva:** `UnitPriceOnDocument × QuantityOnDocument × árfolyam` (`EkaerValueCalculator`). Pénznem a `Partner.Currency`-ből; EUR-HUF árfolyam configból (`Ekaer:ExchangeRate:EurHuf`, MNB középárfolyam). | Árfolyam dinamizálása (jelenleg „drót" config-érték) — későbbi. | | 4 | Tömeg (`weight`) | `ShippingItem.MeasuredGrossWeight` (bruttó). | Az EKÁER **bruttó** tömeget vár — megerősíteni, hogy ez bruttó és nem nettó. | | 5 | Granularitás | 1 `ShippingDocument` → 1 `tradeCard`; 1 `Shipping` → több művelet (több dokumentum). | Megerősíteni: tényleg dokumentumonként egy tradeCard? | | 6 | Eladó (`seller*`) mezők | `ShippingDocument.Partner`-ből: `Name`, `TaxId` (első 8 jegy = adószám-törzs), `CountryCode`, `City`, `Street`, `PostalCode`. | A `Partner` (`PartnerBase`) pontos mezőnevei/forrásai — megerősíteni. | -| 7 | Kimenő irány (`Order` → EKÁER) | Egyelőre nincs. A vevő gyakran maga viszi el (saját fuvar) → nem mi jelentünk. | Mikor kell mégis nekünk export/kimenő (E) tradeCard-ot küldeni? | +| 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`MapOrder`): belföldi értékesítés (**D**/**S**), mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). | **Nyitott:** a vonó jármű / fuvarozó forrása — az Orderen NINCS; az OrderDto-ba bekötendő (a `MapOrder` `Vehicle`/`CarrierText` TODO-ja). Export (**E**) + külföldi deviza + a `Customer.CountryId` ISO-feloldása: későbbi. | | 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md). Hosszú távon külön `Vtsz` mező. | | 9 | tétel `tradeReason` | `A` (beszerzés) — a mapper ezt használja bejövő árura. | Korábban tévesen `S`-nek feltételezve; az enum valójában `S`=értékesítés, `A`=beszerzés. Üzletileg megerősítendő. | | 10 | `value` mező | ✅ **Megoldva:** a value most töltve (lásd #3); 0 / ismeretlen ár esetén `null` marad. | A NAV 2021-től `value > 0`-t vár — a validátorban a `value > 0` üzleti szabály még hozzáadandó. | From b7a38c3efbcdd10fe73eba0bbb4dd60b6bab66e4 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 12 Jun 2026 09:00:33 +0200 Subject: [PATCH 07/13] Add EkaerHistoryFilter to GetEkaerHistories method Enhanced GetEkaerHistories to accept an EkaerHistoryFilter parameter and apply server-side filtering based on StatusId. Updated logging to include the filter value. The method now returns filtered results according to the selected filter (ToSubmit, Sent, NeedsCompletion, or all), instead of always returning all records. --- .../Controllers/FruitBankDataController.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index d07fb60..bc00e5b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -270,11 +270,22 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers } [SignalR(SignalRTags.GetEkaerHistories)] - public async Task> GetEkaerHistories() + public async Task> GetEkaerHistories(EkaerHistoryFilter ekaerHistoryFilter) { - _logger.Detail($"GetEkaerHistories invoked"); + _logger.Detail($"GetEkaerHistories invoked; ekaerHistoryFilter: {ekaerHistoryFilter}"); - return await ctx.EkaerHistories.GetAll().ToListAsync(); + // Szerver-oldali szűrés (a tabokhoz) a StatusId int-oszlopon — a helper-ek nem fordulnak SQL-re, + // ezért közvetlen StatusId-összehasonlítás. All / ismeretlen → minden. + IQueryable query = ctx.EkaerHistories.GetAll(); + query = ekaerHistoryFilter switch + { + EkaerHistoryFilter.ToSubmit => query.Where(eh => eh.StatusId != (int)EkaerStatus.Sent && eh.StatusId != (int)EkaerStatus.SentWithMissingData), + EkaerHistoryFilter.Sent => query.Where(eh => eh.StatusId == (int)EkaerStatus.Sent), + EkaerHistoryFilter.NeedsCompletion => query.Where(eh => eh.StatusId == (int)EkaerStatus.SentWithMissingData), + _ => query, + }; + + return await query.ToListAsync(); } [SignalR(SignalRTags.GetEkaerHistoryById)] From 3bde0b4063216b55624463d4cd30b051d07a3be3 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 12 Jun 2026 18:45:53 +0200 Subject: [PATCH 08/13] Refactor EkaerHistory filtering; add count endpoint Refactored GetEkaerHistories to use a new GetByFilter method in EkaerHistoryDbTable for SQL-compatible, flag-based filtering. Added GetEkaerHistoryCount endpoint and supporting CountByFilterAsync method. Improved status partitioning logic and clarified related comments. --- .../Controllers/FruitBankDataController.cs | 19 ++++++--------- .../Domains/DataLayer/EkaerHistoryDbTable.cs | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index bc00e5b..61171a8 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -273,19 +273,14 @@ 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(); + } - // Szerver-oldali szűrés (a tabokhoz) a StatusId int-oszlopon — a helper-ek nem fordulnak SQL-re, - // ezért közvetlen StatusId-összehasonlítás. All / ismeretlen → minden. - IQueryable query = ctx.EkaerHistories.GetAll(); - query = ekaerHistoryFilter switch - { - EkaerHistoryFilter.ToSubmit => query.Where(eh => eh.StatusId != (int)EkaerStatus.Sent && eh.StatusId != (int)EkaerStatus.SentWithMissingData), - EkaerHistoryFilter.Sent => query.Where(eh => eh.StatusId == (int)EkaerStatus.Sent), - EkaerHistoryFilter.NeedsCompletion => query.Where(eh => eh.StatusId == (int)EkaerStatus.SentWithMissingData), - _ => query, - }; - - return await query.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)] diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs index 3e4923f..5931959 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/EkaerHistoryDbTable.cs @@ -20,4 +20,27 @@ public class EkaerHistoryDbTable : MgDbTableBase public Task GetByIdAsync(int id) => GetAll().FirstOrDefaultAsync(p => p.Id == id); public IQueryable GetByForeignKey(int foreignKey) => GetAll().Where(p => p.ForeignKey == foreignKey); + + // 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])]; + + /// 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) + { + // HasFlag(All) mindig true (0 bit), ezért előbb az All-ágat zárjuk rövidre. + if (filter == EkaerHistoryFilter.All) return GetAll(); + + 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)); + } + + // 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)); } From 24164b518945b4225d06c9afd56c12ca9b7dcfc6 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 15 Jun 2026 11:53:00 +0200 Subject: [PATCH 09/13] =?UTF-8?q?Refactor=20EK=C3=81ER=20history=20creatio?= =?UTF-8?q?n=20&=20update=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored CreateMissingEkaerHistories to return detailed results (created count + messages), improved grouping, obligation checks, and error handling for both inbound and outbound records. - Updated AcBinaryHubProtocol to use Bytes mode and set FlushTimeout. - Enhanced documentation: clarified tradeType and carrier mapping, explained threshold logic and reconciliation workflow, and warned about over-reporting risks. - Minor code and comment cleanups for clarity. --- .../Controllers/FruitBankDataController.cs | 80 ++++++++++++------- .../Infrastructure/PluginNopStartup.cs | 4 +- .../docs/EKAER/EKAER_TODO.md | 10 ++- Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md | 27 ++++++- 4 files changed, 87 insertions(+), 34 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index 61171a8..b4bba8d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -363,13 +363,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers // 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)); + _ = 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)); + _ = 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 }; @@ -384,12 +382,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers /// idempotens, és bármikor újrafuttatható (maga a gomb a "reconciliation"). /// [SignalR(SignalRTags.CreateMissingEkaerHistories)] - public async Task CreateMissingEkaerHistories(DateTime fromDate) + public async Task CreateMissingEkaerHistories(DateTime fromDate) { _logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}"); - // Bejövő: rekord nélküli szállítólevelek a dátumtól — Partnerrel és tételekkel betöltve a kapuhoz - // (IsEkaer-mentesség + tömeg/érték küszöb). A fájl-blobokat NEM töltjük (csak Partner + Items). + var toCreate = new List(); + 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. var candidates = await ctx.ShippingDocuments.GetAll() .Where(sd => sd.ShippingDate >= fromDate) .Where(sd => !ctx.EkaerHistories.Table.Any(eh => eh.ForeignKey == sd.Id && !eh.IsOutgoing)) @@ -397,33 +398,58 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers .LoadWith(sd => sd.ShippingItems) .ToListAsync(); - var missingInbound = new List(); - foreach (var doc in candidates) + // 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))) { - // Explicit mentesítés (pl. nagybani piac, azonos cím — nincs közúti fuvar a partnerek között). - if (doc.Partner?.IsEkaer == false) continue; + 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; - var items = doc.ShippingItems ?? []; - var totalWeight = items.Sum(i => i.MeasuredGrossWeight); - var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(doc.Partner?.Currency, ekaerSettings.EurHufRate); - var totalValueHuf = items.Sum(EkaerValueCalculator.ItemLineValue) * rateToHuf; - - // Küszöb alatt (tömeg ÉS érték is) → nem kötelező EKÁER → nem hozunk létre sort. - if (totalWeight < ekaerSettings.ThresholdWeightKg && totalValueHuf < ekaerSettings.ThresholdValueHuf) continue; - - missingInbound.Add(new EkaerHistory { ForeignKey = doc.Id, IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }); + // 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 }); + } + 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}"); + } } - // Kimenő: rekord nélküli, lezárt (Complete) rendelések, DateOfReceipt a dátumtól (jövőbeli is — előre-bejelentés). - var missingOutgoing = await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false) + // ── 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.EkaerHistories.Table.Any(eh => eh.ForeignKey == o.Id && eh.IsOutgoing)) - .Select(o => new EkaerHistory { ForeignKey = o.Id, IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }) + .Select(o => o.Id) .ToListAsync(); - var createdCount = 0; + var missingOrders = missingOrderIds.Count == 0 ? [] : await ctx.OrderDtos.GetAllByIds(missingOrderIds, true).ToListAsync(); - foreach (var ekaerHistory in missingInbound.Concat(missingOutgoing)) + 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 { ForeignKey = order.Id, IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }); + } + 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 ekaerHistory in toCreate) { try { @@ -436,8 +462,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers } } - _logger.Info($"CreateMissingEkaerHistories; created: {createdCount} (inbound: {missingInbound.Count}, outgoing: {missingOutgoing.Count}); fromDate: {fromDate:yyyy-MM-dd}"); - return createdCount; + _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)] diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 6f6059d..967692d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -259,8 +259,10 @@ public class PluginNopStartup : INopStartup }) .AddAcBinaryProtocol(opts => { - opts.ProtocolMode = BinaryProtocolMode.AsyncSegment; + //opts.ProtocolMode = BinaryProtocolMode.AsyncSegment; + opts.ProtocolMode = BinaryProtocolMode.Bytes; opts.FlushPolicy = FlushPolicy.Coalesced; + opts.FlushTimeout = TimeSpan.FromSeconds(15); // Explicit AcLogger instance (name-based category, matches the previous setup). // If omitted, the options extension falls back to ILogger from DI. diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index 67127b0..4ef6ecc 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -15,13 +15,13 @@ A `ShippingToEkaerMapper` megírása előtt tisztázandó pontok. Minden sor a j | # | Téma | Jelenlegi feltételezés | Nyitott kérdés | |---|------|------------------------|----------------| -| 1 | `tradeType` (E/I/D irány) | Bejövő = beszerzés. HU feladó → **D** (belföldi), egyébként → **I** (import). A feladó országát a `ShippingDocument.Partner` / `Country` adja. | Automatikusan az országból dőljön el, vagy kézzel (UI) állítható? A kimenő (**E**, export) egyelőre hatókörön kívül (lásd #7). | +| 1 | `tradeType` (E/I/D irány) | ✅ **Frissítve (2026-06):** a feladó és a címzett **országkódjának összevetése** dönt: egyezik → **D** (belföld); eltér → kimenőnél **E** (export), bejövőnél **I** (import). (Korábban: HU ↔ nem-HU.) | A `Customer.CountryId` → ISO-feloldás kimenőnél még TODO (#7) → addig a vevő HU, ezért a kimenő ténylegesen D marad. | | 2 | Célhely / telephely | ✅ **Megoldva:** configból (`Ekaer:Company` + `Site` al-szekció) → `IEkaerSettings.Company` + `EkaerCompanyInfo.Site` (`LocationType`, fel-/lerakodás). Nincs hardcode. | Több telephely (mint a `PartnerDepot`) — későbbi, ha kell. | | 3 | Érték (`value`, HUF) | ✅ **Megoldva:** `UnitPriceOnDocument × QuantityOnDocument × árfolyam` (`EkaerValueCalculator`). Pénznem a `Partner.Currency`-ből; EUR-HUF árfolyam configból (`Ekaer:ExchangeRate:EurHuf`, MNB középárfolyam). | Árfolyam dinamizálása (jelenleg „drót" config-érték) — későbbi. | | 4 | Tömeg (`weight`) | `ShippingItem.MeasuredGrossWeight` (bruttó). | Az EKÁER **bruttó** tömeget vár — megerősíteni, hogy ez bruttó és nem nettó. | | 5 | Granularitás | 1 `ShippingDocument` → 1 `tradeCard`; 1 `Shipping` → több művelet (több dokumentum). | Megerősíteni: tényleg dokumentumonként egy tradeCard? | | 6 | Eladó (`seller*`) mezők | `ShippingDocument.Partner`-ből: `Name`, `TaxId` (első 8 jegy = adószám-törzs), `CountryCode`, `City`, `Street`, `PostalCode`. | A `Partner` (`PartnerBase`) pontos mezőnevei/forrásai — megerősíteni. | -| 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`MapOrder`): belföldi értékesítés (**D**/**S**), mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). | **Nyitott:** a vonó jármű / fuvarozó forrása — az Orderen NINCS; az OrderDto-ba bekötendő (a `MapOrder` `Vehicle`/`CarrierText` TODO-ja). Export (**E**) + külföldi deviza + a `Customer.CountryId` ISO-feloldása: későbbi. | +| 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`ToConsignment(OrderDto)`): mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). **Fuvarozó = `Customer.Company`** (a vevő viszi el — 2026-06). | **Nyitott:** a vonó jármű (**rendszám**) forrása — a `Customer`-hez még nincs bekötve (üres → warning, a felrakodás megkezdéséig pótolandó). Külföldi deviza + `Customer.CountryId` ISO-feloldás (export `E`): későbbi. | | 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md). Hosszú távon külön `Vtsz` mező. | | 9 | tétel `tradeReason` | `A` (beszerzés) — a mapper ezt használja bejövő árura. | Korábban tévesen `S`-nek feltételezve; az enum valójában `S`=értékesítés, `A`=beszerzés. Üzletileg megerősítendő. | | 10 | `value` mező | ✅ **Megoldva:** a value most töltve (lásd #3); 0 / ismeretlen ár esetén `null` marad. | A NAV 2021-től `value > 0`-t vár — a validátorban a `value > 0` üzleti szabály még hozzáadandó. | @@ -44,7 +44,11 @@ Az EKÁER bejelentés-kötelezettség küszöbe **termékfüggő** — a VTSZ ko | Kockázatos egyéb (nem élelmiszer) | 500 kg | 1 000 000 Ft | | Nem kockázatos | 2500 kg | 5 000 000 Ft | -**Jelenlegi (átmeneti):** a `CreateMissingEkaerHistories` egyetlen, globális küszöböt használ az `EkaerSettings`-ből (config: 200 kg / 250 000 Ft — a legszigorúbb, hogy ne legyen aluljelentés). +> **A fenti értékek a 13/2020. (XII. 23.) PM rendelet kategóriái — REFERENCIA, 2026-06 állapot.** Az alkalmazott küszöb a kódba **NINCS drótozva** (`appsettings` → `Ekaer:Thresholds`), és a jogszabály/beállítás változásával módosul; ezt a táblát csak tájékoztatásul, **dátumozva** tartjuk. +> +> ⚠️ **A túljelentés is bírság** (megerősítve, 2026-06): a globális „legszigorúbb" küszöb egy **nem-élelmiszer** terméket (ami csak a magasabb határnál kötelező) **túljelentene**. Fruit-importőrnél (minden áru kockázatos élelmiszer) ez nem gond — de amint nem-élelmiszer is bekerül, ez a per-termék kategória **szükségessé** válik (nem csak P2). + +**Jelenlegi (átmeneti):** a `CreateMissingEkaerHistories` egyetlen, globális küszöböt használ az `EkaerSettings`-ből — **configból** (`Ekaer:Thresholds`), a kódba **NEM drótozva**. Jelenleg a **kockázatos-élelmiszer** határ van beállítva (a fruit-importőr profil miatt, a legszigorúbb a beszerzéseknél). A kapu a küszöböt `(Shipping, Partner)`-re **aggregálva** nézi, külföldinél küszöb nélkül kötelez (lásd README → kapu-szakasz). **Cél:** a **terméknél (Product) kézzel megadható** küszöb-**KATEGÓRIA** (nem a konkrét érték!) — egy **enum** (pl. `EkaerThresholdCategory { RiskyFood, RiskyOther, NonRisky }`) —, **DB-ben tárolva** (Product oszlop / GenericAttribute). A konkrét kg/Ft értékeket a **kód** rendeli a kategóriához → értékhatár-változáskor egy helyen módosul. A küszöb-kapu dokumentumonként a tételek kategóriái szerint alkalmazza a megfelelő határt. (Későbbi lépés: a kategória automatikus levezetése a NAV kockázatos-VTSZ-listákból.) diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md index 4f8b81b..ec67695 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md @@ -11,7 +11,7 @@ A FruitBank EKÁER-bejelentés **szerver-oldali** (a kliens már standalone WASM | Réteg | Hol | Mit ad | |---|---|---| | **NAV/EKÁER framework** | `AyCode.Services/Nav/` + `Nav/Ekaer/` (AyCode.Core) | transport, auth (SHA-512), generált modellek, **`EkaerTradeCardValidator`** (NAV-séma + üzleti szabályok), **`EkaerSubmitService`** (validate→send), `EkaerManageService` (HTTP). Általános — **nem** FruitBank-specifikus. | -| **FruitBank leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — `Shipping` → `tradeCard` ops (tiszta leképező, nem validál) | +| **FruitBank leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — a forrást (bejövő `ShippingDocument`-csoport / kimenő `OrderDto`) egy irány-független **`EkaerConsignment`**-re képezi (`ToConsignment`), abból épül a `tradeCard` (`BuildTradeCard`). **`EkaerReportability`** — bejelentés-kötelezettség (küszöb/külföld). **`EkaerValueCalculator`** — tétel-érték HUF-ban. | | **FruitBank fogyasztó** | `FruitBank.Common.Server/Services/Ekaer/` | **`FruitBankEkaerService`** — szerver-oldali orchestráció: credentials/options + map → submit | | **nopCommerce-integráció (ez a plugin)** | ez a folder + a plugin kód | DI-bekötés, NAV-fiók settings, a VTSZ-forrás (`Product.Gtin`), SignalR-trigger a kliens felől, és ez a doksi | @@ -26,6 +26,10 @@ 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. + ## Companion fájlok - [`EKAER_ISSUES.md`](EKAER_ISSUES.md) — ismert problémák (pl. `MGFBANKPLUG-EKAER-I-T3X8`: GTIN≠VTSZ). @@ -36,7 +40,7 @@ Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki k | EKÁER | FruitBank forrás | |---|---| | `seller*` (feladó) | `ShippingDocument.Partner` (a beszállító) | -| `carrierText` (fuvarozó, szöveges) | `Shipping.CargoPartner.Name` | +| `carrierText` (fuvarozó, szöveges) | **bejövő:** `Shipping.CargoPartner.Name`; **kimenő:** `Customer.Company` (a vevő viszi el az árut) | | `vehicle` / `vehicle2` | `Shipping.CargoTruck` / `CargoTrailer` (`LicencePlate` + `CountryCode`, normalizálva) | | tétel `productVtsz` | `ShippingItem.ProductDto.Gtin` (átmenetileg — lásd `EKAER_ISSUES.md`) | | tétel `productName` | `ShippingItem.ProductName` | @@ -45,6 +49,23 @@ Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki k A nyitott pontok (tradeType, destination, value/deviza, granularitás, kimenő irány): [`EKAER_TODO.md`](EKAER_TODO.md). +## Reconciliation-kapu, kötelezettség, életciklus (2026-06) + +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): +`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; +- **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. + +> **A küszöb configból jön** (`Ekaer:Thresholds:WeightKg` / `:ValueHuf`) — a kódba **NINCS drótozva**, mert a NAV-jogszabály és a beállítás szerint **változhat**. A konkrét, dátumozott értékeket és a termékkategóriákat lásd [`EKAER_TODO.md`](EKAER_TODO.md) **T-W3R8**. A NAV **mindkét irányban büntet** (alul- ÉS túljelentés), ezért a küszöbnek pontosnak kell lennie. + +**Egyesített leképezés:** a bejövő/kimenő forrás egy **`EkaerConsignment`** köztes modellre képződik (két adapter), és **abból** épül MIND a tradeCard (`BuildTradeCard`), MIND a kötelezettség-döntés — így az irányfüggő rész két adapterre szorul, a közös logika egy helyen van. A `MapDocument` / `MapOrder` / `MapShipping` ennek vékony wrappere. + ## Bekötés a szerveren (DI) A plugin (vagy a szerver-startup) köti be a láncot — a NAV-fiók titkos adatai a szerver config/secret store-jából: @@ -54,6 +75,6 @@ services.AddHttpClient(/* NAV TLS 1.2 */); services.AddScoped(); services.AddScoped(); services.AddScoped(); -services.AddSingleton(/* EkaerMappingOptions: destination + saját raktár */); +services.AddSingleton(/* EkaerSettings: Ekaer:Company + Site + küszöbök + EUR-HUF árfolyam — configból */); services.AddScoped(); ``` From e783d7c1162e0fc396287e5b488e530035c39ee0 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 15 Jun 2026 18:01:48 +0200 Subject: [PATCH 10/13] =?UTF-8?q?EK=C3=81ER=20docs:=20VTSZ=20grouping,=20a?= =?UTF-8?q?rchive=20rules,=20reopened=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clarified docs-archive skill to use year-month buckets and archive only Closed entries. - Reopened and updated the Product.Gtin/VTSZ separation issue in EKAER_ISSUES.md. - Added new TODO (T-V9G3) for grouping tradeCardItems by VTSZ and resolving group names from Category, with requirements and affected code. - Updated README to reflect VTSZ-based grouping and productName resolution. --- .../.github/copilot-instructions.md | 2 +- .../docs/EKAER/EKAER_ISSUES.md | 5 ++-- .../docs/EKAER/EKAER_TODO.md | 25 ++++++++++++++++++- Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md b/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md index 4089bf5..1d77eb9 100644 --- a/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md +++ b/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md @@ -61,7 +61,7 @@ Skills defined in other repos. **Two reactive skills are pre-loaded at session s Location: `AyCode.Core/.github/skills/adr-author/SKILL.md` **Invoke on explicit user request** ("let's plan X", "decide Y vs Z", "design the W module") **or proactively flag** when the conversation looks ADR-worthy (user must confirm — never auto-invoke). -- **docs-archive** — Rotate closed entries (Status: Fixed/Resolved/Won't fix/Superseded by X) from active `_ISSUES.md`, `_TODO.md`, and `LLM_PROTOCOL_DECISIONS.md` into year-bucketed archive companions (`*_.md`). Year of the Status update determines destination file. Active files retain only Open/Partially Fixed entries plus a pointer to the archives. Archive files are NOT auto-loaded — agents read them on-demand when historical context becomes relevant. +- **docs-archive** — Rotate entries from active `_ISSUES.md`, `_TODO.md`, and `LLM_PROTOCOL_DECISIONS.md` into **year-month bucket** archive companions (`*__.md`). Two rules: **(A)** `_ISSUES.md` / `_TODO.md` — only `Status: Closed` entries are archive-eligible (per `TOPIC_CODES.md` 3-value vocabulary: `Open` / `InProgress` / `Closed`); **(B)** `LLM_PROTOCOL_DECISIONS.md` — last-15-active rolling window (LLMP-DEC-67), older entries rotate by entry date. Bucket derived from a date in the entry body. Active files retain `Open` / `InProgress` entries plus a pointer to the archives. Archive files are NOT auto-loaded — agents read them on-demand when historical context becomes relevant. Location: `AyCode.Core/.github/skills/docs-archive/SKILL.md` **Invoke on explicit user request** ("archive ISSUES", "rotate logs") **or proactively flag** when an active artifact file has many closed prior entries (user must confirm — never auto-invoke). diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md index a01943f..f4ee6b5 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md @@ -9,10 +9,9 @@ Scope: a FruitBank EKÁER-bejelentés szerver-oldali / nopCommerce-integrációs ## MGFBANKPLUG-EKAER-I-T3X8: A `Product.Gtin` átmenetileg a VTSZ-t tárolja — szétválasztandó -**Status:** Closed (2026-06-02) — superseded · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás) +**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás) -### Resolution -**Áthelyezve** az általános adatmodell-topicba: **`FBANKAPP-DMODEL-I-P6X4`** (`FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md#fbankapp-dmodel-i-p6x4`). Indok: a GTIN/VTSZ szétválasztás **általános** adatmodell-kérdés, nem EKÁER-specifikus — az EKÁER csak felhasználója. A követés ott folytatódik; az alábbi leírás referencia. +> **Újranyitva (2026-06-15):** a probléma **továbbra is fennáll** — a `Gtin` jelenleg is a VTSZ-t tárolja, a szétválasztás nem történt meg. A 2026-06-02-i `FBANKAPP-DMODEL-I-P6X4`-be (DATAMODEL) helyezés visszavonva; a követés ITT (EKÁER) folytatódik. *(A `FBANKAPP-DMODEL-I-P6X4` ennek megfelelően lezárva — DATAMODEL.)* Az EKÁER `tradeCardItem.productVtsz` (kötelező, 8 jegyű vámtarifaszám) forrása jelenleg a nopCommerce **`Product.Gtin`** oszlop (a `ProductDto.Gtin`-en keresztül). A GTIN és a VTSZ **fogalmilag különböző**: - **GTIN** — globális kereskedelmi cikkszám (vonalkód-azonosító, EAN/UPC). diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index 4ef6ecc..106992b 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -22,7 +22,7 @@ A `ShippingToEkaerMapper` megírása előtt tisztázandó pontok. Minden sor a j | 5 | Granularitás | 1 `ShippingDocument` → 1 `tradeCard`; 1 `Shipping` → több művelet (több dokumentum). | Megerősíteni: tényleg dokumentumonként egy tradeCard? | | 6 | Eladó (`seller*`) mezők | `ShippingDocument.Partner`-ből: `Name`, `TaxId` (első 8 jegy = adószám-törzs), `CountryCode`, `City`, `Street`, `PostalCode`. | A `Partner` (`PartnerBase`) pontos mezőnevei/forrásai — megerősíteni. | | 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`ToConsignment(OrderDto)`): mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). **Fuvarozó = `Customer.Company`** (a vevő viszi el — 2026-06). | **Nyitott:** a vonó jármű (**rendszám**) forrása — a `Customer`-hez még nincs bekötve (üres → warning, a felrakodás megkezdéséig pótolandó). Külföldi deviza + `Customer.CountryId` ISO-feloldás (export `E`): későbbi. | -| 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md). Hosszú távon külön `Vtsz` mező. | +| 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md); hosszú távon külön `Vtsz` mező. A VTSZ-**csoportosítás** + `productName` **külön** kérdés: **T-V9G3**. | | 9 | tétel `tradeReason` | `A` (beszerzés) — a mapper ezt használja bejövő árura. | Korábban tévesen `S`-nek feltételezve; az enum valójában `S`=értékesítés, `A`=beszerzés. Üzletileg megerősítendő. | | 10 | `value` mező | ✅ **Megoldva:** a value most töltve (lásd #3); 0 / ismeretlen ár esetén `null` marad. | A NAV 2021-től `value > 0`-t vár — a validátorban a `value > 0` üzleti szabály még hozzáadandó. | @@ -56,3 +56,26 @@ Az EKÁER bejelentés-kötelezettség küszöbe **termékfüggő** — a VTSZ ko - `FruitBank.Common/Dtos/ProductDto.cs` — küszöb-kategória mező (enum, DB-ben) - `EkaerSettings` / `EkaerValueCalculator` (`FruitBank.Common/Services/Ekaer/`) — kategória → küszöbérték leképezés - `FruitBankDataController.CreateMissingEkaerHistories` — a kapu a tételek kategóriái szerint + +## MGFBANKPLUG-EKAER-T-V9G3: tradeCardItem-ek csoportosítása VTSZ szerint + `productName` Category-ből + +**Status:** Open · **Priority:** P2 · **Type:** T (mapping) · **2026-06** + +A NAV felé **egy VTSZ-re egy `tradeCardItem`** kell — több azonos-VTSZ-ű termék (pl. kajszibarack/nektarin/őszibarack, mind `08093000`) **egy** tételbe vonva, **összegzett** tömeggel/értékkel. Jelenleg a mapper terméktételenként ad egy `tradeCardItem`-et → azonos VTSZ-nél több sor (a NAV felé nem jó). A nehézség a **`productName`** (a csoport tagjainak más a neve). + +> **Ez NEM a VTSZ/GTIN-szétválasztás** — az **külön** nyitott kérdés: [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md) (lásd #8). A T-V9G3 **csak a csoportosítást és a csoport-nevet** oldja meg, a VTSZ tárolási helyétől (jelenleg `Product.Gtin`) **függetlenül**. + +**Eldöntött terv (2026-06):** +- **`productName` (a csoport neve):** egy nopCommerce **`Category`**-ből, ami VTSZ→név **helper**: a `Category.MetaKeywords` első 8 karaktere = a VTSZ, a `Category.Name` = a `productName`. A kategóriát a **termék VTSZ-e** (jelenleg a `Gtin`-ben — I-T3X8) **= `MetaKeywords`[0..8]** match azonosítja → a termék kategória-**tagsága lényegtelen** (több kategóriából csak az egyező számít). A kategóriák a weben is pontosak maradnak. +- **`itemExternalId`:** **sorszám** (1, 2, …) a VTSZ-csoportok fölött — a séma szerint „bejelentésen belüli **tetszőleges** azonosító a **kérés↔válasz** párosításhoz" (max 50), NEM kell stabilnak / forrás-Id-nek lennie; int-parse-olható. +- **Csoportosítás helye:** `BuildTradeCard` (VTSZ szerint group → Σtömeg, Σérték, név a lookupból). A **kapu** (`EvaluateObligation`) NEM érintett (csak Σ-t számol → nincs plusz query a reconciliation-nél). +- **Név-feloldás:** **csak generáláskor**, szerver-oldalon — a `ProductDto`-t NEM szennyezzük EKÁER-only mezővel. A termék-query `(productId, categoryId)` tuple-t ad (a VTSZ↔`MetaKeywords` joinból), majd egy batch `Category`-load `ToDictionary`-be → `productId → categoryName`. +- **Fallback:** ha egy VTSZ-hez nincs kategória → a `productName` üres marad → a **generate-validátor** jelzi (mint a `value > 0`). + +**Üzemeltetési feltétel:** a VTSZ-forrás (jelenleg `Gtin`) és a kategória `MetaKeywords`-VTSZ-e legyen **szinkronban** (FruitBank-felelősség; a beírásnál a VTSZ-t elöl). + +**Affected:** +- `FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs` — `BuildTradeCard` VTSZ-group + `vtszNames` lookup +- `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 diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md index ec67695..3c0b90c 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md @@ -43,7 +43,7 @@ Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki k | `carrierText` (fuvarozó, szöveges) | **bejövő:** `Shipping.CargoPartner.Name`; **kimenő:** `Customer.Company` (a vevő viszi el az árut) | | `vehicle` / `vehicle2` | `Shipping.CargoTruck` / `CargoTrailer` (`LicencePlate` + `CountryCode`, normalizálva) | | tétel `productVtsz` | `ShippingItem.ProductDto.Gtin` (átmenetileg — lásd `EKAER_ISSUES.md`) | -| tétel `productName` | `ShippingItem.ProductName` | +| tétel `productName` | `ShippingItem.ProductName` (a VTSZ-szerinti csoportosítás + a csoport-név `Category`-ből: TODO **T-V9G3**) | | tétel `weight` | `ShippingItem.MeasuredGrossWeight` (bruttó) | | tétel `tradeReason` | `A` (beszerzés — bejövő áru). ⚠️ Az enum: `S`=értékesítés, `A`=beszerzés, `W`=bérmunka, `O`=egyéb. | From 1d56eba8ecb868125dcd7386121215c8ec39c506 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 16 Jun 2026 12:02:19 +0200 Subject: [PATCH 11/13] =?UTF-8?q?Update=20DB=20connection,=20EK=C3=81ER=20?= =?UTF-8?q?mapping,=20and=20partner=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switched appsettings.json connection string to FruitBank_DEV. - FruitBankDataController: GetPartners now loads PartnerDepots; GetPartnerById uses new overload. - Clarified in EKAER_TODO.md and README.md that carrierText is optional and left empty for outgoing shipments (customer is the carrier). --- .../Controllers/FruitBankDataController.cs | 6 ++++-- Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md | 2 +- Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index b4bba8d..c82fed4 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -107,7 +107,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers { _logger.Detail($"GetPartners invoked"); - return await ctx.Partners.GetAll().ToListAsync(); + // 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)] @@ -118,7 +120,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers //var customers = await ctx.GetCustormersBySystemRoleName("Measuring").ToListAsync(); //_logger.Error($"COUNT: {customers.Count}"); - return await ctx.Partners.GetByIdAsync(id); + return await ctx.Partners.GetByIdAsync(id, false); } [SignalR(SignalRTags.AddPartner)] diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md index 106992b..9e842ec 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_TODO.md @@ -21,7 +21,7 @@ A `ShippingToEkaerMapper` megírása előtt tisztázandó pontok. Minden sor a j | 4 | Tömeg (`weight`) | `ShippingItem.MeasuredGrossWeight` (bruttó). | Az EKÁER **bruttó** tömeget vár — megerősíteni, hogy ez bruttó és nem nettó. | | 5 | Granularitás | 1 `ShippingDocument` → 1 `tradeCard`; 1 `Shipping` → több művelet (több dokumentum). | Megerősíteni: tényleg dokumentumonként egy tradeCard? | | 6 | Eladó (`seller*`) mezők | `ShippingDocument.Partner`-ből: `Name`, `TaxId` (első 8 jegy = adószám-törzs), `CountryCode`, `City`, `Street`, `PostalCode`. | A `Partner` (`PartnerBase`) pontos mezőnevei/forrásai — megerősíteni. | -| 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`ToConsignment(OrderDto)`): mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). **Fuvarozó = `Customer.Company`** (a vevő viszi el — 2026-06). | **Nyitott:** a vonó jármű (**rendszám**) forrása — a `Customer`-hez még nincs bekötve (üres → warning, a felrakodás megkezdéséig pótolandó). Külföldi deviza + `Customer.CountryId` ISO-feloldás (export `E`): későbbi. | +| 7 | Kimenő irány (`Order` → EKÁER) | ✅ **Implementálva** (`ToConsignment(OrderDto)`): mi=eladó, vevő=címzett; tömeg `OrderItemDto.GrossWeight`, érték nettó (UnitPriceExclTax × qty), jelenleg minden HUF (rate 1). **`carrierText` kimenőnél NEM töltve** (üres) — a vevő maga viszi el, ő már a címzett; a carrierText opcionális (2026-06). | **Nyitott:** a vonó jármű (**rendszám**) forrása — a `Customer`-hez még nincs bekötve (üres → warning, a felrakodás megkezdéséig pótolandó). Külföldi deviza + `Customer.CountryId` ISO-feloldás (export `E`): későbbi. | | 8 | `productVtsz` forrás | `ProductDto.Gtin` (átmeneti). | GTIN ≠ VTSZ — lásd [`MGFBANKPLUG-EKAER-I-T3X8`](EKAER_ISSUES.md); hosszú távon külön `Vtsz` mező. A VTSZ-**csoportosítás** + `productName` **külön** kérdés: **T-V9G3**. | | 9 | tétel `tradeReason` | `A` (beszerzés) — a mapper ezt használja bejövő árura. | Korábban tévesen `S`-nek feltételezve; az enum valójában `S`=értékesítés, `A`=beszerzés. Üzletileg megerősítendő. | | 10 | `value` mező | ✅ **Megoldva:** a value most töltve (lásd #3); 0 / ismeretlen ár esetén `null` marad. | A NAV 2021-től `value > 0`-t vár — a validátorban a `value > 0` üzleti szabály még hozzáadandó. | diff --git a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md index 3c0b90c..0fd5ccb 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md @@ -40,7 +40,7 @@ Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki k | EKÁER | FruitBank forrás | |---|---| | `seller*` (feladó) | `ShippingDocument.Partner` (a beszállító) | -| `carrierText` (fuvarozó, szöveges) | **bejövő:** `Shipping.CargoPartner.Name`; **kimenő:** `Customer.Company` (a vevő viszi el az árut) | +| `carrierText` (szállítmányozó — **opcionális**, NAV nem követeli) | **bejövő:** `Shipping.CargoPartner.Name` (valós, külön fuvarozó); **kimenő:** — **üres** (a vevő maga viszi el, ő már a címzett; nincs külön fuvarozónk) | | `vehicle` / `vehicle2` | `Shipping.CargoTruck` / `CargoTrailer` (`LicencePlate` + `CountryCode`, normalizálva) | | tétel `productVtsz` | `ShippingItem.ProductDto.Gtin` (átmenetileg — lásd `EKAER_ISSUES.md`) | | tétel `productName` | `ShippingItem.ProductName` (a VTSZ-szerinti csoportosítás + a csoport-név `Category`-ből: TODO **T-V9G3**) | From c71bf2fcd86677bfcb6c71d187cb92c6b699a948 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 16 Jun 2026 21:45:49 +0200 Subject: [PATCH 12/13] =?UTF-8?q?EK=C3=81ER:=20add=20mapping=20table,=20gr?= =?UTF-8?q?oup=20logic,=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. From fdde2b3c6b5375bfc345d03f4607de658614ede8 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 17 Jun 2026 13:54:52 +0200 Subject: [PATCH 13/13] Set EkaerHistory summary fields and update dev DB config Updated the connection string to use the FruitBank_DEV database. Ensured EkaerHistory summary fields (ShippingDate, Partner/Customer) are consistently set via fruitBankEkaerService.SetSummary when creating or updating records, improving data consistency for both inbound and outbound cases. --- .../Controllers/FruitBankDataController.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs index a948e96..fe37962 100644 --- a/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Controllers/FruitBankDataController.cs @@ -344,6 +344,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers 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); + + // Összegző (ShippingDate + Partner) frissítése — a csoporton belül invariáns, így bármelyik dok jó. + fruitBankEkaerService.SetSummary(ekaerHistory, documents[0].ShippingDate, documents[0].Partner?.Name); } else { @@ -351,6 +354,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers var order = await ctx.OrderDtos.GetByIdAsync(foreignKeys[0], true) ?? throw new ArgumentException($"Order not found; id: {foreignKeys[0]}"); ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(order, ekaerHistory); + + fruitBankEkaerService.SetSummary(ekaerHistory, order.DateOfReceipt, order.Customer?.Company); } await ctx.EkaerHistories.UpdateAsync(ekaerHistory); @@ -376,18 +381,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers if (existing != null) return existing; - // A forrás-entitás léte irányfüggő: bejövő → ShippingDocument, kimenő → Order. + // A forrás-entitás létét ellenőrizzük és betöltjük (relations) az összegző mezőkhöz (ShippingDate + Partner). + var ekaerHistory = new EkaerHistory { IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }; if (!isOutgoing) { - _ = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey)); + var doc = await ctx.ShippingDocuments.GetByIdAsync(foreignKey, true) ?? throw new ArgumentException($"ShippingDocument not found; id: {foreignKey}", nameof(foreignKey)); + fruitBankEkaerService.SetSummary(ekaerHistory, doc.ShippingDate, doc.Partner?.Name); } else { - _ = await ctx.OrderDtos.GetByIdAsync(foreignKey, false) ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey)); + var order = await ctx.OrderDtos.GetByIdAsync(foreignKey, true) ?? throw new ArgumentException($"Order not found; id: {foreignKey}", nameof(foreignKey)); + fruitBankEkaerService.SetSummary(ekaerHistory, order.DateOfReceipt, order.Customer?.Company); } // EGY EkaerHistory + EGY mapping sor (egyelemű csoport), atomikusan. - var ekaerHistory = await ctx.AddEkaerHistoryWithMappingsAsync(new EkaerHistory { IsOutgoing = isOutgoing, Status = EkaerStatus.Pending }, [foreignKey]); + ekaerHistory = await ctx.AddEkaerHistoryWithMappingsAsync(ekaerHistory, [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); @@ -426,8 +434,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers 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())); + // Kötelező → EGY EkaerHistory a csoportra (összegző: ShippingDate + Partner — invariáns) + a dokumentumok mapping-foreignKey-ként. + var history = new EkaerHistory { IsOutgoing = false, StatusId = (int)EkaerStatus.Pending }; + fruitBankEkaerService.SetSummary(history, docs[0].ShippingDate, docs[0].Partner?.Name); + toCreate.Add((history, docs.Select(d => d.Id).ToList())); } catch (Exception ex) { @@ -454,7 +464,9 @@ 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 { IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }, [order.Id])); + var history = new EkaerHistory { IsOutgoing = true, StatusId = (int)EkaerStatus.Pending }; + fruitBankEkaerService.SetSummary(history, order.DateOfReceipt, order.Customer?.Company); + toCreate.Add((history, [order.Id])); } catch (Exception ex) {