This commit is contained in:
Adam 2026-06-23 23:37:40 +02:00
commit ab912a4ab9
59 changed files with 2605 additions and 295 deletions

View File

@ -0,0 +1,25 @@
# Claude Code PreToolUse hook: DB-vedelem — csak _DEV nevu adatbazis erheto el shell-parancsbol.
# Bemenet: hook JSON a stdin-en; ha a parancs connection stringet tartalmaz (Initial Catalog= / Database=)
# es a DB-nev NEM tartalmazza a "_DEV"-et, a tool-hivast DENY-jal blokkolja.
$payloadText = [Console]::In.ReadToEnd()
if ([string]::IsNullOrWhiteSpace($payloadText)) { exit 0 }
try { $payload = $payloadText | ConvertFrom-Json } catch { exit 0 }
$command = $payload.tool_input.command
if ([string]::IsNullOrWhiteSpace($command)) { exit 0 }
$pattern = [regex]'(?i)(?:Initial\s+Catalog|Database)\s*=\s*([^;"''\s]+)'
foreach ($match in $pattern.Matches($command)) {
$dbName = $match.Groups[1].Value
if ($dbName -notmatch '(?i)_DEV') {
$reason = "Blokkolt: csak _DEV adatbazis modosithato! (talalt adatbazis: $dbName)"
$result = @{ hookSpecificOutput = @{ hookEventName = 'PreToolUse'; permissionDecision = 'deny'; permissionDecisionReason = $reason } }
Write-Output ($result | ConvertTo-Json -Depth 5 -Compress)
exit 0
}
}
exit 0

17
.claude/settings.json Normal file
View File

@ -0,0 +1,17 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"H:/Applications/Mango/Source/FruitBankHybridApp/.claude/hooks/db-dev-guard.ps1\"",
"timeout": 30,
"statusMessage": "DB guard (_DEV) ellenőrzés..."
}
]
}
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
using AyCode.Services.Nav;
using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models; using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities; using FruitBank.Common.Entities;
using FruitBank.Common.Services.Ekaer; using FruitBank.Common.Services.Ekaer;
@ -8,20 +10,22 @@ namespace FruitBank.Common.Server.Services.Ekaer;
/// <inheritdoc cref="IFruitBankEkaerService"/> /// <inheritdoc cref="IFruitBankEkaerService"/>
/// <remarks> /// <remarks>
/// A teljes lánc: <c>map</c> (<see cref="IShippingToEkaerMapper"/>, FruitBank.Common) → /// A teljes lánc: <c>map</c> (<see cref="IShippingToEkaerMapper"/>, FruitBank.Common) →
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A NAV-fiók hitelesítő adatait /// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A saját cégadatot
/// (<c>INavCredentials</c>) és a saját cégadatokat (<see cref="EkaerCompanyInfo"/>) a DI szolgáltatja. /// (<see cref="EkaerSettings.Company"/>) és a NAV-fiók hitelesítő adatait a DI szolgáltatja.
/// </remarks> /// </remarks>
public sealed class FruitBankEkaerService : IFruitBankEkaerService public sealed class FruitBankEkaerService : IFruitBankEkaerService
{ {
private readonly IShippingToEkaerMapper _mapper; private readonly IShippingToEkaerMapper _mapper;
private readonly IEkaerSubmitService _submitService; private readonly IEkaerSubmitService _submitService;
private readonly EkaerCompanyInfo _company; private readonly IEkaerTradeCardValidator _validator;
private readonly IEkaerSettings _settings;
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerCompanyInfo company) public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, IEkaerSettings settings)
{ {
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService)); _submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
_company = company ?? throw new ArgumentNullException(nameof(company)); _validator = validator ?? throw new ArgumentNullException(nameof(validator));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
} }
public Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default) public Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default)
@ -29,7 +33,75 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
ArgumentNullException.ThrowIfNull(shipping); ArgumentNullException.ThrowIfNull(shipping);
// map (FruitBank.Common) → submit: validate → send (AyCode.Services) // map (FruitBank.Common) → submit: validate → send (AyCode.Services)
var operations = _mapper.MapShipping(shipping, _company, operation); var operations = _mapper.MapShipping(shipping, _settings.Company, operation);
return _submitService.SubmitAsync(operations, cancellationToken); return _submitService.SubmitAsync(operations, cancellationToken);
} }
public EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection<ShippingDocument> documents, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(documents);
if (documents.Count == 0) throw new ArgumentException("documents is empty", nameof(documents));
ekaerHistory ??= new EkaerHistory { IsOutgoing = false };
// A csoport azonos Partneré (a kapu így csoportosít) → az első dokumentum pénzneme a mérvadó.
var currency = documents.First().Partner?.Currency;
// A csoport ÖSSZES dokumentuma EGY tradeCard-dá (összevont tömeg/érték): ToConsignment → BuildTradeCard.
var tradeCard = _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company));
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, tradeCard, currency);
}
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(order);
ekaerHistory ??= new EkaerHistory { IsOutgoing = true };
// Kimenő pénznem: jelenleg minden HUF (a deviza az OrderDto-ba kerül, amint bekötik) → ConversionRate = 1.
const string currency = "HUF";
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapOrder(order, _settings.Company), currency);
}
public EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents)
=> EkaerReportability.Evaluate(_mapper.ToConsignment(documents, _settings.Company), _settings);
public EkaerObligationResult EvaluateObligation(OrderDto order)
=> EkaerReportability.Evaluate(_mapper.ToConsignment(order, _settings.Company), _settings);
public void SetSummary(EkaerHistory history, DateTime? shippingDate, string? partner)
{
history.ShippingDate = shippingDate;
history.Partner = partner;
}
/// <summary>Config-kapu: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a leképezés ELŐTT
/// ValidationError (nincs félrevezető XML). <c>null</c> = rendben, mehet a generálás.</summary>
private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency)
{
if (EkaerValueCalculator.IsHuf(currency) || _settings.EurHufRate > 0) return null;
ekaerHistory.Status = EkaerStatus.ValidationError;
ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható.";
return ekaerHistory;
}
/// <summary>Közös befejezés (bejövő/kimenő): validál, szerializál; az XML validációs hibánál IS tárolódik
/// (a detail-nézethez), és rögzíti a ténylegesen alkalmazott árfolyamot (HUF → 1, külföldi → FX-ráta).</summary>
private EkaerHistory Finalize(EkaerHistory ekaerHistory, TradeCardType tradeCard, string? currency)
{
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
var messages = _validator.Validate(operation);
ekaerHistory.XmlDoc = NavXmlHelper.Serialize(tradeCard);
ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(currency, _settings.EurHufRate);
// Blokkoló hiba → ValidationError (nem küldhető); csak warning → GeneratedWithWarning (küldhető, de pótlandó);
// semmi → Generated. Az üzeneteket súlyossággal prefixeljük, hogy a detail-nézet megkülönböztesse őket.
ekaerHistory.Status = messages.HasErrors() ? EkaerStatus.ValidationError
: messages.HasWarnings() ? EkaerStatus.GeneratedWithWarning
: EkaerStatus.Generated;
// Error-ok elöl, warningok hátul (a detail-nézet soronként színez); a prefix [Error]/[Warning] hordozza a szintet.
ekaerHistory.ErrorText = messages.Count == 0 ? null
: string.Join(Environment.NewLine, messages
.OrderByDescending(m => m.GetSeverity())
.Select(m => $"[{m.GetSeverity()}] {m.ErrorMessage}"));
return ekaerHistory;
}
} }

View File

@ -1,6 +1,8 @@
using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models; using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities; using FruitBank.Common.Entities;
using FruitBank.Common.Services.Ekaer;
namespace FruitBank.Common.Server.Services.Ekaer; namespace FruitBank.Common.Server.Services.Ekaer;
@ -16,4 +18,36 @@ public interface IFruitBankEkaerService
/// vagy a NAV-válasz — lásd <see cref="EkaerSubmitResult"/>. /// vagy a NAV-válasz — lásd <see cref="EkaerSubmitResult"/>.
/// </summary> /// </summary>
Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default); Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default);
/// <summary>
/// Egy szállítólevél-CSOPORTBÓL (egy EKÁER-egység dokumentumai) legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új)
/// <see cref="EkaerHistory"/> rekordot tölti: <c>XmlDoc</c> + <c>Status</c>
/// (<see cref="EkaerStatus.Generated"/> / <see cref="EkaerStatus.ValidationError"/>) + <c>ErrorText</c>.
/// NEM perzisztál és NEM hív NAV-ot — a mentés (upsert) a hívó SignalR endpoint dolga.
/// </summary>
/// <param name="documents">A deklarációhoz tartozó szállítólevél-CSOPORT (egy (Shipping, Partner, PartnerDepot) egység) a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek) — összevont tömeggel/értékkel EGY tradeCard.</param>
/// <param name="ekaerHistory">A csoport meglévő rekordja (újrageneráláskor); <c>null</c> → új rekord készül.</param>
EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection<ShippingDocument> documents, EkaerHistory? ekaerHistory = null);
/// <summary>
/// Egy kimenő rendelésből (<see cref="OrderDto"/>) legenerálja az EKÁER tradeCard XML-t és validálja —
/// a (meglévő vagy új) <see cref="EkaerHistory"/> rekordot tölti (<c>IsOutgoing = true</c>). Mi vagyunk az
/// ELADÓ, a vevő a CÍMZETT (belföldi értékesítés). NEM perzisztál és NEM hív NAV-ot — a mentés a hívó dolga.
/// </summary>
EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null);
/// <summary>
/// Eldönti egy bejövő (Shipping, Partner) CSOPORT EKÁER-kötelezettségét: külföldi (a feladó és a címzett
/// országkódja eltér) → mindig kötelező; belföld → az AGGREGÁLT tömeg/érték a küszöbhöz; érvénytelen országkód →
/// <see cref="EkaerObligation.DataError"/>. A küszöb-summa a csoport ÖSSZES dokumentumának tételeire megy.
/// </summary>
EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents);
/// <summary>Eldönti egy kimenő rendelés EKÁER-kötelezettségét (ugyanaz a logika, a rendelés tételeire).</summary>
EkaerObligationResult EvaluateObligation(OrderDto order);
/// <summary>Beállítja a deklaráció (<see cref="EkaerHistory"/>) összegző mezőit (DB-mentes): <c>ShippingDate</c> + <c>Partner</c>.
/// A csoporton belül invariánsak; a forrás-kinyerés (bejövő: doc.ShippingDate + Partner.Name; kimenő: order.DateOfReceipt +
/// Customer.Company) a hívóé. Fallback NINCS — null/üres marad, ha hiányzik.</summary>
void SetSummary(EkaerHistory history, DateTime? shippingDate, string? partner);
} }

View File

@ -0,0 +1,13 @@
using AyCode.Core.Serializers.Attributes;
namespace FruitBank.Common.Dtos;
/// <summary>A „hiányzó EKÁER-sorok létrehozása" (reconciliation gomb) eredménye a SignalR-dróton: hány Pending sor
/// jött létre, és a felhasználónak szóló sima string üzenetek (adathiány / formátumhiba miatt kihagyott jelöltek —
/// pl. érvénytelen országkód). A küszöb alatti, nem kötelező jelölteknél NINCS üzenet. A megjelenítés a hívó dolga.</summary>
[AcBinarySerializable(false, false, false, false, false, false)]
public sealed class EkaerCreateResult
{
public int CreatedCount { get; set; }
public List<string> Messages { get; set; } = [];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@
using AyCode.Core.Serializers.Toons; using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Interfaces; using FruitBank.Common.Interfaces;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities; namespace FruitBank.Common.Entities;
@ -12,6 +11,12 @@ namespace FruitBank.Common.Entities;
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
public sealed class Partner : PartnerBase, IPartner public sealed class Partner : PartnerBase, IPartner
{ {
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.ShippingId), CanBeNull = true)] [ToonDescription(Purpose = "Whether shipments from this supplier are subject to NAV EKÁER reporting. Default true; set false for partners that do not require EKÁER (e.g. multiple companies sharing a wholesale-market address where no road transport between them occurs).")]
public bool IsEkaer { get; set; } = true;
[Association(ThisKey = nameof(Id), OtherKey = nameof(PartnerDepot.PartnerId), CanBeNull = true)]
public List<PartnerDepot>? PartnerDepots { get; set; }
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.PartnerId), CanBeNull = true)]
public List<ShippingDocument>? ShippingDocuments { get; set; } public List<ShippingDocument>? ShippingDocuments { get; set; }
} }

View File

@ -0,0 +1,38 @@
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Attributes;
using AyCode.Interfaces.TimeStampInfo;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
//[ToonDescription("Business partner with address and tax information", Purpose = "Represents an external legal entity, specifically a Supplier who provides goods or a business partner involved in the procurement chain")]
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.PartnerDepotDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDepotDbTableName)]
public sealed class PartnerDepot : MgEntityBase, ITimeStampInfo, ICompanyInfoBase
{
public int PartnerId { get; set; }
public string Name { get; set;}
[NotColumn, NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore]
public string? TaxId => Partner?.TaxId;
public string CountryCode { get; set; }
public string PostalCode { get; set;}
public string City { get; set;}
public string Street { get; set;}
[NotColumn, NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore]
public string FullAddress => this.ComposeFullAddress() ?? string.Empty;
[Association(ThisKey = nameof(PartnerId), OtherKey = nameof(Partner.Id), CanBeNull = true)]
public Partner? Partner { get; set; }
[SkipValuesOnUpdate]
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}

View File

@ -15,6 +15,7 @@ public sealed class ShippingDocument : MgEntityBase, IShippingDocument
{ {
public int PartnerId { get; set; } public int PartnerId { get; set; }
public int? ShippingId { get; set; } public int? ShippingId { get; set; }
public int? PartnerDepotId { get; set; }
public string DocumentIdNumber { get; set; } public string DocumentIdNumber { get; set; }
public string PdfFileName { get; set; } public string PdfFileName { get; set; }
@ -32,6 +33,12 @@ public sealed class ShippingDocument : MgEntityBase, IShippingDocument
[Association(ThisKey = nameof(PartnerId), OtherKey = nameof(Partner.Id), CanBeNull = true)] [Association(ThisKey = nameof(PartnerId), OtherKey = nameof(Partner.Id), CanBeNull = true)]
public Partner? Partner { get; set; } public Partner? Partner { get; set; }
// Csak az EKÁER felrakodási címhez kell. SZÁNDÉKOSAN nincs benne semelyik alapértelmezett loadRelations-ben:
// kizárólag a generálás/reconciliation kéri explicit `LoadWith(sd => sd.PartnerDepot)`-tal, így a többi
// lekérdezést/sorosítást nem terheli (betöltetlenül null marad). Legacy szállítólevélnél PartnerDepotId = null.
[Association(ThisKey = nameof(PartnerDepotId), OtherKey = nameof(PartnerDepot.Id), CanBeNull = true)]
public PartnerDepot? PartnerDepot { get; set; }
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingItem.ShippingDocumentId), CanBeNull = true)] [Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingItem.ShippingDocumentId), CanBeNull = true)]
public List<ShippingItem>? ShippingItems { get; set; } public List<ShippingItem>? ShippingItems { get; set; }

View File

@ -30,6 +30,10 @@ public static class FruitBankConstClient
public const string PalletDbTableName = "fbPallet"; public const string PalletDbTableName = "fbPallet";
public const string FilesDbTableName = "fbFiles"; public const string FilesDbTableName = "fbFiles";
public const string PartnerDbTableName = "fbPartner"; public const string PartnerDbTableName = "fbPartner";
public const string PartnerDepotDbTableName = "fbPartnerDepot";
public const string EkaerHistoryDbTableName = "fbEkaerHistory";
public const string EkaerHistoryMappingDbTableName = "fbEkaerHistoryMapping";
public const string OrderItemPalletDbTableName = "fbOrderItemPallet"; public const string OrderItemPalletDbTableName = "fbOrderItemPallet";

View File

@ -20,6 +20,26 @@ public interface IFruitBankDataControllerCommon
public Task<Partner?> UpdatePartner(Partner partner); public Task<Partner?> UpdatePartner(Partner partner);
#endregion Partner #endregion Partner
#region PartnerDepot
public Task<List<PartnerDepot>?> GetPartnerDepots();
public Task<PartnerDepot?> GetPartnerDepotById(int id);
public Task<List<PartnerDepot>?> GetPartnerDepotsByPartnerId(int partnerId);
public Task<PartnerDepot?> AddPartnerDepot(PartnerDepot partnerDepot);
public Task<PartnerDepot?> UpdatePartnerDepot(PartnerDepot partnerDepot);
#endregion PartnerDepot
#region EkaerHistory
public Task<List<EkaerHistory>?> GetEkaerHistories(EkaerHistoryFilter filter);
public Task<EkaerHistory?> GetEkaerHistoryById(int id);
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey);
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId);
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
#endregion EkaerHistory
#region CargoPartner #region CargoPartner
public Task<List<CargoPartner>?> GetCargoPartners(); public Task<List<CargoPartner>?> GetCargoPartners();
public Task<CargoPartner?> GetCargoPartnerById(int id); public Task<CargoPartner?> GetCargoPartnerById(int id);

View File

@ -13,6 +13,8 @@ public interface ICargoPartner : IPartnerBase
public interface IPartner : IPartnerBase public interface IPartner : IPartnerBase
{ {
bool IsEkaer { get; set; }
List<PartnerDepot>? PartnerDepots { get; set; }
List<ShippingDocument>? ShippingDocuments { get; set; } List<ShippingDocument>? ShippingDocuments { get; set; }
} }

View File

