Compare commits

...

15 Commits

Author SHA1 Message Date
Loretta 2221d4a68e Add PartnerDepot to ShippingDocument, EKÁER grid updates
- Introduced PartnerDepotId and PartnerDepot navigation to ShippingDocument and IShippingDocument, loaded only for EKÁER scenarios.
- Updated ShippingToEkaerMapper to leave CarrierName empty for outgoing shipments.
- Enhanced GridShippingDocument.razor: added PartnerDepot column with edit/display templates, improved Partner selection with cascade depot logic, and optimized partner lookup.
- Fixed Ekaer tab and count logic to use NeedsCompletion filter.
- Minor code and comment cleanups.
2026-06-16 12:02:24 +02:00
Loretta 47fd53cc4e Switch archive policy to year-month buckets (LLMP-DEC-67)
Updated all docs, skills, and examples to use year-month archive files (`*_<YYYY>_<MM>.md`) for `_ISSUES.md`, `_TODO.md`, and `LLM_PROTOCOL_DECISIONS.md`. Decision Log rolling window now governed by LLMP-DEC-67 (last 15 entries active). Clarified archive file handling, updated uniqueness checks, and revised references throughout. Closed and documented resolution for `FBANKAPP-DMODEL-I-P6X4` in `DATAMODEL_ISSUES.md`.
2026-06-15 18:01:44 +02:00
Loretta c722a7b242 EKÁER: unify consignment mapping & obligation logic
- Introduced EkaerConsignment model for direction-agnostic mapping (inbound/outbound), centralizing normalization and aggregation.
- Refactored IShippingToEkaerMapper and service interfaces to use new model and expose obligation evaluation.
- Added EkaerReportability logic for robust, threshold-based reporting obligation checks with error handling.
- Updated UI and SignalR to support detailed creation results and user feedback for skipped entries.
- Enhanced tests and documentation to cover new mapping, evaluation, and legal context.
- Minor config/protocol adjustments for improved reliability.
2026-06-15 11:53:05 +02:00
Loretta 6f46aaebb0 EkaerHistoryFilter: add [Flags], tab count refresh, API
- Refactored EkaerHistoryFilter as a [Flags] enum for combinable filters and improved docs.
- Added GetEkaerHistoryCount SignalR API and client method.
- GridEkaerHistory now notifies parent on data changes via OnDataChanged callback.
- Ekaer.razor tabs now show live "NeedsCompletion" counts and refresh on tab/data changes.
- Updated tab order, filter assignments, and disabled tab reordering.
2026-06-12 18:45:58 +02:00
Loretta 1e41632599 EKÁER: add warning/error distinction, status, and filters
- Validation now distinguishes blocking errors from warnings; only errors block submission, warnings are shown but allow progress.
- Introduced EkaerSeverity, EkaerValidationMessage, and severity-aware extensions for validation results.
- Extended EkaerHistory.Status with GeneratedWithWarning and SentWithMissingData; added EkaerStatusExtensions for status categorization.
- Added EkaerHistoryFilter for server-side filtering; updated SignalR and controller interfaces accordingly.
- UI now displays error/warning messages with color/icons, supports new status tabs/filters, and restricts XML copying to submittable records.
- Refactored validation and error handling to use new severity logic; updated comments and minor code for clarity.
2026-06-12 09:00:26 +02:00
Loretta 9383041504 Add EKÁER XML support for outgoing orders, refactor config
- Introduced IEkaerSettings interface and refactored code to use it for DI/testability
- Added support for generating EKÁER XML from outgoing orders (OrderDto)
- Extended service and mapper interfaces for outgoing order handling
- Implemented MapOrder and value calculation for order items
- Replaced UnloadLocation with Site in EkaerCompanyInfo for general site usage
- Updated tests, SignalR/controller interfaces, and UI to support new features and signatures
- Improved documentation and code clarity throughout
2026-06-12 07:54:38 +02:00
Loretta f23aebff2d EKÁER config refactor: centralize settings, add audit rate
Centralized EKÁER configuration in new EkaerSettings class (company info, EUR-HUF rate, thresholds). Refactored service and mapper to use EkaerSettings. Added EkaerValueCalculator for HUF value logic. Extended EkaerHistory with ConversionRate for audit. Added IsEkaer to Partner. Updated mapping, VTSZ normalization, UI grid, CSS, and tests. Added diagnostic commands to settings.local.json.
2026-06-11 17:40:33 +02:00
Loretta ea29ad18bd EKÁER grid: status enum fix, batch create, UI/UX, DB guard
- Fixed EkaerHistory status persistence (int+enum wrapper for linq2db)
- Added batch EKÁER record creation (toolbar date picker, SignalR, UI)
- Enhanced GridEkaerHistory: status, error, XML copy, manual edit
- Improved column display format handling in MgGridInfoPanel
- Updated SignalR binary protocol streaming strategy doc
- Added DB guard PowerShell hook: blocks non-_DEV DB shell access
- New styles for EKÁER toolbar date picker
2026-06-11 08:52:10 +02:00
Loretta 76cb8adbe6 Add EKÁER XML generation, validation, and tests
- Added GenerateEkaerXmlDocument to service and interface for EKÁER tradeCard XML generation and validation from ShippingDocument.
- Extended IShippingToEkaerMapper to support document-level mapping.
- Updated controller and SignalR client/interfaces with new methods and tags for EKÁER XML and EkaerHistory creation.
- Formatted Created/Modified columns in Blazor grids.
- Added tests for EkaerHistory creation, XML generation, and idempotency.
- Improved null-safety and argument validation in mapping logic.
2026-06-10 18:09:33 +02:00
Loretta 82f0f00196 Add EKÁER history grid, detail view, and entity support
- Introduced `EkaerHistory` entity with status enum and new properties for EKÁER declaration lifecycle tracking.
- Added CRUD methods for `EkaerHistory` in controller interface and SignalR client.
- Implemented `GridEkaerHistory` and `GridEkaerDetail` components for displaying EKÁER records and trade card details.
- Enhanced `TradeCardItemType` with `IId<int>` for grid compatibility.
- Improved AcSignalRDataSource to skip server refresh for tagless/local data sources.
- Updated settings to include .NET 9 runtime install commands.
2026-06-10 16:24:54 +02:00
Loretta 735eabf008 Add EKÁER admin Blazor page with tabs and logging
Introduced a new Blazor page for EKÁER administration, including a tabbed UI for pending and sent records (grids to be implemented). The code-behind handles user role checks, navigation, and logging initialization. Tab switching logic is stubbed for future development.
2026-06-09 15:28:23 +02:00
Loretta b8f52e0c84 Add PartnerDepot grid, EKÁER menu, and test refactor
- Added PartnerDepots navigation to Partner entity/interface
- Introduced GridPartnerDepot component and detail tab in partner grid
- Added EKÁER menu entry with icon and supporting styles
- Moved partner/cargo tests to FruitBankPartnerTests; added PartnerDepot tests
- Improved test assertions for clarity
- Enabled AOT compilation by default in project file
- Updated SignalR binary protocol doc with send-path analysis
2026-06-09 14:51:23 +02:00
Loretta 9a0d5698f0 Refactor: remove Server DLLs, unify SignalR references
- Removed all AyCode.Services.Server and Server DLL references from projects and code; now use only AyCode.Services and AyCode.Services.SignalRs.
- Updated all documentation links to reference AyCode.Services/docs/SIGNALR_DATASOURCE/README.md.
- Enabled AOT compilation in FruitBankHybrid.Web.Client.
- Added InternalsVisibleTo for AyCode.Services.Server.Tests to support internal testing.
- Introduced AcBinaryProtocolThreadPoolTests for thread-pool regression and contract testing.
- Added P2 feature TODO for per-message protocol mode selection in AcBinaryHubProtocol.
- Cleaned up using directives and removed obsolete Server-specific references.
- Minor formatting and whitespace improvements.
2026-06-08 16:53:27 +02:00
Loretta 3b22b07d02 PartnerDepot API, SignalRTags fixes, and test for duplicates
- Added PartnerDepot CRUD API to IFruitBankDataControllerCommon and FruitBankSignalRClient.
- Fixed Partner entity association to ShippingDocument.PartnerId.
- Changed EkaerHistory to sealed, added ForeignKey, IsOutgoing, Created.
- Exposed GetEkaerHistoriesByForeignKey in SignalRTags.
- Fixed SignalRTags usage in ShippingDocument queries.
- Implemented ProductDtoTableItem deserialization workaround.
- Added unit test to detect duplicate SignalRTags constant values.
2026-06-07 07:20:07 +02:00
Loretta 0083a7bd6e Add PartnerDepot and EkaerHistory entities, update tags
- Introduced PartnerDepot and EkaerHistory entity classes with serialization and DB mapping.
- Made ICompanyInfoBase.TaxId nullable.
- Added new DB table name constants for PartnerDepot and EkaerHistory.
- Added SignalR tag constants for PartnerDepot and EkaerHistory operations.
- Updated SQL schema compare connection strings and item order.
2026-06-05 06:18:33 +02:00
57 changed files with 2531 additions and 298 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.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Services.Ekaer;
@ -8,20 +10,22 @@ namespace FruitBank.Common.Server.Services.Ekaer;
/// <inheritdoc cref="IFruitBankEkaerService"/>
/// <remarks>
/// 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>INavCredentials</c>) és a saját cégadatokat (<see cref="EkaerCompanyInfo"/>) a DI szolgáltatja.
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A saját cégadatot
/// (<see cref="EkaerSettings.Company"/>) és a NAV-fiók hitelesítő adatait a DI szolgáltatja.
/// </remarks>
public sealed class FruitBankEkaerService : IFruitBankEkaerService
{
private readonly IShippingToEkaerMapper _mapper;
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));
_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)
@ -29,7 +33,65 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
ArgumentNullException.ThrowIfNull(shipping);
// 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);
}
public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(document);
ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false };
var currency = document.Partner?.Currency;
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapDocument(document, _settings.Company), currency);
}
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(order);
ekaerHistory ??= new EkaerHistory { ForeignKey = order.Id, 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);
/// <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.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Services.Ekaer;
namespace FruitBank.Common.Server.Services.Ekaer;
@ -16,4 +18,31 @@ public interface IFruitBankEkaerService
/// vagy a NAV-válasz — lásd <see cref="EkaerSubmitResult"/>.
/// </summary>
Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default);
/// <summary>
/// Egy szállítólevélből 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="document">A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek).</param>
/// <param name="ekaerHistory">A dokumentum meglévő rekordja (újrageneráláskor); <c>null</c> → új rekord készül.</param>
EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, 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);
}

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; } = [];
}

