Compare commits

..

No commits in common. "1d56eba8ecb868125dcd7386121215c8ec39c506" and "6b105bb5ed13f8a874722171ad248d5ce9277fdd" have entirely different histories.

11 changed files with 31 additions and 476 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,6 @@ 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;
@ -14,8 +13,6 @@ 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;
@ -23,7 +20,6 @@ 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;
@ -44,8 +40,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
ICustomerRegistrationService customerRegistrationService,
ILocalizationService localizationService,
PreOrderConversionService preorderConversionService,
IFruitBankEkaerService fruitBankEkaerService,
IEkaerSettings ekaerSettings,
IEnumerable<IAcLogWriterBase> logWriters)
: BasePluginController, IFruitBankDataControllerServer
{
@ -107,9 +101,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
{
_logger.Detail($"GetPartners invoked");
// A telephelyeket is behúzzuk (CSAK a depókat, NEM a teljes GetAll(true) láncot) — a partnerek
// úgyis cache-eltek, így a ShippingDocument-grid telephely-legördülője kliens-oldalon kaszkádolhat.
return await ctx.Partners.GetAll().LoadWith(p => p.PartnerDepots).ToListAsync();
return await ctx.Partners.GetAll().ToListAsync();
}
[SignalR(SignalRTags.GetPartnerById)]
@ -120,7 +112,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, false);
return await ctx.Partners.GetByIdAsync(id);
}
[SignalR(SignalRTags.AddPartner)]
@ -227,247 +219,6 @@ 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).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);
}
[SignalR(SignalRTags.GetEkaerHistoriesByForeignKey)]
public async Task<List<EkaerHistory>> GetEkaerHistoriesByForeignKey(int foreignKey)
{
_logger.Detail($"GetEkaerHistoriesByForeignKey invoked; foreignKey: {foreignKey}");
return await ctx.EkaerHistories.GetByForeignKey(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);
}
[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);
}
[SignalR(SignalRTags.GenerateEkaerXmlDocument)]
public async Task<EkaerHistory> GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing)
{
_logger.Detail($"GenerateEkaerXmlDocument invoked; foreignKey: {foreignKey}; isOutgoing: {isOutgoing}");
// 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);
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);
return await ctx.EkaerHistories.GetByIdAsync(ekaerHistory.Id) ?? ekaerHistory;
}
/// <summary>
/// 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.
/// </summary>
[SignalR(SignalRTags.CreateEkaerHistory)]
public async Task<EkaerHistory> 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;
}
/// <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>();
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) = 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))
.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)))
{
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ő → 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 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 => o.Id)
.ToListAsync();
var missingOrders = missingOrderIds.Count == 0 ? [] : await ctx.OrderDtos.GetAllByIds(missingOrderIds, true).ToListAsync();
foreach (var order in missingOrders)
{
try
{
var result = fruitBankEkaerService.EvaluateObligation(order);
if (result.Obligation == EkaerObligation.DataError) { messages.UnionWith(result.Errors); continue; }
if (result.Obligation == EkaerObligation.NotRequired) continue;
toCreate.Add(new EkaerHistory { 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
{
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}; 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

@ -1,46 +0,0 @@
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. Nincs asszociáció → nincs LoadWith / loadRelations.
public override IOrderedQueryable<EkaerHistory> GetAll() => base.GetAll().OrderByDescending(p => p.Id);
public Task<EkaerHistory> GetByIdAsync(int id) => GetAll().FirstOrDefaultAsync(p => p.Id == id);
public IQueryable<EkaerHistory> 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<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)
{
// 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<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().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));
}

View File

@ -55,8 +55,6 @@ 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 CargoPartnerDbTable CargoPartners { get; set; }
public CargoTruckDbTable CargoTrucks{ get; set; }
@ -83,7 +81,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, PartnerDepotDbTable partnerDepotDbTable, EkaerHistoryDbTable ekaerHistoryDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable,
CargoPartnerDbTable cargoPartnerDbTable, CargoTruckDbTable cargoTruckDbTable, PartnerDbTable partnerDbTable, 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,8 +108,6 @@ public class FruitBankDbContext : MgDbContextBase,
Files = filesDbTable;
Partners = partnerDbTable;
PartnerDepots = partnerDepotDbTable;
EkaerHistories = ekaerHistoryDbTable;
CargoPartners = cargoPartnerDbTable;
CargoTrucks = cargoTruckDbTable;

View File

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

View File

@ -1,27 +0,0 @@
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,7 +21,6 @@ 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;
@ -44,7 +43,6 @@ 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;
@ -85,8 +83,6 @@ public class PluginNopStartup : INopStartup
services.AddScoped<OrderItemPalletDbTable>();
services.AddScoped<PartnerDbTable>();
services.AddScoped<PartnerDepotDbTable>();
services.AddScoped<EkaerHistoryDbTable>();
services.AddScoped<CargoPartnerDbTable>();
services.AddScoped<CargoTruckDbTable>();
@ -194,44 +190,18 @@ 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ó (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 =>
services.AddSingleton(sp =>
{
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer");
var co = c.GetSection("Company");
var site = co.GetSection("Site");
return new EkaerSettings
var c = sp.GetRequiredService<IConfiguration>().GetSection("Ekaer:Company");
return new EkaerCompanyInfo
{
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"),
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.
};
});
@ -259,10 +229,8 @@ public class PluginNopStartup : INopStartup
})
.AddAcBinaryProtocol(opts =>
{
//opts.ProtocolMode = BinaryProtocolMode.AsyncSegment;
opts.ProtocolMode = BinaryProtocolMode.Bytes;
opts.ProtocolMode = BinaryProtocolMode.AsyncSegment;
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,9 +49,6 @@ public partial class NameCompatibility : INameCompatibility
{ typeof(CargoPartner), FruitBankConstClient.CargoPartnerDbTableName},
{ typeof(CargoTruck), FruitBankConstClient.CargoTruckDbTableName},
{ typeof(PartnerDepot), FruitBankConstClient.PartnerDepotDbTableName},
{ typeof(EkaerHistory), FruitBankConstClient.EkaerHistoryDbTableName},
};

View File

@ -9,9 +9,10 @@ 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:** Open · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
**Status:** Closed (2026-06-02) — superseded · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
> **Ú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.)*
### 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.
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).
@ -26,18 +27,3 @@ 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

View File

@ -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`** — 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 leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — `Shipping``tradeCard` ops (tiszta leképező, nem validál) |
| **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,10 +26,6 @@ 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).
@ -40,32 +36,15 @@ 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` (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) |
| `carrierText` (fuvarozó, szöveges) | `Shipping.CargoPartner.Name` |
| `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**) |
| tétel `productName` | `ShippingItem.ProductName` |
| 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. |
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:
@ -75,6 +54,6 @@ services.AddHttpClient<EkaerManageService>(/* NAV TLS 1.2 */);
services.AddScoped<IEkaerTradeCardValidator, EkaerTradeCardValidator>();
services.AddScoped<IEkaerSubmitService, EkaerSubmitService>();
services.AddScoped<IShippingToEkaerMapper, ShippingToEkaerMapper>();
services.AddSingleton<IEkaerSettings>(/* EkaerSettings: Ekaer:Company + Site + küszöbök + EUR-HUF árfolyam — configból */);
services.AddSingleton(/* EkaerMappingOptions: destination + saját raktár */);
services.AddScoped<IFruitBankEkaerService, FruitBankEkaerService>();
```