@ -9,7 +9,8 @@ public interface IShippingDocument: IEntityInt, ITimeStampInfo//, IMeasured
{ {
public int PartnerId { get; set; } public int PartnerId { get; set; }
public int? ShippingId { get; set; } public int? ShippingId { get; set; }
public int? PartnerDepotId { get; set; }
public string DocumentIdNumber { get; set; } public string DocumentIdNumber { get; set; }
public string PdfFileName { get; set; } public string PdfFileName { get; set; }

View File

@ -0,0 +1,70 @@
using AyCode.Services.Nav.Ekaer.Models;
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// Irány-független, normalizált szállítmány-modell. A bejövő (<c>ShippingDocument</c>-csoport) és a kimenő
/// (<c>OrderDto</c>) forrás EBBE képződik (a <see cref="IShippingToEkaerMapper"/> adapterei), és innen épül MIND a
/// NAV tradeCard (<see cref="IShippingToEkaerMapper.BuildTradeCard"/>), MIND a bejelentés-kötelezettség
/// (<see cref="EkaerReportability"/>). Így az irányfüggő tudás a két adapterre szorul, a közös logika egy helyen van.
/// Szerver-oldali köztes típus — NEM megy a SignalR-dróton.
/// </summary>
/// <remarks>
/// A fel-/lerakodási hely és a jármű már a NAV-típus (<see cref="LocationType"/>, <see cref="BasicVehicleDetailType"/>),
/// mert a saját telephely (<c>company.Site</c>) kész LocationType (Phone/Email/FELIR-mezőkkel) — azt átalakítás nélkül,
/// MEZŐVESZTÉS nélkül kell átengedni. A normalizálás (kötelezettséghez számít) a feladó/címzett országkódjára és a
/// tételek tömeg/érték-aggregálására korlátozódik.
/// </remarks>
public sealed class EkaerConsignment
{
/// <summary>A forrás azonosítója (bejövőnél ShippingDocument.Id, kimenőnél Order.Id). Csoport-kiértékelésnél
/// (több dokumentum) az első forrásé — a kötelezettség-döntés nem használja.</summary>
public int ForeignKey { get; init; }
public bool IsOutgoing { get; init; }
/// <summary>Számla-pénznem (ISO 4217). A tétel <see cref="EkaerLine.ValueHuf"/> már KISZÁMOLT HUF-ban.</summary>
public string? Currency { get; init; }
/// <summary>Feladó / eladó (a kötelezettség az országkódját nézi).</summary>
public EkaerEndpoint Seller { get; init; } = new();
/// <summary>Címzett / vevő (a kötelezettség az országkódját nézi).</summary>
public EkaerEndpoint Buyer { get; init; } = new();
/// <summary>Felrakodási hely (NAV LocationType — pl. a saját telephely átengedve).</summary>
public LocationType? LoadLocation { get; init; }
/// <summary>Lerakodási hely (NAV LocationType — pl. a saját telephely átengedve).</summary>
public LocationType? UnloadLocation { get; init; }
public IReadOnlyList<EkaerLine> Lines { get; init; } = [];
public BasicVehicleDetailType? Vehicle { get; init; }
public BasicVehicleDetailType? Trailer { get; init; }
public string? CarrierName { get; init; }
}
/// <summary>Egy fél (feladó/címzett) normalizálatlan adatai a NAV seller/destination mezőkhöz ÉS a kötelezettség
/// országkód-vizsgálatához. (A cím egysoros; a tagolt fel-/lerakodási helyet a <see cref="EkaerConsignment.LoadLocation"/>
/// / <see cref="EkaerConsignment.UnloadLocation"/> hordozza.)</summary>
public sealed class EkaerEndpoint
{
public string? Name { get; init; }
public string? VatNumber { get; init; }
public string? CountryCode { get; init; }
public string? FullAddress { get; init; }
}
/// <summary>Egy normalizált tétel. A <see cref="WeightKg"/> és a <see cref="ValueHuf"/> már KISZÁMOLT (HUF-ban),
/// hogy a küszöb-summa és a tradeCard ugyanazt használja (egyetlen érték-forrás).</summary>
public sealed class EkaerLine
{
public string ExternalId { get; init; } = string.Empty;
public string? Vtsz { get; init; }
public string? Name { get; init; }
public double WeightKg { get; init; }
public long? ValueHuf { get; init; }
public TradeReasonType TradeReason { get; init; }
}

View File

@ -0,0 +1,83 @@
using AyCode.Services.Nav.Ekaer;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>Egy szállítmány EKÁER bejelentés-kötelezettsége.</summary>
public enum EkaerObligation
{
/// <summary>Kötelező bejelenteni (sort kell létrehozni).</summary>
Required,
/// <summary>Nem kötelező (belföld, küszöb alatt) — NEM hiba, nincs üzenet.</summary>
NotRequired,
/// <summary>A döntés nem hozható meg adathiány/formátumhiba miatt (pl. érvénytelen országkód) — NINCS sor, az okok az üzenetekben.</summary>
DataError,
}
/// <summary>A kötelezettség-kiértékelés eredménye: a döntés + (csak DataError esetén) a felhasználónak szóló okok.</summary>
public sealed class EkaerObligationResult
{
public EkaerObligation Obligation { get; private init; }
public IReadOnlyList<string> Errors { get; private init; } = [];
public static EkaerObligationResult Required { get; } = new() { Obligation = EkaerObligation.Required };
public static EkaerObligationResult NotRequired { get; } = new() { Obligation = EkaerObligation.NotRequired };
public static EkaerObligationResult DataError(IReadOnlyList<string> errors) =>
new() { Obligation = EkaerObligation.DataError, Errors = errors };
}
/// <summary>
/// EKÁER bejelentés-kötelezettség eldöntése egy normalizált <see cref="EkaerConsignment"/>-re — KÖZÖS a bejövő
/// (document-csoport) és a kimenő (order) ágon. A NAV mindkét irányban büntet (a felesleges bejelentés ÉS a kimaradás
/// is bírság), ezért: a bizonytalan adatot (érvénytelen országkód) NEM döntjük el magától → <see cref="EkaerObligation.DataError"/>.
/// </summary>
/// <remarks>
/// Sorrend: (1) az AGGREGÁLT tömeg/érték küszöb FELETT → Required, az országkódtól FÜGGETLENÜL — belföld+küszöb
/// felett és külföld egyaránt kötelező; az országkód-hibát ilyenkor a generate-validálás jelzi, a sor létrejön;
/// (2) küszöb ALATT → itt számít az ország: hiányzó/érvénytelen országkód → DataError (a foreign-vs-belföld nem
/// dönthető el); (3) küszöb alatt, érvényes országkódok: külföld (eltér) → Required, belföld (egyezik) → NotRequired.
/// A 13/2020. (XII. 23.) PM rendelet alapján a küszöb feladó→címzett→jármű relációra aggregált — a hívó már
/// így állítja össze a <see cref="EkaerConsignment.Lines"/>-t (bejövőnél a (Shipping, Partner) csoport tételei).
/// </remarks>
public static class EkaerReportability
{
public static EkaerObligationResult Evaluate(EkaerConsignment consignment, IEkaerSettings settings)
{
ArgumentNullException.ThrowIfNull(consignment);
ArgumentNullException.ThrowIfNull(settings);
// (1) Küszöb FELETT → kötelező, FÜGGETLENÜL az országkódtól: belföld+küszöb felett ÉS külföld egyaránt kötelező.
// Az országkód-hibát ilyenkor NEM itt blokkoljuk — a sor létrejön, a hibát a generate-validálás jelzi.
var totalWeight = consignment.Lines.Sum(l => l.WeightKg);
var totalValueHuf = consignment.Lines.Sum(l => l.ValueHuf ?? 0L);
if (totalWeight >= settings.ThresholdWeightKg || totalValueHuf >= settings.ThresholdValueHuf)
return EkaerObligationResult.Required;
// (2) Küszöb ALATT: itt MÁR az ország dönt — csak a külföldi (eltérő országkód) reláció kötelező. Ehhez érvényes
// ISO-2 országkódok kellenek; hiányzó/érvénytelen → a foreign-vs-belföld nem dönthető el → DataError.
var subject = Subject(consignment);
var errors = new List<string>();
if (!IsValidCountry(consignment.Seller.CountryCode))
errors.Add($"{subject}: a feladó országkódja hiányzik vagy érvénytelen ('{consignment.Seller.CountryCode}').");
if (!IsValidCountry(consignment.Buyer.CountryCode))
errors.Add($"{subject}: a címzett országkódja hiányzik vagy érvénytelen ('{consignment.Buyer.CountryCode}').");
if (errors.Count > 0)
return EkaerObligationResult.DataError(errors);
// (3) Küszöb alatt, érvényes országkódok: külföld (eltér) → kötelező, belföld (egyezik) → nem kötelező (üzenet nélkül).
return CountryEquals(consignment.Seller.CountryCode, consignment.Buyer.CountryCode)
? EkaerObligationResult.NotRequired
: EkaerObligationResult.Required;
}
/// <summary>Érvényes EKÁER országkód: pontosan 2 ASCII betű (ISO-2). Üres / más hosszúságú → érvénytelen.</summary>
private static bool IsValidCountry(string? code)
{
var c = code?.Trim();
return c is { Length: 2 } && c.All(char.IsAsciiLetter);
}
private static bool CountryEquals(string? a, string? b) => string.Equals(a?.Trim(), b?.Trim(), StringComparison.OrdinalIgnoreCase);
private static string Subject(EkaerConsignment c)
=> c.IsOutgoing ? $"Rendelés #{c.ForeignKey}" : string.IsNullOrWhiteSpace(c.Seller.Name) ? $"Szállítólevél #{c.ForeignKey}" : c.Seller.Name!;
}

View File

@ -0,0 +1,29 @@
using AyCode.Services.Nav.Ekaer;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// A teljes EKÁER konfiguráció egy helyen (configból, <c>appsettings.json</c> „Ekaer" szekció): a bejelentő
/// cégadata + a küszöbök + az árfolyam. A küszöbök és az árfolyam évente / jogszabály szerint változhatnak,
/// ezért configban élnek — nem a kódban beégetve.
/// </summary>
public sealed class EkaerSettings : IEkaerSettings
{
/// <summary>A bejelentő saját cégadatai (címzett a bejövő relációban) + a lerakodási hely.</summary>
public EkaerCompanyInfo Company { get; set; } = new();
/// <summary>EUR→HUF átváltási árfolyam a tétel-érték HUF-ra számításához. Forrás: MNB napi középárfolyam
/// (a NAV nem közöl külön EKÁER-árfolyamot). SZÁNDÉKOSAN nincs default: ha a config nem töltődik be, 0 marad,
/// és a külföldi érték-átváltás hibát dob (<see cref="EkaerValueCalculator.ResolveRateToHuf"/>) — így elavult /
/// beégetett árfolyammal soha nem számolunk.</summary>
public double EurHufRate { get; set; }
/// <summary>Tömeg-küszöb kg-ban: e felett (vagy az érték-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 200 kg.
/// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé).</summary>
public double ThresholdWeightKg { get; set; }
/// <summary>Érték-küszöb HUF-ban (nettó): e felett (vagy a tömeg-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 250 000 Ft.
/// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé).
/// <c>int</c>: a legmagasabb küszöb 5 millió Ft, bőven belefér.</summary>
public int ThresholdValueHuf { get; set; }
}

View File

@ -0,0 +1,54 @@
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// A tétel-érték HUF-ra számítása — közös a leképezés (tradeCardItem.value) és a küszöb-kapu
/// (<c>CreateMissingEkaerHistories</c>) között, hogy a kétféle számítás ne csúszhasson el egymástól.
/// </summary>
/// <remarks>
/// A forrás-pénznem a <c>Partner.Currency</c> (ISO 4217). Jelenleg csak HUF és EUR van használatban
/// (100%-ban a partnerből következik): HUF → nincs átváltás, minden más → EUR-ként, az árfolyammal.
/// Ha valaha más deviza is megjelenik, ezt a feltevést bővíteni kell (akkor per-deviza árfolyam kell).
/// </remarks>
public static class EkaerValueCalculator
{
public static bool IsHuf(string? currency) => string.Equals(currency?.Trim(), "HUF", StringComparison.OrdinalIgnoreCase);
/// <summary>A forrás-pénznem → HUF szorzó. HUF → 1 (árfolyam nem kell). Minden más (EUR) → a megadott
/// EUR-HUF árfolyam; ha az hiányzik/0, <see cref="InvalidOperationException"/> — soha nem számolunk elavult /
/// üres árfolyammal (a config-default szándékosan nincs).</summary>
public static double ResolveRateToHuf(string? currency, double eurHufRate)
{
if (IsHuf(currency)) return 1d;
if (eurHufRate <= 0)
throw new InvalidOperationException(
"EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — külföldi (nem HUF) értéket nem lehet HUF-ra számolni.");
return eurHufRate;
}
/// <summary>Egy tétel értéke a számla pénznemében (egységár × dokumentum-mennyiség), átváltás előtt.</summary>
public static double ItemLineValue(ShippingItem item) => item.UnitPriceOnDocument * item.QuantityOnDocument;
/// <summary>Egy tétel értéke HUF-ban, egészre kerekítve. A NAV <c>value &gt; 0</c>-t vár — 0/ismeretlen
/// esetén <c>null</c> (a mezőt nem töltjük, a validátor jelzi, ha kötelező lenne).</summary>
public static long? ItemValueHuf(ShippingItem item, double rateToHuf)
{
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
return huf > 0 ? (long)huf : null;
}
// ── Kimenő (Order) változatok ────────────────────────────────────────────
// Az érték a NETTÓ (ÁFA nélküli) egységár × mennyiség — az EKÁER „nettó érték"-et vár.
/// <summary>Egy rendelés-tétel nettó értéke a számla pénznemében (UnitPriceExclTax × mennyiség), átváltás előtt.</summary>
public static double ItemLineValue(OrderItemDto item) => (double)item.UnitPriceExclTax * item.Quantity;
/// <summary>Egy rendelés-tétel értéke HUF-ban, egészre kerekítve. 0/ismeretlen → <c>null</c> (lásd a bejövő párját).</summary>
public static long? ItemValueHuf(OrderItemDto item, double rateToHuf)
{
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
return huf > 0 ? (long)huf : null;
}
}

View File

@ -1,5 +1,6 @@
using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models; using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities; using FruitBank.Common.Entities;
namespace FruitBank.Common.Services.Ekaer; namespace FruitBank.Common.Services.Ekaer;
@ -22,4 +23,31 @@ public interface IShippingToEkaerMapper
/// <param name="company">A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely.</param> /// <param name="company">A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely.</param>
/// <param name="operation">A tradeCard művelet típusa. Alapértelmezés: <see cref="OperationType.Create"/>.</param> /// <param name="operation">A tradeCard művelet típusa. Alapértelmezés: <see cref="OperationType.Create"/>.</param>
IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create); IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create);
/// <summary>
/// EGY szállítólevelet (<see cref="ShippingDocument"/>) képez le egy tradeCard-dá — a dokumentum-szintű
/// EKÁER-granularitás egysége (1 dokumentum = 1 tradeCard = 1 TCN). A jármű/fuvarozó adatok a
/// <c>document.Shipping</c>-ből jönnek; ha az nincs betöltve, ezek üresen maradnak (a validátor jelzi).
/// </summary>
TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company);
/// <summary>
/// EGY kimenő rendelést (<see cref="FruitBank.Common.Dtos.OrderDto"/>) képez le egy tradeCard-dá: a bejelentő
/// (mi) az ELADÓ, a vevő a CÍMZETT, az irány belföldi értékesítés (<c>D</c>/<c>S</c>). A vonó jármű forrása
/// a kimenő fuvar-adat (OrderDto) — amíg az nincs bekötve, üresen marad (a validátor jelzi).
/// </summary>
TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company);
/// <summary>
/// Bejövő forrás → normalizált <see cref="EkaerConsignment"/>. Egy vagy több <see cref="ShippingDocument"/>-tel
/// hívható: EGY dokumentummal a tradeCard-generáláshoz, a (Shipping, Partner) CSOPORTtal a kötelezettség-summához
/// (a tételek összevonva). A hívó gondoskodik róla, hogy a csoport azonos partneré és azonos Shippingé legyen.
/// </summary>
EkaerConsignment ToConsignment(IReadOnlyCollection<ShippingDocument> documents, EkaerCompanyInfo company);
/// <summary>Kimenő forrás → normalizált <see cref="EkaerConsignment"/> (egy rendelés = egy szállítmány).</summary>
EkaerConsignment ToConsignment(OrderDto order, EkaerCompanyInfo company);
/// <summary>Normalizált szállítmány → NAV tradeCard. KÖZÖS a két irányra; a <c>MapDocument</c>/<c>MapOrder</c> ezt hívja.</summary>
TradeCardType BuildTradeCard(EkaerConsignment consignment);
} }

View File