View File

@ -0,0 +1,147 @@
using System.Text.Json.Serialization;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.TimeStampInfo;
using LinqToDB;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration — one row per tradeCard: an incoming ShippingDocument (delivery note) or a completed outgoing Order. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV).")]
[Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)]
public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo
{
[ToonDescription(Purpose = "Id of the source entity the declaration belongs to: ShippingDocument.Id when IsOutgoing is false (one declaration per delivery note, matching NAV's one-TCN-per-tradeCard granularity), Order id when IsOutgoing is true.")]
public int ForeignKey { get; set; }
[ToonDescription(Purpose = "Direction of the goods movement: false = incoming shipment (Shipping), true = outgoing delivery (Order).")]
public bool IsOutgoing { get; set; }
[ToonDescription(Purpose = "Lifecycle state of the declaration, stored as int (see EkaerStatus): 0 Pending (auto-created, not yet generated), 1 Generated (tradeCard XML produced and valid), 2 ValidationError (generation produced errors, see ErrorText), 3 Sent (accepted by NAV, EkaerNumber filled), 4 SendError (NAV call failed, see ErrorText).")]
public int StatusId { get; set; }
/// <summary>A <see cref="StatusId"/> enum-nézete. A tárolt érték az int oszlop (StatusId) — a linq2db réteg
/// az enum-property-t nem perzisztálta (az oszlop kimaradt az insertből, a DB default írt 0-t), ezért a nop-minta:
/// int oszlop + nem-mappelt enum wrapper.</summary>
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore]
public EkaerStatus Status
{
get => (EkaerStatus)StatusId;
set => StatusId = (int)value;
}
[ToonDescription(Purpose = "The generated NAV EKÁER tradeCard request XML exactly as it was (or will be) submitted — audit copy and source of the read-only detail view. Null until the first Generate.")]
public string? XmlDoc { get; set; }
[Column(DataType = DataType.DecFloat)]
[ToonDescription(Purpose = "The conversion rate actually applied when computing this declaration's item values to HUF: 1 for domestic (HUF) suppliers (no conversion), the FX rate (e.g. EUR→HUF) for foreign suppliers. Null before the first Generate. Currency-agnostic by design — works for any source currency. Audit trail: the invoice amount times this rate yields the HUF value in the tradeCard; the NAV schema has no rate field, so this column preserves how the value was derived.")]
public double? ConversionRate { get; set; }
[ToonDescription(Purpose = "The EKÁER number (TCN) assigned by NAV after a successful submission. Null until the declaration is accepted.")]
public string? EkaerNumber { get; set; }
[ToonDescription(Purpose = "When the declaration was successfully submitted to NAV. Null until sent.")]
public DateTime? SentDate { get; set; }
[ToonDescription(Purpose = "Validation or NAV submission error details for the ValidationError / SendError states. Null when the last operation succeeded.")]
public string? ErrorText { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
/// <summary>Az EKÁER-bejelentés életciklus + kimenet állapota. Append-only: új érték a végére, meglévő értéke nem
/// változhat (DB-ben int-ként tárolt). Az állapot egyetlen mező; a kategorizálás a <see cref="EkaerStatusExtensions"/>
/// segédekkel megy (külön oszlop nélkül) — a folyamat lineáris, ezért a single-enum állapotgép a tiszta illeszkedés.</summary>
public enum EkaerStatus
{
/// <summary>Automatikusan létrejött, még nem volt Generate.</summary>
Pending = 0,
/// <summary>A tradeCard XML legenerálva és valid — küldhető.</summary>
Generated = 1,
/// <summary>A Generate BLOKKOLÓ (error) hibákkal zárult (ErrorText) — a forrásadat javítandó, nem küldhető.</summary>
ValidationError = 2,
/// <summary>NAV által befogadva (EkaerNumber + SentDate töltve), hiánytalanul.</summary>
Sent = 3,
/// <summary>A NAV-hívás hibával zárult (ErrorText) — újraküldhető.</summary>
SendError = 4,
/// <summary>Legenerálva és KÜLDHETŐ, de warninggal (pl. hiányzó rendszám, ami a felrakodás megkezdéséig pótolható).</summary>
GeneratedWithWarning = 5,
/// <summary>NAV-hoz beküldve, de PÓTLÁSRA VÁR (pl. a rendszám a felrakodás megkezdéséig megadandó).</summary>
SentWithMissingData = 6,
}
/// <summary>Állapot-kategóriák — a tárolás egy mező (Status), a csoportosítás ezekkel (külön oszlop nélkül).</summary>
public static class EkaerStatusExtensions
{
/// <summary>Blokkoló hibás állapot (a bejelentés nem küldhető / a NAV-hívás bukott).</summary>
public static bool IsError(this EkaerStatus status) => status is EkaerStatus.ValidationError or EkaerStatus.SendError;
/// <summary>Beküldhető: legenerálva, blokkoló hiba nélkül (warninggal is) — innen mehet a NAV-hoz.</summary>
public static bool IsSubmittable(this EkaerStatus status) => status is EkaerStatus.Generated or EkaerStatus.GeneratedWithWarning;
/// <summary>A NAV-nál van (akár hiányosan).</summary>
public static bool IsSent(this EkaerStatus status) => status is EkaerStatus.Sent or EkaerStatus.SentWithMissingData;
/// <summary>Elküldve, de pótlásra vár (hiányos adat — pl. rendszám).</summary>
public static bool NeedsCompletion(this EkaerStatus status) => status is EkaerStatus.SentWithMissingData;
}
/// <summary>EkaerHistory-lekérdezés szűrő a tabokhoz, <see cref="FlagsAttribute"/>. A kategóriák diszjunkt
/// StatusId-partíciók, ezért kombinálhatók (pl. <c>ToSubmit | NeedsCompletion</c> = minden, ami még nincs lezárva).
/// <see cref="All"/> = 0 (nincs bit = nincs szűrő → minden), egyúttal a biztonságos default; szándékosan NEM nullable.
/// Vigyázat: <c>HasFlag(All)</c> mindig true (0 bit), ezért a szerver előbb a <c>== All</c> ágat nézi.</summary>
[Flags]
public enum EkaerHistoryFilter
{
/// <summary>Minden rekord (nincs szűrés). 0 = nincs bit → minden; egyúttal a biztonságos default.</summary>
All = 0,
/// <summary>Beküldésre váró: minden, ami még NINCS a NAV-nál (Pending/Generated/GeneratedWithWarning/ValidationError/SendError).</summary>
ToSubmit = 1,
/// <summary>Elküldött és hiánytalan (<see cref="EkaerStatus.Sent"/>).</summary>
Sent = 2,
/// <summary>Elküldve, de pótlásra vár (<see cref="EkaerStatus.SentWithMissingData"/>).</summary>
NeedsCompletion = 4,
}
//public sealed class EkaerHistoryShipping : EkaerHistoryBase
//{
// public int ShippingId
// {
// get => ForeignItemId;
// set => ForeignItemId = value;
// }
// [Association(ThisKey = nameof(ShippingId), OtherKey = nameof(Shipping.Id), CanBeNull = true)]
// public Shipping? Shipping { get; set; }
//}
//public sealed class EkaerHistoryOrder : EkaerHistoryBase
//{
// public int ShippingId
// {
// get => ForeignItemId;
// set => ForeignItemId = value;
// }
// [Association(ThisKey = nameof(ShippingId), OtherKey = nameof(Shipping.Id), CanBeNull = true)]
// public Shipping? Shipping { get; set; }
//}
//public abstract class EkaerHistoryBase : MgEntityBase, ITimeStampInfo
//{
// [NotColumn]
// protected int ForeignItemId;
// [NotColumn]
// [ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")]
// public int ForeignKey => ForeignItemId;
// public DateTime Created { get; set; }
// public DateTime Modified { get; set; }
//}

View File

@ -2,7 +2,6 @@
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
@ -12,6 +11,12 @@ namespace FruitBank.Common.Entities;
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
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; }
}

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? ShippingId { get; set; }
public int? PartnerDepotId { get; set; }
public string DocumentIdNumber { 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)]
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)]
public List<ShippingItem>? ShippingItems { get; set; }

