diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 696d3504..a019b102 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -75,7 +75,13 @@ "WebFetch(domain:www.itrack.hu)", "WebFetch(domain:docplayer.hu)", "WebFetch(domain:supportcenter.devexpress.com)", - "PowerShell($c = Get-Command claude -ErrorAction SilentlyContinue; if \\($c\\) { $c.Source; & claude --help 2>&1 | Select-Object -First 90 } else { \"claude not on PATH\" })" + "PowerShell($c = Get-Command claude -ErrorAction SilentlyContinue; if \\($c\\) { $c.Source; & claude --help 2>&1 | Select-Object -First 90 } else { \"claude not on PATH\" })", + "PowerShell(dotnet test \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services.Tests\\\\AyCode.Services.Tests.csproj\" --filter \"FullyQualifiedName~EkaerTradeCardValidatorTests\" --nologo -v minimal)", + "PowerShell(dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\FruitBank.Common.csproj\" -v q --nologo 2>&1)", + "WebFetch(domain:fruitveb.hu)", + "PowerShell(Get-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services\\\\Nav\\\\docs\\\\eKAERManagementService_2.2.pdf\" | Select-Object Length, LastWriteTime, CreationTime; try { $r = Invoke-WebRequest -Uri \"https://ekaer.nav.gov.hu/faq/content/uploads/2025/05/eKAERManagementService_2.2.pdf\" -Method Head -UseBasicParsing; \"Online Content-Length: $\\($r.Headers['Content-Length']\\); Last-Modified: $\\($r.Headers['Last-Modified']\\)\" } catch { \"HEAD request failed: $\\($_.Exception.Message\\)\" })", + "PowerShell($out = dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\\\\Nop.Plugin.Misc.FruitBankPlugin.csproj\" -v q --nologo 2>&1 | Out-String; $errLines = \\($out -split \"`n\"\\) | Where-Object { $_ -match \": error \" }; \"Osszes hiba-sor: $\\($errLines.Count\\)\"; $errLines | ForEach-Object { if \\($_ -match \"\\([A-Za-z]+\\\\.cs\\)\"\\) { $matches[1] } } | Group-Object | Sort-Object Count -Descending | Format-Table Name, Count -AutoSize)", + "PowerShell($out = dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\\\\Nop.Plugin.Misc.FruitBankPlugin.csproj\" -v q --nologo 2>&1 | Out-String; $errLines = \\($out -split \"`n\"\\) | Where-Object { $_ -match \": error \" }; \"Osszes hiba-sor: $\\($errLines.Count\\)\"; $errLines | Where-Object { $_ -match \"FruitBankDataController\" } | Select-Object -First 5)" ] } } diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index 23f266a0..65c470fe 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -45,9 +45,11 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService // 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); + // Guardok a leképezés ELŐTT (nincs félrevezető XML), majd a csoport ÖSSZES dokumentuma EGY tradeCard-dá + // (összevont tömeg/érték): ToConsignment → BuildTradeCard. + return TryDepotError(ekaerHistory, documents) + ?? TryConfigError(ekaerHistory, currency) + ?? Finalize(ekaerHistory, _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company)), currency); } public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null) @@ -72,6 +74,18 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService history.Partner = partner; } + /// Depot-kapu (T-D8R4): a PartnerDepot az EKÁER-hez kötelező — a felrakodási hely a depot címe. Hiányzó + /// vagy be nem töltött telephelynél a leképezés ELŐTT ValidationError (nincs félrevezető, partner-központi-címes XML). + /// null = rendben, mehet a generálás. + private static EkaerHistory? TryDepotError(EkaerHistory ekaerHistory, IReadOnlyCollection documents) + { + if (documents.All(d => d.PartnerDepot != null)) return null; + + ekaerHistory.Status = EkaerStatus.ValidationError; + ekaerHistory.ErrorText = "A szállítólevélhez nincs telephely (PartnerDepot) rendelve — EKÁER-hez kötelező (a felrakodási hely a telephely címe); pótlás után generáld újra."; + return ekaerHistory; + } + /// 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). null = rendben, mehet a generálás. private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency) diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 23fe64e0..7e1277b4 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -117,4 +117,18 @@ public interface IFruitBankDataControllerCommon public Task UpdateGenericAttributeDto(GenericAttributeDto genericAttributeDto); + /// + /// Returns the stored grid-layout JSON for the composite layout key, or null if none saved. + /// + public Task GetGridLayout(string key, int userId); + + /// + /// Saves (upserts) the grid-layout JSON under the composite layout key for the given user. + /// + public Task SaveGridLayout(string key, string layoutJson, int userId); + + /// + /// Deletes the stored grid layout for the composite layout key. Returns false if the user does not exist. + /// + public Task DeleteGridLayout(string key, int userId); } \ No newline at end of file diff --git a/FruitBank.Common/Services/Ekaer/EkaerReportability.cs b/FruitBank.Common/Services/Ekaer/EkaerReportability.cs index 16aa7751..437130fb 100644 --- a/FruitBank.Common/Services/Ekaer/EkaerReportability.cs +++ b/FruitBank.Common/Services/Ekaer/EkaerReportability.cs @@ -31,10 +31,11 @@ public sealed class EkaerObligationResult /// is bírság), ezért: a bizonytalan adatot (érvénytelen országkód) NEM döntjük el magától → . /// /// -/// 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. +/// Sorrend: (0) TÉTELENKÉNTI előfeltétel — bruttó súly > 0, különben DataError (a 0 súly csendben alul-számolná a +/// küszöb-summát → alul-jelentés); (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 -t (bejövőnél a (Shipping, Partner) csoport tételei). /// @@ -45,6 +46,18 @@ public static class EkaerReportability ArgumentNullException.ThrowIfNull(consignment); ArgumentNullException.ThrowIfNull(settings); + var subject = Subject(consignment); + + // (0) Tételenkénti előfeltétel: bruttó súly > 0. A 0 súlyú tétel (kitöltetlen dokumentum-súly / méretlen + // rendelés) csendben alul-számolná a küszöb-aggregátumot → alul-jelentési kockázat; a NAV amúgy is tételenként + // pozitív bruttó súlyt vár. Hiányzó súly → DataError (a sor nem jön létre; pótlás után a következő futás felveszi). + var weightErrors = consignment.Lines + .Where(l => l.WeightKg <= 0) + .Select(l => $"{subject} / {(string.IsNullOrWhiteSpace(l.Name) ? $"tétel #{l.ExternalId}" : l.Name)}: a tétel bruttó súlya hiányzik (0 kg) — a kötelezettség nem dönthető el.") + .ToList(); + if (weightErrors.Count > 0) + return EkaerObligationResult.DataError(weightErrors); + // (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); @@ -54,7 +67,6 @@ public static class EkaerReportability // (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(); if (!IsValidCountry(consignment.Seller.CountryCode)) errors.Add($"{subject}: a feladó országkódja hiányzik vagy érvénytelen ('{consignment.Seller.CountryCode}')."); diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs index 2dee6d65..828cc30f 100644 --- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -74,7 +74,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper // ── Adapterek: forrás → normalizált EkaerConsignment (az EGYETLEN irányfüggő rész) ──────────── - /// Bejövő: egy vagy több ShippingDocument (a hívó (Shipping, Partner)-re csoportosít) → egy + /// Bejövő: egy vagy több ShippingDocument (a hívó (Shipping, Partner, PartnerDepot)-ra 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. public EkaerConsignment ToConsignment(IReadOnlyCollection documents, EkaerCompanyInfo company) { @@ -83,19 +83,24 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper 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 partner = first.Partner; // a csoport azonos partneré (a hívó (Shipping, Partner, PartnerDepot)-ra csoportosít) var shipping = first.Shipping; // azonos Shipping; a kapunál lehet null (a jármű ott nem kell) var rateToHuf = SafeRateToHuf(partner?.Currency); + // A depot TaxId-ja Partner-passthrough (a nested Partner-t nem töltjük külön query-vel) — ugyanaz a minta, + // mint a MapShipping `document.Shipping ??=` visszakötése. + if (first.PartnerDepot is { } depot) depot.Partner ??= partner; var lines = documents .SelectMany(d => d.ShippingItems ?? []) .Select(item => new EkaerLine { ExternalId = item.Id.ToString(), - TradeReason = TradeReasonType.A, // bejövő áru = beszerzés + TradeReason = TradeReasonType.S, // S = értékesítés ÉS beszerzés (egy kód); az A kivezetett, D-nél csak S adható Vtsz = NormalizeVtsz(item.ProductDto?.Gtin), Name = item.ProductName, - WeightKg = item.MeasuredGrossWeight, + // A szállítólevélen szereplő bruttó — a bejelentés a kísérő dokumentum adatairól megy (mint a value), + // nem a (későbbi) mérlegelésről; méretlen tétel így nem 0 kg-mal jelenik meg. + WeightKg = item.GrossWeightOnDocument, ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), }) .ToList(); @@ -107,7 +112,9 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper 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) + // A felrakodási hely a DEPOT címe (T-D8R4) — a generate-út betölti (LoadWith) és guardolja (TryDepotError). + // A kapu-útvonalon a depot nincs betöltve → partner-központi cím, ami ott rendben van: a kötelezettség-döntés címet nem használ. + LoadLocation = BuildLocation((ICompanyInfoBase?)first.PartnerDepot ?? partner), UnloadLocation = company.Site, // a saját telephely (kész LocationType — mezővesztés nélkül átengedve) Lines = lines, Vehicle = BuildVehicle(shipping?.CargoTruck), diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index c18aafc8..a441c500 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -238,6 +238,17 @@ namespace FruitBankHybrid.Shared.Services.SignalRs => PostDataAsync(SignalRTags.AuthenticateUser, loginModelRequest); #endregion Authenticate + #region GridLayout + public Task GetGridLayout(string key, int userId) + => GetByIdAsync(SignalRTags.GetGridLayoutTag, [key, userId]); + + public Task SaveGridLayout(string key, string layoutJson, int userId) + => PostAsync(SignalRTags.SaveGridLayoutTag, [key, layoutJson, userId]); + + public Task DeleteGridLayout(string key, int userId) + => PostAsync(SignalRTags.DeleteGridLayoutTag, [key, userId]); + #endregion GridLayout + #region Orders public Task?> GetAllOrderDtoTableItems() => GetAllAsync>(SignalRTags.GetAllOrderDtos);