@ -2,7 +2,9 @@ using AyCode.Core.Interfaces;
using AyCode.Entities; using AyCode.Entities;
using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models; using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities; using FruitBank.Common.Entities;
using Nop.Core.Domain.Customers;
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType; using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType; using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType;
@ -10,15 +12,28 @@ namespace FruitBank.Common.Services.Ekaer;
/// <inheritdoc cref="IShippingToEkaerMapper"/> /// <inheritdoc cref="IShippingToEkaerMapper"/>
/// <remarks> /// <remarks>
/// Tiszta (állapotmentes) leképező. A feladót és a saját céget egységesen <see cref="ICompanyInfoBase"/>-ként kezeli. /// Tiszta (állapotmentes) leképező. A bejövő (<c>ShippingDocument</c>-csoport) és a kimenő (<c>OrderDto</c>) forrás
/// A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be a <c>Models.TradeCardType</c> osztály /// előbb egy irány-független <see cref="EkaerConsignment"/>-re képződik (<see cref="ToConsignment(IReadOnlyCollection{ShippingDocument}, EkaerCompanyInfo)"/>
/// és a <c>Models.Common.TradeCardType</c> enum névütközése miatt. /// / <see cref="ToConsignment(OrderDto, EkaerCompanyInfo)"/>), majd EBBŐL épül a NAV tradeCard (<see cref="BuildTradeCard"/>).
/// Így az irányfüggő tudás a két adapterre szorul, a NAV-build közös. A fel-/lerakodási hely és a jármű már a NAV-típus
/// (a saját telephely kész <see cref="LocationType"/>, mezővesztés nélkül átengedve); a feladó/címzett normalizálása a
/// build-ben történik. A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be a névütközés miatt.
/// </remarks> /// </remarks>
public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
{ {
/// <summary>A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import.</summary> /// <summary>A NAV EKÁER magyar rendszer — a „belföld" alapértéke HU (a vevő-ország feloldásáig).</summary>
private const string HomeCountry = "HU"; private const string HomeCountry = "HU";
/// <summary>Kimenő pénznem — jelenleg minden HUF (a vevő/rendelés devizája az OrderDto-ban még nincs leképezve).</summary>
private const string OutboundCurrency = "HUF";
private readonly IEkaerSettings _settings;
public ShippingToEkaerMapper(IEkaerSettings settings)
=> _settings = settings ?? throw new ArgumentNullException(nameof(settings));
// ── Belépési pontok (vékony wrapperek a köztes modell + a közös build köré) ───────────────────
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create) public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
{ {
ArgumentNullException.ThrowIfNull(shipping); ArgumentNullException.ThrowIfNull(shipping);
@ -30,86 +45,202 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
// Granularitás: egy ShippingDocument → egy tradeCard (lásd EKAER_TODO #5). // Granularitás: egy ShippingDocument → egy tradeCard (lásd EKAER_TODO #5).
foreach (var document in shipping.ShippingDocuments ?? []) foreach (var document in shipping.ShippingDocuments ?? [])
{ {
document.Shipping ??= shipping; // a vontató/fuvarozó a Shippingről jön
index++; index++;
operations.Add(new TradeCardOperationType operations.Add(new TradeCardOperationType
{ {
Index = index, Index = index,
Operation = operation, Operation = operation,
TradeCard = BuildTradeCard(shipping, document, company), TradeCard = BuildTradeCard(ToConsignment([document], company)),
}); });
} }
return operations; return operations;
} }
private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerCompanyInfo company) public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company)
{ {
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(company);
return BuildTradeCard(ToConsignment([document], company));
}
public TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company)
{
ArgumentNullException.ThrowIfNull(order);
ArgumentNullException.ThrowIfNull(company);
return BuildTradeCard(ToConsignment(order, company));
}
// ── Adapterek: forrás → normalizált EkaerConsignment (az EGYETLEN irányfüggő rész) ────────────
/// <summary>Bejövő: egy vagy több <c>ShippingDocument</c> (a hívó (Shipping, Partner)-re csoportosít) → egy
/// szállítmány a csoport ÖSSZES tételével. Egy dokumentummal a generáláshoz, a csoporttal a kötelezettség-summához.</summary>
public EkaerConsignment ToConsignment(IReadOnlyCollection<ShippingDocument> documents, EkaerCompanyInfo company)
{
ArgumentNullException.ThrowIfNull(documents);
ArgumentNullException.ThrowIfNull(company);
if (documents.Count == 0) throw new ArgumentException("Legalább egy ShippingDocument szükséges.", nameof(documents));
var first = documents.First();
var partner = first.Partner; // a csoport azonos partneré (a hívó (Shipping, Partner)-re csoportosít)
var shipping = first.Shipping; // azonos Shipping; a kapunál lehet null (a jármű ott nem kell)
var rateToHuf = SafeRateToHuf(partner?.Currency);
var lines = documents
.SelectMany(d => d.ShippingItems ?? [])
.Select(item => new EkaerLine
{
ExternalId = item.Id.ToString(),
TradeReason = TradeReasonType.A, // bejövő áru = beszerzés
Vtsz = NormalizeVtsz(item.ProductDto?.Gtin),
Name = item.ProductName,
WeightKg = item.MeasuredGrossWeight,
ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf),
})
.ToList();
return new EkaerConsignment
{
ForeignKey = first.Id,
IsOutgoing = false,
Currency = partner?.Currency,
Seller = PartnerEndpoint(partner),
Buyer = CompanyEndpoint(company),
LoadLocation = BuildLocation(partner), // a beszállító telephelye (a PartnerDepot-bekötés külön feladat)
UnloadLocation = company.Site, // a saját telephely (kész LocationType — mezővesztés nélkül átengedve)
Lines = lines,
Vehicle = BuildVehicle(shipping?.CargoTruck),
Trailer = BuildVehicle(shipping?.CargoTrailer),
CarrierName = shipping?.CargoPartner?.Name,
};
}
/// <summary>Kimenő: egy rendelés → egy szállítmány. Nincs Shipping; a kötelezettséget a rendelésre nézzük.</summary>
public EkaerConsignment ToConsignment(OrderDto order, EkaerCompanyInfo company)
{
ArgumentNullException.ThrowIfNull(order);
ArgumentNullException.ThrowIfNull(company);
var customer = order.Customer;
var rateToHuf = SafeRateToHuf(OutboundCurrency); // jelenleg minden HUF
var lines = (order.OrderItemDtos ?? [])
.Select(item => new EkaerLine
{
ExternalId = item.Id.ToString(),
TradeReason = TradeReasonType.S, // kimenő áru = értékesítés
Vtsz = NormalizeVtsz(item.ProductDto?.Gtin),
Name = item.ProductDto?.Name,
WeightKg = item.GrossWeight,
ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf),
})
.ToList();
return new EkaerConsignment
{
ForeignKey = order.Id,
IsOutgoing = true,
Currency = OutboundCurrency,
Seller = CompanyEndpoint(company),
Buyer = CustomerEndpoint(customer),
LoadLocation = company.Site,
UnloadLocation = BuildCustomerLocation(customer),
Lines = lines,
// Kimenőnél NEM töltjük a szállítmányozót: a vevő maga viszi el, és a vevő MÁR a címzett (destinationName) —
// külön fuvarozót nem tartunk nyilván; a carrierText opcionális (a NAV nem követeli) → üresen hagyjuk.
CarrierName = null,
// A vonó jármű (rendszám) a customer-hez még nincs bekötve → üresen marad, a felrakodás megkezdéséig pótolandó
// (a validátor warningolja). Amint bekötik, a Vehicle is innen jön.
};
}
// ── Közös build: normalizált szállítmány → NAV tradeCard (a korábbi két ág helyett egy) ───────
public TradeCardType BuildTradeCard(EkaerConsignment consignment)
{
ArgumentNullException.ThrowIfNull(consignment);
var tradeCard = new TradeCardType var tradeCard = new TradeCardType
{ {
TradeType = ResolveTradeType(seller), TradeType = ResolveTradeType(consignment),
ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat
// Feladó / eladó = a beszállító SellerName = consignment.Seller.Name,
SellerName = seller?.Name, SellerVatNumber = NormalizeVatNumber(consignment.Seller.VatNumber),
SellerVatNumber = NormalizeVatNumber(seller?.TaxId), SellerCountry = NormalizeCountryCode(consignment.Seller.CountryCode, 2),
SellerCountry = NormalizeCountryCode(seller?.CountryCode, 2), SellerAddress = Truncate(consignment.Seller.FullAddress, 200),
SellerAddress = Truncate(seller?.FullAddress, 200),
// Címzett = a bejelentő saját cége (bejövő relációban) DestinationName = consignment.Buyer.Name,
DestinationName = company.Name, DestinationVatNumber = NormalizeVatNumber(consignment.Buyer.VatNumber),
DestinationVatNumber = NormalizeVatNumber(company.TaxId), DestinationCountry = NormalizeCountryCode(consignment.Buyer.CountryCode, 2),
DestinationCountry = NormalizeCountryCode(company.CountryCode, 2), DestinationAddress = Truncate(consignment.Buyer.FullAddress, 200),
DestinationAddress = Truncate(company.FullAddress, 200),
// Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név. CarrierText = consignment.CarrierName,
CarrierText = shipping.CargoPartner?.Name,
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye. // A helyek kész NAV LocationType-ok (a saját telephely érintetlenül) — átengedve.
UnloadLocation = company.UnloadLocation, LoadLocation = consignment.LoadLocation,
LoadLocation = BuildLoadLocation(seller), UnloadLocation = consignment.UnloadLocation,
}; };
// Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2). if (consignment.Vehicle != null) tradeCard.Vehicle = consignment.Vehicle;
if (shipping.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); if (consignment.Trailer != null) tradeCard.Vehicle2 = consignment.Trailer;
if (shipping.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer);
foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item)); foreach (var line in consignment.Lines) tradeCard.Items.Add(BuildItem(line));
return tradeCard; return tradeCard;
} }
/// <summary> /// <summary>Belföld (a feladó és a címzett országkódja megegyezik) → <c>D</c>; eltérő országok → kimenőnél
/// Belföldi feladó (HU) → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import). A NAV EKÁER magyar, /// export (<c>E</c>), bejövőnél import (<c>I</c>).</summary>
/// így a belföld mindig HU; az export (<c>E</c>) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7). private static TradeType ResolveTradeType(EkaerConsignment c)
/// </summary>
private static TradeType ResolveTradeType(ICompanyInfoBase? seller)
=> string.Equals(seller?.CountryCode, HomeCountry, StringComparison.OrdinalIgnoreCase)
? TradeType.D
: TradeType.I;
private static TradeCardItemType BuildItem(ShippingItem item) => new()
{ {
ItemExternalId = item.Id.ToString(), if (string.Equals(c.Seller.CountryCode?.Trim(), c.Buyer.CountryCode?.Trim(), StringComparison.OrdinalIgnoreCase))
// Bejövő áru = beszerzés → A. (Enum: S=értékesítés, A=beszerzés, W=bérmunka, O=egyéb.) Lásd EKAER_TODO #9. return TradeType.D;
TradeReason = TradeReasonType.A, return c.IsOutgoing ? TradeType.E : TradeType.I;
ProductVtsz = item.ProductDto?.Gtin, // VTSZ — átmenetileg a Gtin oszlopban (FBANKAPP-DMODEL-I-P6X4) }
ProductName = item.ProductName,
Weight = (decimal)item.MeasuredGrossWeight, // bruttó tömeg kg-ban (lásd EKAER_TODO #4) private static TradeCardItemType BuildItem(EkaerLine line) => new()
// Value (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3, #10. {
ItemExternalId = line.ExternalId,
TradeReason = line.TradeReason,
ProductVtsz = line.Vtsz, // már normalizált (az adapterben)
ProductName = line.Name,
Weight = (decimal)line.WeightKg,
Value = line.ValueHuf,
}; };
private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new() // ── Forrás → normalizálatlan végpont (feladó/címzett: név/adószám/ország/egysoros cím) ─────────
private static EkaerEndpoint PartnerEndpoint(ICompanyInfoBase? partner) => new()
{ {
PlateNumber = NormalizePlateNumber(truck.LicencePlate), Name = partner?.Name,
Country = NormalizeCountryCode(truck.CountryCode, 3), VatNumber = partner?.TaxId,
CountryCode = partner?.CountryCode,
FullAddress = partner?.FullAddress,
}; };
/// <summary> private static EkaerEndpoint CompanyEndpoint(EkaerCompanyInfo company) => new()
/// Felrakodási hely a beszállító adataiból. (Magyar feladónál a NAV a Phone/Email-t is kéri, ami az {
/// entitásban nincs — lásd EKAER_TODO #6.) Name = company.Name,
/// </summary> VatNumber = company.TaxId,
private static LocationType? BuildLoadLocation(ICompanyInfoBase? seller) CountryCode = company.CountryCode,
FullAddress = company.FullAddress,
};
/// <summary>A vevő mint fél. Ország jelenleg HU — a Customer.CountryId→ISO feloldás (export E) külön feladat.</summary>
private static EkaerEndpoint CustomerEndpoint(Customer? customer) => new()
{
Name = customer?.Company,
VatNumber = customer?.VatNumber,
CountryCode = HomeCountry,
FullAddress = ComposeCustomerAddress(customer),
};
// ── Forrás → NAV LocationType / jármű (a saját telephelyet az adapter közvetlenül adja) ────────
/// <summary>Felrakodási hely a beszállító adataiból (bejövő). A NAV magyar feladónál a Phone/Email-t is kéri, ami
/// az entitásban nincs — lásd EKAER_TODO #6.</summary>
private static LocationType? BuildLocation(ICompanyInfoBase? seller)
{ {
if (seller is null) return null; if (seller is null) return null;
return new LocationType return new LocationType
@ -123,6 +254,48 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
}; };
} }
/// <summary>Lerakodási hely a vevő adataiból (kimenő). Ország jelenleg HU (belföld).</summary>
private static LocationType? BuildCustomerLocation(Customer? customer)
{
if (customer is null) return null;
return new LocationType
{
Name = customer.Company,
VatNumber = NormalizeVatNumber(customer.VatNumber),
Country = HomeCountry,
ZipCode = NormalizeZipCode(customer.ZipPostalCode),
City = customer.City,
Street = customer.StreetAddress,
};
}
private static BasicVehicleDetailType? BuildVehicle(CargoTruck? truck)
{
if (truck is null) return null;
return new BasicVehicleDetailType
{
PlateNumber = NormalizePlateNumber(truck.LicencePlate),
Country = NormalizeCountryCode(truck.CountryCode, 3),
};
}
/// <summary>A számla-pénznem → HUF szorzó, NEM dobó változat (a kapu külföldinél küszöb nélkül jelent, ott az érték
/// nem kell): HUF → 1; külföldi + érvényes árfolyam → árfolyam; külföldi + hiányzó árfolyam → 0 (a tétel-érték null lesz).
/// Generáláskor a service config-kapuja (TryConfigError) előbb elvágja a hiányzó-árfolyamos külföldi esetet.</summary>
private double SafeRateToHuf(string? currency)
=> EkaerValueCalculator.IsHuf(currency) ? 1d : (_settings.EurHufRate > 0 ? _settings.EurHufRate : 0d);
/// <summary>A vevő egysoros címe (irsz + város + utca) a NAV <c>destinationAddress</c>-hez.</summary>
private static string? ComposeCustomerAddress(Customer? customer)
{
if (customer is null) return null;
var parts = new[] { customer.ZipPostalCode, customer.City, customer.StreetAddress, customer.StreetAddress2 }
.Where(p => !string.IsNullOrWhiteSpace(p));
return EmptyToNull(string.Join(" ", parts).Trim());
}
// ── Normalizálók (NAV pattern-ek) ─────────────────────────────────────────────────────────────
/// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary> /// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
private static string? NormalizeVatNumber(string? value) private static string? NormalizeVatNumber(string? value)
{ {
@ -155,6 +328,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
return Truncate(EmptyToNull(cleaned), 10); return Truncate(EmptyToNull(cleaned), 10);
} }
/// <summary>VTSZ normalizálása. Pattern: <c>[0-9]{4,8}</c> — a forrásban tagolt formátum is lehet
/// („0805 10 00", „0805.10.00"), ezért csak a számjegyek maradnak; a 4-nél rövidebbet a validátor jelzi.</summary>
private static string? NormalizeVtsz(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var cleaned = new string([.. value.Where(char.IsAsciiDigit)]);
return Truncate(EmptyToNull(cleaned), 8);
}
private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value; private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value;
private static string? Truncate(string? value, int maxLen) private static string? Truncate(string? value, int maxLen)

View File

@ -112,6 +112,21 @@ public class SignalRTags : AcSignalRTags
public const int GetStockTakingItemsByStockTakingId = 178; public const int GetStockTakingItemsByStockTakingId = 178;
public const int AddOrUpdateMeasuredStockTakingItemPallet = 179; public const int AddOrUpdateMeasuredStockTakingItemPallet = 179;
public const int GetPartnerDepots = 180;
public const int GetPartnerDepotById = 181;
public const int GetPartnerDepotsByPartnerId = 182;
public const int AddPartnerDepot = 183;
public const int UpdatePartnerDepot = 184;
public const int GetEkaerHistories = 185;
public const int GetEkaerHistoryById = 186;
public const int GetEkaerHistoriesByForeignKey = 187;
public const int AddEkaerHistory = 188;
public const int UpdateEkaerHistory = 189;
public const int GenerateEkaerXmlDocument = 190;
public const int CreateEkaerHistory = 191;
public const int CreateMissingEkaerHistories = 192;
public const int GetEkaerHistoryCount = 193;
public const int AuthenticateUser = 195; public const int AuthenticateUser = 195;
public const int RefreshToken = 200; public const int RefreshToken = 200;

View File

@ -29,9 +29,10 @@ A `PartnerBase` (és így `Partner` / `CargoPartner`) + a `CargoTruck` a nopComm
## FBANKAPP-DMODEL-I-P6X4: A `Product.Gtin` átmenetileg a VTSZ-t tárolja — szétválasztandó ## FBANKAPP-DMODEL-I-P6X4: 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-15) — superseded by `MGFBANKPLUG-EKAER-I-T3X8` · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
> **Supersedes** `MGFBANKPLUG-EKAER-I-T3X8` — áthelyezve az EKÁER-topicból ide, mert a GTIN/VTSZ szétválasztás **általános** adatmodell-kérdés (a GTIN globális termékazonosító, a VTSZ vámtarifaszám), nem EKÁER-specifikus. ### Resolution
**Visszahelyezve** az EKÁER-topicba: **`MGFBANKPLUG-EKAER-I-T3X8`** (`docs/EKAER/EKAER_ISSUES.md`, újra **Open**). A 2026-06-02-i ide-helyezés **visszavonva** — a VTSZ-forrás (`Product.Gtin`) szétválasztását a konkrét EKÁER-leképezéshez kötve, ott követjük (egy tracker). 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ő**: 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). - **GTIN** — globális kereskedelmi cikkszám (vonalkód-azonosító, EAN/UPC).

View File