View File

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

View File

@ -20,6 +20,26 @@ public interface IFruitBankDataControllerCommon
public Task<Partner?> UpdatePartner(Partner 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 foreignKey, bool isOutgoing);
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
#endregion EkaerHistory
#region CargoPartner
public Task<List<CargoPartner>?> GetCargoPartners();
public Task<CargoPartner?> GetCargoPartnerById(int id);

View File

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

View File

@ -9,6 +9,7 @@ public interface IShippingDocument: IEntityInt, ITimeStampInfo//, IMeasured
{
public int PartnerId { get; set; }
public int? ShippingId { get; set; }
public int? PartnerDepotId { get; set; }
public string DocumentIdNumber { 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.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
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="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);
/// <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.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using Nop.Core.Domain.Customers;
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType;
@ -10,15 +12,28 @@ namespace FruitBank.Common.Services.Ekaer;
/// <inheritdoc cref="IShippingToEkaerMapper"/>
/// <remarks>
/// Tiszta (állapotmentes) leképező. A feladót és a saját céget egységesen <see cref="ICompanyInfoBase"/>-ként kezeli.
/// A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be a <c>Models.TradeCardType</c> osztály
/// és a <c>Models.Common.TradeCardType</c> enum névütközése miatt.
/// Tiszta (állapotmentes) leképező. A bejövő (<c>ShippingDocument</c>-csoport) és a kimenő (<c>OrderDto</c>) forrás
/// előbb egy irány-független <see cref="EkaerConsignment"/>-re képződik (<see cref="ToConsignment(IReadOnlyCollection{ShippingDocument}, EkaerCompanyInfo)"/>
/// / <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>
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";
/// <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)
{
ArgumentNullException.ThrowIfNull(shipping);
@ -30,86 +45,202 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
// Granularitás: egy ShippingDocument → egy tradeCard (lásd EKAER_TODO #5).
foreach (var document in shipping.ShippingDocuments ?? [])
{
document.Shipping ??= shipping; // a vontató/fuvarozó a Shippingről jön
index++;
operations.Add(new TradeCardOperationType
{
Index = index,
Operation = operation,
TradeCard = BuildTradeCard(shipping, document, company),
TradeCard = BuildTradeCard(ToConsignment([document], company)),
});
}
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
{
TradeType = ResolveTradeType(seller),
TradeType = ResolveTradeType(consignment),
ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat
// Feladó / eladó = a beszállító
SellerName = seller?.Name,
SellerVatNumber = NormalizeVatNumber(seller?.TaxId),
SellerCountry = NormalizeCountryCode(seller?.CountryCode, 2),
SellerAddress = Truncate(seller?.FullAddress, 200),
SellerName = consignment.Seller.Name,
SellerVatNumber = NormalizeVatNumber(consignment.Seller.VatNumber),
SellerCountry = NormalizeCountryCode(consignment.Seller.CountryCode, 2),
SellerAddress = Truncate(consignment.Seller.FullAddress, 200),
// Címzett = a bejelentő saját cége (bejövő relációban)
DestinationName = company.Name,
DestinationVatNumber = NormalizeVatNumber(company.TaxId),
DestinationCountry = NormalizeCountryCode(company.CountryCode, 2),
DestinationAddress = Truncate(company.FullAddress, 200),
DestinationName = consignment.Buyer.Name,
DestinationVatNumber = NormalizeVatNumber(consignment.Buyer.VatNumber),
DestinationCountry = NormalizeCountryCode(consignment.Buyer.CountryCode, 2),
DestinationAddress = Truncate(consignment.Buyer.FullAddress, 200),
// Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név.
CarrierText = shipping.CargoPartner?.Name,
CarrierText = consignment.CarrierName,
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye.
UnloadLocation = company.UnloadLocation,
LoadLocation = BuildLoadLocation(seller),
// A helyek kész NAV LocationType-ok (a saját telephely érintetlenül) — átengedve.
LoadLocation = consignment.LoadLocation,
UnloadLocation = consignment.UnloadLocation,
};
// Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2).
if (shipping.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck);
if (shipping.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer);
if (consignment.Vehicle != null) tradeCard.Vehicle = consignment.Vehicle;
if (consignment.Trailer != null) tradeCard.Vehicle2 = consignment.Trailer;
foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item));
foreach (var line in consignment.Lines) tradeCard.Items.Add(BuildItem(line));
return tradeCard;
}
/// <summary>
/// Belföldi feladó (HU) → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import). A NAV EKÁER magyar,
/// így a belföld mindig HU; az export (<c>E</c>) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7).
/// </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()
/// <summary>Belföld (a feladó és a címzett országkódja megegyezik) → <c>D</c>; eltérő országok → kimenőnél
/// export (<c>E</c>), bejövőnél import (<c>I</c>).</summary>
private static TradeType ResolveTradeType(EkaerConsignment c)
{
ItemExternalId = item.Id.ToString(),
// 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.
TradeReason = TradeReasonType.A,
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)
// Value (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3, #10.
if (string.Equals(c.Seller.CountryCode?.Trim(), c.Buyer.CountryCode?.Trim(), StringComparison.OrdinalIgnoreCase))
return TradeType.D;
return c.IsOutgoing ? TradeType.E : TradeType.I;
}
private static TradeCardItemType BuildItem(EkaerLine line) => new()
{
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),
Country = NormalizeCountryCode(truck.CountryCode, 3),
Name = partner?.Name,
VatNumber = partner?.TaxId,
CountryCode = partner?.CountryCode,
FullAddress = partner?.FullAddress,
};
/// <summary>
/// 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.)
/// </summary>
private static LocationType? BuildLoadLocation(ICompanyInfoBase? seller)
private static EkaerEndpoint CompanyEndpoint(EkaerCompanyInfo company) => new()
{
Name = company.Name,
VatNumber = company.TaxId,
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;
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>
private static string? NormalizeVatNumber(string? value)
{
@ -155,6 +328,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
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? Truncate(string? value, int maxLen)

View File

@ -112,6 +112,21 @@ public class SignalRTags : AcSignalRTags
public const int GetStockTakingItemsByStockTakingId = 178;
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 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ó
**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ő**:
- **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]
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 ------------------------------------------------------------
@ -67,18 +69,42 @@ public sealed class ShippingToEkaerMapperTests
PostalCode = "1102",
City = "Budapest",
Street = "Raktar utca 5",
UnloadLocation = new LocationType
Site = new LocationType
{
Name = "FruitBank Raktár",
VatNumber = "98765432-2-41",
Phone = "+36301234567", // NAV-formátum: + / 06 prefix kötelező
Email = "raktar@fruitbank.hu",
Country = "HU",
ZipCode = "1102",
ZipCode = "1239",
City = "Budapest",
Street = "Raktar utca",
StreetNumber = "5",
Street = "Nagykőrösi út",
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 ----------------------------------------------
[TestMethod]
@ -166,7 +192,7 @@ public sealed class ShippingToEkaerMapperTests
Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName);
Assert.AreEqual("HU", tradeCard.DestinationCountry);
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]
@ -212,4 +238,55 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_NullCompany_Throws()
=> 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));
}
#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
[TestMethod]
public async Task GetShippingsTest()
@ -299,7 +156,7 @@ namespace FruitBankHybrid.Shared.Tests
else
{
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}");
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.IsTrue(shippingItem.NetWeightOnDocument > 0, "NetWeightOnDocument == 0");
Assert.IsTrue(shippingItem.GrossWeightOnDocument > 0, "GrossWeightOnDocument == 0");
Assert.IsGreaterThan(0, shippingItem.QuantityOnDocument, "QuantityOnDocument == 0");
Assert.IsGreaterThan(0, shippingItem.NetWeightOnDocument, "NetWeightOnDocument == 0");
Assert.IsGreaterThan(0, shippingItem.GrossWeightOnDocument, "GrossWeightOnDocument == 0");
return shippingItem;
}

