Compare commits

..

11 Commits

Author SHA1 Message Date
Loretta 1d56eba8ec Update DB connection, EKÁER mapping, and partner loading
- 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).
2026-06-16 12:02:19 +02:00
Loretta e783d7c116 EKÁER docs: VTSZ grouping, archive rules, reopened issue
- 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.
2026-06-15 18:01:48 +02:00
Loretta 24164b5189 Refactor EKÁER history creation & update docs
- 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.
2026-06-15 11:53:00 +02:00
Loretta 3bde0b4063 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.
2026-06-12 18:45:53 +02:00
Loretta b7a38c3efb 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.
2026-06-12 09:00:33 +02:00
Loretta cd49b572ad 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.
2026-06-12 07:54:33 +02:00
Loretta f1db5a9a99 Update EKÁER config, thresholds, and value logic
- 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
2026-06-11 17:40:42 +02:00
Loretta 75ce276f64 Add method to create missing EKÁER history records
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.
2026-06-11 08:52:15 +02:00
Loretta c8ecbc94a2 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.
2026-06-10 18:09:26 +02:00
Loretta 9cdd65eebd 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.
2026-06-10 16:24:59 +02:00
Loretta b782d6a88d 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.
2026-06-09 15:28:57 +02:00
11 changed files with 476 additions and 31 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,247 @@ 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

@ -0,0 +1,46 @@
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,6 +55,8 @@ 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; }
@ -81,7 +83,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, 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 +110,8 @@ public class FruitBankDbContext : MgDbContextBase,
Files = filesDbTable;
Partners = partnerDbTable;
PartnerDepots = partnerDepotDbTable;
EkaerHistories = ekaerHistoryDbTable;
CargoPartners = cargoPartnerDbTable;
CargoTrucks = cargoTruckDbTable;

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,8 @@ public class PluginNopStartup : INopStartup
services.AddScoped<OrderItemPalletDbTable>();
services.AddScoped<PartnerDbTable>();
services.AddScoped<PartnerDepotDbTable>();
services.AddScoped<EkaerHistoryDbTable>();
services.AddScoped<CargoPartnerDbTable>();
services.AddScoped<CargoTruckDbTable>();
@ -190,18 +194,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"),
};
});
@ -229,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<AcBinaryHubProtocol> from DI.

View File

@ -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},
};

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

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`** — `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,15 +40,32 @@ 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` (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` |
| 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. |
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<EkaerManageService>(/* NAV TLS 1.2 */);
services.AddScoped<IEkaerTradeCardValidator, EkaerTradeCardValidator>();
services.AddScoped<IEkaerSubmitService, EkaerSubmitService>();
services.AddScoped<IShippingToEkaerMapper, ShippingToEkaerMapper>();
services.AddSingleton(/* EkaerMappingOptions: destination + saját raktár */);
services.AddSingleton<IEkaerSettings>(/* EkaerSettings: Ekaer:Company + Site + küszöbök + EUR-HUF árfolyam — configból */);
services.AddScoped<IFruitBankEkaerService, FruitBankEkaerService>();
```