@ -0,0 +1,140 @@
using FruitBank.Common.Services.Ekaer;
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
namespace FruitBankHybrid.Shared.Tests.Ekaer;
/// <summary>
/// Unit tesztek a <see cref="EkaerReportability"/> bejelentés-kötelezettség-eldöntésére. Tisztán memóriában felépített
/// <see cref="EkaerConsignment"/>-eken fut (nincs hálózat/DB), determinisztikus. Küszöb: 200 kg / 250 000 Ft.
/// </summary>
[TestClass]
public sealed class EkaerReportabilityTests
{
private static readonly EkaerSettings Settings = new()
{
EurHufRate = 356,
ThresholdWeightKg = 200,
ThresholdValueHuf = 250_000,
};
private static EkaerConsignment Consignment(string? sellerCountry, string? buyerCountry, params EkaerLine[] lines) => new()
{
Seller = new EkaerEndpoint { Name = "Feladó", CountryCode = sellerCountry },
Buyer = new EkaerEndpoint { Name = "Címzett", CountryCode = buyerCountry },
Lines = lines,
};
private static EkaerLine Line(double weightKg, long? valueHuf) =>
new() { ExternalId = "1", WeightKg = weightKg, ValueHuf = valueHuf, TradeReason = TradeReasonType.A };
// ---- Belföld + küszöb ----------------------------------------------------
[TestMethod]
public void Domestic_BelowBothThresholds_NotRequired()
{
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(100, 100_000)), Settings);
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
Assert.AreEqual(0, result.Errors.Count, "küszöb alatt NINCS üzenet (nem hiba)");
}
[TestMethod]
public void Domestic_WeightAtOrOverThreshold_Required()
{
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(200, 0)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "a tömeg eléri a küszöböt → kötelező (VAGY-logika)");
}
[TestMethod]
public void Domestic_ValueOverThreshold_Required()
{
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(10, 300_000)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "az érték átlépi a küszöböt → kötelező");
}
[TestMethod]
public void Domestic_AggregatedOverThreshold_Required()
{
// Egyenként 200 kg ALATT, EGYÜTT fölötte → kötelező. Ez a (Shipping, Partner)-aggregálás lényege.
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(120, 0), Line(120, 0)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation);
}
// ---- Külföld (a két országkód eltér) → küszöb nélkül kötelező ------------
[TestMethod]
public void CrossBorder_BelowThreshold_StillRequired()
{
var result = EkaerReportability.Evaluate(Consignment("DE", "HU", Line(1, 1)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "eltérő országkód → mindig kötelező, küszöb nélkül");
}
[TestMethod]
public void SameForeignCountryBothEnds_TreatedAsDomesticThreshold()
{
// Mindkét vég azonos (nem HU) ország → NEM határátlépő → a küszöb dönt.
var result = EkaerReportability.Evaluate(Consignment("DE", "DE", Line(1, 1)), Settings);
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
}
// ---- Adathiba: érvénytelen/hiányzó országkód → DataError CSAK küszöb alatt ----
[TestMethod]
public void BelowThreshold_MissingSellerCountry_DataError()
{
// Küszöb alatt MÁR számít az ország (foreign-vs-belföld) — hiányzó országkód → nem dönthető el → DataError.
var result = EkaerReportability.Evaluate(Consignment(null, "HU", Line(10, 1000)), Settings);
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
Assert.IsTrue(result.Errors.Count > 0, "a hiányzó országkódot jelezni kell a felhasználónak");
}
[TestMethod]
public void BelowThreshold_InvalidSellerCountry_NotIso2_DataError()
{
// Teljes név (nem ISO-2) → érvénytelen → küszöb alatt nem dönthető el → DataError.
var result = EkaerReportability.Evaluate(Consignment("Magyarország", "HU", Line(10, 1000)), Settings);
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
}
[TestMethod]
public void BelowThreshold_MissingBuyerCountry_DataError()
{
var result = EkaerReportability.Evaluate(Consignment("HU", "", Line(10, 1000)), Settings);
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
}
[TestMethod]
public void OverThreshold_InvalidCountry_Required_NotDataError()
{
// Küszöb FELETT (tömeg 500 ≥ 200) a kötelezettség az országkódtól FÜGGETLEN → a sor létrejön (a hibát a
// generate-validálás jelzi), NEM DataError.
var result = EkaerReportability.Evaluate(Consignment(null, "Magyarország", Line(500, 0)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation);
Assert.AreEqual(0, result.Errors.Count);
}
// ---- Hibás / üres adatok (robusztusság) ----------------------------------
[TestMethod]
public void NoLines_Domestic_NotRequired()
{
// Üres szállítmány (nincs tétel) → tömeg/érték 0 → küszöb alatt → belföld → nem kötelező (NEM dob).
var result = EkaerReportability.Evaluate(Consignment("HU", "HU"), Settings);
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
}
[TestMethod]
public void BothCountriesInvalid_BelowThreshold_DataError_WithTwoMessages()
{
var result = EkaerReportability.Evaluate(Consignment(null, "", Line(10, 1000)), Settings);
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
Assert.AreEqual(2, result.Errors.Count, "mindkét hibás országkódot külön jelezni kell");
}
[TestMethod]
public void NullLineValue_HandledAsZero_WeightDecides()
{
// Hiányzó (null) tétel-érték (pl. külföldi deviza árfolyam nélkül) → 0-ként számít, NEM dob; a tömeg dönt.
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(500, null)), Settings);
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "a tömeg átlépi a küszöböt — a null érték nem akadály");
}
}

View File

@ -15,7 +15,9 @@ namespace FruitBankHybrid.Shared.Tests.Ekaer;
[TestClass] [TestClass]
public sealed class ShippingToEkaerMapperTests public sealed class ShippingToEkaerMapperTests
{ {
private static readonly ShippingToEkaerMapper Mapper = new(); // EurHufRate > 0 kell: a teszt-partnernek nincs Currency-je (null → külföldiként kezelt), és árfolyam nélkül
// a value-számítás ResolveRateToHuf-ja dobna. A value-t a tesztek nem ellenőrzik; ez csak az átváltáshoz kell.
private static readonly ShippingToEkaerMapper Mapper = new(new EkaerSettings { EurHufRate = 356 });
// ---- Helpers ------------------------------------------------------------ // ---- Helpers ------------------------------------------------------------
@ -67,18 +69,42 @@ public sealed class ShippingToEkaerMapperTests
PostalCode = "1102", PostalCode = "1102",
City = "Budapest", City = "Budapest",
Street = "Raktar utca 5", Street = "Raktar utca 5",
UnloadLocation = new LocationType Site = new LocationType
{ {
Name = "FruitBank Raktár", Name = "FruitBank Raktár",
VatNumber = "98765432-2-41", VatNumber = "98765432-2-41",
Phone = "+36301234567", // NAV-formátum: + / 06 prefix kötelező
Email = "raktar@fruitbank.hu",
Country = "HU", Country = "HU",
ZipCode = "1102", ZipCode = "1239",
City = "Budapest", City = "Budapest",
Street = "Raktar utca", Street = "Nagykőrösi út",
StreetNumber = "5", StreetNumber = "353",
}, },
}; };
private static ShippingDocument CreateInboundDocument(int itemId, double weight, string sellerCountry = "HU") => new()
{
Country = sellerCountry,
Partner = new Partner
{
Name = "Beszállító Kft",
TaxId = "12345678-2-42",
CountryCode = sellerCountry,
PostalCode = "1011",
City = "Budapest",
Street = "Fő utca 1",
},
ShippingItems = [new ShippingItem
{
Id = itemId,
Name = "Alma",
ProductDto = new ProductDto { Gtin = "08081010", Name = "Alma" },
MeasuredGrossWeight = weight,
UnitPriceOnDocument = 5.0,
}],
};
// ---- Granularitás / index ---------------------------------------------- // ---- Granularitás / index ----------------------------------------------
[TestMethod] [TestMethod]
@ -166,7 +192,7 @@ public sealed class ShippingToEkaerMapperTests
Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName); Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName);
Assert.AreEqual("HU", tradeCard.DestinationCountry); Assert.AreEqual("HU", tradeCard.DestinationCountry);
StringAssert.Contains(tradeCard.DestinationAddress, "Budapest", "a destinationAddress a company FullAddress-éből jön"); StringAssert.Contains(tradeCard.DestinationAddress, "Budapest", "a destinationAddress a company FullAddress-éből jön");
Assert.AreSame(company.UnloadLocation, tradeCard.UnloadLocation, "a lerakodási hely a cégadatból jön"); Assert.AreSame(company.Site, tradeCard.UnloadLocation, "a lerakodási hely a cég telephelyéből jön");
} }
[TestMethod] [TestMethod]
@ -212,4 +238,55 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod] [TestMethod]
public void MapShipping_NullCompany_Throws() public void MapShipping_NullCompany_Throws()
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(CreateShipping(), null!)); => Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(CreateShipping(), null!));
// ---- Köztes modell: ToConsignment (bejövő) ------------------------------
[TestMethod]
public void ToConsignment_Inbound_MultipleDocuments_CombinesLinesAndAggregatesWeight()
{
// Két szállítólevél (azonos partner, azonos Shipping) → egy szállítmány, MINDKÉT tétellel — a küszöb-aggregáláshoz.
var consignment = Mapper.ToConsignment(
[CreateInboundDocument(itemId: 1, weight: 100), CreateInboundDocument(itemId: 2, weight: 150)],
CreateCompany());
Assert.AreEqual(2, consignment.Lines.Count, "a csoport ÖSSZES tétele egy szállítmányban");
Assert.AreEqual(250d, consignment.Lines.Sum(l => l.WeightKg), 0.001, "a tömeg a dokumentumokból összegződik");
Assert.IsFalse(consignment.IsOutgoing);
Assert.AreEqual("HU", consignment.Seller.CountryCode, "feladó = a beszállító partner");
Assert.AreEqual("HU", consignment.Buyer.CountryCode, "címzett = a saját cég");
}
[TestMethod]
public void ToConsignment_Inbound_BuildTradeCard_PreservesCompanySiteAsUnloadLocation()
{
var company = CreateCompany();
var tradeCard = Mapper.BuildTradeCard(Mapper.ToConsignment([CreateInboundDocument(1, 100)], company));
Assert.AreSame(company.Site, tradeCard.UnloadLocation, "a saját telephely érintetlenül (mezővesztés nélkül) megy át");
}
[TestMethod]
public void ToConsignment_Inbound_EmptyDocuments_Throws()
=> Assert.ThrowsExactly<ArgumentException>(() => Mapper.ToConsignment([], CreateCompany()));
[TestMethod]
public void ToConsignment_Inbound_NullPartner_DoesNotThrow_SellerCountryNull()
{
// Hibás adat: nincs Partner a szállítólevélen → az adapter NEM dob; a feladó-ország null (ezt a kötelezettség-
// értékelő / generate-validálás kezeli, nem a leképező).
var doc = new ShippingDocument { Partner = null, ShippingItems = [new ShippingItem { Id = 1, MeasuredGrossWeight = 50 }] };
var consignment = Mapper.ToConsignment([doc], CreateCompany());
Assert.IsNull(consignment.Seller.CountryCode, "nincs partner → nincs feladó-ország");
Assert.AreEqual(1, consignment.Lines.Count);
}
[TestMethod]
public void ToConsignment_Inbound_NullShippingItems_EmptyLines()
{
// Hibás/hiányos adat: nincs tétel a szállítólevélen → üres Lines, NEM dob.
var doc = new ShippingDocument { Partner = new Partner { CountryCode = "HU" }, ShippingItems = null };
var consignment = Mapper.ToConsignment([doc], CreateCompany());
Assert.AreEqual(0, consignment.Lines.Count);
}
} }

View File

@ -34,149 +34,6 @@ namespace FruitBankHybrid.Shared.Tests
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
} }
#region Partner
[TestMethod]
public async Task GetPartnersTest()
{
var partners = await _signalRClient.GetPartners();
Assert.IsNotNull(partners);
Assert.IsNotEmpty(partners);
}
//[TestMethod]
//[DataRow(1)]
public async Task<Partner> GetPartnerByIdTest(int partnerId)
{
var partner = await _signalRClient.GetPartnerById(partnerId);
Assert.IsNotNull(partner);
Assert.AreEqual(partnerId, partner.Id);
return partner;
}
[TestMethod]
[DataRow(2)]
public async Task UpdatePartnerTest(int partnerId)
{
var partner = await GetPartnerByIdTest(partnerId);
var newName = GetFixtureName(partner.Name);
partner.Name = newName;
partner = await _signalRClient.UpdatePartner(partner);
Assert.IsNotNull(partner);
Assert.IsTrue(partner.Name == newName);
partner.Name = GetOriginalName(partner.Name);
partner = await _signalRClient.UpdatePartner(partner);
Assert.IsNotNull(partner);
Assert.AreEqual(partnerId, partner.Id);
}
#endregion Partner
#region CargoPartner
[TestMethod]
public async Task GetCargoPartnersTest()
{
var partners = await _signalRClient.GetCargoPartners();
Assert.IsNotNull(partners);
Assert.IsNotEmpty(partners);
}
//[TestMethod]
//[DataRow(1)]
public async Task<CargoPartner> GetCargoPartnerByIdTest(int cargoPartnerId)
{
var cargoPartner = await _signalRClient.GetCargoPartnerById(cargoPartnerId);
Assert.IsNotNull(cargoPartner);
Assert.AreEqual(cargoPartnerId, cargoPartner.Id);
return cargoPartner;
}
[TestMethod]
[DataRow(1)]
public async Task UpdateCargoPartnerTest(int cargoPartnerId)
{
var cargoPartner = await GetCargoPartnerByIdTest(cargoPartnerId);
var newName = GetFixtureName(cargoPartner.Name);
cargoPartner.Name = newName;
cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner);
Assert.IsNotNull(cargoPartner);
Assert.IsTrue(cargoPartner.Name == newName);
cargoPartner.Name = GetOriginalName(cargoPartner.Name);
cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner);
Assert.IsNotNull(cargoPartner);
Assert.AreEqual(cargoPartnerId, cargoPartner.Id);
}
#endregion CargoPartner
#region CargoTruck
[TestMethod]
public async Task GetCargoTrucksTest()
{
var cargoTrucks = await _signalRClient.GetCargoTrucks();
Assert.IsNotNull(cargoTrucks);
Assert.IsNotEmpty(cargoTrucks);
}
//[TestMethod]
//[DataRow(1)]
public async Task<CargoTruck> GetCargoTruckByIdTest(int cargoTruckId)
{
var cargoTruck = await _signalRClient.GetCargoTruckById(cargoTruckId);
Assert.IsNotNull(cargoTruck);
Assert.AreEqual(cargoTruckId, cargoTruck.Id);
return cargoTruck;
}
[TestMethod]
[DataRow(1)]
public async Task GetCargoTrucksByCargoPartnerIdTest(int cargoPartnerId)
{
var cargoTrucks = await _signalRClient.GetCargoTrucksByCargoPartnerId(cargoPartnerId);
Assert.IsNotNull(cargoTrucks);
Assert.IsNotEmpty(cargoTrucks);
}
[TestMethod]
[DataRow(1)]
public async Task UpdateCargoTruckTest(int cargoTruckId)
{
var cargoTruck = await GetCargoTruckByIdTest(cargoTruckId);
var newLicencePlate = GetFixtureName(cargoTruck.LicencePlate);
cargoTruck.LicencePlate = newLicencePlate;
cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck);
Assert.IsNotNull(cargoTruck);
Assert.IsTrue(cargoTruck.LicencePlate == newLicencePlate);
cargoTruck.LicencePlate = GetOriginalName(cargoTruck.LicencePlate);
cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck);
Assert.IsNotNull(cargoTruck);
Assert.AreEqual(cargoTruckId, cargoTruck.Id);
}
#endregion CargoTruck
#region Shipping #region Shipping
[TestMethod] [TestMethod]
public async Task GetShippingsTest() public async Task GetShippingsTest()
@ -299,7 +156,7 @@ namespace FruitBankHybrid.Shared.Tests
else else
{ {
Assert.IsNotNull(shippingItem.ShippingDocument); Assert.IsNotNull(shippingItem.ShippingDocument);
Assert.IsTrue(shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count == 0); Assert.AreEqual(0, shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count);
} }
} }
@ -313,11 +170,11 @@ namespace FruitBankHybrid.Shared.Tests
if (shippingItem.IsMeasurable) Assert.IsNotNull(shippingItem.Pallet, $"shippingItem.Pallet == null; shippingItem.PalletId: {shippingItem.PalletId}"); if (shippingItem.IsMeasurable) Assert.IsNotNull(shippingItem.Pallet, $"shippingItem.Pallet == null; shippingItem.PalletId: {shippingItem.PalletId}");
Assert.IsNotNull(shippingItem.ProductDto, $"shippingItem.Product == null; shippingItem.ProductId: {shippingItem.ProductId}"); Assert.IsNotNull(shippingItem.ProductDto, $"shippingItem.Product == null; shippingItem.ProductId: {shippingItem.ProductId}");
Assert.IsTrue(shippingItem.Id == shippingItemId); Assert.AreEqual(shippingItemId, shippingItem.Id);
Assert.IsTrue(shippingItem.QuantityOnDocument > 0, "QuantityOnDocument == 0"); Assert.IsGreaterThan(0, shippingItem.QuantityOnDocument, "QuantityOnDocument == 0");
Assert.IsTrue(shippingItem.NetWeightOnDocument > 0, "NetWeightOnDocument == 0"); Assert.IsGreaterThan(0, shippingItem.NetWeightOnDocument, "NetWeightOnDocument == 0");
Assert.IsTrue(shippingItem.GrossWeightOnDocument > 0, "GrossWeightOnDocument == 0"); Assert.IsGreaterThan(0, shippingItem.GrossWeightOnDocument, "GrossWeightOnDocument == 0");
return shippingItem; return shippingItem;
} }

View File