View File

@ -0,0 +1,123 @@
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.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey);
Assert.IsFalse(ekaerHistory.IsOutgoing);
}
// 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)
{
var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id, false);
// 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.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey);
Assert.IsFalse(ekaerHistory.IsOutgoing);
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;
var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false);
var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false);
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

@ -1,4 +1,4 @@
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Utils.Extensions
@using FruitBank.Common.Dtos
@using FruitBank.Common.Entities
@ -26,10 +26,23 @@
<Columns>
<MgGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true"/>
<DxGridDataColumn FieldName="PartnerId" Caption="Partner" Visible="@(!ParentDataItemIsPartner)" ReadOnly="@ParentDataItemIsPartner">
<EditSettings>
<DxComboBoxSettings Data="Partners"
ValueFieldName="Id"
TextFieldName="Name"
<CellDisplayTemplate>
@{
var sdPd = (ShippingDocument)context.DataItem;
<text>@(PartnersDictById.GetValueOrDefault(sdPd.PartnerId)?.Name)</text>
}
</CellDisplayTemplate>
<CellEditTemplate>
@{
var sdPe = (ShippingDocument)context.EditModel;
}
<DxComboBox TData="Partner" TValue="int"
Data="@Partners"
Value="@sdPe.PartnerId"
ValueChanged="@((int v) => OnPartnerChanged(sdPe, v))"
ValueExpression="@(() => sdPe.PartnerId)"
ValueFieldName="@nameof(Partner.Id)"
TextFieldName="@nameof(Partner.Name)"
DropDownBodyCssClass="dd-body-class"
ListRenderMode="ListRenderMode.Entire"
SearchMode="ListSearchMode.AutoSearch"
@ -40,8 +53,36 @@
<DxListEditorColumn FieldName="@nameof(Partner.Name)" />
<DxListEditorColumn FieldName="@nameof(Partner.TaxId)" />
</Columns>
</DxComboBoxSettings>
</EditSettings>
</DxComboBox>
</CellEditTemplate>
</DxGridDataColumn>
<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">
@ -185,6 +226,9 @@
private LoggerClient<GridShippingDocument> _logger = null!;
private int _activeTabIndex;
// 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());
@ -223,9 +267,13 @@
{
if (Shippings == null && ParentDataItem is Shipping shippingParent) Shippings = [shippingParent];
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;
using (await ObjectLock.GetSemaphore<ShippingDocument>().UseWaitAsync())
@ -269,6 +317,16 @@
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)
{
_activeTabIndex = activeTabIndex;

View File

@ -38,8 +38,8 @@
<DxGridDataColumn FieldName="City" />
<DxGridDataColumn FieldName="Street" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" />
<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>

View File

@ -32,8 +32,8 @@
<DxGridDataColumn FieldName="@nameof(CargoTruck.CountryCode)" />
<DxGridDataColumn FieldName="@nameof(CargoTruck.IsTrailer)" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" />
<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>
<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,294 @@
@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 FieldName="@nameof(EkaerHistory.ForeignKey)" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(EkaerHistory.IsOutgoing)" 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.ForeignKey, ekaerHistory.IsOutgoing);
if (updated == null)
{
_logger.Error($"GenerateEkaerXmlDocument null választ adott; ForeignKey: {ekaerHistory.ForeignKey}");
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; ForeignKey: {ekaerHistory.ForeignKey}", 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,73 @@
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];
// A ForeignKey általános (Shipping/Order stb.) — bármely szülőnél a ForeignKey-re szűrünk.
GetAllMessageTag = SignalRTags.GetEkaerHistoriesByForeignKey;
if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(EkaerHistory.ForeignKey);
}
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="TaxId" />
<DxGridDataColumn FieldName="CertificationNumber" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.Currency)" />
<DxGridDataColumn FieldName="@nameof(Partner.Currency)" />
<DxGridDataColumn FieldName="@nameof(Partner.CountryCode)" />
<DxGridDataColumn FieldName="PostalCode" />
<DxGridDataColumn FieldName="@nameof(Partner.Country)" />
@ -38,14 +38,15 @@
<DxGridDataColumn FieldName="City" />
<DxGridDataColumn FieldName="Street" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" />
<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 partner = ((Partner)context.DataItem);
var partnerDepots = partner?.PartnerDepots ?? [];
var shippingDocuments = partner?.ShippingDocuments ?? [];
<DxTabs>
@ -62,6 +63,13 @@
<GridShippingItemTemplate ShippingItems="@observableShippingItems" ParentDataItem="@partner" />
}
</DxTabPage>
<DxTabPage Text="Telephelyek">
@{
var observablePartnerDepots = new AcObservableCollection<PartnerDepot>(partnerDepots);
<GridPartnerDepot PartnerDepots="observablePartnerDepots" ParentDataItem="@partner"></GridPartnerDepot>
}
</DxTabPage>
</DxTabs>
}
</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="CargoCompany" Caption="Fuvarozó" ReadOnly="false" />
<DxGridDataColumn FieldName="IsAllMeasured" Caption="Mérések kész" ReadOnly="true" />
<DxGridDataColumn FieldName="Created" Caption="Létrehozva" ReadOnly="true" />
<DxGridDataColumn FieldName="Modified" Caption="Módosítva" 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" DisplayFormat="yyyy.MM.dd hh:mm" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns>
<DetailRowTemplate>

