This commit is contained in:
Adam 2026-06-24 00:47:45 +02:00
commit 330aee3e5f
12 changed files with 573 additions and 30 deletions

File diff suppressed because one or more lines are too long

View File

@ -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;
@ -13,6 +14,8 @@ using FruitBank.Common.Loggers;
using FruitBank.Common.Models;
using FruitBank.Common.Server;
using FruitBank.Common.Server.Interfaces;
using FruitBank.Common.Server.Services.Ekaer;
using FruitBank.Common.Services.Ekaer;
using FruitBank.Common.SignalRs;
using LinqToDB;
using Mango.Nop.Core.Dtos;
@ -20,6 +23,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;
@ -40,6 +44,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
ICustomerRegistrationService customerRegistrationService,
ILocalizationService localizationService,
PreOrderConversionService preorderConversionService,
IFruitBankEkaerService fruitBankEkaerService,
IEkaerSettings ekaerSettings,
IEnumerable<IAcLogWriterBase> logWriters)
: BasePluginController, IFruitBankDataControllerServer
{
@ -101,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)]
@ -112,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)]
@ -219,6 +227,274 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true);
}
[SignalR(SignalRTags.GetPartnerDepots)]
public async Task<List<PartnerDepot>> GetPartnerDepots()
{
_logger.Detail($"GetPartnerDepots invoked");
return await ctx.PartnerDepots.GetAll(true).ToListAsync();
}
[SignalR(SignalRTags.GetPartnerDepotById)]
public async Task<PartnerDepot> GetPartnerDepotById(int id)
{
_logger.Detail($"GetPartnerDepotById invoked; id: {id}");
return await ctx.PartnerDepots.GetByIdAsync(id, true);
}
[SignalR(SignalRTags.GetPartnerDepotsByPartnerId)]
public async Task<List<PartnerDepot>> GetPartnerDepotsByPartnerId(int partnerId)
{
_logger.Detail($"GetPartnerDepotsByPartnerId invoked; partnerId: {partnerId}");
return await ctx.PartnerDepots.GetByPartnerId(partnerId, true).ToListAsync();
}
[SignalR(SignalRTags.AddPartnerDepot)]
public async Task<PartnerDepot> 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<PartnerDepot> UpdatePartnerDepot(PartnerDepot partnerDepot)
{
ArgumentNullException.ThrowIfNull(partnerDepot);
_logger.Detail($"UpdatePartnerDepot invoked; id: {partnerDepot.Id}");
await ctx.PartnerDepots.UpdateAsync(partnerDepot);
return await ctx.PartnerDepots.GetByIdAsync(partnerDepot.Id, true);
}
[SignalR(SignalRTags.GetEkaerHistories)]
public async Task<List<EkaerHistory>> GetEkaerHistories(EkaerHistoryFilter ekaerHistoryFilter)
{
_logger.Detail($"GetEkaerHistories invoked; ekaerHistoryFilter: {ekaerHistoryFilter}");
return await ctx.EkaerHistories.GetByFilter(ekaerHistoryFilter, true).ToListAsync();
}
[SignalR(SignalRTags.GetEkaerHistoryCount)]
public async Task<int> GetEkaerHistoryCount(EkaerHistoryFilter ekaerHistoryFilter)
{
_logger.Detail($"GetEkaerHistoryCount invoked; ekaerHistoryFilter: {ekaerHistoryFilter}");
return await ctx.EkaerHistories.CountByFilterAsync(ekaerHistoryFilter);
}
[SignalR(SignalRTags.GetEkaerHistoryById)]
public async Task<EkaerHistory> GetEkaerHistoryById(int id)
{
_logger.Detail($"GetEkaerHistoryById invoked; id: {id}");
return await ctx.EkaerHistories.GetByIdAsync(id, true);
}
[SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)]
public async Task<List<EkaerHistory>> GetEkaerHistoriesByForeignKey(int foreignKey)
{
_logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}");
// A forrás-rekordot (ShippingDocument/Order Id) lefedő EKÁER-deklaráció(k), a Mappings-szel betöltve — EGY query
// (EXISTS a mapping-táblára), nem kettő.
return await ctx.EkaerHistories.GetAll(true)
.Where(eh => ctx.EkaerHistoryMappings.Table.Any(m => m.EkaerHistoryId == eh.Id && m.ForeignKey == foreignKey))
.ToListAsync();
}
[SignalR(SignalRTags.AddEkaerHistory)]
public async Task<EkaerHistory> AddEkaerHistory(EkaerHistory ekaerHistory)
{
ArgumentNullException.ThrowIfNull(ekaerHistory);
_logger.Detail($"AddEkaerHistory invoked; id: {ekaerHistory.Id}");
await ctx.EkaerHistories.InsertAsync(ekaerHistory);
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true);
}
[SignalR(SignalRTags.UpdateEkaerHistory)]
public async Task<EkaerHistory> UpdateEkaerHistory(EkaerHistory ekaerHistory)
{
ArgumentNullException.ThrowIfNull(ekaerHistory);
_logger.Detail($"UpdateEkaerHistory invoked; id: {ekaerHistory.Id}");
await ctx.EkaerHistories.UpdateAsync(ekaerHistory);
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id, true);
}
[SignalR(SignalRTags.GenerateEkaerXmlDocument)]
public async Task<EkaerHistory> GenerateEkaerXmlDocument(int ekaerHistoryId)
{
_logger.Detail($"GenerateEkaerXmlDocument invoked; ekaerHistoryId: {ekaerHistoryId}");
var ekaerHistory = await ctx.EkaerHistories.GetByIdAsync(ekaerHistoryId, true)
?? throw new ArgumentException($"EkaerHistory not found; id: {ekaerHistoryId}", nameof(ekaerHistoryId));
// A deklarációhoz tartozó forrás-rekordok a BETÖLTÖTT mappingből (bejövő: N szállítólevél; kimenő: 1 rendelés).
var foreignKeys = ekaerHistory.Mappings?.Select(m => m.ForeignKey).ToList() ?? [];
if (foreignKeys.Count == 0) throw new InvalidOperationException($"EkaerHistory #{ekaerHistoryId} has no mapped source records.");
if (!ekaerHistory.IsOutgoing)
{
// Bejövő: a csoport ÖSSZES szállítólevele EGY tradeCard-dá (összevont tömeg/érték); GetAll(true) = teljes gráf.
var documents = await ctx.ShippingDocuments.GetAll(true).Where(sd => foreignKeys.Contains(sd.Id)).ToListAsync();
if (documents.Count == 0) throw new InvalidOperationException($"EkaerHistory #{ekaerHistoryId}: no ShippingDocuments found for the mapped ids.");
ekaerHistory = fruitBankEkaerService.GenerateEkaerXmlDocument(documents, ekaerHistory);
// Ö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
{
// 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);
fruitBankEkaerService.SetSummary(ekaerHistory, order.DateOfReceipt, order.Customer?.Company);
}
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);
}
/// <summary>
/// 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).
/// </summary>
[SignalR(SignalRTags.CreateEkaerHistory)]
public async Task<EkaerHistory> CreateEkaerHistory(int foreignKey, bool isOutgoing)
{
_logger.Detail($"CreateEkaerHistory invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}");
// Idempotencia: a forrás-rekordra (azonos irányban) már létező mapping → a hozzá tartozó EkaerHistory,
// a Mappings-szel betöltve (GetAll(true)) — EGY query (EXISTS a mappingre), nincs külön újraolvasás.
var existing = await LinqToDB.AsyncExtensions.FirstOrDefaultAsync(
ctx.EkaerHistories.GetAll(true).Where(eh => eh.IsOutgoing == isOutgoing
&& ctx.EkaerHistoryMappings.Table.Any(m => m.EkaerHistoryId == eh.Id && m.ForeignKey == foreignKey)));
if (existing != null) return existing;
// A forrás-entitás lé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)
{
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
{
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.
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);
}
/// <summary>
/// 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").
/// </summary>
[SignalR(SignalRTags.CreateMissingEkaerHistories)]
public async Task<EkaerCreateResult> CreateMissingEkaerHistories(DateTime fromDate)
{
_logger.Detail($"CreateMissingEkaerHistories invoked; fromDate: {fromDate:yyyy-MM-dd}");
var toCreate = new List<(EkaerHistory History, List<int> ForeignKeys)>();
var messages = new HashSet<string>(); // dedup: ugyanaz a partner (pl. hiányzó országkód) több csoportban is előjöhet
// ── Bejövő ── Rekord nélküli szállítólevelek a dátumtól, Partnerrel + tételekkel betöltve a kötelezettség-kapuhoz.
// A bejelentés-egység (Shipping, Partner, PartnerDepot) = feladó(+telephely)→címzett→jármű: EGY EkaerHistory/csoport, a küszöb a csoport ÖSSZES dokumentumára aggregál.
var candidates = await ctx.ShippingDocuments.GetAll()
.Where(sd => sd.ShippingDate >= fromDate)
.Where(sd => !ctx.EkaerHistoryMappings.Table.Any(m => m.ForeignKey == sd.Id && ctx.EkaerHistories.Table.Any(eh => eh.Id == m.EkaerHistoryId && !eh.IsOutgoing)))
.LoadWith(sd => sd.Partner)
.LoadWith(sd => sd.ShippingItems)
.ToListAsync();
// Partner nélküli (még nincs rendesen felvett) szállítólevél CSENDBEN kihagyva — nincs feladó, nem kész;
// a mentes partner (IsEkaer=false) is. A maradék (Shipping, Partner, PartnerDepot) csoportban (a dokumentum mindig kap Shippinget).
foreach (var group in candidates.Where(sd => sd.Partner != null && sd.Partner.IsEkaer != false).GroupBy(sd => (sd.ShippingId, sd.PartnerId, sd.PartnerDepotId)))
{
var docs = group.ToList();
try
{
var result = fruitBankEkaerService.EvaluateObligation(docs);
if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; }
if (result.Obligation == EkaerObligation.NotRequired) continue;
// Kötelező → EGY EkaerHistory a csoportra (ö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)
{
_logger.Error($"CreateMissingEkaerHistories; inbound evaluate failed; ShippingId: {group.Key.ShippingId}; PartnerId: {group.Key.PartnerId}; PartnerDepotId: {group.Key.PartnerDepotId}", ex);
messages.Add($"Szállítólevél-csoport (Shipping #{group.Key.ShippingId}, Partner #{group.Key.PartnerId}, Telephely #{group.Key.PartnerDepotId?.ToString() ?? ""}): a kötelezettség nem dönthető el — {ex.Message}");
}
}
// ── Kimenő ── Rekord nélküli, lezárt (Complete) rendelések a dátumtól; rendelésenként vizsgáljuk (nincs Shipping).
// Előbb csak az azonosítók (könnyű szűrés), majd EGY batch-betöltés a teljes gráffal (GetAllByIds) — nincs N+1.
var missingOrderIds = await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Complete, false)
.Where(o => o.GenericAttributes.Any(ga => ga.Key == nameof(OrderDto.DateOfReceipt) && DateTime.Parse(ga.Value) >= fromDate.Date))
.Where(o => !ctx.EkaerHistoryMappings.Table.Any(m => m.ForeignKey == o.Id && ctx.EkaerHistories.Table.Any(eh => eh.Id == m.EkaerHistoryId && eh.IsOutgoing)))
.Select(o => o.Id)
.ToListAsync();
var missingOrders = missingOrderIds.Count == 0 ? [] : await ctx.OrderDtos.GetAllByIds(missingOrderIds, true).ToListAsync();
foreach (var order in missingOrders)
{
try
{
var result = fruitBankEkaerService.EvaluateObligation(order);
if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; }
if (result.Obligation == EkaerObligation.NotRequired) continue;
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)
{
_logger.Error($"CreateMissingEkaerHistories; outbound evaluate failed; OrderId: {order.Id}", ex);
messages.Add($"Rendelés #{order.Id}: a kötelezettség nem dönthető el — {ex.Message}");
}
}
// ── Beszúrás ──
var createdCount = 0;
foreach (var (history, foreignKeys) in toCreate)
{
try
{
// EkaerHistory + a forrás-mapping sorok atomikusan (a junction tartja a kapcsolatot, nem a rendszer-tábla).
await ctx.AddEkaerHistoryWithMappingsAsync(history, foreignKeys);
createdCount++;
}
catch (Exception ex)
{
_logger.Error($"CreateMissingEkaerHistories; insert failed; IsOutgoing: {history.IsOutgoing}; foreignKeys: [{string.Join(",", foreignKeys)}]", ex);
}
}
_logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; messages: {messages.Count}; fromDate: {fromDate:yyyy-MM-dd}");
return new EkaerCreateResult { CreatedCount = createdCount, Messages = [.. messages.OrderBy(m => m)] };
}
[SignalR(SignalRTags.GetShippings)]
public async Task<List<Shipping>> GetShippings()
{

View File

@ -0,0 +1,51 @@
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<EkaerHistory>
{
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.
public override IOrderedQueryable<EkaerHistory> GetAll() => base.GetAll().OrderByDescending(p => p.Id);
// loadRelations → a deklarációt lefedő forrás-mapping sorok (Mappings) is töltődnek (a kliensnek kellenek).
public IQueryable<EkaerHistory> 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<EkaerHistory> 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<EkaerStatus>().Select(s => (int)s).Except([(int)EkaerStatus.Sent, (int)EkaerStatus.SentWithMissingData])];
/// <summary>Flag-alapú szűrés a tabokhoz/count-hoz. <see cref="EkaerHistoryFilter.All"/> (= 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.</summary>
public IQueryable<EkaerHistory> 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(loadRelations);
var statusIds = new List<int>();
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(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<int> CountByFilterAsync(EkaerHistoryFilter filter) => LinqToDB.AsyncExtensions.CountAsync(GetByFilter(filter, false));
}

View File

@ -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<EkaerHistoryMapping>
{
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<EkaerHistoryMapping> 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<EkaerHistoryMapping> 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<EkaerHistoryMapping> GetByForeignKey(int foreignKey, bool loadRelations) => GetAll(loadRelations).Where(m => m.ForeignKey == foreignKey);
}

View File

@ -55,6 +55,9 @@ public class FruitBankDbContext : MgDbContextBase,
public OrderItemDtoDbTable OrderItemDtos { get; set; }
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; }
@ -86,7 +89,7 @@ public class FruitBankDbContext : MgDbContextBase,
public IRepository<StockQuantityHistoryExt> 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, 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,
@ -115,6 +118,9 @@ public class FruitBankDbContext : MgDbContextBase,
Files = filesDbTable;
Partners = partnerDbTable;
PartnerDepots = partnerDepotDbTable;
EkaerHistories = ekaerHistoryDbTable;
EkaerHistoryMappings = ekaerHistoryMappingDbTable;
CargoPartners = cargoPartnerDbTable;
CargoTrucks = cargoTruckDbTable;
@ -775,6 +781,25 @@ public class FruitBankDbContext : MgDbContextBase,
return list;
}
/// <summary>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.</summary>
public async Task<EkaerHistory> AddEkaerHistoryWithMappingsAsync(EkaerHistory ekaerHistory, IEnumerable<int> 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<CustomerAddressMapping?> AddCustomerAddressMappingAsync(int customerId, int addressId)
{
var customerAddressMapping = new CustomerAddressMapping

View File

@ -22,9 +22,10 @@ public class PartnerDbTable : MgDbTableBase<Partner>
{
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();
}

View File

@ -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<PartnerDepot>
{
public PartnerDepotDbTable(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
: base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
{
}
public override IOrderedQueryable<PartnerDepot> GetAll() => base.GetAll().OrderBy(p => p.PartnerId);
public IQueryable<PartnerDepot> GetAll(bool loadRelations)
{
return loadRelations ? GetAll().LoadWith(sd => sd.Partner) : GetAll();
}
public Task<PartnerDepot> GetByIdAsync(int id, bool loadRelations) => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id);
public IQueryable<PartnerDepot> GetByPartnerId(int partnerId, bool loadRelations) => GetAll(loadRelations).Where(p => p.PartnerId == partnerId);
}

View File

@ -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;
@ -43,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;
@ -83,6 +85,9 @@ public class PluginNopStartup : INopStartup
services.AddScoped<OrderItemPalletDbTable>();
services.AddScoped<PartnerDbTable>();
services.AddScoped<PartnerDepotDbTable>();
services.AddScoped<EkaerHistoryDbTable>();
services.AddScoped<EkaerHistoryMappingDbTable>();
services.AddScoped<CargoPartnerDbTable>();
services.AddScoped<CargoTruckDbTable>();
@ -198,18 +203,44 @@ public class PluginNopStartup : INopStartup
BaseUrl = c["BaseUrl"] ?? "https://import-test-b.ekaer.nav.gov.hu", // TEST; PROD: https://import.ekaer.nav.gov.hu
};
});
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<IEkaerSettings>(sp =>
{
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer:Company");
return new EkaerCompanyInfo
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer");
var co = c.GetSection("Company");
var site = co.GetSection("Site");
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"],
// 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<double>("ExchangeRate:EurHuf"),
ThresholdWeightKg = c.GetValue<double>("Thresholds:WeightKg"),
ThresholdValueHuf = c.GetValue<int>("Thresholds:ValueHuf"),
};
});
@ -237,8 +268,10 @@ public class PluginNopStartup : INopStartup
})
.AddAcBinaryProtocol(opts =>
{
//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<AcBinaryHubProtocol> from DI.

View File

@ -49,6 +49,10 @@ public partial class NameCompatibility : INameCompatibility
{ typeof(CargoPartner), FruitBankConstClient.CargoPartnerDbTableName},
{ typeof(CargoTruck), FruitBankConstClient.CargoTruckDbTableName},
{ typeof(PartnerDepot), FruitBankConstClient.PartnerDepotDbTableName},
{ typeof(EkaerHistory), FruitBankConstClient.EkaerHistoryDbTableName},
{ typeof(EkaerHistoryMapping), FruitBankConstClient.EkaerHistoryMappingDbTableName},
};

View File

@ -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).
@ -27,3 +26,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`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long