@ -0,0 +1,131 @@
using AyCode.Services.Nav;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common;
using FruitBank.Common.Entities;
using FruitBankHybrid.Shared.Services.SignalRs;
namespace FruitBankHybrid.Shared.Tests
{
[TestClass]
public sealed class FruitBankEkaerTests
{
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public void TestInit()
{
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests));
}
#region EkaerHistory / Create
/// <summary>
/// Backfill: minden meglévő szállítólevélre létrehozza az EkaerHistory rekordot (Pending, XML nélkül).
/// A generálás a grid Generate gombjával, kézzel történik. Idempotens: újrafuttatva nem duplikál.
/// </summary>
[TestMethod]
public async Task CreateEkaerHistoryForAllShippingDocumentsTest()
{
var shippingDocuments = await _signalRClient.GetShippingDocuments();
Assert.IsNotNull(shippingDocuments);
Assert.IsNotEmpty(shippingDocuments);
foreach (var shippingDocument in shippingDocuments)
{
var ekaerHistory = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false);
Console.WriteLine($"doc#{shippingDocument.Id}: EkaerHistory Id: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Id.ToString())}; Status: {ekaerHistory?.Status}");
Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}");
Assert.IsGreaterThan(0, ekaerHistory.Id, $"shippingDocument.Id: {shippingDocument.Id}");
Assert.IsFalse(ekaerHistory.IsOutgoing);
Assert.IsNotNull(ekaerHistory.Mappings, $"Mappings null (loadRelations?); shippingDocument.Id: {shippingDocument.Id}");
Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}");
}
// Idempotencia: a második hívás a meglévőt adja vissza, nem duplikál.
var firstDocumentId = shippingDocuments[0].Id;
var again = await _signalRClient.CreateEkaerHistory(firstDocumentId, false);
var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(firstDocumentId);
Assert.IsNotNull(again);
Assert.IsNotNull(histories);
Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {firstDocumentId}");
}
#endregion EkaerHistory / Create
#region EkaerHistory / Generate
/// <summary>
/// Backfill + teljes Generate-út teszt: minden meglévő szállítólevélre legenerálja az EKÁER XML-t
/// (rekord upsert a szerveren), így a grid valós adatot kap és a Generate gomb útvonala tesztelt.
/// </summary>
//[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog.
public async Task GenerateEkaerXmlDocumentForAllShippingDocumentsTest()
{
var shippingDocuments = await _signalRClient.GetShippingDocuments();
Assert.IsNotNull(shippingDocuments);
Assert.IsNotEmpty(shippingDocuments);
foreach (var shippingDocument in shippingDocuments)
{
// A generálás mostantól EkaerHistory-Id alapú → előbb a (mapping-elt) rekordot hozzuk létre.
var created = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false);
Assert.IsNotNull(created, $"CreateEkaerHistory null; shippingDocument.Id: {shippingDocument.Id}");
var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
// A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon.
Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}");
if (!string.IsNullOrWhiteSpace(ekaerHistory?.ErrorText)) Console.WriteLine($" ErrorText: {ekaerHistory.ErrorText}");
Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}");
Assert.IsFalse(ekaerHistory.IsOutgoing);
Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings!.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}");
Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; shippingDocument.Id: {shippingDocument.Id}");
Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError,
$"Status: {ekaerHistory.Status}; shippingDocument.Id: {shippingDocument.Id}; ErrorText: {ekaerHistory.ErrorText}");
// A grid útvonala: az XmlDoc visszaolvasható tradeCard-dá.
var tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc!);
Assert.IsNotNull(tradeCard);
Console.WriteLine($" items: {tradeCard.Items.Count}");
}
}
/// <summary>Idempotencia: az újragenerálás NEM duplikál — dokumentumonként egy bejövő rekord marad.</summary>
//[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog.
public async Task GenerateEkaerXmlDocumentIsIdempotentTest()
{
var shippingDocuments = await _signalRClient.GetShippingDocuments();
Assert.IsNotNull(shippingDocuments);
Assert.IsNotEmpty(shippingDocuments);
var shippingDocumentId = shippingDocuments[0].Id;
// Generálás EkaerHistory-Id alapján → előbb a rekord (mapping-gel), majd kétszeri generálás ugyanarra az Id-ra.
var created = await _signalRClient.CreateEkaerHistory(shippingDocumentId, false);
Assert.IsNotNull(created);
var first = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
var second = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
Assert.IsNotNull(first);
Assert.IsNotNull(second);
Assert.AreEqual(first.Id, second.Id, "Az újragenerálás új rekordot hozott létre frissítés helyett!");
var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(shippingDocumentId);
Assert.IsNotNull(histories);
Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {shippingDocumentId}");
}
#endregion EkaerHistory / Generate
}
}

View File

@ -0,0 +1,225 @@
using FruitBank.Common;
using FruitBank.Common.Entities;
using FruitBankHybrid.Shared.Services.SignalRs;
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace FruitBankHybrid.Shared.Tests
{
[TestClass]
public sealed class FruitBankPartnerTests
{
private const int CustomerIdAasdDsserverCom = 6;//aasd@dsserver.com
private const string Fixture = "_test.temp";
private FruitBankSignalRClient _signalRClient = null!;
private static string GetFixtureName(string name) => $"{GetOriginalName(name)}{Fixture}";
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
[TestInitialize]
public void TestInit()
{
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
#region Partner
[TestMethod]
public async Task GetPartnersTest()
{
var partners = await _signalRClient.GetPartners();
Assert.IsNotNull(partners);
Assert.IsNotEmpty(partners);
}
//[TestMethod]
//[DataRow(1)]
public async Task<Partner> GetPartnerByIdTest(int partnerId)
{
var partner = await _signalRClient.GetPartnerById(partnerId);
Assert.IsNotNull(partner);
Assert.AreEqual(partnerId, partner.Id);
return partner;
}
[TestMethod]
[DataRow(2)]
public async Task UpdatePartnerTest(int partnerId)
{
var partner = await GetPartnerByIdTest(partnerId);
var newName = GetFixtureName(partner.Name);
partner.Name = newName;
partner = await _signalRClient.UpdatePartner(partner);
Assert.IsNotNull(partner);
Assert.AreEqual(newName, partner.Name);
partner.Name = GetOriginalName(partner.Name);
partner = await _signalRClient.UpdatePartner(partner);
Assert.IsNotNull(partner);
Assert.AreEqual(partnerId, partner.Id);
}
#endregion Partner
#region CargoPartner
[TestMethod]
public async Task GetCargoPartnersTest()
{
var partners = await _signalRClient.GetCargoPartners();
Assert.IsNotNull(partners);
Assert.IsNotEmpty(partners);
}
//[TestMethod]
//[DataRow(1)]
public async Task<CargoPartner> GetCargoPartnerByIdTest(int cargoPartnerId)
{
var cargoPartner = await _signalRClient.GetCargoPartnerById(cargoPartnerId);
Assert.IsNotNull(cargoPartner);
Assert.AreEqual(cargoPartnerId, cargoPartner.Id);
return cargoPartner;
}
[TestMethod]
[DataRow(1)]
public async Task UpdateCargoPartnerTest(int cargoPartnerId)
{
var cargoPartner = await GetCargoPartnerByIdTest(cargoPartnerId);
var newName = GetFixtureName(cargoPartner.Name);
cargoPartner.Name = newName;
cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner);
Assert.IsNotNull(cargoPartner);
Assert.AreEqual(newName, cargoPartner.Name);
cargoPartner.Name = GetOriginalName(cargoPartner.Name);
cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner);
Assert.IsNotNull(cargoPartner);
Assert.AreEqual(cargoPartnerId, cargoPartner.Id);
}
#endregion CargoPartner
#region CargoTruck
[TestMethod]
public async Task GetCargoTrucksTest()
{
var cargoTrucks = await _signalRClient.GetCargoTrucks();
Assert.IsNotNull(cargoTrucks);
Assert.IsNotEmpty(cargoTrucks);
}
//[TestMethod]
//[DataRow(1)]
public async Task<CargoTruck> GetCargoTruckByIdTest(int cargoTruckId)
{
var cargoTruck = await _signalRClient.GetCargoTruckById(cargoTruckId);
Assert.IsNotNull(cargoTruck);
Assert.AreEqual(cargoTruckId, cargoTruck.Id);
return cargoTruck;
}
[TestMethod]
[DataRow(1)]
public async Task GetCargoTrucksByCargoPartnerIdTest(int cargoPartnerId)
{
var cargoTrucks = await _signalRClient.GetCargoTrucksByCargoPartnerId(cargoPartnerId);
Assert.IsNotNull(cargoTrucks);
Assert.IsNotEmpty(cargoTrucks);
}
[TestMethod]
[DataRow(1)]
public async Task UpdateCargoTruckTest(int cargoTruckId)
{
var cargoTruck = await GetCargoTruckByIdTest(cargoTruckId);
var newLicencePlate = GetFixtureName(cargoTruck.LicencePlate);
cargoTruck.LicencePlate = newLicencePlate;
cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck);
Assert.IsNotNull(cargoTruck);
Assert.AreEqual(newLicencePlate, cargoTruck.LicencePlate);
cargoTruck.LicencePlate = GetOriginalName(cargoTruck.LicencePlate);
cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck);
Assert.IsNotNull(cargoTruck);
Assert.AreEqual(cargoTruckId, cargoTruck.Id);
}
#endregion CargoTruck
#region PartnerDepot
[TestMethod]
public async Task GetPartnerDepotsTest()
{
var partnerDepots = await _signalRClient.GetPartnerDepots();
Assert.IsNotNull(partnerDepots);
Assert.IsNotEmpty(partnerDepots);
}
//[TestMethod]
//[DataRow(1)]
public async Task<PartnerDepot> GetPartnerDepotByIdTest(int partnerDepotId)
{
var partnerDepot = await _signalRClient.GetPartnerDepotById(partnerDepotId);
Assert.IsNotNull(partnerDepot);
Assert.AreEqual(partnerDepotId, partnerDepot.Id);
return partnerDepot;
}
[TestMethod]
[DataRow(1)]
public async Task GetPartnerDepotsByPartnerIdTest(int partnerId)
{
var partnerDepots = await _signalRClient.GetPartnerDepotsByPartnerId(partnerId);
Assert.IsNotNull(partnerDepots);
Assert.IsNotEmpty(partnerDepots);
}
[TestMethod]
[DataRow(1)]
public async Task UpdatePartnerDepotTest(int partnerDepotId)
{
var partnerDepot = await GetPartnerDepotByIdTest(partnerDepotId);
var newName = GetFixtureName(partnerDepot.Name);
partnerDepot.Name = newName;
partnerDepot = await _signalRClient.UpdatePartnerDepot(partnerDepot);
Assert.IsNotNull(partnerDepot);
Assert.AreEqual(newName, partnerDepot.Name);
partnerDepot.Name = GetOriginalName(partnerDepot.Name);
partnerDepot = await _signalRClient.UpdatePartnerDepot(partnerDepot);
Assert.IsNotNull(partnerDepot);
Assert.AreEqual(partnerDepotId, partnerDepot.Id);
}
#endregion PartnerDepot
}
}

View File

@ -0,0 +1,38 @@
using System.Reflection;
using FruitBank.Common.SignalRs;
namespace FruitBankHybrid.Shared.Tests
{
[TestClass]
public sealed class SignalRTagsTests
{
/// <summary>
/// A <see cref="SignalRTags"/> értékei a drót-protokoll azonosítói: két azonos értékű
/// konstans (jellemzően copy-paste után) némán másik endpointra irányítaná a hívást.
/// A kézi számkiosztás szándékos (stabil szerződés), ez a teszt csak alá feszít egy hálót:
/// kibukik a duplikátumon, és megnevezi a vétkes konstansokat.
/// </summary>
/// <remarks>
/// Csak a <see cref="SignalRTags"/>-ben DEKLARÁLT konstansokat vizsgálja (<c>DeclaredOnly</c>) —
/// ide ír a fejlesztő kézzel. A framework-ős <c>AcSignalRTags</c>-szal való átfedés
/// ellenőrzéséhez <c>BindingFlags.FlattenHierarchy</c> kellene.
/// </remarks>
[TestMethod]
public void SignalRTags_HasNoDuplicateValues()
{
var tags = typeof(SignalRTags)
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(f => f.IsLiteral && f.FieldType == typeof(int))
.Select(f => (f.Name, Value: (int)f.GetRawConstantValue()!))
.ToList();
var duplicates = tags
.GroupBy(t => t.Value)
.Where(g => g.Count() > 1)
.Select(g => $" {g.Key} = {string.Join(", ", g.Select(t => t.Name))}")
.ToList();
Assert.AreEqual(0, duplicates.Count, $"Több SignalRTag ugyanazt az értéket kapta:{Environment.NewLine}{string.Join(Environment.NewLine, duplicates)}");
}
}
}

View File

@ -88,6 +88,7 @@ public class FullProcessModel
public List<OrderDto> Orders { get; set; } public List<OrderDto> Orders { get; set; }
public List<PreOrder> PreOrders { get; set; } public List<PreOrder> PreOrders { get; set; }
public List<StockTaking> StockTakings { get; set; } public List<StockTaking> StockTakings { get; set; }
public List<EkaerHistory> EkaerHistories { get; set; }
} }
[TestClass] [TestClass]

View File