View File

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

View File

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

View File

@ -63,6 +63,12 @@
<span class="icon shippings-icon" aria-hidden="true"></span> Szállítmányok
</NavLink>
</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

View File

@ -59,6 +59,10 @@
--icon-mask-image: var(--icon-login-mask-image);
}
::deep .ekaer-icon {
--icon-mask-image: var(--icon-ekaer-mask-image);
}
.nav-item {
font-size: 0.9rem;
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.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions;
using DevExpress.Blazor;

View File

@ -1,7 +1,6 @@
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using DevExpress.Utils.About;
using FruitBank.Common;
@ -66,6 +65,26 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
public Task<Partner?> UpdatePartner(Partner partner) => PostDataAsync(SignalRTags.UpdatePartner, 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 foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.GenerateEkaerXmlDocument, [foreignKey, isOutgoing]);
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
public Task<List<CargoPartner>?> GetCargoPartners() => GetAllAsync<List<CargoPartner>>(SignalRTags.GetCargoPartners);
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]);
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)
=> GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByShippingId, [partnerId]);
=> GetAllAsync<List<ShippingDocument>>(SignalRTags.GetShippingDocumentsByPartnerId, [partnerId]);
public Task<ShippingDocument?> AddShippingDocument(ShippingDocument shippingDocument)
=> PostDataAsync(SignalRTags.AddShippingDocument, shippingDocument);
@ -185,18 +204,29 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
#endregion Customer
#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()
{
var dtos = await GetProductDtos();
if (dtos == null) return null;
public Task<List<ProductDtoTableItem>?> GetProductDtoTableItems()
=> GetAllAsync<List<ProductDtoTableItem>>(SignalRTags.GetProductDtos);
var bytes = SignalRSerializationHelper.SerializeToBinary(dtos);
return SignalRSerializationHelper.DeserializeFromBinary<List<ProductDtoTableItem>>(bytes);
}
public Task<List<ProductDto>?> GetProductDtos()
=> GetAllAsync<List<ProductDto>>(SignalRTags.GetProductDtos);
public Task<List<ProductDto>?> GetProductDtos() => GetAllAsync<List<ProductDto>>(SignalRTags.GetProductDtos);
//public Task<List<MeasuringProductDto>?> GetAllMeasuringProductDtos()
// => GetAllAsync<List<MeasuringProductDto>>(SignalRTags.GetAllMeasuringProductDtos);
public Task<ProductDto?> GetProductDtoById(int productId)
=> GetByIdAsync<ProductDto?>(SignalRTags.GetMeasuringProductDtoById, productId);
public Task<ProductDto?> GetProductDtoById(int productId) => GetByIdAsync<ProductDto?>(SignalRTags.GetMeasuringProductDtoById, productId);
#endregion Product

View File

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

View File

@ -36,9 +36,9 @@
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"ProtocolMode": "Bytes",
"BufferSize": 4096,
"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-shippings-mask-image: url("images/shippings-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 {

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>
<TargetFramework>net10.0</TargetFramework>
@ -7,7 +7,7 @@
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<RunAOTCompilation>false</RunAOTCompilation>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
@ -51,33 +51,18 @@
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference>

View File

@ -36,9 +36,9 @@
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"ProtocolMode": "Bytes",
"BufferSize": 4096,
"FlushPolicy": "Coalesced",
"FlushTimeout": "00:00:10"
"FlushTimeout": "00:00:15"
}
}

View File

@ -36,9 +36,9 @@
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"ProtocolMode": "Bytes",
"BufferSize": 4096,
"FlushPolicy": "Coalesced",
"FlushTimeout": "00:00:10"
"FlushTimeout": "00:00:15"
}
}

View File

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

View File

@ -3,12 +3,12 @@
<Version>10</Version>
<SourceModelProvider>
<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>
</SourceModelProvider>
<TargetModelProvider>
<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>
</TargetModelProvider>
<SchemaCompareSettingsService>
@ -357,7 +357,7 @@
</PropertyElementName>
<PropertyElementName>
<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>
<Name>TreatVerificationErrorsAsWarnings</Name>
@ -1002,17 +1002,13 @@
<Name>dbo</Name>
<Name>PK_AvalaraItemClassification</Name>
</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">
<Name>dbo</Name>
<Name>PK_FacebookPixelConfiguration</Name>
</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>GoogleAuthenticatorRecord</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">
<Name>dbo</Name>
@ -1020,12 +1016,16 @@
</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>
<Name>GoogleAuthenticatorRecord</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">
<Name>dbo</Name>
<Name>PK_TaxTransactionLog</Name>
</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">
<Name>dbo</Name>
<Name>vOrder</Name>