@ -1,4 +1,4 @@
@using AyCode.Blazor.Components.Components.Grids @using AyCode.Blazor.Components.Components.Grids
@using AyCode.Utils.Extensions @using AyCode.Utils.Extensions
@using FruitBank.Common.Dtos @using FruitBank.Common.Dtos
@using FruitBank.Common.Entities @using FruitBank.Common.Entities
@ -26,24 +26,65 @@
<Columns> <Columns>
<MgGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true"/> <MgGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true"/>
<DxGridDataColumn FieldName="PartnerId" Caption="Partner" Visible="@(!ParentDataItemIsPartner)" ReadOnly="@ParentDataItemIsPartner"> <DxGridDataColumn FieldName="PartnerId" Caption="Partner" Visible="@(!ParentDataItemIsPartner)" ReadOnly="@ParentDataItemIsPartner">
<EditSettings> <CellDisplayTemplate>
<DxComboBoxSettings Data="Partners" @{
ValueFieldName="Id" var sdPd = (ShippingDocument)context.DataItem;
TextFieldName="Name" <text>@(PartnersDictById.GetValueOrDefault(sdPd.PartnerId)?.Name)</text>
DropDownBodyCssClass="dd-body-class" }
ListRenderMode="ListRenderMode.Entire" </CellDisplayTemplate>
SearchMode="ListSearchMode.AutoSearch" <CellEditTemplate>
SearchFilterCondition="ListSearchFilterCondition.Contains" @{
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"> var sdPe = (ShippingDocument)context.EditModel;
<Columns> }
<DxListEditorColumn FieldName="@nameof(Partner.Id)" /> <DxComboBox TData="Partner" TValue="int"
<DxListEditorColumn FieldName="@nameof(Partner.Name)" /> Data="@Partners"
<DxListEditorColumn FieldName="@nameof(Partner.TaxId)" /> Value="@sdPe.PartnerId"
</Columns> ValueChanged="@((int v) => OnPartnerChanged(sdPe, v))"
</DxComboBoxSettings> ValueExpression="@(() => sdPe.PartnerId)"
</EditSettings> ValueFieldName="@nameof(Partner.Id)"
TextFieldName="@nameof(Partner.Name)"
DropDownBodyCssClass="dd-body-class"
ListRenderMode="ListRenderMode.Entire"
SearchMode="ListSearchMode.AutoSearch"
SearchFilterCondition="ListSearchFilterCondition.Contains"
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto">
<Columns>
<DxListEditorColumn FieldName="@nameof(Partner.Id)" />
<DxListEditorColumn FieldName="@nameof(Partner.Name)" />
<DxListEditorColumn FieldName="@nameof(Partner.TaxId)" />
</Columns>
</DxComboBox>
</CellEditTemplate>
</DxGridDataColumn> </DxGridDataColumn>
<DxGridDataColumn FieldName="Shipping.ShippingDate" Caption="Beérkezés" ReadOnly="true" /> <DxGridDataColumn FieldName="PartnerDepotId" Caption="Telephely">
<CellDisplayTemplate>
@{
var sdD = (ShippingDocument)context.DataItem;
var depotD = PartnersDictById.GetValueOrDefault(sdD.PartnerId)?.PartnerDepots?.FirstOrDefault(d => d.Id == sdD.PartnerDepotId);
<div style="text-align:left">@depotD?.Name</div>
}
</CellDisplayTemplate>
<CellEditTemplate>
@{
var sdE = (ShippingDocument)context.EditModel;
var depotsE = PartnersDictById.GetValueOrDefault(sdE.PartnerId)?.PartnerDepots ?? new List<PartnerDepot>();
}
<DxComboBox TData="PartnerDepot" TValue="int?"
Data="@depotsE"
@bind-Value="sdE.PartnerDepotId"
ValueFieldName="@nameof(PartnerDepot.Id)"
TextFieldName="@nameof(PartnerDepot.Name)"
NullText="(telephely…)"
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto">
<Columns>
<DxListEditorColumn FieldName="@nameof(PartnerDepot.Name)" Caption="Név" />
<DxListEditorColumn FieldName="@nameof(PartnerDepot.FullAddress)" Caption="Cím" />
</Columns>
</DxComboBox>
</CellEditTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Shipping.ShippingDate" Caption="Beérkezés" ReadOnly="true" />
<DxGridDataColumn FieldName="ShippingId" Caption="Shipping" Visible="@(!ParentDataItemIsShipping)" ReadOnly="@ParentDataItemIsShipping"> <DxGridDataColumn FieldName="ShippingId" Caption="Shipping" Visible="@(!ParentDataItemIsShipping)" ReadOnly="@ParentDataItemIsShipping">
<EditSettings> <EditSettings>
<DxComboBoxSettings Data="Shippings" <DxComboBoxSettings Data="Shippings"
@ -185,7 +226,10 @@
private LoggerClient<GridShippingDocument> _logger = null!; private LoggerClient<GridShippingDocument> _logger = null!;
private int _activeTabIndex; private int _activeTabIndex;
protected override async Task OnInitializedAsync() // Partner-lookup ID szerint a cella-templatekhez (a ReloadDataFromDb tölti) — renderenkénti lineáris keresés helyett.
private Dictionary<int, Partner> PartnersDictById = new();
protected override async Task OnInitializedAsync()
{ {
_logger = new LoggerClient<GridShippingDocument>(LogWriters.ToArray()); _logger = new LoggerClient<GridShippingDocument>(LogWriters.ToArray());
@ -223,9 +267,13 @@
{ {
if (Shippings == null && ParentDataItem is Shipping shippingParent) Shippings = [shippingParent]; if (Shippings == null && ParentDataItem is Shipping shippingParent) Shippings = [shippingParent];
if (Partners == null && ParentDataItem is Partner partnerParent) Partners = [partnerParent]; if (Partners == null && ParentDataItem is Partner partnerParent) Partners = [partnerParent];
return;
} }
// Partner-lookup ID szerint a cella-templatekhez (O(1) a renderenkénti FirstOrDefault helyett), a Partners véglegesülése után.
PartnersDictById = Partners?.ToDictionary(p => p.Id) ?? new();
if (!IsMasterGrid) return;
if (Grid == null) return; if (Grid == null) return;
using (await ObjectLock.GetSemaphore<ShippingDocument>().UseWaitAsync()) using (await ObjectLock.GetSemaphore<ShippingDocument>().UseWaitAsync())
@ -269,6 +317,16 @@
EditItemsEnabled = true; EditItemsEnabled = true;
} }
// Cascade: a Partner-combo ValueChanged-je. Beírjuk az új partnert; ha a partnernek PONTOSAN EGY telephelye van,
// arra auto-állunk, különben nullázzuk (a régi nem az új partneré); majd újrarenderelünk → a Telephely-combo frissül.
void OnPartnerChanged(ShippingDocument sd, int partnerId)
{
sd.PartnerId = partnerId;
var depots = PartnersDictById.GetValueOrDefault(partnerId)?.PartnerDepots;
sd.PartnerDepotId = depots?.Count == 1 ? depots[0].Id : null;
StateHasChanged();
}
protected async Task OnActiveTabChanged(int activeTabIndex) protected async Task OnActiveTabChanged(int activeTabIndex)
{ {
_activeTabIndex = activeTabIndex; _activeTabIndex = activeTabIndex;

View File

@ -38,8 +38,8 @@
<DxGridDataColumn FieldName="City" /> <DxGridDataColumn FieldName="City" />
<DxGridDataColumn FieldName="Street" /> <DxGridDataColumn FieldName="Street" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" /> <DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" /> <DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn> <DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns> </Columns>
<DetailRowTemplate> <DetailRowTemplate>

View File

@ -32,8 +32,8 @@
<DxGridDataColumn FieldName="@nameof(CargoTruck.CountryCode)" /> <DxGridDataColumn FieldName="@nameof(CargoTruck.CountryCode)" />
<DxGridDataColumn FieldName="@nameof(CargoTruck.IsTrailer)" /> <DxGridDataColumn FieldName="@nameof(CargoTruck.IsTrailer)" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" /> <DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" /> <DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn> <DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns> </Columns>
<ToolbarTemplate> <ToolbarTemplate>

View File

@ -0,0 +1,119 @@
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Core.Helpers
@using AyCode.Core.Interfaces
@using AyCode.Core.Loggers
@using AyCode.Services.Nav.Ekaer.Models
@using FruitBank.Common.Entities
@using FruitBankHybrid.Shared.Services.Loggers
@using FruitBankHybrid.Shared.Services.SignalRs
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
@if (TradeCard != null)
{
@* Fejléc: csak a user-nek fontos, a mapper által ténylegesen töltött mezők. *@
<div style="display:grid; grid-template-columns:auto 1fr auto 1fr; gap:2px 12px; padding:8px 12px; font-size:0.85rem;">
<b>Irány:</b><span>@TradeCard.TradeType</span>
<b>Fuvarozó:</b><span>@TradeCard.CarrierText</span>
<b>Eladó:</b><span>@FormatParty(TradeCard.SellerName, TradeCard.SellerVatNumber, TradeCard.SellerAddress)</span>
<b>Címzett:</b><span>@FormatParty(TradeCard.DestinationName, TradeCard.DestinationVatNumber, TradeCard.DestinationAddress)</span>
<b>Felrakodás:</b><span>@FormatLocation(TradeCard.LoadLocation)</span>
<b>Lerakodás:</b><span>@FormatLocation(TradeCard.UnloadLocation)</span>
<b>Vontató:</b><span>@TradeCard.Vehicle?.PlateNumber</span>
<b>Pótkocsi:</b><span>@TradeCard.Vehicle2?.PlateNumber</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(ErrorText))
{
@* A generálás/küldés üzenetei SORONKÉNT, a saját súlyosságukkal színezve: error → piros, warning → arany/sárga. *@
<div style="padding:0 12px 8px 12px; font-size:0.85rem;">
@foreach (var line in ErrorMessageLines)
{
<div style="color:@(line.IsError ? "#dc3545" : "#caa000");">@line.Text</div>
}
</div>
}
<MgGridWithInfoPanel ShowInfoPanel="false">
<GridContent>
<GridEkaerDetailBase @ref="Grid"
DataSource="TradeCardItems"
ParentDataItem="ParentDataItem"
SignalRClient="FruitBankSignalRClient"
AutoSaveLayoutName="GridEkaerDetail"
Logger="_logger"
CssClass="@GridCss"
ValidationEnabled="false">
<Columns>
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.ItemExternalId)" Caption="Tétel az." ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.TradeReason)" Caption="Jogcím" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.ProductVtsz)" Caption="VTSZ" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.ProductName)" Caption="Megnevezés" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.Weight)" Caption="Bruttó tömeg (kg)" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(TradeCardItemType.Value)" Caption="Érték (HUF)" ReadOnly="true" />
</Columns>
</GridEkaerDetailBase>
</GridContent>
</MgGridWithInfoPanel>
@code {
[Parameter] public TradeCardType? TradeCard { get; set; }
[Parameter] public IId<int>? ParentDataItem { get; set; }
public GridEkaerDetailBase Grid { get; set; }
public bool IsMasterGrid => ParentDataItem == null;
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
private string? ErrorText => (ParentDataItem as EkaerHistory)?.ErrorText;
// Soronkénti üzenet + súlyosság az [Error]/[Warning] prefixből (a service így fűzi). Prefix nélküli (pl. config-hiba) → error.
private IEnumerable<(bool IsError, string Text)> ErrorMessageLines
{
get
{
if (string.IsNullOrWhiteSpace(ErrorText)) yield break;
foreach (var raw in ErrorText.Split('\n'))
{
var line = raw.Trim();
if (line.Length == 0) continue;
if (line.StartsWith("[Warning]")) yield return (false, line["[Warning]".Length..].TrimStart());
else if (line.StartsWith("[Error]")) yield return (true, line["[Error]".Length..].TrimStart());
else yield return (true, line);
}
}
}
private AcObservableCollection<TradeCardItemType> TradeCardItems = [];
private LoggerClient<GridEkaerDetail> _logger;
protected override void OnInitialized()
{
_logger = new LoggerClient<GridEkaerDetail>(LogWriters.ToArray());
}
protected override void OnParametersSet()
{
TradeCardItems = TradeCard?.Items is { } items ? new AcObservableCollection<TradeCardItemType>(items) : [];
}
private static string FormatParty(string? name, string? vatNumber, string? address)
{
var party = string.IsNullOrWhiteSpace(vatNumber) ? name : $"{name} ({vatNumber})";
return string.IsNullOrWhiteSpace(address) ? party ?? string.Empty : $"{party} — {address}";
}
private static string FormatLocation(LocationType? location)
{
if (location == null) return string.Empty;
return string.Join(" ", new[] { location.ZipCode, location.City, location.Street }.Where(p => !string.IsNullOrWhiteSpace(p)));
}
}

View File

@ -0,0 +1,19 @@
using AyCode.Services.Nav.Ekaer.Models;
using DevExpress.Blazor;
using FruitBankHybrid.Shared.Pages;
using Microsoft.AspNetCore.Components;
namespace FruitBankHybrid.Shared.Components.Grids.Ekaers;
/// <summary>
/// Readonly tétel-grid az EKÁER detail row-hoz: a generált tradeCard tételeit (<see cref="TradeCardItemType"/>)
/// mutatja, az <c>EkaerHistory.XmlDoc</c>-ból lokálisan deszerializálva. SZÁNDÉKOSAN nincs Get/Add/Update/Remove
/// tag — tag nélkül a grid readonly, a DataSource pedig lokális (nem SignalR-ből töltött).
/// A <see cref="TradeCardItemType"/> az IId&lt;int&gt;-et explicit partial-lal teljesíti (Id = ItemExternalId-ból számítva).
/// </summary>
public class GridEkaerDetailBase : FruitBankGridBase<TradeCardItemType>, IGrid
{
public GridEkaerDetailBase() : base()
{
}
}

View File

@ -0,0 +1,313 @@
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Core.Helpers
@using AyCode.Core.Interfaces
@using AyCode.Core.Loggers
@using AyCode.Services.Nav
@using AyCode.Services.Nav.Ekaer.Models
@using AyCode.Utils.Extensions
@using FruitBank.Common.Dtos
@using FruitBank.Common.Entities
@using FruitBankHybrid.Shared.Databases
@using FruitBankHybrid.Shared.Extensions
@using FruitBankHybrid.Shared.Services.Loggers
@using FruitBankHybrid.Shared.Services.SignalRs
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
@inject IJSRuntime JS
@inject IDialogService DialogService
<MgGridWithInfoPanel ShowInfoPanel="@IsMasterGrid">
<GridContent>
<GridEkaerHistoryBase @ref="Grid"
DataSource="EkaerHistories"
ParentDataItem="ParentDataItem"
Filter="Filter"
AutoSaveLayoutName="GridEkaerHistory"
SignalRClient="FruitBankSignalRClient"
Logger="_logger"
CssClass="@GridCss"
ValidationEnabled="false"
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
<DxGridDataColumn Caption="ForeignKey(s)" AllowSort="false" AllowGroup="false">
@* A lefedett forrás-rekordok (ShippingDocument/Order Id-k) a betöltött Mappings-ből összefűzve —
egyből látszik, ha több dokumentum van egy EKÁER-deklaráció alá vonva. *@
<CellDisplayTemplate>
@{
var row = (EkaerHistory)context.DataItem;
}
@if (row.Mappings is { Count: > 0 } maps)
{
var noun = row.IsOutgoing ? "rendelés" : "szállítólevél";
<span title="@(maps.Count > 1 ? $"{maps.Count} {noun} egy EKÁER alá vonva" : $"1 {noun}")">@string.Join(", ", maps.Select(m => $"#{m.ForeignKey}"))</span>
}
else
{
<span>—</span>
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ShippingDate)" Caption="Dátum" DisplayFormat="yyyy.MM.dd" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(EkaerHistory.Partner)" Caption="Partner" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(EkaerHistory.IsOutgoing)" Caption="Kimenő?" ReadOnly="true" />
@* A kézi NAV-beadás fázisában a Status / EKÁER szám / SentDate kézzel szerkeszthető. *@
<DxGridDataColumn FieldName="@nameof(EkaerHistory.Status)" />
<DxGridDataColumn FieldName="@nameof(EkaerHistory.EkaerNumber)" Caption="EKÁER szám" />
<DxGridDataColumn FieldName="@nameof(EkaerHistory.SentDate)" DisplayFormat="yyyy.MM.dd HH:mm" />
@* Audit: a value-számításhoz alkalmazott árfolyam (HUF feladónál 1). ReadOnly — generáláskor töltődik. *@
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ConversionRate)" Caption="Árfolyam" ReadOnly="true" DisplayFormat="0.00##" />
@* Üzenet-oszlop: warningot ÉS errort is mutat. Ikon a súlyosság szerint: ⛔ blokkoló hiba, ⚠️ warning (pótlandó). *@
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ErrorText)" Caption="Üzenet" ReadOnly="true">
<CellDisplayTemplate>
@if (!string.IsNullOrWhiteSpace(context.DisplayText))
{
var isError = ((EkaerHistory)context.DataItem).Status.IsError();
<span title="@context.DisplayText">
<span aria-hidden="true" style="font-size:1.2em; margin-right:3px;">@(isError ? "⛔" : "⚠️")</span>@context.DisplayText
</span>
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
<DxGridDataColumn Caption="Műveletek" Width="200" AllowSort="false" AllowGroup="false">
<CellDisplayTemplate>
@{
var row = (EkaerHistory)context.DataItem;
<DxButton Text="Generate"
SizeMode="SizeMode.Small"
RenderStyle="ButtonRenderStyle.Primary"
Enabled="@(CanGenerate(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = row.ErrorText ?? "EKÁER XML generálása és validálása" })"
Click="async () => await OnGenerateClick(row)" />
<DxButton Text="Copy"
SizeMode="SizeMode.Small"
RenderStyle="ButtonRenderStyle.Secondary"
Enabled="@(CanCopy(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = CanCopy(row) ? "A generált XML vágólapra másolása (kézi NAV-beadáshoz)" : "Hibás bejelentés nem másolható — javítsd az adatokat és generáld újra." })"
Click="async () => await OnCopyClick(row)" />
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns>
<DetailRowTemplate>
@{
var ekaerHistory = (EkaerHistory)context.DataItem;
TradeCardType? tradeCard = null;
if (!string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc))
{
try
{
tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc);
}
catch (Exception ex)
{
_logger.Error($"EkaerHistory XmlDoc deserialize error; id: {ekaerHistory.Id}", ex);
}
}
<GridEkaerDetail TradeCard="@tradeCard" ParentDataItem="@ekaerHistory"></GridEkaerDetail>
}
</DetailRowTemplate>
<ToolbarTemplate>
@if (IsMasterGrid)
{
@* EKÁER-rekord a toolbar "sorok létrehozása" gombbal készül (new/delete tiltva — nincs automata
érzékelés, a létrehozás explicit és idempotens). Az Edit a kézi NAV-beadás fázisában
engedélyezett: Status / EKÁER szám / SentDate kézi rögzítéséhez. *@
<MgGridToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" EnableNew="false" EnableEdit="true" EnableDelete="false">
<ToolbarItemsExtended>
@* A dátumválasztó editor csak Template-ben mehet toolbarba; a gomb natív DxToolbarItem,
hogy a toolbar stílusát/igazítását kapja (lásd: "Ai process..." minta a GridShippingDocument-ben). *@
@* dxbl-toolbar-edit: a DevExpress téma saját opt-in osztálya editor-t hordozó toolbar-itemre —
ettől kapja a beépített középre igazítást és a toolbarba simuló editor-stílust. *@
<DxToolbarItem BeginGroup="true" CssClass="dxbl-toolbar-edit ekaer-create-from-date-item">
<Template Context="toolbarItemContext">
<DxDateEdit @bind-Date="CreateFromDate"
Format="yyyy.MM.dd"
MinDate="@DateTime.Today.AddDays(-7)"
MaxDate="@DateTime.Today"
CssClass="ekaer-create-from-date" />
</Template>
</DxToolbarItem>
<DxToolbarItem Text="EKÁER sorok létrehozása"
Enabled="@(!_creatingMissing)"
Tooltip="A választott dátumtól kezdődő, rekord nélküli szállítólevelekre/rendelésekre Pending sort hoz létre (idempotens)."
Click="OnCreateMissingClick">
</DxToolbarItem>
</ToolbarItemsExtended>
</MgGridToolbarTemplate>
}
</ToolbarTemplate>
</GridEkaerHistoryBase>
</GridContent>
</MgGridWithInfoPanel>
@code {
[Inject] public required DatabaseClient Database { get; set; }
[Parameter] public AcObservableCollection<EkaerHistory>? EkaerHistories { get; set; }
const string ExportFileName = "ExportResult";
string GridSearchText = "";
bool EditItemsEnabled { get; set; }
int FocusedRowVisibleIndex { get; set; }
public GridEkaerHistoryBase Grid { get; set; }
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
[Parameter] public IId<int>? ParentDataItem { get; set; }
/// <summary>Szerver-oldali szűrő (a tabok adják): ToSubmit / Sent / NeedsCompletion. Default All = minden.</summary>
[Parameter] public EkaerHistoryFilter Filter { get; set; } = EkaerHistoryFilter.All;
/// <summary>A szülő (oldal) értesítése, ha egy művelet státuszt válthatott (generálás / kézi mentés / jövőbeni beküldés) —
/// hogy a fül-számlálók azonnal frissüljenek, mert a sor átkerülhet másik tabra.</summary>
[Parameter] public EventCallback OnDataChanged { get; set; }
/// <summary>A szülő (oldal) értesítése a hosszú művelet (sorok létrehozása) elejéről (true) / végéről (false) —
/// hogy az oldal Loading panelje megjelenjen/eltűnjön.</summary>
[Parameter] public EventCallback<bool> OnBusyChanged { get; set; }
public bool IsMasterGrid => ParentDataItem == null;
private LoggerClient<GridEkaerHistory> _logger;
protected override async Task OnInitializedAsync()
{
_logger = new LoggerClient<GridEkaerHistory>(LogWriters.ToArray());
await ReloadDataFromDb(false);
}
private async Task ReloadDataFromDb(bool forceReload = false)
{
if (!IsMasterGrid) return;
if (Grid == null) return;
using (await ObjectLock.GetSemaphore<EkaerHistory>().UseWaitAsync())
if (forceReload) await Grid.ReloadDataSourceAsync();
if (forceReload) Grid.Reload();
}
async Task Grid_FocusedRowChanged(GridFocusedRowChangedEventArgs args)
{
if (Grid == null) return;
if (Grid.IsEditing() && !Grid.IsEditingNewRow())
{
await Grid.SaveChangesAsync();
await NotifyDataChanged(); // kézi szerkesztés (pl. Status → SentWithMissingData) → fül-szám frissítése
}
FocusedRowVisibleIndex = args.VisibleIndex;
EditItemsEnabled = true;
}
private readonly HashSet<int> _generatingIds = [];
// Elküldött (NAV-nál lévő) bejelentést nem generálunk újra némán; minden más állapot újragenerálható.
private bool CanGenerate(EkaerHistory ekaerHistory)
=> !ekaerHistory.Status.IsSent() && !_generatingIds.Contains(ekaerHistory.Id);
// Csak BLOKKOLÓ hiba (ValidationError) esetén NEM másolható; a warning (pl. hiányzó rendszám) küldhető → másolható.
private static bool CanCopy(EkaerHistory ekaerHistory)
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc);
// A szülő-oldal (fül-számlálók) értesítése egy státuszt érintő művelet után. Üres delegáltnál no-op.
private Task NotifyDataChanged() => OnDataChanged.HasDelegate ? OnDataChanged.InvokeAsync() : Task.CompletedTask;
private async Task OnGenerateClick(EkaerHistory ekaerHistory)
{
if (!_generatingIds.Add(ekaerHistory.Id)) return;
try
{
var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.Id);
if (updated == null)
{
_logger.Error($"GenerateEkaerXmlDocument null választ adott; EkaerHistory #{ekaerHistory.Id}");
return;
}
// A sor frissítése helyben — a grid ugyanazt a példányt mutatja.
ekaerHistory.Status = updated.Status;
ekaerHistory.XmlDoc = updated.XmlDoc;
ekaerHistory.ConversionRate = updated.ConversionRate;
ekaerHistory.ErrorText = updated.ErrorText;
ekaerHistory.EkaerNumber = updated.EkaerNumber;
ekaerHistory.SentDate = updated.SentDate;
ekaerHistory.Modified = updated.Modified;
}
catch (Exception ex)
{
_logger.Error($"GenerateEkaerXmlDocument hiba; EkaerHistory #{ekaerHistory.Id}", ex);
}
finally
{
_generatingIds.Remove(ekaerHistory.Id);
Grid?.Reload();
await NotifyDataChanged();
}
}
DateTime CreateFromDate { get; set; } = DateTime.Today.AddDays(-3);
private bool _creatingMissing;
private async Task OnCreateMissingClick()
{
if (_creatingMissing) return;
_creatingMissing = true;
await OnBusyChanged.InvokeAsync(true); // Loading panel BE — a művelet lassú lehet (kimenő ág)
EkaerCreateResult? result = null;
try
{
result = await FruitBankSignalRClient.CreateMissingEkaerHistories(CreateFromDate);
var createdCount = result?.CreatedCount ?? 0;
_logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; messages: {result?.Messages.Count ?? 0}; fromDate: {CreateFromDate:yyyy.MM.dd}");
foreach (var message in result?.Messages ?? [])
_logger.Warning($"CreateMissingEkaerHistories; skipped: {message}");
if (createdCount > 0)
{
await ReloadDataFromDb(true);
await NotifyDataChanged(); // a fül-számlálók frissítése (új sorok)
}
}
catch (Exception ex)
{
_logger.Error($"CreateMissingEkaerHistories hiba; fromDate: {CreateFromDate:yyyy.MM.dd}", ex);
}
finally
{
_creatingMissing = false;
await OnBusyChanged.InvokeAsync(false); // Loading panel KI
}
// Az adathiány/formátumhiba miatt KIHAGYOTT jelöltek üzenetei popupban — ezeket javítani kell, mielőtt bekerülhetnek.
if (result?.Messages is { Count: > 0 } skipped)
await DialogService.ShowMessageBoxAsync("EKÁER — kihagyott tételek", string.Join(Environment.NewLine, skipped), MessageBoxRenderStyle.Warning);
}
private async Task OnCopyClick(EkaerHistory ekaerHistory)
{
if (!CanCopy(ekaerHistory)) return;
try
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", ekaerHistory.XmlDoc);
}
catch (Exception ex)
{
_logger.Error($"XmlDoc vágólapra másolása sikertelen; EkaerHistory.Id: {ekaerHistory.Id}", ex);
}
}
}

View File

@ -0,0 +1,75 @@
using AyCode.Utils.Extensions;
using DevExpress.Blazor;
using FruitBank.Common.Entities;
using FruitBank.Common.SignalRs;
using FruitBankHybrid.Shared.Pages;
using Microsoft.AspNetCore.Components;
namespace FruitBankHybrid.Shared.Components.Grids.Ekaers;
public class GridEkaerHistoryBase : FruitBankGridBase<EkaerHistory>, IGrid
{
private bool _isFirstInitializeParameterCore;
private bool _isFirstInitializeParameters;
/// <summary>Szerver-oldali szűrő a master-grid lekérdezéséhez (a tabok adják át). All = minden.</summary>
[Parameter] public EkaerHistoryFilter Filter { get; set; } = EkaerHistoryFilter.All;
public GridEkaerHistoryBase() : base()
{
//GetAllMessageTag = SignalRTags.GetEkaerHistories;
AddMessageTag = SignalRTags.AddEkaerHistory;
UpdateMessageTag = SignalRTags.UpdateEkaerHistory;
//RemoveMessageTag = SignalRTags.;
}
protected override async Task OnInitializedAsync()
{
if (GetAllMessageTag > 0) return;
if (IsMasterGrid)
{
GetAllMessageTag = SignalRTags.GetEkaerHistories;
ContextIds = [Filter]; // szerver-oldali szűrő a tabokhoz (All = minden) — a GetEkaerHistories(filter) param-ja
}
else
{
if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id];
// Detail-mód: a forrás-rekord (Shipping/Order/ShippingDocument) EKÁER-deklarációi a mappingen át.
// Megj.: az EkaerHistory már NEM hordoz per-szülő mezőt (a kapcsolat az EkaerHistoryMapping junctionben él),
// ezért a kliens-oldali KeyFieldNameToParentId itt nem alkalmazható — a szerver-oldali szűrés
// (GetEkaerHistoriesByForeignKey → mapping) adja a szülőhöz tartozó sorokat. (Jelenleg nincs detail-használat.)
GetAllMessageTag = SignalRTags.GetEkaerHistoriesByForeignKey;
}
await base.OnInitializedAsync();
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (!_isFirstInitializeParameters)
{
_isFirstInitializeParameters = false;
}
}
protected override async Task SetParametersAsyncCore(ParameterView parameters)
{
await base.SetParametersAsyncCore(parameters);
if (!_isFirstInitializeParameterCore)
{
//ShowFilterRow = true;
//ShowGroupPanel = true;
//AllowSort = false;
//etc...
_isFirstInitializeParameterCore = false;
}
}
}

View File

@ -29,7 +29,7 @@
<DxGridDataColumn FieldName="Name" /> <DxGridDataColumn FieldName="Name" />
<DxGridDataColumn FieldName="TaxId" /> <DxGridDataColumn FieldName="TaxId" />
<DxGridDataColumn FieldName="CertificationNumber" /> <DxGridDataColumn FieldName="CertificationNumber" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.Currency)" /> <DxGridDataColumn FieldName="@nameof(Partner.Currency)" />
<DxGridDataColumn FieldName="@nameof(Partner.CountryCode)" /> <DxGridDataColumn FieldName="@nameof(Partner.CountryCode)" />
<DxGridDataColumn FieldName="PostalCode" /> <DxGridDataColumn FieldName="PostalCode" />
<DxGridDataColumn FieldName="@nameof(Partner.Country)" /> <DxGridDataColumn FieldName="@nameof(Partner.Country)" />
@ -38,14 +38,15 @@
<DxGridDataColumn FieldName="City" /> <DxGridDataColumn FieldName="City" />
<DxGridDataColumn FieldName="Street" /> <DxGridDataColumn FieldName="Street" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" /> <DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" /> <DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn> <DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns> </Columns>
<DetailRowTemplate> <DetailRowTemplate>
@if (IsMasterGrid) @if (IsMasterGrid)
{ {
var partner = ((Partner)context.DataItem); var partner = ((Partner)context.DataItem);
var partnerDepots = partner?.PartnerDepots ?? [];
var shippingDocuments = partner?.ShippingDocuments ?? []; var shippingDocuments = partner?.ShippingDocuments ?? [];
<DxTabs> <DxTabs>
@ -62,6 +63,13 @@
<GridShippingItemTemplate ShippingItems="@observableShippingItems" ParentDataItem="@partner" /> <GridShippingItemTemplate ShippingItems="@observableShippingItems" ParentDataItem="@partner" />
} }
</DxTabPage> </DxTabPage>
<DxTabPage Text="Telephelyek">
@{
var observablePartnerDepots = new AcObservableCollection<PartnerDepot>(partnerDepots);
<GridPartnerDepot PartnerDepots="observablePartnerDepots" ParentDataItem="@partner"></GridPartnerDepot>
}
</DxTabPage>
</DxTabs> </DxTabs>
} }
</DetailRowTemplate> </DetailRowTemplate>

View File

@ -0,0 +1,124 @@
@using System.Collections.ObjectModel
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Core.Helpers
@using AyCode.Core.Interfaces
@using AyCode.Core.Loggers
@using AyCode.Utils.Extensions
@using FruitBank.Common.Dtos
@using FruitBank.Common.Entities
@using FruitBankHybrid.Shared.Components.Grids.Shippings
@using FruitBankHybrid.Shared.Databases
@using FruitBankHybrid.Shared.Services.Loggers
@using FruitBankHybrid.Shared.Services.SignalRs
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
<MgGridWithInfoPanel ShowInfoPanel="@IsMasterGrid">
<GridContent>
<GridPartnerDepotBase @ref="Grid"
DataSource="PartnerDepots"
ParentDataItem="ParentDataItem"
AutoSaveLayoutName="GridPartnerDepot"
SignalRClient="FruitBankSignalRClient"
Logger="_logger"
CssClass="@GridCss"
ValidationEnabled="false"
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(PartnerDepot.PartnerId)" />
<DxGridDataColumn FieldName="Name" />
<DxGridDataColumn FieldName="@nameof(PartnerDepot.CountryCode)" />
<DxGridDataColumn FieldName="PostalCode" />
<DxGridDataColumn FieldName="City" />
<DxGridDataColumn FieldName="Street" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns>
@* <DetailRowTemplate>
@if (IsMasterGrid)
{
var partnerDepot = ((PartnerDepot)context.DataItem);
<DxTabs>
<DxTabPage Text="Szállítólevelek">
@{
var observableShippingDocuments = new AcObservableCollection<ShippingDocument>(shippingDocuments);
<GridShippingDocument ShippingDocuments="@observableShippingDocuments" ParentDataItem="@partner" Partners="@Partners"></GridShippingDocument>
}
</DxTabPage>
<DxTabPage Text="Szállítmány tételek">
@{
var observableShippingItems = new AcObservableCollection<ShippingItem>(shippingDocuments.SelectMany(sd => sd.ShippingItems ?? []));
<GridShippingItemTemplate ShippingItems="@observableShippingItems" ParentDataItem="@partner" />
}
</DxTabPage>
</DxTabs>
}
</DetailRowTemplate>
*@ <ToolbarTemplate>
@if (IsMasterGrid)
{
<MgGridToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" />
}
</ToolbarTemplate>
</GridPartnerDepotBase>
</GridContent>
</MgGridWithInfoPanel>
@code {
//[Inject] public required ObjectLock ObjectLock { get; set; }
[Inject] public required DatabaseClient Database { get; set; }
[Parameter] public AcObservableCollection<PartnerDepot>? PartnerDepots { get; set; }
const string ExportFileName = "ExportResult";
string GridSearchText = "";
bool EditItemsEnabled { get; set; }
int FocusedRowVisibleIndex { get; set; }
public GridPartnerDepotBase Grid { get; set; }
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
[Parameter] public IId<int>? ParentDataItem { get; set; }
public bool IsMasterGrid => ParentDataItem == null;
public bool ParentDataItemIsPartner => (ParentDataItem is Partner);
private int _activeTabIndex;
private LoggerClient<GridPartnerDepot> _logger;
protected override async Task OnInitializedAsync()
{
_logger = new LoggerClient<GridPartnerDepot>(LogWriters.ToArray());
await ReloadDataFromDb(false);
}
private async Task ReloadDataFromDb(bool forceReload = false)
{
if (!IsMasterGrid) return;
if (Grid == null) return;
using (await ObjectLock.GetSemaphore<PartnerDepot>().UseWaitAsync())
if (forceReload) await Grid.ReloadDataSourceAsync();
if (forceReload) Grid.Reload();
}
async Task Grid_FocusedRowChanged(GridFocusedRowChangedEventArgs args)
{
if (Grid == null) return;
if (Grid.IsEditing() && !Grid.IsEditingNewRow())
await Grid.SaveChangesAsync();
FocusedRowVisibleIndex = args.VisibleIndex;
EditItemsEnabled = true;
}
}

View File

@ -0,0 +1,85 @@
using AyCode.Core.Interfaces;
using AyCode.Utils.Extensions;
using DevExpress.Blazor;
using FruitBank.Common.Entities;
using FruitBank.Common.Interfaces;
using FruitBank.Common.SignalRs;
using FruitBankHybrid.Shared.Pages;
using Microsoft.AspNetCore.Components;
namespace FruitBankHybrid.Shared.Components.Grids.Partners;
public class GridPartnerDepotBase: FruitBankGridBase<PartnerDepot>, IGrid
{
private bool _isFirstInitializeParameterCore;
private bool _isFirstInitializeParameters;
public GridPartnerDepotBase() : base()
{
//GetAllMessageTag = SignalRTags.GetPartnerDepots;
AddMessageTag = SignalRTags.AddPartnerDepot;
UpdateMessageTag = SignalRTags.UpdatePartnerDepot;
//RemoveMessageTag = SignalRTags.;
}
protected override async Task OnInitializedAsync()
{
if (GetAllMessageTag > 0) return;
if (IsMasterGrid) GetAllMessageTag = SignalRTags.GetPartnerDepots;
else
{
if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id];
switch (ParentDataItem)
{
case IPartner:
GetAllMessageTag = SignalRTags.GetPartnerDepotsByPartnerId;
if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(PartnerDepot.PartnerId);
break;
}
}
await base.OnInitializedAsync();
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (!_isFirstInitializeParameters)
{
//if (!IsMasterGrid && (ContextIds == null || ContextIds.Length == 0))
//{
// ContextIds = [ParentDataItem!.Id];
// GetAllMessageTag = SignalRTags.GetShippingItemsByDocumentId;
//}
_isFirstInitializeParameters = false;
}
}
protected override async Task SetParametersAsyncCore(ParameterView parameters)
{
await base.SetParametersAsyncCore(parameters);
if (!_isFirstInitializeParameterCore)
{
//if (!IsMasterGrid && (ContextIds == null || ContextIds.Length == 0))
//{
// ContextIds = [ParentDataItem!.Id];
// GetAllMessageTag = SignalRTags.GetShippingItemsByDocumentId;
//}
//ShowFilterRow = true;
//ShowGroupPanel = true;
//AllowSort = false;
//etc...
_isFirstInitializeParameterCore = false;
}
}
}

View File

@ -86,8 +86,8 @@
<DxGridDataColumn FieldName="Comment" Caption="Megjegyzés" ReadOnly="false" /> <DxGridDataColumn FieldName="Comment" Caption="Megjegyzés" ReadOnly="false" />
<DxGridDataColumn FieldName="CargoCompany" Caption="Fuvarozó" ReadOnly="false" /> <DxGridDataColumn FieldName="CargoCompany" Caption="Fuvarozó" ReadOnly="false" />
<DxGridDataColumn FieldName="IsAllMeasured" Caption="Mérések kész" ReadOnly="true" /> <DxGridDataColumn FieldName="IsAllMeasured" Caption="Mérések kész" ReadOnly="true" />
<DxGridDataColumn FieldName="Created" Caption="Létrehozva" ReadOnly="true" /> <DxGridDataColumn FieldName="Created" Caption="Létrehozva" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridDataColumn FieldName="Modified" Caption="Módosítva" ReadOnly="true" /> <DxGridDataColumn FieldName="Modified" Caption="Módosítva" ReadOnly="true" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn> <DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns> </Columns>
<DetailRowTemplate> <DetailRowTemplate>

View File

@ -70,6 +70,8 @@ public class ProductDtoTable(FruitBankSignalRClient fruitBankSignalRClient) : Ac
//Clear(); //Clear();
this.Replace(items); this.Replace(items);
//this.Replace(items.Cast<ProductDtoTableItem>());
//foreach (var productDto in items) //foreach (var productDto in items)
//{ //{
// this.UpdateCollection(productDto, false); // this.UpdateCollection(productDto, false);

View File

@ -43,9 +43,6 @@
<Reference Include="AyCode.Services"> <Reference Include="AyCode.Services">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Services.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils"> <Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference> </Reference>

View File

@ -63,6 +63,12 @@
<span class="icon shippings-icon" aria-hidden="true"></span> Szállítmányok <span class="icon shippings-icon" aria-hidden="true"></span> Szállítmányok
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Ekaer" title="EKÁER">
<span class="icon ekaer-icon" aria-hidden="true"></span> EKÁER
</NavLink>
</div>
} }
} }
else else

View File

@ -59,6 +59,10 @@
--icon-mask-image: var(--icon-login-mask-image); --icon-mask-image: var(--icon-login-mask-image);
} }
::deep .ekaer-icon {
--icon-mask-image: var(--icon-ekaer-mask-image);
}
.nav-item { .nav-item {
font-size: 0.9rem; font-size: 0.9rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;

View File

@ -0,0 +1,30 @@
@page "/Ekaer"
@using FruitBank.Common.Entities
@using FruitBankHybrid.Shared.Components
@using FruitBankHybrid.Shared.Components.Grids.Ekaers
@using FruitBankHybrid.Shared.Databases
<h3>EKÁER - Adminisztrátor</h3>
<DxDialogProvider />
<div style="margin-top: 30px;">
<DxLoadingPanel @bind-Visible="LoadingPanelVisibility.Visible"
IsContentBlocked="true"
ApplyBackgroundShading="true"
IndicatorAreaVisible="false"
Text="Adatok szinkronizálása folyamatban...">
<DxTabs ActiveTabIndexChanged="(i) => OnActiveTabChanged(i)" RenderMode="TabsRenderMode.OnDemand" AllowTabReorder="false">
<DxTabPage Text="Beküldésre váró">
<GridEkaerHistory @ref="gridEkaerHistoryPending" Filter="EkaerHistoryFilter.ToSubmit" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
</DxTabPage>
<DxTabPage Text="@NeedsCompletionTabText">
<GridEkaerHistory @ref="gridEkaerHistoryNeedsCompletion" Filter="EkaerHistoryFilter.NeedsCompletion" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
</DxTabPage>
<DxTabPage Text="Elküldött">
<GridEkaerHistory @ref="gridEkaerHistorySent" Filter="EkaerHistoryFilter.Sent" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
</DxTabPage>
</DxTabs>
</DxLoadingPanel>
</div>

View File

@ -0,0 +1,70 @@
using AyCode.Core.Loggers;
using FruitBank.Common.Entities;
using FruitBank.Common.Models;
using FruitBankHybrid.Shared.Components.Grids.Ekaers;
using FruitBankHybrid.Shared.Databases;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Mango.Nop.Core.Loggers;
using Microsoft.AspNetCore.Components;
namespace FruitBankHybrid.Shared.Pages;
public partial class Ekaer : ComponentBase
{
[Inject] public required IEnumerable<IAcLogWriterClientBase> LogWriters { get; set; }
[Inject] public required NavigationManager NavManager { get; set; }
[Inject] public required LoggedInModel LoggedInModel { get; set; }
[Inject] public required FruitBankSignalRClient FruitBankSignalRClient { get; set; }
private GridEkaerHistory gridEkaerHistoryPending;
private GridEkaerHistory gridEkaerHistorySent;
private GridEkaerHistory gridEkaerHistoryNeedsCompletion;
private ILogger _logger = null!;
public int ActiveTabIndex;
/// <summary>Pótlásra váró (SentWithMissingData) rekordok száma a fül feliratához — hogy ránézésre látsszon, ha van pótolnivaló.</summary>
private int _needsCompletionCount;
public string NeedsCompletionTabText => _needsCompletionCount > 0 ? $"Pótlásra váró ({_needsCompletionCount})" : "Pótlásra váró";
protected override async Task OnInitializedAsync()
{
if (!LoggedInModel.IsRevisor) NavManager.NavigateTo("/Login");
_logger = new LoggerClient<Ekaer>(LogWriters.ToArray());
_logger.Info("OnInitializedAsync");
await base.OnInitializedAsync();
await RefreshNeedsCompletionCountAsync();
}
protected async Task OnActiveTabChanged(int activeTabIndex)
{
ActiveTabIndex = activeTabIndex;
// Fülváltáskor frissítjük a számlálót — a gridben végzett művelet (generálás/státusz-állítás) megváltoztathatta.
await RefreshNeedsCompletionCountAsync();
}
/// <summary>A grid jelzi a hosszú művelet (sorok létrehozása) elejét (true) / végét (false) → az oldal Loading panelje meg/elrejtve.</summary>
private Task OnGridBusyChanged(bool busy)
{
LoadingPanelVisibility.Visible = busy;
return InvokeAsync(StateHasChanged);
}
/// <summary>A „Pótlásra váró" rekordok számának frissítése a fül feliratához (a count endpointról, flag-szűrővel).</summary>
private async Task RefreshNeedsCompletionCountAsync()
{
try
{
_needsCompletionCount = await FruitBankSignalRClient.GetEkaerHistoryCount(EkaerHistoryFilter.NeedsCompletion);
//_needsCompletionCount = await FruitBankSignalRClient.GetEkaerHistoryCount(EkaerHistoryFilter.ToSubmit);
StateHasChanged();
}
catch (Exception ex)
{
_logger.Error($"GetEkaerHistoryCount hiba: {ex.Message}");
}
}
}

View File

@ -1,6 +1,5 @@
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions; using AyCode.Utils.Extensions;
using DevExpress.Blazor; using DevExpress.Blazor;

View File

@ -1,7 +1,6 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;
using DevExpress.Utils.About; using DevExpress.Utils.About;
using FruitBank.Common; using FruitBank.Common;
@ -66,6 +65,26 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
public Task<Partner?> UpdatePartner(Partner partner) => PostDataAsync(SignalRTags.UpdatePartner, partner); public Task<Partner?> UpdatePartner(Partner partner) => PostDataAsync(SignalRTags.UpdatePartner, partner);
#endregion Partner #endregion Partner
#region PartnerDepot
public Task<List<PartnerDepot>?> GetPartnerDepots() => GetAllAsync<List<PartnerDepot>>(SignalRTags.GetPartnerDepots);
public Task<PartnerDepot?> GetPartnerDepotById(int id) => GetByIdAsync<PartnerDepot?>(SignalRTags.GetPartnerDepotById, id);
public Task<List<PartnerDepot>?> GetPartnerDepotsByPartnerId(int partnerId) => GetAllAsync<List<PartnerDepot>>(SignalRTags.GetPartnerDepotsByPartnerId, [partnerId]);
public Task<PartnerDepot?> AddPartnerDepot(PartnerDepot partnerDepot) => PostDataAsync(SignalRTags.AddPartnerDepot, partnerDepot);
public Task<PartnerDepot?> UpdatePartnerDepot(PartnerDepot partnerDepot) => PostDataAsync(SignalRTags.UpdatePartnerDepot, partnerDepot);
#endregion PartnerDepot
#region EkaerHistory
public Task<List<EkaerHistory>?> GetEkaerHistories(EkaerHistoryFilter filter) => GetAllAsync<List<EkaerHistory>>(SignalRTags.GetEkaerHistories, [filter]);
public Task<EkaerHistory?> GetEkaerHistoryById(int id) => GetByIdAsync<EkaerHistory?>(SignalRTags.GetEkaerHistoryById, id);
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync<List<EkaerHistory>>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]);
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory);
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.GenerateEkaerXmlDocument, ekaerHistoryId);
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<EkaerCreateResult?>(SignalRTags.CreateMissingEkaerHistories, fromDate);
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync<int>(SignalRTags.GetEkaerHistoryCount, filter);
#endregion EkaerHistory
#region CargoPartner #region CargoPartner
public Task<List<CargoPartner>?> GetCargoPartners() => GetAllAsync<List<CargoPartner>>(SignalRTags.GetCargoPartners); public Task<List<CargoPartner>?> GetCargoPartners() => GetAllAsync<List<CargoPartner>>(SignalRTags.GetCargoPartners);
public Task<CargoPartner?> GetCargoPartnerById(int id) => GetByIdAsync<CargoPartner?>(SignalRTags.GetCargoPartnerById, id); public Task<CargoPartner?> GetCargoPartnerById(int id) => GetByIdAsync<CargoPartner?>(SignalRTags.GetCargoPartnerById, id);
@ -163,10 +182,10 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
=> GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByShippingId, [shippingId]); => GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByShippingId, [shippingId]);
public Task<List<ShippingDocument>?> GetShippingDocumentsByProductId(int productId) public Task<List<ShippingDocument>?> GetShippingDocumentsByProductId(int productId)
=> GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByShippingId, [productId]); => GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByProductId, [productId]);
public Task<List<ShippingDocument>?> GetShippingDocumentsByPartnerId(int partnerId) public Task<List<ShippingDocument>?> GetShippingDocumentsByPartnerId(int partnerId)
=> GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByShippingId, [partnerId]); => GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByPartnerId, [partnerId]);
public Task<ShippingDocument?> AddShippingDocument(ShippingDocument shippingDocument) public Task<ShippingDocument?> AddShippingDocument(ShippingDocument shippingDocument)
=> PostDataAsync(SignalRTags.AddShippingDocument, shippingDocument); => PostDataAsync(SignalRTags.AddShippingDocument, shippingDocument);
@ -185,9 +204,15 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
#endregion Customer #endregion Customer
#region Product #region Product
//TODO: itt ProductDtoTableItem kéne, csak az AycodeHubProtocol ProductDto-ra deserialize-ol! - J.
//public Task<List<ProductDto>?> GetProductDtoTableItems() => GetAllAsync<List<ProductDto>>(SignalRTags.GetProductDtos);
// IDEIGLENES gyors fix: a GetProductDtos tagon a szerver List<ProductDto>-t küld, a binár
// protokoll viszont a wire-típusba (ProductDto) deszerializál — így a közvetlen
// List<ProductDtoTableItem> kérés üres lett (a ProductDtoTableItem a ProductDto kliens-oldali
// leszármazottja, amit a szerver nem ismer). A működő GetProductDtos()-t hívjuk, és bináris
// round-trippel a kért leszármazottra alakítjuk (azonos mezők). VÉGLEGES megoldás: requestId →
// kért-típus a protokollban (AQN-leváltás) — lásd AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.
public async Task<List<ProductDtoTableItem>?> GetProductDtoTableItems() public async Task<List<ProductDtoTableItem>?> GetProductDtoTableItems()
//=> GetAllAsync<List<ProductDtoTableItem>>(SignalRTags.GetProductDtos);
{ {
var dtos = await GetProductDtos(); var dtos = await GetProductDtos();
if (dtos == null) return null; if (dtos == null) return null;
@ -196,14 +221,12 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
return SignalRSerializationHelper.DeserializeFromBinary<List<ProductDtoTableItem>>(bytes); return SignalRSerializationHelper.DeserializeFromBinary<List<ProductDtoTableItem>>(bytes);
} }
public Task<List<ProductDto>?> GetProductDtos() public Task<List<ProductDto>?> GetProductDtos() => GetAllAsync<List<ProductDto>>(SignalRTags.GetProductDtos);
=> GetAllAsync<List<ProductDto>>(SignalRTags.GetProductDtos);
//public Task<List<MeasuringProductDto>?> GetAllMeasuringProductDtos() //public Task<List<MeasuringProductDto>?> GetAllMeasuringProductDtos()
// => GetAllAsync<List<MeasuringProductDto>>(SignalRTags.GetAllMeasuringProductDtos); // => GetAllAsync<List<MeasuringProductDto>>(SignalRTags.GetAllMeasuringProductDtos);
public Task<ProductDto?> GetProductDtoById(int productId) public Task<ProductDto?> GetProductDtoById(int productId) => GetByIdAsync<ProductDto?>(SignalRTags.GetMeasuringProductDtoById, productId);
=> GetByIdAsync<ProductDto?>(SignalRTags.GetMeasuringProductDtoById, productId);
#endregion Product #endregion Product

View File

@ -1,7 +1,6 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;

View File

@ -42,6 +42,6 @@
"ProtocolMode": "Bytes", "ProtocolMode": "Bytes",
"BufferSize": 4096, "BufferSize": 4096,
"FlushPolicy": "Coalesced", "FlushPolicy": "Coalesced",
"FlushTimeout": "00:00:10" "FlushTimeout": "00:00:15"
} }
} }

View File

@ -64,6 +64,22 @@ h1:focus {
--icon-orders-mask-image: url("images/orders-fluent.svg"); --icon-orders-mask-image: url("images/orders-fluent.svg");
--icon-shippings-mask-image: url("images/shippings-fluent.svg"); --icon-shippings-mask-image: url("images/shippings-fluent.svg");
--icon-login-mask-image: url("images/login-fluent.svg"); --icon-login-mask-image: url("images/login-fluent.svg");
--icon-ekaer-mask-image: url("images/ekaer-fluent.svg");
}
/* EKÁER toolbar dátumválasztó (GridEkaerHistory) — kompakt szélesség, hogy illeszkedjen a toolbar-elemek közé. */
.ekaer-create-from-date {
width: 9rem;
}
/* A Template-es toolbar-item tartalma alapból felülre csúszik. Az elsődleges igazítást a DevExpress saját
dxbl-toolbar-edit osztálya adja (a téma-CSS szállítja, a CssClass-ban kapja meg az item) ez itt a
fallback, ami a markup-szerkezettől függetlenül középre igazít. */
.ekaer-create-from-date-item,
.ekaer-create-from-date-item > div {
display: flex;
align-items: center;
height: 100%;
} }
.icon { .icon {

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 5.4 10 3l6 2.4V10c0 3.5-2.5 6.4-6 7.4-3.5-1-6-3.9-6-7.4V5.4Z" stroke="black" stroke-width="1.4" stroke-linejoin="round"/>
<path d="M7.2 10l2 2 3.8-4.4" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
@ -7,7 +7,7 @@
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile> <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode> <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<RunAOTCompilation>false</RunAOTCompilation> <RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT> <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
@ -58,33 +58,18 @@
<Reference Include="AyCode.Core"> <Reference Include="AyCode.Core">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities"> <Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces"> <Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Models"> <Reference Include="AyCode.Models">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Models.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Services"> <Reference Include="AyCode.Services">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
</Reference> </Reference>
<Reference Include="AyCode.Services.Server">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils"> <Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath> <HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference> </Reference>
@ -94,7 +79,7 @@
<Reference Include="DevExpress.Data.v25.1"></Reference> <Reference Include="DevExpress.Data.v25.1"></Reference>
<Reference Include="DevExpress.Utils.v25.1"></Reference> <Reference Include="DevExpress.Utils.v25.1"></Reference>
<Reference Include="Mango.Nop.Core"> <Reference Include="Mango.Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath> <HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>

View File

@ -42,6 +42,6 @@
"ProtocolMode": "Bytes", "ProtocolMode": "Bytes",
"BufferSize": 4096, "BufferSize": 4096,
"FlushPolicy": "Coalesced", "FlushPolicy": "Coalesced",
"FlushTimeout": "00:00:10" "FlushTimeout": "00:00:15"
} }
} }

View File

@ -42,6 +42,6 @@
"ProtocolMode": "Bytes", "ProtocolMode": "Bytes",
"BufferSize": 4096, "BufferSize": 4096,
"FlushPolicy": "Coalesced", "FlushPolicy": "Coalesced",
"FlushTimeout": "00:00:10" "FlushTimeout": "00:00:15"
} }
} }

View File

@ -23,7 +23,7 @@
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<PackageVersion>$(ApplicationDisplayVersion)</PackageVersion> <PackageVersion>$(ApplicationDisplayVersion)</PackageVersion>
<RunAOTCompilation>false</RunAOTCompilation> <RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT> <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
@ -47,9 +47,9 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.Contains('-android'))"> <!--<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
<RunAOTCompilation>false</RunAOTCompilation> <RunAOTCompilation>false</RunAOTCompilation>
</PropertyGroup> </PropertyGroup>-->
<PropertyGroup Condition="$(TargetFramework.Contains('-ios')) and '$(Configuration)' == 'Release'"> <PropertyGroup Condition="$(TargetFramework.Contains('-ios')) and '$(Configuration)' == 'Release'">
<UseInterpreter>true</UseInterpreter> <UseInterpreter>true</UseInterpreter>

View File

@ -3,12 +3,12 @@
<Version>10</Version> <Version>10</Version>
<SourceModelProvider> <SourceModelProvider>
<ConnectionBasedModelProvider> <ConnectionBasedModelProvider>
<ConnectionString>Data Source=195.26.231.218;Initial Catalog=FruitBank_DEV;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True</ConnectionString> <ConnectionString>Data Source=100.73.220.50;Initial Catalog=FruitBank_DEV;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Trust Server Certificate=True</ConnectionString>
</ConnectionBasedModelProvider> </ConnectionBasedModelProvider>
</SourceModelProvider> </SourceModelProvider>
<TargetModelProvider> <TargetModelProvider>
<ConnectionBasedModelProvider> <ConnectionBasedModelProvider>
<ConnectionString>Data Source=195.26.231.218;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True</ConnectionString> <ConnectionString>Data Source=100.73.220.50;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Trust Server Certificate=True</ConnectionString>
</ConnectionBasedModelProvider> </ConnectionBasedModelProvider>
</TargetModelProvider> </TargetModelProvider>
<SchemaCompareSettingsService> <SchemaCompareSettingsService>
@ -357,7 +357,7 @@
</PropertyElementName> </PropertyElementName>
<PropertyElementName> <PropertyElementName>
<Name>TargetConnectionString</Name> <Name>TargetConnectionString</Name>
<Value>Data Source=195.26.231.218;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True;Application Name="Microsoft SQL Server Data Tools, Schema Compare"</Value> <Value>Data Source=100.73.220.50;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True;Application Name="Microsoft SQL Server Data Tools, Schema Compare"</Value>
</PropertyElementName> </PropertyElementName>
<PropertyElementName> <PropertyElementName>
<Name>TreatVerificationErrorsAsWarnings</Name> <Name>TreatVerificationErrorsAsWarnings</Name>
@ -1002,17 +1002,13 @@
<Name>dbo</Name> <Name>dbo</Name>
<Name>PK_AvalaraItemClassification</Name> <Name>PK_AvalaraItemClassification</Name>
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name>
<Name>FacebookPixelConfiguration</Name>
</SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
<Name>PK_FacebookPixelConfiguration</Name> <Name>PK_FacebookPixelConfiguration</Name>
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
<Name>GoogleAuthenticatorRecord</Name> <Name>FacebookPixelConfiguration</Name>
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
@ -1020,12 +1016,16 @@
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
<Name>TaxTransactionLog</Name> <Name>GoogleAuthenticatorRecord</Name>
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlPrimaryKeyConstraint, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
<Name>PK_TaxTransactionLog</Name> <Name>PK_TaxTransactionLog</Name>
</SelectedItem> </SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name>
<Name>TaxTransactionLog</Name>
</SelectedItem>
<SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlView, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"> <SelectedItem Type="Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlView, Microsoft.Data.Tools.Schema.Sql, Version=170.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91">
<Name>dbo</Name> <Name>dbo</Name>
<Name>vOrder</Name> <Name>vOrder</Name>