Compare commits
No commits in common. "FruitBank_v0.0.8.0" and "main" have entirely different histories.
|
|
@ -1,25 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"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
|
|
@ -1,13 +0,0 @@
|
|||
# Topic Codes — FruitBankHybridApp (`FBANKAPP`)
|
||||
|
||||
Per-repo topic registry for the FruitBankHybridApp product, per the **per-repo extension convention** in `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`. Lists ONLY this repo's own topic codes; inherited (ACCORE / AyCode.Blazor / Mango.Nop) topics are reached through `@repo.own-dep-repos`.
|
||||
|
||||
**Foundational conventions are defined once at the framework layer — not restated here:**
|
||||
- ID format, type codes (`I`/`T`/`B`/`C`), ID rules, Status vocabulary, archival, registry-maintenance workflow → `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`
|
||||
- `<PREFIX>` (this repo = `FBANKAPP`, from the `@repo.prefix` in `.github/copilot-instructions.md`) + `<RAND>` spec → `AyCode.Core/.github/REPO_PREFIXES.md`
|
||||
|
||||
## This repo's own topic codes
|
||||
|
||||
| Code | Topic | Scope | Docs location |
|
||||
|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|
|
||||
| `DMODEL` | DATA-MODEL | FruitBank entitások adatmodell-normalizálása: nopCommerce referencia-FK-k szabad string helyett (Country/Currency), azonosító-szétválasztások (GTIN/VTSZ), és hasonló átmeneti adatmodell-megoldások. **Általános** — nem funkció-specifikus (pl. nem EKÁER). | `FruitBank.Common/docs/DATAMODEL/` |
|
||||
File diff suppressed because one or more lines are too long
17
CLAUDE.md
17
CLAUDE.md
|
|
@ -1,17 +0,0 @@
|
|||
CRITICAL: Your FIRST action in every session MUST be reading `.github/copilot-instructions.md`. Execute ALL session-start instructions found there before responding to any user query. It is the single source of truth for this repo.
|
||||
|
||||
## SEQUENTIAL EXECUTION OVERRIDE
|
||||
The AI AGENT CORE PROTOCOL in copilot-instructions.md requires STRICT SEQUENTIAL execution. This OVERRIDES your default parallelization behavior. Do NOT parallelize doc reads with code searches. The sequence is:
|
||||
1. Read copilot-instructions.md → process its rules FULLY
|
||||
2. Read ALL docs/ .md files listed in the protocol → wait for completion
|
||||
3. Output [LOADED_DOCS: ...] prefix
|
||||
4. ONLY THEN respond to the user's query or search code
|
||||
|
||||
## Tool mapping for AI AGENT CORE PROTOCOL
|
||||
The copilot-instructions.md references Copilot tool names. Map them to Claude Code tools:
|
||||
- `get_file` / `file_search` → `Read`, `Glob`, `Grep`
|
||||
- `code_search` / `get_symbols_by_name` / `find_symbol` → `Grep`, `Glob`
|
||||
- `replace_string_in_file` / `edit_file` → `Edit`
|
||||
- `create_file` → `Write`
|
||||
|
||||
Follow the protocol using YOUR tools. The rules (LOADED_DOCS prefix, hard-gate, no-re-read, context recovery, explicit consent) apply equally to Claude Code.
|
||||
|
|
@ -19,13 +19,13 @@ namespace FruitBank.Common.Server
|
|||
/// DateTime generic attribute on Product.
|
||||
/// The start of the window during which this product is visible for preordering.
|
||||
/// </summary>
|
||||
public const string PreOrderWindowStart = "PreOrderWindowStart";
|
||||
public const string PreorderWindowStart = "PreorderWindowStart";
|
||||
|
||||
/// <summary>
|
||||
/// DateTime generic attribute on Product.
|
||||
/// The end of the window during which this product is visible for preordering.
|
||||
/// </summary>
|
||||
public const string PreOrderWindowEnd = "PreOrderWindowEnd";
|
||||
public const string PreorderWindowEnd = "PreorderWindowEnd";
|
||||
|
||||
static FruitBankConst()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# Interfaces
|
||||
|
||||
Server-side marker interfaces extending the shared Common interfaces. Used for DI registration and type safety.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`IFruitBankDataControllerServer.cs`** — Extends IFruitBankDataControllerCommon. Empty server marker.
|
||||
- **`ICustomOrderSignalREndpointServer.cs`** — Extends ICustomOrderSignalREndpointCommon. Empty server marker.
|
||||
- **`IStockSignalREndpointServer.cs`** — Extends IStockSignalREndpointCommon. Empty server marker.
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# FruitBank.Common.Server
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Interfaces, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Server-side library: SignalR hubs, real-time broadcast service, logging infrastructure, and nopCommerce integration constants.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Interfaces/`](Interfaces/README.md) | Server-side endpoint marker interfaces |
|
||||
| [`Services/`](Services/README.md) | SignalR hubs, broadcast service, logging |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`FruitBankConst.cs`** — Server constants: project GUID, role system names ("Measuring", "MeasuringRevisor"), product attribute "IsMeasurable", project salt.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- nopCommerce via Mango.Nop.Core
|
||||
- AyCode.Core, AyCode.Services.Server (DLL references)
|
||||
- Microsoft.AspNetCore.SignalR
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
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;
|
||||
|
||||
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 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 IEkaerTradeCardValidator _validator;
|
||||
private readonly IEkaerSettings _settings;
|
||||
|
||||
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, IEkaerSettings settings)
|
||||
{
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
|
||||
_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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(shipping);
|
||||
|
||||
// map (FruitBank.Common) → submit: validate → send (AyCode.Services)
|
||||
var operations = _mapper.MapShipping(shipping, _settings.Company, operation);
|
||||
return _submitService.SubmitAsync(operations, cancellationToken);
|
||||
}
|
||||
|
||||
public EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection<ShippingDocument> documents, EkaerHistory? ekaerHistory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(documents);
|
||||
if (documents.Count == 0) throw new ArgumentException("documents is empty", nameof(documents));
|
||||
ekaerHistory ??= new EkaerHistory { IsOutgoing = false };
|
||||
|
||||
// A csoport azonos Partneré (a kapu így csoportosít) → az első dokumentum pénzneme a mérvadó.
|
||||
var currency = documents.First().Partner?.Currency;
|
||||
// A csoport ÖSSZES dokumentuma EGY tradeCard-dá (összevont tömeg/érték): ToConsignment → BuildTradeCard.
|
||||
var tradeCard = _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company));
|
||||
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, tradeCard, currency);
|
||||
}
|
||||
|
||||
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(order);
|
||||
ekaerHistory ??= new EkaerHistory { IsOutgoing = true };
|
||||
|
||||
// Kimenő pénznem: jelenleg minden HUF (a deviza az OrderDto-ba kerül, amint bekötik) → ConversionRate = 1.
|
||||
const string currency = "HUF";
|
||||
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapOrder(order, _settings.Company), currency);
|
||||
}
|
||||
|
||||
public EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents)
|
||||
=> EkaerReportability.Evaluate(_mapper.ToConsignment(documents, _settings.Company), _settings);
|
||||
|
||||
public EkaerObligationResult EvaluateObligation(OrderDto order)
|
||||
=> EkaerReportability.Evaluate(_mapper.ToConsignment(order, _settings.Company), _settings);
|
||||
|
||||
public void SetSummary(EkaerHistory history, DateTime? shippingDate, string? partner)
|
||||
{
|
||||
history.ShippingDate = shippingDate;
|
||||
history.Partner = partner;
|
||||
}
|
||||
|
||||
/// <summary>Config-kapu: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a leképezés ELŐTT
|
||||
/// ValidationError (nincs félrevezető XML). <c>null</c> = rendben, mehet a generálás.</summary>
|
||||
private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency)
|
||||
{
|
||||
if (EkaerValueCalculator.IsHuf(currency) || _settings.EurHufRate > 0) return null;
|
||||
|
||||
ekaerHistory.Status = EkaerStatus.ValidationError;
|
||||
ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható.";
|
||||
return ekaerHistory;
|
||||
}
|
||||
|
||||
/// <summary>Közös befejezés (bejövő/kimenő): validál, szerializál; az XML validációs hibánál IS tárolódik
|
||||
/// (a detail-nézethez), és rögzíti a ténylegesen alkalmazott árfolyamot (HUF → 1, külföldi → FX-ráta).</summary>
|
||||
private EkaerHistory Finalize(EkaerHistory ekaerHistory, TradeCardType tradeCard, string? currency)
|
||||
{
|
||||
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
|
||||
var messages = _validator.Validate(operation);
|
||||
|
||||
ekaerHistory.XmlDoc = NavXmlHelper.Serialize(tradeCard);
|
||||
ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(currency, _settings.EurHufRate);
|
||||
// Blokkoló hiba → ValidationError (nem küldhető); csak warning → GeneratedWithWarning (küldhető, de pótlandó);
|
||||
// semmi → Generated. Az üzeneteket súlyossággal prefixeljük, hogy a detail-nézet megkülönböztesse őket.
|
||||
ekaerHistory.Status = messages.HasErrors() ? EkaerStatus.ValidationError
|
||||
: messages.HasWarnings() ? EkaerStatus.GeneratedWithWarning
|
||||
: EkaerStatus.Generated;
|
||||
// Error-ok elöl, warningok hátul (a detail-nézet soronként színez); a prefix [Error]/[Warning] hordozza a szintet.
|
||||
ekaerHistory.ErrorText = messages.Count == 0 ? null
|
||||
: string.Join(Environment.NewLine, messages
|
||||
.OrderByDescending(m => m.GetSeverity())
|
||||
.Select(m => $"[{m.GetSeverity()}] {m.ErrorMessage}"));
|
||||
return ekaerHistory;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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;
|
||||
|
||||
/// <summary>
|
||||
/// A FruitBank szerver-oldali EKÁER-fogyasztója: egy <see cref="Shipping"/>-et leképez EKÁER tradeCard-okra
|
||||
/// (a mapperrel), majd beküldi (az általános submit-orchestrátorral). Ez a vékony, projekt-specifikus réteg;
|
||||
/// az általános NAV/EKÁER logika (validátor, submit, manage) az <c>AyCode.Services</c>-ben él.
|
||||
/// </summary>
|
||||
public interface IFruitBankEkaerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Leképezi és beküldi a szállítmányt. Az eredmény vagy validációs hibák (nem ment ki kérés),
|
||||
/// 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él-CSOPORTBÓL (egy EKÁER-egység dokumentumai) legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új)
|
||||
/// <see cref="EkaerHistory"/> rekordot tölti: <c>XmlDoc</c> + <c>Status</c>
|
||||
/// (<see cref="EkaerStatus.Generated"/> / <see cref="EkaerStatus.ValidationError"/>) + <c>ErrorText</c>.
|
||||
/// NEM perzisztál és NEM hív NAV-ot — a mentés (upsert) a hívó SignalR endpoint dolga.
|
||||
/// </summary>
|
||||
/// <param name="documents">A deklarációhoz tartozó szállítólevél-CSOPORT (egy (Shipping, Partner, PartnerDepot) egység) a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek) — összevont tömeggel/értékkel EGY tradeCard.</param>
|
||||
/// <param name="ekaerHistory">A csoport meglévő rekordja (újrageneráláskor); <c>null</c> → új rekord készül.</param>
|
||||
EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection<ShippingDocument> documents, EkaerHistory? ekaerHistory = null);
|
||||
|
||||
/// <summary>
|
||||
/// Egy kimenő rendelésből (<see cref="OrderDto"/>) legenerálja az EKÁER tradeCard XML-t és validálja —
|
||||
/// a (meglévő vagy új) <see cref="EkaerHistory"/> rekordot tölti (<c>IsOutgoing = true</c>). Mi vagyunk az
|
||||
/// ELADÓ, a vevő a CÍMZETT (belföldi értékesítés). NEM perzisztál és NEM hív NAV-ot — a mentés a hívó dolga.
|
||||
/// </summary>
|
||||
EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null);
|
||||
|
||||
/// <summary>
|
||||
/// Eldönti egy bejövő (Shipping, Partner) CSOPORT EKÁER-kötelezettségét: külföldi (a feladó és a címzett
|
||||
/// országkódja eltér) → mindig kötelező; belföld → az AGGREGÁLT tömeg/érték a küszöbhöz; érvénytelen országkód →
|
||||
/// <see cref="EkaerObligation.DataError"/>. A küszöb-summa a csoport ÖSSZES dokumentumának tételeire megy.
|
||||
/// </summary>
|
||||
EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents);
|
||||
|
||||
/// <summary>Eldönti egy kimenő rendelés EKÁER-kötelezettségét (ugyanaz a logika, a rendelés tételeire).</summary>
|
||||
EkaerObligationResult EvaluateObligation(OrderDto order);
|
||||
|
||||
/// <summary>Beállítja a deklaráció (<see cref="EkaerHistory"/>) összegző mezőit (DB-mentes): <c>ShippingDate</c> + <c>Partner</c>.
|
||||
/// A csoporton belül invariánsak; a forrás-kinyerés (bejövő: doc.ShippingDate + Partner.Name; kimenő: order.DateOfReceipt +
|
||||
/// Customer.Company) a hívóé. Fallback NINCS — null/üres marad, ha hiányzik.</summary>
|
||||
void SetSummary(EkaerHistory history, DateTime? shippingDate, string? partner);
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Loggers
|
||||
|
||||
Server-side logging implementations.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`ConsoleLogWriter.cs`** — Lightweight console logger extending AcConsoleLogWriter. Configurable by AppType, LogLevel, category.
|
||||
- **`LoggerToLoggerApiController.cs`** — Aggregates multiple IAcLogWriterBase implementations into a single logger for API controllers.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Services
|
||||
|
||||
Server-side SignalR hubs, real-time broadcast, and logging infrastructure.
|
||||
|
||||
## Subfolders
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Loggers/`](Loggers/README.md) | Console and API controller log writers |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR hubs and broadcast service |
|
||||
|
|
@ -13,6 +13,22 @@ using Microsoft.Extensions.Configuration;
|
|||
|
||||
namespace FruitBank.Common.Server.Services.SignalRs;
|
||||
|
||||
//public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
||||
//{
|
||||
// public DevAdminSignalRHub(IConfiguration configuration, IEnumerable<IAcLogWriterBase> logWriters)
|
||||
// : base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
||||
// {
|
||||
// Logger.Info("DevAdminSignalRHub");
|
||||
// }
|
||||
|
||||
// public Task ReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
||||
// {
|
||||
|
||||
// Clients.All.SendAsync("TestMessage", "Hello from server");
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
||||
{
|
||||
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
|
||||
|
|
@ -21,13 +37,11 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
|||
{
|
||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||
SerializerOptions = new AcBinarySerializerOptions();
|
||||
//SerializerOptions = new AcJsonSerializerOptions();
|
||||
|
||||
// Use the new lazy Registry - no reflection at construction time
|
||||
DynamicMethodRegistry.CahcheSizeCapacity = 3;
|
||||
|
||||
DynamicMethodRegistry.Register(fruitBankDataController);
|
||||
DynamicMethodRegistry.Register(customOrderSignalREndpoint);
|
||||
DynamicMethodRegistry.Register(stockSignalREndpointServer);
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(fruitBankDataController));
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(customOrderSignalREndpoint));
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(stockSignalREndpointServer));
|
||||
}
|
||||
|
||||
protected override void LogContextUserNameAndId()
|
||||
|
|
@ -35,5 +49,83 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
|||
return;
|
||||
base.LogContextUserNameAndId();
|
||||
}
|
||||
// ...existing commented code...
|
||||
//public override Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
||||
//{
|
||||
// return ProcessOnReceiveMessage(messageTag, message, requestId, async tagName =>
|
||||
// {
|
||||
// switch (messageTag)
|
||||
// {
|
||||
// case SignalRTags.GetAddress:
|
||||
// //var id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
||||
// var id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
||||
|
||||
// var address = await _adminDal.GetAddressByIdAsync(id);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.GetAddressesByContextId:
|
||||
// //id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
||||
// id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
||||
|
||||
// address = await _adminDal.GetAddressByIdAsync(id);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, new List<Address> { address! }), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.UpdateAddress:
|
||||
// address = message!.MessagePackTo<SignalPostJsonDataMessage<Address>>().PostData;
|
||||
|
||||
// await _adminDal.UpdateAddressAsync(address);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.UpdateProfile:
|
||||
// var profile = message!.MessagePackTo<SignalPostJsonDataMessage<Profile>>().PostData;
|
||||
|
||||
// await _adminDal.UpdateProfileAsync(profile);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, profile), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// //case SignalRTags.GetTransfersAsync:
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _transferDataApiController.GetTransfers()), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.GetPropertiesByOwnerIdAsync:
|
||||
// // var ownerId = message!.MessagePackTo<SignalRequestByIdMessage>().Id;
|
||||
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProvidersByOwnerId(ownerId)), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.UpdateTransferAsync:
|
||||
// // var transfer = message!.MessagePackTo<SignalPostJsonDataMessage<Transfer>>().PostData;
|
||||
|
||||
// // await _transferDataApiController.UpdateTransfer(transfer);
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, transfer), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.GetCompaniesAsync:
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProviders()), requestId);
|
||||
|
||||
// // return;
|
||||
// //case SignalRTags.UpdateCompanyAsync:
|
||||
|
||||
// // var updateCompany = message!.MessagePackTo<SignalPostJsonDataMessage<Company>>().PostData;
|
||||
|
||||
// // await _serviceProviderApiController.UpdateServiceProvider(updateCompany);
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, updateCompany), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// default:
|
||||
// Logger.Error($"Server OnReceiveMessage; messageTag not found! {tagName}");
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# SignalRs
|
||||
|
||||
SignalR hub implementations and real-time broadcast service.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DevAdminSignalRHub.cs`** — Main admin hub. Dependencies: IConfiguration, IFruitBankDataControllerServer, ICustomOrderSignalREndpointServer, IStockSignalREndpointServer. Registers all three endpoint interfaces with DynamicMethodRegistry. Uses AcBinary serialization.
|
||||
- **`AcWebSignalRHubWithSessionBase.cs`** — Generic base hub with session management (OnConnected/OnDisconnected hooks).
|
||||
- **`SignalRSendToClientService.cs`** — Broadcasts real-time notifications to all clients: SendOrderChanged, SendOrderItemChanged, SendShippingChanged, SendProductChanged, etc.
|
||||
- **`LoggerSignalRHub.cs`** — Minimal hub for logging/diagnostics.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Databases
|
||||
|
||||
Local in-memory database abstraction for offline/cached data using ConcurrentDictionary.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DatabaseLocalBase.cs`** — Abstract base with generic table management for IEntityInt entities. Thread-safe AddTable, GetRows, GetRow, AddRow, AddRows, DeleteRow.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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; } = [];
|
||||
}
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(GenericAttribute))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(GenericAttribute))]
|
||||
[ToonDescription($"Data transfer object for {nameof(GenericAttribute)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(GenericAttribute)])]
|
||||
public class GenericAttributeDto : MgGenericAttributeDto
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Utils.Extensions;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Enums;
|
||||
|
|
@ -22,10 +20,6 @@ using System.Linq.Expressions;
|
|||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(Order))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Order))]
|
||||
[ToonDescription($"Data transfer object for {nameof(Order)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Order)])]
|
||||
public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -37,7 +31,6 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => Id > 0 && OrderItemDtos.Count > 0 && OrderItemDtos.All(x => x.IsMeasured)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public bool IsMeasured
|
||||
{
|
||||
get => IsMeasuredAndValid();
|
||||
|
|
@ -45,7 +38,6 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.Any(oi => oi.IsMeasurable)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
|
||||
|
|
@ -73,23 +65,18 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrNull<DateTime>('DateOfReceipt')")]
|
||||
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('RevisorId', 0)")]
|
||||
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('MeasurementOwnerId', 0)")]
|
||||
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited)")]
|
||||
public bool IsAllOrderItemAudited => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.All(oi => oi.AverageWeightIsValid)")]
|
||||
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -114,7 +101,6 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
{ }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderStatus == OrderStatus.Complete")]
|
||||
public bool IsComplete => OrderStatus == OrderStatus.Complete;
|
||||
|
||||
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
|
|
@ -16,10 +14,6 @@ using System.Linq.Expressions;
|
|||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(OrderItem))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(OrderItem))]
|
||||
[ToonDescription("Order item with measurements, pallets, and validation", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(OrderItem)])]
|
||||
public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -37,7 +31,6 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
public OrderDto OrderDto { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => IsMeasuredAndValid()")]
|
||||
public bool IsMeasured
|
||||
{
|
||||
get => IsMeasuredAndValid();
|
||||
|
|
@ -45,7 +38,6 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => ProductDto!.IsMeasurable")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => ProductDto!.IsMeasurable;
|
||||
|
|
@ -53,7 +45,6 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemPallets.Sum(x => x.TrayQuantity)")]
|
||||
public int TrayQuantity
|
||||
{
|
||||
get => OrderItemPallets.Sum(x => x.TrayQuantity);
|
||||
|
|
@ -61,7 +52,6 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||
public double NetWeight
|
||||
{
|
||||
get
|
||||
|
|
@ -79,7 +69,6 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||
public double GrossWeight
|
||||
{
|
||||
get
|
||||
|
|
@ -97,23 +86,18 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d")]
|
||||
public double AverageWeight => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0")]
|
||||
public double AverageWeightDifference => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => !IsMeasurable || (ProductDto!.AverageWeight > 0 && ((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold)")]
|
||||
public bool AverageWeightIsValid => !IsMeasurable || (ProductDto!.AverageWeight > 0 && Math.Abs((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited)")]
|
||||
public bool IsAudited => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsAudited, IsMeasured, and OrderItemPallets status")]
|
||||
public MeasuringStatus MeasuringStatus
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,45 +1,26 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Mango.Nop.Core.Extensions;
|
||||
using Mango.Nop.Core.Interfaces.ForeignKeys;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
|
||||
//using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(Product))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Product))]
|
||||
[ToonDescription("Product data with measurements and generic attributes", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Product)])]
|
||||
public class ProductDto : MgProductDto, IProductDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
private static Expression<Func<ProductDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) =>
|
||||
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(Product);// nameof(Product);
|
||||
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == "Product";// nameof(Product);
|
||||
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttributeDto.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = false)]
|
||||
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A nopCommerce <c>Product.Gtin</c> oszlopa. <b>Átmenetileg az EKÁER VTSZ-t (vámtarifaszámot) tárolja</b> —
|
||||
/// a jövőbeli <c>ShippingToEkaerMapper</c> innen olvassa a <c>tradeCardItem.productVtsz</c> értékét.
|
||||
/// ⚠️ A GTIN ≠ VTSZ (a GTIN globális kereskedelmi cikkszám, a VTSZ vámtarifaszám). Külön mezőbe választandó —
|
||||
/// lásd <c>FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md#fbankapp-dmodel-i-p6x4</c>.
|
||||
/// </summary>
|
||||
[LinqToDB.Mapping.Column(nameof(Product.Gtin))]
|
||||
[ToonDescription(Purpose = "nopCommerce Gtin column — holds the EKÁER VTSZ (customs tariff number) used as the trade-card item productVtsz in NAV road-freight reporting.")]
|
||||
public string? Gtin { get; set; }
|
||||
|
||||
public ProductDto() :base()
|
||||
{ }
|
||||
public ProductDto(int productId) : base(productId)
|
||||
|
|
@ -48,7 +29,6 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
//{ }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Master flag: if false, the system bypasses weight validation but still creates one Measurement Record (PalletItem) with TrayQuantity.", BusinessRule = "get => GenericAttributes.GetValueOrDefault<bool>('IsMeasurable')")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
|
||||
|
|
@ -63,7 +43,6 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('Tare')")]
|
||||
public double Tare
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(ITare.Tare));
|
||||
|
|
@ -72,7 +51,6 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('NetWeight')")]
|
||||
public double NetWeight
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(IMeasuringNetWeight.NetWeight));
|
||||
|
|
@ -80,7 +58,6 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<int>('IncomingQuantity')")]
|
||||
public int IncomingQuantity
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
|
||||
|
|
@ -88,22 +65,19 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
//set
|
||||
//{
|
||||
// var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ??
|
||||
// GenericAttributes.AddNewGenericAttribute(nameof(Product), nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
||||
// GenericAttributes.AddNewGenericAttribute("Product", nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
||||
|
||||
// ga.Value = value.ToString();
|
||||
//}
|
||||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantity + IncomingQuantity")]
|
||||
public int AvailableQuantity => StockQuantity + IncomingQuantity;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeight')")]
|
||||
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeightTreshold')")]
|
||||
public double AverageWeightTreshold => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeightTreshold));
|
||||
|
||||
public bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# Dtos
|
||||
|
||||
Binary-serializable DTOs for efficient SignalR communication. All marked with `[AcBinarySerializable]`.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`OrderDto.cs`** — Order with items, measurement status, auditing, receipt date, GenericAttributes. Computed: IsMeasured, IsComplete, MeasuringStatus, RevisorId.
|
||||
- **`OrderItemDto.cs`** — Order line item with OrderItemPallet collection. Computed: NetWeight, GrossWeight, AverageWeight, AverageWeightIsValid, IsMeasured, IsAudited.
|
||||
- **`ProductDto.cs`** — Product with GenericAttribute-backed properties: IsMeasurable, Tare, AverageWeight, AverageWeightTreshold, IncomingQuantity, NetWeight.
|
||||
- **`StockQuantityHistoryDto.cs`** — Stock history with net weight adjustments and inconsistency detection.
|
||||
- **`GenericAttributeDto.cs`** — Key-value attribute wrapper. Polymorphic: KeyGroup = owner type, EntityId = owner ID.
|
||||
|
||||
## Why DTOs Exist
|
||||
|
||||
nopCommerce entities (Order, OrderItem, Product) are extended with measurement logic via these DTOs. The DTOs add computed properties and GenericAttribute access that the raw nopCommerce entities don't have.
|
||||
|
|
@ -1,31 +1,22 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Mango.Nop.Core.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Dtos
|
||||
{
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(StockQuantityHistory))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(StockQuantityHistory))]
|
||||
[ToonDescription("Stock quantity history with net weight adjustments", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(StockQuantityHistory)])]
|
||||
public class StockQuantityHistoryDto : MgStockQuantityHistoryDto<ProductDto>, IStockQuantityHistoryDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.StockQuantityHistoryId")]
|
||||
public int? StockQuantityHistoryId
|
||||
{
|
||||
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
|
||||
|
|
@ -33,7 +24,6 @@ namespace FruitBank.Common.Dtos
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeightAdjustment")]
|
||||
public double? NetWeightAdjustment
|
||||
{
|
||||
get => StockQuantityHistoryExt?.NetWeightAdjustment;
|
||||
|
|
@ -41,7 +31,6 @@ namespace FruitBank.Common.Dtos
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeight")]
|
||||
public double? NetWeight
|
||||
{
|
||||
get => StockQuantityHistoryExt?.NetWeight;
|
||||
|
|
@ -49,7 +38,6 @@ namespace FruitBank.Common.Dtos
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => StockQuantityHistoryExt?.IsInconsistent ?? false")]
|
||||
public bool IsInconsistent
|
||||
{
|
||||
get => StockQuantityHistoryExt?.IsInconsistent ?? false;
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Transport / haulage company (carrier) with its vehicle fleet", Purpose = "A carrier that delivers goods to the warehouse and owns the CargoTrucks (both trucks and trailers). Distinct from Partner, which is the goods supplier. Name, address and tax fields are inherited from PartnerBase.")]
|
||||
[Table(Name = FruitBankConstClient.CargoPartnerDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.CargoPartnerDbTableName)]
|
||||
public sealed class CargoPartner : PartnerBase, ICargoPartner
|
||||
{
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(CargoTruck.CargoPartnerId), CanBeNull = true)]
|
||||
public List<CargoTruck>? CargoTrucks { get; set; }
|
||||
|
||||
//[Association(ThisKey = nameof(Id), OtherKey = nameof(Shipping.CargoPartnerId), CanBeNull = true)]
|
||||
//public List<Shipping>? Shippings { get; set; }
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@ using Mango.Nop.Core.Entities;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
public sealed class CustomerCredit: MgEntityBase, IEntityComment
|
||||
public class CustomerCredit: MgEntityBase, IEntityComment
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
public decimal CreditLimit { get; set; }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,16 +1,12 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Uploaded file with extracted text content", Purpose = "A centralized repository for all uploaded binary content and metadata, featuring a 'RawText' field that stores OCR-extracted information for full-text search and automated data validation across the system")]
|
||||
[Table(Name = FruitBankConstClient.FilesDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.FilesDbTableName)]
|
||||
public sealed class Files : MgEntityBase, IFiles
|
||||
public class Files : MgEntityBase, IFiles
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string FileSubPath { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -10,8 +8,6 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[ToonDescription("Base class for pallet measurements with net weight calculation",
|
||||
Purpose = "Technically named 'Pallet' for legacy reasons, but represents a General Measurement Record. It is ALWAYS created for every item. If the product is not measurable, weights are 0 and only TrayQuantity is used.")]
|
||||
public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPalletBase
|
||||
{
|
||||
private double _palletWeight;
|
||||
|
|
@ -20,12 +16,9 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
|
||||
[NotColumn]
|
||||
protected int ForeignItemId;
|
||||
|
||||
[NotColumn]
|
||||
[ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")]
|
||||
public int ForeignKey => ForeignItemId;
|
||||
|
||||
[ToonDescription(Purpose = "Always recorded, regardless of measurability")]
|
||||
public int TrayQuantity { get; set; }
|
||||
|
||||
[Column(DataType = DataType.DecFloat)]
|
||||
|
|
@ -36,7 +29,6 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[Column(DataType = DataType.DecFloat)]
|
||||
[ToonDescription(Purpose = "Weight of the physical pallet if used; 0.0 if goods arrive without a pallet")]
|
||||
public double PalletWeight
|
||||
{
|
||||
get => _palletWeight;
|
||||
|
|
@ -44,7 +36,6 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public double NetWeight
|
||||
{
|
||||
get => CalculateNetWeight();
|
||||
|
|
@ -52,7 +43,6 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||
[ToonDescription(Purpose = "Measured gross weight; 0.0 if product is not measurable")]
|
||||
public double GrossWeight
|
||||
{
|
||||
get => _grossWeight;
|
||||
|
|
@ -70,7 +60,6 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
public DateTime Modified { get; set; }
|
||||
|
||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted")]
|
||||
public virtual MeasuringStatus MeasuringStatus => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -12,11 +10,9 @@ using Table = LinqToDB.Mapping.TableAttribute;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet measurements for order items with audit tracking", Purpose = "A measurement record for outgoing goods, used to verify that the net weight being sent to the customer is accurate and audited. NOTE: Despite the 'Pallet' name, this is a general measurement record that is ALWAYS created for every item. If the product is not measurable (IsMeasurable=false), weights are recorded as 0.0 and only TrayQuantity is stored.")]
|
||||
[Table(Name = FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||
public sealed class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||
public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||
{
|
||||
public int OrderItemId
|
||||
{
|
||||
|
|
@ -24,11 +20,7 @@ public sealed class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
set => ForeignItemId = value;
|
||||
}
|
||||
|
||||
[ToonDescription(Purpose = "User/Customer ID of the quality auditor")]
|
||||
public int RevisorId { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => RevisorId > 0")]
|
||||
public bool IsAudited => RevisorId > 0;
|
||||
|
||||
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -36,7 +28,6 @@ public sealed class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
public OrderItemDto? OrderItemDto { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus")]
|
||||
public override MeasuringStatus MeasuringStatus => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus;
|
||||
public override double CalculateNetWeight() => base.CalculateNetWeight();
|
||||
|
||||
|
|
@ -46,7 +37,6 @@ public sealed class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(NetWeight / TrayQuantity, 1)")]
|
||||
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet type definition with size and weight")]
|
||||
[Table(Name = FruitBankConstClient.PalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PalletDbTableName)]
|
||||
public sealed class Pallet : MgEntityBase, IPallet
|
||||
public class Pallet : MgEntityBase, IPallet
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Size { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
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")]
|
||||
[Table(Name = FruitBankConstClient.PartnerDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
|
||||
public sealed class Partner : PartnerBase, IPartner
|
||||
public class Partner : MgEntityBase, IPartner
|
||||
{
|
||||
[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;
|
||||
public string Name { get; set; }
|
||||
public string TaxId { get; set; }
|
||||
public string CertificationNumber { get; set; }
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(PartnerDepot.PartnerId), CanBeNull = true)]
|
||||
public List<PartnerDepot>? PartnerDepots { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string State { get; set; }
|
||||
public string County { get; set; }
|
||||
public string City { get; set; }
|
||||
public string Street { get; set; }
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.PartnerId), CanBeNull = true)]
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.ShippingId), CanBeNull = true)]
|
||||
public List<ShippingDocument>? ShippingDocuments { get; set; }
|
||||
|
||||
|
||||
[SkipValuesOnUpdate]
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Entities;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using AyCode.Core.Interfaces;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
public abstract class PartnerBase : MgEntityBase, IPartnerBase
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string TaxId { get; set; }
|
||||
public string CertificationNumber { get; set; }
|
||||
|
||||
public string CountryCode { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "ISO 4217 currency code the company trades and settles in with this partner (e.g. EUR, HUF). For supplier partners it is the source currency for converting shipping-item values to example: HUF in NAV EKÁER reporting.")]
|
||||
public string Currency { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string State { get; set; }
|
||||
public string County { get; set; }
|
||||
public string City { get; set; }
|
||||
public string Street { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "The PostalCode + City + Street joined into a single-line postal address (non-empty parts).")]
|
||||
public string? FullAddress => this.ComposeFullAddress();
|
||||
|
||||
|
||||
[SkipValuesOnUpdate]
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,35 +1,17 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Enums;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[Table(Name = FruitBankConstClient.PreOrderItemDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PreOrderItemDbTableName)]
|
||||
[ToonDescription("Single product line of a customer preorder with fulfilment tracking", Purpose = "A requested product line within a PreOrder. Tracks requested versus cumulatively fulfilled quantity as incoming stock is allocated across one or more shipping-document conversion runs.")]
|
||||
public sealed class PreOrderItem : MgEntityBase
|
||||
public class PreorderItem : MgEntityBase
|
||||
{
|
||||
[ToonDescription(Purpose = "FK to the parent PreOrder.")]
|
||||
public int PreOrderId { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "FK to the nopCommerce Product being preordered.")]
|
||||
public int PreorderId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Quantity of the product the customer requested.", Constraints = "positive")]
|
||||
public int RequestedQuantity { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Quantity allocated from incoming stock so far; accumulates across conversion runs until it reaches RequestedQuantity.",
|
||||
BusinessRule = "this >= 0 && this <= RequestedQuantity")]
|
||||
public int FulfilledQuantity { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Gross unit price locked at preorder time. Used as the order-item price on conversion for non-measurable products; measurable products are priced 0 at conversion and weighed afterwards.",
|
||||
Constraints = "non-negative")]
|
||||
public decimal UnitPriceInclTax { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Item lifecycle: Pending / Fulfilled (fully allocated) / PartiallyFulfilled (partly allocated) / Dropped (expired or no incoming stock).",
|
||||
BusinessRule = "set during conversion: FulfilledQuantity >= RequestedQuantity ? Fulfilled : FulfilledQuantity > 0 ? PartiallyFulfilled : Dropped; stays Pending until first allocation")]
|
||||
public PreOrderItemStatus Status { get; set; }
|
||||
public PreorderItemStatus Status { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# Entities
|
||||
|
||||
Domain entities for inbound/outbound goods tracking and inventory. All map to `fb`-prefixed database tables.
|
||||
|
||||
## Shipping (Inbound)
|
||||
|
||||
- **`Shipping.cs`** — Physical delivery event (truck arrival). Table: `fbShipping`.
|
||||
- **`ShippingDocument.cs`** — Supplier delivery note/invoice. Table: `fbShippingDocument`.
|
||||
- **`ShippingItem.cs`** — Product line on document with declared vs measured discrepancies. Table: `fbShippingItem`.
|
||||
- **`ShippingItemPallet.cs`** — Measurement record for incoming goods. Table: `fbShippingItemPallet`.
|
||||
- **`ShippingDocumentToFiles.cs`** — Many-to-many link: document ↔ file with DocumentType. Table: `fbShippingDocumentToFiles`.
|
||||
- **`Partner.cs`** — External supplier with address and tax info. Table: `fbPartner`.
|
||||
|
||||
## Cargo / Logistics
|
||||
|
||||
- **`CargoPartner.cs`** — Freight/haulage partner (carrier). Distinct from `Partner` (supplier) — this is the transport side. Has `CargoTrucks` and `Shippings` collections. Table: `fbCargoPartner`.
|
||||
- **`CargoTruck.cs`** — Individual truck belonging to a `CargoPartner` (`LicencePlate`, `CountryCode`, `IsTrailer` for trailers). Table: `fbCargoTruck`.
|
||||
|
||||
## Order (Outbound)
|
||||
|
||||
- **`OrderItemPallet.cs`** — Measurement record for outgoing goods with RevisorId for audit. Table: `fbOrderItemPallet`.
|
||||
|
||||
## Inventory
|
||||
|
||||
- **`StockTaking.cs`** — Inventory session record. Table: `fbStockTaking`.
|
||||
- **`StockTakingItem.cs`** — Line item reconciling snapshot vs measured quantities. Table: `fbStockTakingItem`.
|
||||
- **`StockTakingItemPallet.cs`** — Measurement record for inventory. Table: `fbStockTakingItemPallet`.
|
||||
- **`StockQuantityHistoryExt.cs`** — Extended weight metadata for stock reconciliation.
|
||||
|
||||
## Shared
|
||||
|
||||
- **`MeasuringItemPalletBase.cs`** — Abstract base for all three measurement hierarchies. Defines NetWeight formula, validation methods, CreatorId/ModifierId tracking.
|
||||
- **`Pallet.cs`** — Physical pallet type definition (name, size, weight). Table: `fbPallet`.
|
||||
- **`Files.cs`** — Uploaded file with OCR-extracted RawText. Table: `fbFiles`.
|
||||
|
||||
## Critical: "Pallet" Naming
|
||||
|
||||
Despite the name, `XxxItemPallet` entities are **measurement records**, NOT physical pallets. They are ALWAYS created for every item. For non-measurable products, weights = 0.0 and only TrayQuantity is tracked.
|
||||
|
|
@ -1,22 +1,14 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.EntityComment;
|
||||
using AyCode.Interfaces.EntityComment;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping record with documents and measurement tracking", Purpose = "Inbound delivery event (truck arrival) at the warehouse. Created early and filled progressively — carrier, truck and trailer are assigned later.")]
|
||||
[Table(Name = FruitBankConstClient.ShippingDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)]
|
||||
public sealed class Shipping : MgEntityBase, IShipping, IEntityComment
|
||||
public class Shipping : MgEntityBase, IShipping, IEntityComment
|
||||
{
|
||||
public int? CargoPartnerId { get; set; }
|
||||
public int? CargoTruckId { get; set; }
|
||||
public int? CargoTrailerId { get; set; }
|
||||
|
||||
public DateTime ShippingDate { get; set; } = DateTime.Now;
|
||||
public string LicencePlate { get; set; }
|
||||
public bool IsAllMeasured { get; set; }
|
||||
|
|
@ -25,18 +17,6 @@ public sealed class Shipping : MgEntityBase, IShipping, IEntityComment
|
|||
|
||||
public DateTime? MeasuredDate { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Carrier (transport company); assigned later, null until known. Supplier is separate — see ShippingDocument.Partner.")]
|
||||
[Association(ThisKey = nameof(CargoPartnerId), OtherKey = nameof(CargoPartner.Id), CanBeNull = true)]
|
||||
public CargoPartner CargoPartner { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Tractor unit (CargoTruck, IsTrailer=false) from the carrier's fleet; assigned later, null until known.")]
|
||||
[Association(ThisKey = nameof(CargoTruckId), OtherKey = nameof(CargoTruck.Id), CanBeNull = true)]
|
||||
public CargoTruck CargoTruck { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Trailer (CargoTruck table, IsTrailer=true); optional, assigned later — null if none or not yet known.")]
|
||||
[Association(ThisKey = nameof(CargoTrailerId), OtherKey = nameof(CargoTruck.Id), CanBeNull = true)]
|
||||
public CargoTruck CargoTrailer { get; set; }
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.ShippingId), CanBeNull = true)]
|
||||
public List<ShippingDocument>? ShippingDocuments { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,16 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using System.Collections.ObjectModel;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping document with partner, items and files", Purpose = "Supplier's delivery note or invoice for the shipment; reconciles paper data with measured reality. Populated progressively — much entered at order time, the rest as it becomes known.")]
|
||||
[Table(Name = FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||
public sealed class ShippingDocument : MgEntityBase, IShippingDocument
|
||||
public 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; }
|
||||
|
|
@ -33,12 +28,6 @@ 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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Links shipping documents to files with document type", Purpose = "A many-to-many link table that associates general uploaded files with specific shipping documents, assigning a functional context (DocumentType) to each file, such as identifying which PDF is the supplier's invoice versus the packing list")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||
public sealed class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
|
||||
public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
|
||||
{
|
||||
public int FilesId { get; set; }
|
||||
|
||||
public int ShippingDocumentId { get; set; }
|
||||
|
||||
[ToonDescription(Constraints = "enum-reference: DocumentType")]
|
||||
public int DocumentTypeId { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => DocumentTypeId")]
|
||||
public DocumentType DocumentType
|
||||
{
|
||||
get => (DocumentType)DocumentTypeId;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using AyCode.Core.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -12,15 +10,13 @@ using Nop.Core.Domain.Customers;
|
|||
using Nop.Core.Domain.Orders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
//using Nop.Core.Domain.Catalog;
|
||||
using DataType = LinqToDB.DataType;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
using Table = LinqToDB.Mapping.TableAttribute;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping document item with measurements and pallets", Purpose = "Represents a specific product line item within a shipping document, storing the discrepancy between the supplier's declared weight/quantity and the warehouse's measured values")]
|
||||
[Table(Name = FruitBankConstClient.ShippingItemDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemDbTableName)]
|
||||
public class ShippingItem : MgEntityBase, IShippingItem
|
||||
|
|
@ -33,8 +29,10 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
|||
public string NameOnDocument { get; set; }
|
||||
public string HungarianName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// get => ProductDto?.Name ?? Name
|
||||
/// </summary>
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => ProductDto?.Name ?? Name")]
|
||||
public string ProductName => ProductDto?.Name ?? Name;
|
||||
|
||||
public int PalletsOnDocument { get; set; }
|
||||
|
|
@ -86,7 +84,6 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
|||
public DateTime Modified { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsMeasured and ShippingItemPallets status")]
|
||||
public MeasuringStatus MeasuringStatus
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
|
@ -8,11 +6,9 @@ using System.Security.Cryptography.X509Certificates;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet measurements for shipping items", Purpose = "The smallest unit of measurement tracking, representing a single physical measurement event. NOTE: Technically named 'Pallet' for legacy reasons, but it is ALWAYS created even if goods arrive without a physical pallet. For non-measurable products, weights are 0.0 and only TrayQuantity is tracked for tare-weight calculations.")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||
public sealed class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
|
||||
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
|
||||
{
|
||||
public int ShippingItemId
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FruitBank.Common.Entities
|
||||
namespace Mango.Nop.Core.Entities
|
||||
{
|
||||
public interface IStockQuantityHistoryExt : IEntityInt
|
||||
{
|
||||
|
|
@ -15,11 +21,9 @@ namespace FruitBank.Common.Entities
|
|||
public bool IsInconsistent { get; set; }
|
||||
}
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[Table(Name = FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||
[ToonDescription("Extended weight-metadata for StockQuantityHistory", Purpose = "Validates quantity deltas against measured weight to detect inconsistencies")]
|
||||
public sealed class StockQuantityHistoryExt : MgEntityBase, IStockQuantityHistoryExt
|
||||
public class StockQuantityHistoryExt : MgEntityBase, IStockQuantityHistoryExt
|
||||
{
|
||||
public int StockQuantityHistoryId { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using LinqToDB.Mapping;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Inventory session record", Purpose = "Orchestrates inventory sessions by freezing logical stock states")]
|
||||
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
|
||||
public sealed class StockTaking : MgStockTaking<StockTakingItem>
|
||||
public class StockTaking : MgStockTaking<StockTakingItem>
|
||||
{
|
||||
public override bool IsReadyForClose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Dtos;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Newtonsoft.Json;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
using Table = LinqToDB.Mapping.TableAttribute;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Line item for product reconciliation", Purpose = "Reconciles snapshot quantity with physical count to calculate final stock delta")]
|
||||
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
|
||||
public sealed class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||
{
|
||||
public bool IsMeasurable { get; set; }
|
||||
|
||||
|
|
@ -25,34 +21,27 @@ public sealed class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
|||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||
public double MeasuredNetWeight { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Reserved stock buffer (not yet shipped) to prevent double-deduction during closing")]
|
||||
public int InProcessOrdersQuantity { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OriginalStockQuantity + InProcessOrdersQuantity", Purpose = "Snapshot of total logical stock at session start")]
|
||||
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Final adjustment value for Product.StockQuantity", BusinessRule = "get => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0")]
|
||||
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d")]
|
||||
public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d;
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockTakingItemPallet.StockTakingItemId), CanBeNull = true)]
|
||||
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0)")]
|
||||
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => TotalOriginalQuantity < 0")]
|
||||
public bool IsInvalid => TotalOriginalQuantity < 0;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => conditional string based on IsInvalid, IsMeasured, IsRequiredForMeasuring")]
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
|
@ -14,11 +12,9 @@ public interface IStockTakingItemPallet : IMeasuringItemPalletBase
|
|||
public StockTakingItem? StockTakingItem{ get; set; }
|
||||
}
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Weight record for inventory item", Purpose = "Granular weight-based evidence for a stock taking line item. NOTE: This record is mandatory for every inventory item. If weighing is skipped (non-measurable), it serves as a container for TrayQuantity with zeroed weight fields. The term 'Pallet' is a legacy naming convention.")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||
public sealed class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
|
||||
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
|
||||
{
|
||||
public int StockTakingItemId
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace FruitBank.Common.Enums;
|
||||
|
||||
public enum PreOrderItemStatus
|
||||
public enum PreorderItemStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Fulfilled = 10,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace FruitBank.Common.Enums;
|
||||
|
||||
public enum PreOrderStatus
|
||||
public enum PreorderStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Confirmed = 10,
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
# Enums
|
||||
|
||||
Core enumeration types for measurement and document classification.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasuringStatus.cs`** — NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is an intentional legacy typo — do NOT fix.
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -40,10 +39,4 @@
|
|||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,6 @@ 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 EkaerHistoryMappingDbTableName = "fbEkaerHistoryMapping";
|
||||
|
||||
public const string OrderItemPalletDbTableName = "fbOrderItemPallet";
|
||||
|
||||
|
|
@ -48,15 +44,11 @@ public static class FruitBankConstClient
|
|||
public const string StockTakingDbTableName = "fbStockTaking";
|
||||
public const string StockTakingItemDbTableName = "fbStockTakingItem";
|
||||
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
|
||||
|
||||
public const string CustomerCreditDbTableName = "fbCustomerCredit";
|
||||
public const string PreOrderDbTableName = "fbPreOrder";
|
||||
public const string PreOrderItemDbTableName = "fbPreOrderItem";
|
||||
public const string PreOrderDbTableName = "fbPreorder";
|
||||
public const string PreOrderItemDbTableName = "fbPreorderItem";
|
||||
|
||||
public const string CargoPartnerDbTableName = "fbCargoPartner";
|
||||
public const string CargoTruckDbTableName = "fbCargoTruck";
|
||||
|
||||
public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking.";
|
||||
|
||||
//public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
|
||||
//public static Guid[] SysAdmins = new Guid[3]
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# Helpers
|
||||
|
||||
Measurement aggregation utilities.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasuringValuesHelper.cs`** — Static helper for rolling up pallet-level measurements to shipping item level.
|
||||
- `SetShippingItemTotalMeasuringValues()` — Sums quantities and weights from all pallets.
|
||||
- `GetTotalNetAndGrossWeightFromPallets()` — Returns (Quantity, NetWeight, GrossWeight) tuple.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Interfaces;
|
||||
|
||||
public interface ICargoTruck : IEntityInt, ITimeStampInfo
|
||||
{
|
||||
public int CargoPartnerId { get; set; }
|
||||
|
||||
public CargoPartner CargoPartner { get; set; }
|
||||
|
||||
public string CountryCode { get; set; }
|
||||
public string LicencePlate { get; set; }
|
||||
|
||||
public bool IsTrailer { get; set; }
|
||||
|
||||
public string? CargoPartnerName { get; }
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Models;
|
||||
using FruitBank.Common.SignalRs;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Mango.Nop.Core.Models;
|
||||
|
|
@ -20,41 +19,6 @@ 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 ekaerHistoryId);
|
||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
||||
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
|
||||
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
|
||||
#endregion EkaerHistory
|
||||
|
||||
#region CargoPartner
|
||||
public Task<List<CargoPartner>?> GetCargoPartners();
|
||||
public Task<CargoPartner?> GetCargoPartnerById(int id);
|
||||
public Task<CargoPartner?> AddCargoPartner(CargoPartner cargoPartner);
|
||||
public Task<CargoPartner?> UpdateCargoPartner(CargoPartner cargoPartner);
|
||||
#endregion CargoPartner
|
||||
|
||||
#region CargoTruck
|
||||
public Task<List<CargoTruck>?> GetCargoTrucks();
|
||||
public Task<CargoTruck?> GetCargoTruckById(int id);
|
||||
public Task<List<CargoTruck>?> GetCargoTrucksByCargoPartnerId(int cargoPartnerId);
|
||||
public Task<CargoTruck?> AddCargoTruck(CargoTruck cargoTruck);
|
||||
public Task<CargoTruck?> UpdateCargoTruck(CargoTruck cargoTruck);
|
||||
#endregion CargoTruck
|
||||
|
||||
#region Shipping
|
||||
public Task<List<Shipping>?> GetShippings();
|
||||
Task<List<Shipping>?> GetNotMeasuredShippings();
|
||||
|
|
|
|||
|
|
@ -1,28 +1,20 @@
|
|||
using AyCode.Core.Interfaces;
|
||||
using AyCode.Entities;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Interfaces;
|
||||
|
||||
public interface ICargoPartner : IPartnerBase
|
||||
{
|
||||
List<CargoTruck>? CargoTrucks { get; set; }
|
||||
}
|
||||
|
||||
public interface IPartner : IPartnerBase
|
||||
{
|
||||
bool IsEkaer { get; set; }
|
||||
List<PartnerDepot>? PartnerDepots { get; set; }
|
||||
List<ShippingDocument>? ShippingDocuments { get; set; }
|
||||
}
|
||||
|
||||
public interface IPartnerBase : ICompanyInfoBase, IEntityInt, ITimeStampInfo
|
||||
public interface IPartner : IEntityInt, ITimeStampInfo
|
||||
{
|
||||
string Name { get; set; }
|
||||
string TaxId { get; set; }
|
||||
string CertificationNumber { get; set; }
|
||||
string Currency { get; set; }
|
||||
string PostalCode { get; set; }
|
||||
string Country { get; set; }
|
||||
string State { get; set; }
|
||||
string County { get; set; }
|
||||
string City { get; set; }
|
||||
string Street { get; set; }
|
||||
|
||||
List<ShippingDocument>? ShippingDocuments { get; set; }
|
||||
}
|
||||
|
|
@ -6,10 +6,6 @@ namespace FruitBank.Common.Interfaces;
|
|||
|
||||
public interface IShipping : IEntityInt, ITimeStampInfo//, IMeasured
|
||||
{
|
||||
public int? CargoPartnerId { get; set; }
|
||||
public int? CargoTruckId { get; set; }
|
||||
public int? CargoTrailerId { get; set; }
|
||||
|
||||
DateTime ShippingDate { get; set; }
|
||||
string LicencePlate { get; set; }
|
||||
bool IsAllMeasured { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Interfaces
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# Interfaces
|
||||
|
||||
SignalR endpoint contracts, measurement composition traits, and entity interfaces.
|
||||
|
||||
## SignalR Endpoints
|
||||
|
||||
- **`IFruitBankDataControllerCommon.cs`** / **`Client.cs`** — Core CRUD: Partners, Shipping, ShippingDocuments, ShippingItems, ShippingItemPallets, Products, Customers, GenericAttributes.
|
||||
- **`ICustomOrderSignalREndpointCommon.cs`** / **`Client.cs`** — Order operations: GetAllOrderDtos, GetPendingOrderDtos, OrderItem/Pallet management, StartMeasuring, SetOrderStatusToComplete.
|
||||
- **`IStockSignalREndpointCommon.cs`** / **`Client.cs`** — Inventory: StockTaking, StockTakingItem, StockTakingItemPallet CRUD, CloseStockTaking.
|
||||
|
||||
## Measurement Traits (Composition Pattern)
|
||||
|
||||
- **`IMeasuringValues`** = IMeasuringWeights + IMeasuringQuantity
|
||||
- **`IMeasuringWeights`** = IMeasuringNetWeight + IMeasuringGrossWeight
|
||||
- **`IMeasurable`** — IsMeasurable flag
|
||||
- **`IMeasured`** — IsMeasured flag
|
||||
- **`IMeasurableStatus`** — MeasuringStatus property
|
||||
- **`IMeasuringItemPalletBase`** — Full measurement contract with validation
|
||||
|
||||
## Entity & DTO Interfaces
|
||||
|
||||
- **`IPallet`**, **`IPartner`**, **`IShipping`**, **`IShippingDocument`**, **`IShippingItem`**, **`IShippingItemPallet`**, **`IFiles`**
|
||||
- **`IOrderDto`**, **`IOrderItemDto`**, **`IProductDto`**, **`IStockQuantityHistoryDto`**
|
||||
- **`ITare`**, **`IAvailableQuantity`**, **`IIncomingQuantity`** — Quantity/weight property interfaces
|
||||
|
||||
## Service Interfaces
|
||||
|
||||
- **`IMeasurementServiceBase<TLogger>`** — Base service marker
|
||||
- **`ISecureCredentialService`** — Save/retrieve/clear credentials with 2-day expiration
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Loggers
|
||||
|
||||
SignalR client-to-server log writer.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`SignaRClientLogItemWriter.cs`** — Routes client logs to `{BaseUrl}/loggerHub` via SignalR. Configurable by AppType and LogLevel.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# Models
|
||||
|
||||
Application and view models for UI state management.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`LoggedInModel.cs`** — Authentication state: IsLoggedIn, IsRevisor, IsAdministrator, IsDeveloper. Auto-login from stored credentials (2-day expiration). Customer and role management.
|
||||
- **`MeasuringAttributeValues.cs`** — IMeasuringAttributeValues implementation: Id, NetWeight, IsMeasurable, HasMeasuringValues().
|
||||
- **`MeasuringModel.cs`** — ViewModel aggregating Shipping + Partners + ShippingItems + ShippingDocuments.
|
||||
|
||||
## Subfolders
|
||||
|
||||
- **`SignalRs/SignalRMessageToClientWithText<T>.cs`** — Generic message wrapper with optional text and typed content.
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# FruitBank.Common
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Entities, AyCode.Interfaces, AyCode.Models, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Shared domain library for the FruitBank nopCommerce plugin. Contains entities, DTOs, interfaces, measurement helpers, SignalR tags, and constants for fruit & vegetable wholesale operations.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Databases/`](Databases/README.md) | Local in-memory database abstraction for offline/cached data |
|
||||
| [`Dtos/`](Dtos/README.md) | Binary-serializable DTOs for Order, OrderItem, Product, StockQuantityHistory |
|
||||
| [`Entities/`](Entities/README.md) | Domain entities: Shipping, Partner, measurement pallets, inventory |
|
||||
| [`Enums/`](Enums/README.md) | MeasuringStatus and DocumentType enums |
|
||||
| [`Helpers/`](Helpers/README.md) | Measurement aggregation utilities |
|
||||
| [`Interfaces/`](Interfaces/README.md) | SignalR endpoint contracts, measurement traits, entity interfaces |
|
||||
| [`Loggers/`](Loggers/README.md) | SignalR client log writer |
|
||||
| [`Models/`](Models/README.md) | Authentication state, measurement view models |
|
||||
| [`Services/`](Services/README.md) | Measurement service base, credential persistence |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR method tags (numeric constants) |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`FruitBankConstClient.cs`** — Global constants: BaseUrl, SignalR hubs, database table names, email templates, system settings.
|
||||
- **`DocumentType.cs`** — Enum: ShippingDocument, OrderConfirmation, Invoice.
|
||||
|
||||
## Key Domain Concepts
|
||||
|
||||
- **Shipping = INBOUND** (supplier → warehouse), **Order = OUTBOUND** (warehouse → customer)
|
||||
- **"Pallet" = measurement record**, always created even for non-measurable products
|
||||
- **NetWeight = GrossWeight − PalletWeight − (TrayQuantity × TareWeight)**
|
||||
- See `docs/GLOSSARY.md` for full terminology
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
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!;
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
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 > 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
using AyCode.Services.Nav.Ekaer;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// FruitBank domain → NAV EKÁER tradeCard leképezés. Egy bejövő <see cref="Shipping"/>-ből
|
||||
/// EKÁER tradeCard műveleteket állít elő (dokumentumonként egyet).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A NAV-protokollt és az authentikációt NEM kezeli — az az <c>AyCode.Services.Nav</c> réteg felelőssége.
|
||||
/// A feladót (beszállító <c>Partner</c>) és a saját céget (<see cref="EkaerCompanyInfo"/>) egységesen,
|
||||
/// <see cref="AyCode.Entities.ICompanyInfoBase"/>-ként kezeli.
|
||||
/// </remarks>
|
||||
public interface IShippingToEkaerMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Leképezi a <paramref name="shipping"/> minden <c>ShippingDocument</c>-jét egy-egy EKÁER tradeCard műveletre.
|
||||
/// </summary>
|
||||
/// <param name="shipping">A bejövő szállítmány (fuvarozó/jármű a Shipping szintjén, eladó/tételek a dokumentum szintjén).</param>
|
||||
/// <param name="company">A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely.</param>
|
||||
/// <param name="operation">A tradeCard művelet típusa. Alapértelmezés: <see cref="OperationType.Create"/>.</param>
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
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;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <inheritdoc cref="IShippingToEkaerMapper"/>
|
||||
/// <remarks>
|
||||
/// 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" 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);
|
||||
ArgumentNullException.ThrowIfNull(company);
|
||||
|
||||
var operations = new List<TradeCardOperationType>();
|
||||
var index = 0;
|
||||
|
||||
// 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(ToConsignment([document], company)),
|
||||
});
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company)
|
||||
{
|
||||
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(consignment),
|
||||
ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat
|
||||
|
||||
SellerName = consignment.Seller.Name,
|
||||
SellerVatNumber = NormalizeVatNumber(consignment.Seller.VatNumber),
|
||||
SellerCountry = NormalizeCountryCode(consignment.Seller.CountryCode, 2),
|
||||
SellerAddress = Truncate(consignment.Seller.FullAddress, 200),
|
||||
|
||||
DestinationName = consignment.Buyer.Name,
|
||||
DestinationVatNumber = NormalizeVatNumber(consignment.Buyer.VatNumber),
|
||||
DestinationCountry = NormalizeCountryCode(consignment.Buyer.CountryCode, 2),
|
||||
DestinationAddress = Truncate(consignment.Buyer.FullAddress, 200),
|
||||
|
||||
CarrierText = consignment.CarrierName,
|
||||
|
||||
// A helyek kész NAV LocationType-ok (a saját telephely érintetlenül) — átengedve.
|
||||
LoadLocation = consignment.LoadLocation,
|
||||
UnloadLocation = consignment.UnloadLocation,
|
||||
};
|
||||
|
||||
if (consignment.Vehicle != null) tradeCard.Vehicle = consignment.Vehicle;
|
||||
if (consignment.Trailer != null) tradeCard.Vehicle2 = consignment.Trailer;
|
||||
|
||||
foreach (var line in consignment.Lines) tradeCard.Items.Add(BuildItem(line));
|
||||
return tradeCard;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
// ── 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()
|
||||
{
|
||||
Name = partner?.Name,
|
||||
VatNumber = partner?.TaxId,
|
||||
CountryCode = partner?.CountryCode,
|
||||
FullAddress = partner?.FullAddress,
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
Name = seller.Name,
|
||||
VatNumber = NormalizeVatNumber(seller.TaxId),
|
||||
Country = NormalizeCountryCode(seller.CountryCode, 2),
|
||||
ZipCode = NormalizeZipCode(seller.PostalCode),
|
||||
City = seller.City,
|
||||
Street = seller.Street,
|
||||
};
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c == '-')]);
|
||||
return Truncate(EmptyToNull(cleaned), 15);
|
||||
}
|
||||
|
||||
/// <summary>Országkód normalizálása. Pattern: <c>[A-Z]{1,maxLen}</c> (seller/location: 2, jármű: 3).</summary>
|
||||
private static string? NormalizeCountryCode(string? value, int maxLen)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var cleaned = new string([.. value.ToUpperInvariant().Where(char.IsAsciiLetter)]);
|
||||
return Truncate(EmptyToNull(cleaned), maxLen);
|
||||
}
|
||||
|
||||
/// <summary>Rendszám normalizálása. Pattern: <c>[A-Z0-9ÖŐÜŰ]{4,15}</c> — kötőjel/szóköz NEM engedett.</summary>
|
||||
private static string? NormalizePlateNumber(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is 'Ö' or 'Ő' or 'Ü' or 'Ű')]);
|
||||
return Truncate(EmptyToNull(cleaned), 15);
|
||||
}
|
||||
|
||||
/// <summary>Irányítószám normalizálása. Pattern: <c>[A-Z0-9 -]{2,10}</c> vagy üres.</summary>
|
||||
private static string? NormalizeZipCode(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is ' ' or '-')]);
|
||||
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)
|
||||
=> value is null ? null : value.Length <= maxLen ? value : value[..maxLen];
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Services
|
||||
|
||||
Business logic services and credential management.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasurementServiceBase.cs`** — Abstract base with generic TLogger injection.
|
||||
- **`ISecureCredentialService.cs`** — Interface: SaveCredentialsAsync (2-day expiry), GetCredentialsAsync, ClearCredentialsAsync. StoredCredentials sealed record.
|
||||
|
||||
Platform implementations: MAUI → SecureStorage, Web → obfuscated localStorage, Server → no-op.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# SignalRs
|
||||
|
||||
SignalR method identifiers as numeric constants for type-safe client-server communication.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`SignalRTags.cs`** — Constant int tags organized by domain:
|
||||
- **0-10:** System (GetMeasuringModels)
|
||||
- **20-27:** Partner CRUD
|
||||
- **40-66:** Shipping, ShippingDocument, ShippingItem
|
||||
- **70-83:** Customer, Product
|
||||
- **94-98:** ShippingItemPallet
|
||||
- **111-138:** Order (OrderDto, OrderItemDto, OrderItemPallet)
|
||||
- **150-151:** StockQuantityHistory
|
||||
- **160-169:** GenericAttribute
|
||||
- **170-179:** StockTaking
|
||||
- **195-200:** Authentication
|
||||
- **500+:** Server→client notifications (SendOrderChanged, SendShippingChanged, etc.)
|
||||
- **1000+:** Diagnostic/Logging
|
||||
|
|
@ -15,17 +15,6 @@ public class SignalRTags : AcSignalRTags
|
|||
public const int AddPartner = 25;
|
||||
public const int UpdatePartner = 26;
|
||||
|
||||
public const int GetCargoPartners = 30;
|
||||
public const int GetCargoPartnerById = 31;
|
||||
public const int AddCargoPartner = 32;
|
||||
public const int UpdateCargoPartner = 33;
|
||||
|
||||
public const int GetCargoTrucks = 35;
|
||||
public const int GetCargoTrucksByCargoPartnerId = 36;
|
||||
public const int GetCargoTruckById = 37;
|
||||
public const int AddCargoTruck = 38;
|
||||
public const int UpdateCargoTruck = 39;
|
||||
|
||||
public const int GetShippings = 40;
|
||||
public const int GetNotMeasuredShippings = 41;
|
||||
public const int GetShippingById = 42;
|
||||
|
|
@ -112,21 +101,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
# DATA-MODEL — Known Issues
|
||||
|
||||
> Companion to [`README.md`](README.md). Topic `DMODEL`, prefix `FBANKAPP` → entry IDs `FBANKAPP-DMODEL-I-<RAND>` (issue) / `-B-` (bug).
|
||||
> ID format, Status vocabulary, type codes, archival → `../../../.github/TOPIC_CODES.md` (→ framework registry).
|
||||
|
||||
Scope: a FruitBank entitások adatmodell-normalizálási teendői (nopCommerce referencia-FK-k, azonosító-szétválasztások).
|
||||
|
||||
## Active entries
|
||||
|
||||
## FBANKAPP-DMODEL-I-K3D9: A Partner-entitások nopCommerce referencia-mezői szabad string-ek, FK helyett
|
||||
|
||||
**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell / normalizálás)
|
||||
|
||||
A `PartnerBase` (és így `Partner` / `CargoPartner`) + a `CargoTruck` a nopCommerce referencia-adatait **szabad string-ként** tárolja, ahelyett hogy a megfelelő nopCommerce tábla **Id-jára FK-zna**:
|
||||
|
||||
| Mező | Jelenlegi | Helyes (hosszú táv) |
|
||||
|---|---|---|
|
||||
| `CountryCode` (string) | szabad szöveg (pl. `"HU"`) | nopCommerce **`Country.Id`** FK; a megjelenített kód a `Country.TwoLetterIsoCode`-ból — `PartnerBase` + `CargoTruck` |
|
||||
| `Currency` (string) | szabad szöveg (pl. `"EUR"`) | nopCommerce **`Currency.Id`** FK; a megjelenített kód a `Currency.CurrencyCode`-ból — `PartnerBase` |
|
||||
|
||||
**Hatás:** működik (a string-eket kézzel töltjük), de **nincs hivatkozás-integritás**, és a kódok elgépelhetők / inkonzisztensek lehetnek. Az EKÁER-leképezés a string pontosságára támaszkodik (`seller`/`destination`/`vehicle` `Country`, és a value→HUF deviza forrás-pénzneme).
|
||||
|
||||
**Javítási irány:** FK-oszlop (`CountryId`, `CurrencyId`) + navigation a nopCommerce táblákra; a megjelenített/exportált kód a referencia-entitásból.
|
||||
|
||||
**Affected:**
|
||||
- `FruitBank.Common/Entities/PartnerBase.cs` → `CountryCode`, `Currency`
|
||||
- `FruitBank.Common/Entities/CargoTruck.cs` → `CountryCode`
|
||||
- felhasználó: `FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs` (a `*Country` + a deviza-konverzió pontossága)
|
||||
|
||||
## FBANKAPP-DMODEL-I-P6X4: A `Product.Gtin` átmenetileg a VTSZ-t tárolja — szétválasztandó
|
||||
|
||||
**Status:** Closed (2026-06-15) — superseded by `MGFBANKPLUG-EKAER-I-T3X8` · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
|
||||
|
||||
### 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).
|
||||
- **VTSZ** — vámtarifaszám (a termék vám-/statisztikai besorolása).
|
||||
|
||||
Egy termékhez a kettő nem azonos; a `Gtin` oszlop VTSZ-ként való használata **átmeneti** megoldás az EKÁER-integráció beindításához.
|
||||
|
||||
**Hatás:** jelenleg nincs üzemszerű gond (a `Gtin` mező szabad, és a VTSZ-t tölthetjük bele). Hosszú távon viszont, ha a valódi GTIN-re is szükség lesz, a kettő ütközik.
|
||||
|
||||
**Javítási irány:** külön `Vtsz` mező/`GenericAttribute` a `Product`-on, és a `ShippingToEkaerMapper` onnan olvasson — a `Gtin` maradjon a valódi GTIN.
|
||||
|
||||
**Affected:**
|
||||
- `FruitBank.Common/Dtos/ProductDto.cs` → `Gtin` property (a `[Column(nameof(Product.Gtin))]` jelöléssel, summary-ban megjelölve)
|
||||
- `FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs` → a `productVtsz` forrása
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# DATA-MODEL — FruitBank adatmodell-normalizálás
|
||||
|
||||
Topic `DMODEL`, prefix `FBANKAPP` → entry ID-k `FBANKAPP-DMODEL-I-<RAND>` (issue) / `-T-` (TODO) / `-B-` (bug).
|
||||
ID-formátum, Status, type-kódok, archiválás → `../../../.github/TOPIC_CODES.md` (→ framework registry).
|
||||
|
||||
A FruitBank entitások (`Partner`, `CargoPartner`, `CargoTruck`, `ProductDto`) **adatmodell-átmenetiségei**: olyan mezők, amelyek hosszú távon a nopCommerce referencia-tábláira FK-znának, vagy külön mezőbe válnának — de jelenleg szabad string / átmeneti megoldás.
|
||||
|
||||
> Ezek **általános** adatmodell-kérdések, NEM funkció-specifikusak. Az EKÁER-, pre-order- stb. doksik csak **hivatkoznak** ide (nem duplikálják).
|
||||
|
||||
## Companion fájlok
|
||||
|
||||
- [`DATAMODEL_ISSUES.md`](DATAMODEL_ISSUES.md) — az aktív adatmodell-issue-k.
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# FruitBank.Common Domain Rules & Glossary
|
||||
|
||||
> This file acts as the single source of truth for the core Measurement System and Common Traps shared across all FruitBank applications (Hybrid App, Blazor, and nopCommerce server plugin).
|
||||
|
||||
## Measurement System
|
||||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **IsMeasurable** | Product-level flag. If `false`: weights = 0.0, only `TrayQuantity` matters. A Pallet record is still created. |
|
||||
| **NetWeight** | `GrossWeight − PalletWeight − (TrayQuantity × TareWeight)` — universal formula across all three hierarchies. |
|
||||
| **TrayQuantity** | Always recorded, regardless of measurability. Count of trays/crates. |
|
||||
| **GrossWeight** | Total weight including pallet and packaging. 0.0 if not measurable. |
|
||||
| **PalletWeight** | Weight of the physical pallet. 0.0 if goods arrive without one. |
|
||||
| **TareWeight** | Weight of a single tray/crate. Used in NetWeight calculation. |
|
||||
| **AverageWeight** | Per-pallet average: `NetWeight / TrayQuantity`. Validated against threshold. |
|
||||
| **MeasuringStatus** | NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is intentional. |
|
||||
| **RevisorId** | Quality auditor's Customer ID. OrderItemPallet becomes "Audited" when RevisorId > 0. |
|
||||
|
||||
## Three Measurement Hierarchies
|
||||
|
||||
All share `MeasuringItemPalletBase` with the same NetWeight formula:
|
||||
|
||||
| Flow | Parent | Pallet Record | Extra |
|
||||
|--------------|-----------------|-------------------------|---------------------------------------|
|
||||
| **Inbound** | ShippingItem | ShippingItemPallet | Declared vs measured discrepancy |
|
||||
| **Outbound** | OrderItemDto | OrderItemPallet | RevisorId for audit |
|
||||
| **Inventory**| StockTakingItem | StockTakingItemPallet | QuantityDiff for stock adjustment |
|
||||
|
||||
## Common Traps
|
||||
|
||||
| Trap | Correct Behavior |
|
||||
|---|---|
|
||||
| "Pallet" = physical pallet | ❌ It's a measurement record. Always created. |
|
||||
| Shipping = outgoing | ❌ Shipping = INBOUND. Order = OUTBOUND. |
|
||||
| Fix "Finnished" spelling | ❌ Intentional legacy typo. Do NOT fix. |
|
||||
| IsMeasurable=false means no Pallet | ❌ Pallet is always created, weights just = 0.0 |
|
||||
| NetWeight is stored/settable | ❌ It is calculated. The setter throws an Exception! It only exists to satisfy the `IMeasuringItemPalletBase` interface boundary. Set `GrossWeight`, `PalletWeight`, `TareWeight` instead. |
|
||||
| Setting MeasuringStatus | ❌ It's a calculated property (evaluates `IsMeasured`, `Id`, or child pallets). Do not try to set it. |
|
||||
| Setting ForeignKey | ❌ `ForeignKey` is read-only. Use `SetForeignKey(id)` method instead. |
|
||||
| GenericAttribute is simple | ❌ It's polymorphic: KeyGroup determines which entity type owns the record |
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# FruitBank.Common documentation
|
||||
|
||||
Topic documentation for the `FruitBank.Common` project (shared types across Hybrid client).
|
||||
|
||||
## Reference docs (flat)
|
||||
|
||||
- [`GLOSSARY.md`](GLOSSARY.md) — Common domain terms for the Hybrid client side
|
||||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Currently only single-file reference.
|
||||
|
||||
## See also
|
||||
|
||||
- **Repo-level glossary**: `../../docs/GLOSSARY.md`
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# FruitBankHybrid.Shared.Common
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
}
|
||||
|
||||
Shared common library. Currently a placeholder — no source files yet. .NET 10.0 with AOT enabled.
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
using AyCode.Services.Nav.Ekaer;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Services.Ekaer;
|
||||
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
|
||||
using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tesztek a <see cref="ShippingToEkaerMapper"/>-re — a FruitBank <c>Shipping</c> → NAV EKÁER tradeCard
|
||||
/// leképezésre. Tisztán memóriában felépített entitásokon fut (nincs hálózat/DB), determinisztikus.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class ShippingToEkaerMapperTests
|
||||
{
|
||||
// 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 ------------------------------------------------------------
|
||||
|
||||
private static Shipping CreateShipping(string sellerCountry = "HU", string plate = "ABC-123", bool withTrailer = true)
|
||||
{
|
||||
var item = new ShippingItem
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Alma",
|
||||
ProductDto = new ProductDto { Gtin = "08081010", Name = "Alma" },
|
||||
MeasuredGrossWeight = 123.5,
|
||||
MeasuredQuantity = 10,
|
||||
UnitPriceOnDocument = 5.0,
|
||||
};
|
||||
|
||||
var document = new ShippingDocument
|
||||
{
|
||||
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 = [item],
|
||||
};
|
||||
|
||||
var shipping = new Shipping
|
||||
{
|
||||
CargoPartner = new CargoPartner { Name = "Fuvaros Zrt", CountryCode = "HU" },
|
||||
CargoTruck = new CargoTruck { LicencePlate = plate, CountryCode = "HU", IsTrailer = false },
|
||||
ShippingDocuments = [document],
|
||||
};
|
||||
|
||||
if (withTrailer)
|
||||
shipping.CargoTrailer = new CargoTruck { LicencePlate = "XYZ-789", CountryCode = "HU", IsTrailer = true };
|
||||
|
||||
return shipping;
|
||||
}
|
||||
|
||||
private static EkaerCompanyInfo CreateCompany() => new()
|
||||
{
|
||||
Name = "FruitBank Kft",
|
||||
TaxId = "98765432-2-41",
|
||||
CountryCode = "HU",
|
||||
PostalCode = "1102",
|
||||
City = "Budapest",
|
||||
Street = "Raktar utca 5",
|
||||
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 = "1239",
|
||||
City = "Budapest",
|
||||
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]
|
||||
public void MapShipping_ProducesOneOperationPerDocument_WithSequentialIndex()
|
||||
{
|
||||
var shipping = CreateShipping();
|
||||
shipping.ShippingDocuments!.Add(new ShippingDocument
|
||||
{
|
||||
Country = "HU",
|
||||
Partner = new Partner { Name = "Másik Beszállító", CountryCode = "HU" },
|
||||
ShippingItems = [],
|
||||
});
|
||||
|
||||
var ops = Mapper.MapShipping(shipping, CreateCompany());
|
||||
|
||||
Assert.AreEqual(2, ops.Count, "dokumentumonként egy tradeCard");
|
||||
Assert.AreEqual(1, ops[0].Index);
|
||||
Assert.AreEqual(2, ops[1].Index);
|
||||
Assert.AreEqual(OperationType.Create, ops[0].Operation, "alapértelmezett művelet: Create");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_NullDocuments_ReturnsEmpty()
|
||||
{
|
||||
var ops = Mapper.MapShipping(new Shipping { ShippingDocuments = null }, CreateCompany());
|
||||
Assert.AreEqual(0, ops.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_HonorsExplicitOperation()
|
||||
{
|
||||
var ops = Mapper.MapShipping(CreateShipping(), CreateCompany(), OperationType.Modify);
|
||||
Assert.AreEqual(OperationType.Modify, ops[0].Operation);
|
||||
}
|
||||
|
||||
// ---- tradeType irány ----------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_DomesticSeller_TradeTypeDomestic()
|
||||
{
|
||||
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "HU"), CreateCompany());
|
||||
Assert.AreEqual(TradeType.D, ops[0].TradeCard.TradeType, "belföldi (HU) feladó → D");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_ForeignSeller_TradeTypeImport()
|
||||
{
|
||||
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "DE"), CreateCompany());
|
||||
Assert.AreEqual(TradeType.I, ops[0].TradeCard.TradeType, "nem-HU feladó → I (import) — a NAV EKÁER magyar");
|
||||
}
|
||||
|
||||
// ---- Tétel-leképezés ----------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_MapsItemFields()
|
||||
{
|
||||
var item = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard.Items[0];
|
||||
|
||||
Assert.AreEqual("08081010", item.ProductVtsz, "productVtsz = ProductDto.Gtin");
|
||||
Assert.AreEqual("Alma", item.ProductName);
|
||||
Assert.AreEqual(123.5m, item.Weight, "weight = MeasuredGrossWeight (bruttó)");
|
||||
Assert.AreEqual(TradeReasonType.A, item.TradeReason, "bejövő áru = beszerzés = A");
|
||||
Assert.IsNull(item.Value, "a HUF érték a deviza tisztázásáig nincs kitöltve");
|
||||
}
|
||||
|
||||
// ---- Eladó (seller*) ----------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_MapsSellerFromPartner()
|
||||
{
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard;
|
||||
|
||||
Assert.AreEqual("Beszállító Kft", tradeCard.SellerName);
|
||||
Assert.AreEqual("12345678-2-42", tradeCard.SellerVatNumber);
|
||||
Assert.AreEqual("HU", tradeCard.SellerCountry);
|
||||
StringAssert.Contains(tradeCard.SellerAddress, "Budapest", "a sellerAddress a Partner FullAddress-éből jön");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_MapsDestinationAndUnloadFromCompany()
|
||||
{
|
||||
var company = CreateCompany();
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(), company)[0].TradeCard;
|
||||
|
||||
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.Site, tradeCard.UnloadLocation, "a lerakodási hely a cég telephelyéből jön");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_MapsCarrierTextFromCargoPartner()
|
||||
{
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard;
|
||||
Assert.AreEqual("Fuvaros Zrt", tradeCard.CarrierText);
|
||||
}
|
||||
|
||||
// ---- Járművek + rendszám-normalizálás ----------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_NormalizesLicencePlate_RemovesHyphenAndUppercases()
|
||||
{
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(plate: "abc-123"), CreateCompany())[0].TradeCard;
|
||||
Assert.AreEqual("ABC123", tradeCard.Vehicle!.PlateNumber, "a NAV pattern nem enged kötőjelet, és nagybetűs");
|
||||
Assert.AreEqual("HU", tradeCard.Vehicle.Country);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_WithTrailer_MapsVehicle2()
|
||||
{
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: true), CreateCompany())[0].TradeCard;
|
||||
Assert.IsNotNull(tradeCard.Vehicle, "vonó jármű");
|
||||
Assert.IsNotNull(tradeCard.Vehicle2, "vontatmány");
|
||||
Assert.AreEqual("XYZ789", tradeCard.Vehicle2!.PlateNumber);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_NoTrailer_Vehicle2Null()
|
||||
{
|
||||
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: false), CreateCompany())[0].TradeCard;
|
||||
Assert.IsNotNull(tradeCard.Vehicle);
|
||||
Assert.IsNull(tradeCard.Vehicle2, "nincs pótkocsi → nincs vehicle2");
|
||||
}
|
||||
|
||||
// ---- Védőkorlátok -------------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void MapShipping_NullShipping_Throws()
|
||||
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(null!, CreateCompany()));
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,9 +31,58 @@ namespace FruitBankHybrid.Shared.Tests
|
|||
{
|
||||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
|
||||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||
{
|
||||
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
|
||||
});
|
||||
}
|
||||
|
||||
#region Partner
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetPartnersTest()
|
||||
{
|
||||
var partners = await _signalRClient.GetPartners();
|
||||
|
||||
Assert.IsNotNull(partners);
|
||||
Assert.IsTrue(partners.Count != 0);
|
||||
}
|
||||
|
||||
//[DataTestMethod]
|
||||
//[DataRow(1)]
|
||||
public async Task<Partner> GetPartnerByIdTest(int partnerId)
|
||||
{
|
||||
var partner = await _signalRClient.GetPartnerById(partnerId);
|
||||
|
||||
Assert.IsNotNull(partner);
|
||||
Assert.IsTrue(partner.Id == partnerId);
|
||||
|
||||
return partner;
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[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.IsTrue(partner.Id == partnerId);
|
||||
}
|
||||
#endregion Partner
|
||||
|
||||
#region Shipping
|
||||
[TestMethod]
|
||||
public async Task GetShippingsTest()
|
||||
|
|
@ -41,7 +90,7 @@ namespace FruitBankHybrid.Shared.Tests
|
|||
var shippings = await _signalRClient.GetShippings();
|
||||
|
||||
Assert.IsNotNull(shippings);
|
||||
Assert.IsNotEmpty(shippings);
|
||||
Assert.IsTrue(shippings.Count != 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -156,7 +205,7 @@ namespace FruitBankHybrid.Shared.Tests
|
|||
else
|
||||
{
|
||||
Assert.IsNotNull(shippingItem.ShippingDocument);
|
||||
Assert.AreEqual(0, shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count);
|
||||
Assert.IsTrue(shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,11 +219,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.AreEqual(shippingItemId, shippingItem.Id);
|
||||
Assert.IsTrue(shippingItem.Id == shippingItemId);
|
||||
|
||||
Assert.IsGreaterThan(0, shippingItem.QuantityOnDocument, "QuantityOnDocument == 0");
|
||||
Assert.IsGreaterThan(0, shippingItem.NetWeightOnDocument, "NetWeightOnDocument == 0");
|
||||
Assert.IsGreaterThan(0, shippingItem.GrossWeightOnDocument, "GrossWeightOnDocument == 0");
|
||||
Assert.IsTrue(shippingItem.QuantityOnDocument > 0, "QuantityOnDocument == 0");
|
||||
Assert.IsTrue(shippingItem.NetWeightOnDocument > 0, "NetWeightOnDocument == 0");
|
||||
Assert.IsTrue(shippingItem.GrossWeightOnDocument > 0, "GrossWeightOnDocument == 0");
|
||||
|
||||
return shippingItem;
|
||||
}
|
||||
|
|
@ -411,7 +460,7 @@ namespace FruitBankHybrid.Shared.Tests
|
|||
[DataRow(5, true)]
|
||||
//[DataRow(6, false)]
|
||||
[DataRow(33, true)]
|
||||
//[DataRow(64, false)]
|
||||
[DataRow(64, false)]
|
||||
[DataRow(7, true)]
|
||||
public async Task GetProductDtoByIdTest(int productId, bool isMeasurableExcepted)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
using AyCode.Services.Nav;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class FruitBankEkaerTests
|
||||
{
|
||||
private FruitBankSignalRClient _signalRClient = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests));
|
||||
}
|
||||
|
||||
#region EkaerHistory / Create
|
||||
|
||||
/// <summary>
|
||||
/// Backfill: minden meglévő szállítólevélre létrehozza az EkaerHistory rekordot (Pending, XML nélkül).
|
||||
/// A generálás a grid Generate gombjával, kézzel történik. Idempotens: újrafuttatva nem duplikál.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task CreateEkaerHistoryForAllShippingDocumentsTest()
|
||||
{
|
||||
var shippingDocuments = await _signalRClient.GetShippingDocuments();
|
||||
|
||||
Assert.IsNotNull(shippingDocuments);
|
||||
Assert.IsNotEmpty(shippingDocuments);
|
||||
|
||||
foreach (var shippingDocument in shippingDocuments)
|
||||
{
|
||||
var ekaerHistory = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false);
|
||||
|
||||
Console.WriteLine($"doc#{shippingDocument.Id}: EkaerHistory Id: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Id.ToString())}; Status: {ekaerHistory?.Status}");
|
||||
|
||||
Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.IsGreaterThan(0, ekaerHistory.Id, $"shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.IsFalse(ekaerHistory.IsOutgoing);
|
||||
Assert.IsNotNull(ekaerHistory.Mappings, $"Mappings null (loadRelations?); shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}");
|
||||
}
|
||||
|
||||
// Idempotencia: a második hívás a meglévőt adja vissza, nem duplikál.
|
||||
var firstDocumentId = shippingDocuments[0].Id;
|
||||
var again = await _signalRClient.CreateEkaerHistory(firstDocumentId, false);
|
||||
var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(firstDocumentId);
|
||||
|
||||
Assert.IsNotNull(again);
|
||||
Assert.IsNotNull(histories);
|
||||
Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {firstDocumentId}");
|
||||
}
|
||||
|
||||
#endregion EkaerHistory / Create
|
||||
|
||||
#region EkaerHistory / Generate
|
||||
|
||||
/// <summary>
|
||||
/// Backfill + teljes Generate-út teszt: minden meglévő szállítólevélre legenerálja az EKÁER XML-t
|
||||
/// (rekord upsert a szerveren), így a grid valós adatot kap és a Generate gomb útvonala tesztelt.
|
||||
/// </summary>
|
||||
//[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog.
|
||||
public async Task GenerateEkaerXmlDocumentForAllShippingDocumentsTest()
|
||||
{
|
||||
var shippingDocuments = await _signalRClient.GetShippingDocuments();
|
||||
|
||||
Assert.IsNotNull(shippingDocuments);
|
||||
Assert.IsNotEmpty(shippingDocuments);
|
||||
|
||||
foreach (var shippingDocument in shippingDocuments)
|
||||
{
|
||||
// A generálás mostantól EkaerHistory-Id alapú → előbb a (mapping-elt) rekordot hozzuk létre.
|
||||
var created = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false);
|
||||
Assert.IsNotNull(created, $"CreateEkaerHistory null; shippingDocument.Id: {shippingDocument.Id}");
|
||||
var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
|
||||
|
||||
// A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon.
|
||||
Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}");
|
||||
if (!string.IsNullOrWhiteSpace(ekaerHistory?.ErrorText)) Console.WriteLine($" ErrorText: {ekaerHistory.ErrorText}");
|
||||
|
||||
Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.IsFalse(ekaerHistory.IsOutgoing);
|
||||
Assert.AreEqual(shippingDocument.Id, ekaerHistory.Mappings!.Single().ForeignKey, $"A mapping nem a forrás-dokumentumra mutat; shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; shippingDocument.Id: {shippingDocument.Id}");
|
||||
Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError,
|
||||
$"Status: {ekaerHistory.Status}; shippingDocument.Id: {shippingDocument.Id}; ErrorText: {ekaerHistory.ErrorText}");
|
||||
|
||||
// A grid útvonala: az XmlDoc visszaolvasható tradeCard-dá.
|
||||
var tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc!);
|
||||
|
||||
Assert.IsNotNull(tradeCard);
|
||||
Console.WriteLine($" items: {tradeCard.Items.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Idempotencia: az újragenerálás NEM duplikál — dokumentumonként egy bejövő rekord marad.</summary>
|
||||
//[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog.
|
||||
public async Task GenerateEkaerXmlDocumentIsIdempotentTest()
|
||||
{
|
||||
var shippingDocuments = await _signalRClient.GetShippingDocuments();
|
||||
|
||||
Assert.IsNotNull(shippingDocuments);
|
||||
Assert.IsNotEmpty(shippingDocuments);
|
||||
|
||||
var shippingDocumentId = shippingDocuments[0].Id;
|
||||
|
||||
// Generálás EkaerHistory-Id alapján → előbb a rekord (mapping-gel), majd kétszeri generálás ugyanarra az Id-ra.
|
||||
var created = await _signalRClient.CreateEkaerHistory(shippingDocumentId, false);
|
||||
Assert.IsNotNull(created);
|
||||
|
||||
var first = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
|
||||
var second = await _signalRClient.GenerateEkaerXmlDocument(created.Id);
|
||||
|
||||
Assert.IsNotNull(first);
|
||||
Assert.IsNotNull(second);
|
||||
Assert.AreEqual(first.Id, second.Id, "Az újragenerálás új rekordot hozott létre frissítés helyett!");
|
||||
|
||||
var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(shippingDocumentId);
|
||||
|
||||
Assert.IsNotNull(histories);
|
||||
Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {shippingDocumentId}");
|
||||
}
|
||||
|
||||
#endregion EkaerHistory / Generate
|
||||
}
|
||||
}
|
||||
|
|
@ -15,18 +15,20 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="bunit" Version="2.4.2" />
|
||||
<PackageReference Include="bunit" Version="2.2.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj" />
|
||||
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
|
||||
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
|
||||
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="AyCode.Core">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="AyCode.Entities">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +190,10 @@ public sealed class JsonExtensionTests
|
|||
{
|
||||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
|
||||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||
{
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ using AyCode.Core.Extensions;
|
|||
using AyCode.Core.Loggers;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Domain.Payments;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.Serialization;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Entities;
|
||||
using Nop.Core.Domain.Common;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
|
|
@ -28,20 +26,23 @@ public sealed class OrderClientTests
|
|||
{
|
||||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests));
|
||||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||
{
|
||||
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllStockTakings()
|
||||
{
|
||||
var stockTakings = await _signalRClient.GetStockTakings(true);
|
||||
var stockTakings = await _signalRClient.GetStockTakings(false);
|
||||
|
||||
Assert.IsNotNull(stockTakings);
|
||||
Assert.IsNotEmpty(stockTakings);
|
||||
Assert.IsTrue(stockTakings.Count != 0);
|
||||
|
||||
Assert.IsTrue(stockTakings.All(o => o.StockTakingItems != null));
|
||||
Assert.IsTrue(stockTakings.All(o => o.StockTakingItems!.All(oi => oi.Product != null && oi.Product.Id == oi.ProductId)));
|
||||
Assert.IsTrue(stockTakings.All(o => o.StockTakingItems.All(oi => oi.Product != null && oi.Product.Id == oi.ProductId)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -50,9 +51,9 @@ public sealed class OrderClientTests
|
|||
var stockTakingItems = await _signalRClient.GetStockTakingItems();
|
||||
|
||||
Assert.IsNotNull(stockTakingItems);
|
||||
Assert.IsNotEmpty(stockTakingItems);
|
||||
Assert.IsTrue(stockTakingItems.Count != 0);
|
||||
|
||||
Assert.IsTrue(stockTakingItems.All(oi => oi is { StockTaking: not null, Product: not null } && oi.Product.Id == oi.ProductId));
|
||||
Assert.IsTrue(stockTakingItems.All(oi => oi.StockTaking != null && oi.Product != null && oi.Product.Id == oi.ProductId));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -76,18 +77,6 @@ public sealed class OrderClientTests
|
|||
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllOrderDtosByFilter()
|
||||
{
|
||||
//Queryable? filter = dto => dto.Id == 15;
|
||||
var orderDtos = await _signalRClient.GetAllOrderDtos();
|
||||
|
||||
Assert.IsNotNull(orderDtos);
|
||||
Assert.IsTrue(orderDtos.Count != 0);
|
||||
|
||||
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# FruitBankHybrid.Shared.Tests
|
||||
|
||||
@project {
|
||||
type = "test"
|
||||
own-dep-projects = [
|
||||
"AyCode.Entities, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core, Mango.Nop.Services (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
MSTest integration and serialization tests. Covers SignalR client operations, JSON reference handling, binary serialization, Toon format, and bunit component rendering.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`TestData/`](TestData/README.md) | Test models for Toon serialization |
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MSTestSettings.cs`** — Parallel test execution at MethodLevel.
|
||||
- **`FruitBankClientTests.cs`** — (~667 lines) Full SignalR integration: Partner, Shipping, ShippingItem, ShippingDocument, Customer, Product, Order, Login tests. Localhost-only safety check.
|
||||
- **`OrderClientTests.cs`** — Order and StockTaking retrieval/manipulation tests.
|
||||
- **`JsonExtensionTests.cs`** — (~715 lines) JSON $id/$ref reference handling, 5-level hierarchies, circular references, DeepPopulateWithMerge.
|
||||
- **`StockTakingSerializerTests.cs`** — Binary serialization round-trips, null collection handling, binary format analysis.
|
||||
- **`ToonTests.cs`** — (~465 lines) Toon format: metadata generation, reference markers, type uniqueness, navigation metadata, property descriptions.
|
||||
- **`SandboxEndpointSimpleTests.cs`** — Endpoint connectivity and SignalR negotiate tests.
|
||||
- **`GridPartnerBaseTests.cs`** — Grid component tests (disabled).
|
||||
- **`GridPartnerRazorTests.cs`** — bunit Blazor rendering tests (disabled).
|
||||
|
|
@ -1,316 +1,320 @@
|
|||
//using AyCode.Core.Enums;
|
||||
//using AyCode.Core.Loggers;
|
||||
//using AyCode.Utils.Extensions;
|
||||
//using FruitBank.Common;
|
||||
//using FruitBank.Common.Dtos;
|
||||
//using FruitBank.Common.Entities;
|
||||
//using FruitBank.Common.Interfaces;
|
||||
//using FruitBank.Common.Loggers;
|
||||
//using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
//using System.Diagnostics.CodeAnalysis;
|
||||
//using FruitBank.Common.SignalRs;
|
||||
//using AyCode.Services.SignalRs;
|
||||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Utils.Extensions;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FruitBank.Common.SignalRs;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
//namespace FruitBankHybrid.Shared.Tests;
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
///// <summary>
|
||||
///// Teszt a TestSignalREndpoint-hoz.
|
||||
///// FONTOS: A SANDBOX-ot manu<6E>lisan kell elind<6E>tani a tesztek futtat<61>sa el<65>tt!
|
||||
///// Ind<6E>t<EFBFBD>s: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
|
||||
///// </summary>
|
||||
//[TestClass]
|
||||
//public class SandboxEndpointSimpleTests
|
||||
//{
|
||||
// private static readonly string SandboxUrl = FruitBankConstClient.BaseUrl; //"http://localhost:59579";
|
||||
// private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
|
||||
/// <summary>
|
||||
/// Teszt a TestSignalREndpoint-hoz.
|
||||
/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása elõtt!
|
||||
/// Indítás: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SandboxEndpointSimpleTests
|
||||
{
|
||||
private static readonly string SandboxUrl = FruitBankConstClient.BaseUrl; //"http://localhost:59579";
|
||||
private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
|
||||
|
||||
// // Teszt SignalR Tags (TestSignalRTags-b<>l)
|
||||
// private const int PingTag = SignalRTags.PingTag;
|
||||
// private const int EchoTag = SignalRTags.EchoTag;
|
||||
// private const int GetTestItemsTag = 9003;
|
||||
// Teszt SignalR Tags (TestSignalRTags-bõl)
|
||||
private const int PingTag = SignalRTags.PingTag;
|
||||
private const int EchoTag = SignalRTags.EchoTag;
|
||||
private const int GetTestItemsTag = 9003;
|
||||
|
||||
// private FruitBankSignalRClient _signalRClient = null!;
|
||||
private FruitBankSignalRClient _signalRClient = null!;
|
||||
|
||||
// [TestInitialize]
|
||||
// public void TestInit()
|
||||
// {
|
||||
// if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTEL<45>NK!");
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
// _signalRClient = TestSignalRClientFactory.Create(nameof(SandboxEndpointSimpleTests));
|
||||
// }
|
||||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||
{
|
||||
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(SandboxEndpointSimpleTests))
|
||||
});
|
||||
}
|
||||
|
||||
// #region HTTP Endpoint Tests
|
||||
#region HTTP Endpoint Tests
|
||||
|
||||
// [TestMethod]
|
||||
// public async Task HealthEndpoint_ReturnsSuccess()
|
||||
// {
|
||||
// using var httpClient = new HttpClient();
|
||||
// var response = await httpClient.GetAsync($"{SandboxUrl}/health");
|
||||
// Assert.IsTrue(response.IsSuccessStatusCode, $"Health endpoint returned {response.StatusCode}");
|
||||
// }
|
||||
[TestMethod]
|
||||
public async Task HealthEndpoint_ReturnsSuccess()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.GetAsync($"{SandboxUrl}/health");
|
||||
Assert.IsTrue(response.IsSuccessStatusCode, $"Health endpoint returned {response.StatusCode}");
|
||||
}
|
||||
|
||||
// [TestMethod]
|
||||
// public async Task RootEndpoint_ReturnsSandboxIsRunning()
|
||||
// {
|
||||
// using var httpClient = new HttpClient();
|
||||
// var response = await httpClient.GetStringAsync(SandboxUrl);
|
||||
// Assert.AreEqual("SANDBOX is running!", response);
|
||||
// }
|
||||
[TestMethod]
|
||||
public async Task RootEndpoint_ReturnsSandboxIsRunning()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.GetStringAsync(SandboxUrl);
|
||||
Assert.AreEqual("SANDBOX is running!", response);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
#endregion
|
||||
|
||||
// #region SignalR Connection Tests
|
||||
#region SignalR Connection Tests
|
||||
|
||||
// [TestMethod]
|
||||
// public async Task SignalR_Negotiate_ReturnsSuccess()
|
||||
// {
|
||||
// using var httpClient = new HttpClient();
|
||||
// var response = await httpClient.PostAsync($"{HubUrl}/negotiate?negotiateVersion=1", null);
|
||||
// Assert.IsTrue(response.IsSuccessStatusCode, $"SignalR negotiate returned {response.StatusCode}");
|
||||
// }
|
||||
[TestMethod]
|
||||
public async Task SignalR_Negotiate_ReturnsSuccess()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync($"{HubUrl}/negotiate?negotiateVersion=1", null);
|
||||
Assert.IsTrue(response.IsSuccessStatusCode, $"SignalR negotiate returned {response.StatusCode}");
|
||||
}
|
||||
|
||||
// [TestMethod]
|
||||
// public async Task SignalR_Connect_Succeeds()
|
||||
// {
|
||||
// var testItems = await _signalRClient.GetAllAsync<List<TestItem>>(GetTestItemsTag);
|
||||
// Assert.IsNotNull(testItems);
|
||||
// }
|
||||
[TestMethod]
|
||||
public async Task SignalR_Connect_Succeeds()
|
||||
{
|
||||
var testItems = await _signalRClient.GetAllAsync<List<TestItem>>(GetTestItemsTag);
|
||||
Assert.IsNotNull(testItems);
|
||||
}
|
||||
|
||||
// public class TestItem
|
||||
// {
|
||||
// public int Id { get; set; }
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
// public decimal Value { get; set; }
|
||||
// }
|
||||
public class TestItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Value { get; set; }
|
||||
}
|
||||
|
||||
////[TestMethod]
|
||||
// //public async Task SignalR_Connect_Succeeds()
|
||||
// //{
|
||||
// // var connection = new HubConnectionBuilder()
|
||||
// // .WithUrl(HubUrl)
|
||||
// // .Build();
|
||||
//[TestMethod]
|
||||
//public async Task SignalR_Connect_Succeeds()
|
||||
//{
|
||||
// var connection = new HubConnectionBuilder()
|
||||
// .WithUrl(HubUrl)
|
||||
// .Build();
|
||||
|
||||
// // try
|
||||
// // {
|
||||
// // await connection.StartAsync();
|
||||
// // Assert.AreEqual(HubConnectionState.Connected, connection.State);
|
||||
// // }
|
||||
// // finally
|
||||
// // {
|
||||
// // await connection.StopAsync();
|
||||
// // }
|
||||
// //}
|
||||
// try
|
||||
// {
|
||||
// await connection.StartAsync();
|
||||
// Assert.AreEqual(HubConnectionState.Connected, connection.State);
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// await connection.StopAsync();
|
||||
// }
|
||||
//}
|
||||
|
||||
// //#endregion
|
||||
//#endregion
|
||||
|
||||
// //#region TestSignalREndpoint Tests
|
||||
//#region TestSignalREndpoint Tests
|
||||
|
||||
// //[TestMethod]
|
||||
// //public async Task SignalR_Ping_ReturnsResponse()
|
||||
// //{
|
||||
// // var testMessage = "Hello SignalR!";
|
||||
// // await TestSignalREndpoint(PingTag, testMessage, "Ping", response =>
|
||||
// // {
|
||||
// // Assert.IsNotNull(response, "Response should not be null");
|
||||
//[TestMethod]
|
||||
//public async Task SignalR_Ping_ReturnsResponse()
|
||||
//{
|
||||
// var testMessage = "Hello SignalR!";
|
||||
// await TestSignalREndpoint(PingTag, testMessage, "Ping", response =>
|
||||
// {
|
||||
// Assert.IsNotNull(response, "Response should not be null");
|
||||
|
||||
// // // Parse JSON response
|
||||
// // using var jsonDoc = JsonDocument.Parse(response);
|
||||
// // var root = jsonDoc.RootElement;
|
||||
// // Parse JSON response
|
||||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // // Ellen<65>rizz<7A>k, hogy van Message property
|
||||
// // Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) ||
|
||||
// // root.TryGetProperty("message", out messageElement),
|
||||
// // "Response should contain 'Message' property");
|
||||
// // Ellenõrizzük, hogy van Message property
|
||||
// Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) ||
|
||||
// root.TryGetProperty("message", out messageElement),
|
||||
// "Response should contain 'Message' property");
|
||||
|
||||
// // Console.WriteLine($"[Ping] Received message: {messageElement.GetString()}");
|
||||
// // });
|
||||
// //}
|
||||
// Console.WriteLine($"[Ping] Received message: {messageElement.GetString()}");
|
||||
// });
|
||||
//}
|
||||
|
||||
// //[TestMethod]
|
||||
// //public async Task SignalR_Echo_ReturnsEchoedData()
|
||||
// //{
|
||||
// // var request = new { Id = 42, Name = "TestName" };
|
||||
// // await TestSignalREndpoint(EchoTag, request, "Echo", response =>
|
||||
// // {
|
||||
// // Assert.IsNotNull(response, "Response should not be null");
|
||||
//[TestMethod]
|
||||
//public async Task SignalR_Echo_ReturnsEchoedData()
|
||||
//{
|
||||
// var request = new { Id = 42, Name = "TestName" };
|
||||
// await TestSignalREndpoint(EchoTag, request, "Echo", response =>
|
||||
// {
|
||||
// Assert.IsNotNull(response, "Response should not be null");
|
||||
|
||||
// // using var jsonDoc = JsonDocument.Parse(response);
|
||||
// // var root = jsonDoc.RootElement;
|
||||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // // Ellen<65>rizz<7A>k az Id-t
|
||||
// // Assert.IsTrue(root.TryGetProperty("Id", out var idElement) ||
|
||||
// // root.TryGetProperty("id", out idElement),
|
||||
// // "Response should contain 'Id' property");
|
||||
// // Assert.AreEqual(42, idElement.GetInt32(), "Id should be 42");
|
||||
// // Ellenõrizzük az Id-t
|
||||
// Assert.IsTrue(root.TryGetProperty("Id", out var idElement) ||
|
||||
// root.TryGetProperty("id", out idElement),
|
||||
// "Response should contain 'Id' property");
|
||||
// Assert.AreEqual(42, idElement.GetInt32(), "Id should be 42");
|
||||
|
||||
// // // Ellen<65>rizz<7A>k a Name-et
|
||||
// // Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) ||
|
||||
// // root.TryGetProperty("name", out nameElement),
|
||||
// // "Response should contain 'Name' property");
|
||||
// // Assert.AreEqual("TestName", nameElement.GetString(), "Name should be 'TestName'");
|
||||
// // Ellenõrizzük a Name-et
|
||||
// Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) ||
|
||||
// root.TryGetProperty("name", out nameElement),
|
||||
// "Response should contain 'Name' property");
|
||||
// Assert.AreEqual("TestName", nameElement.GetString(), "Name should be 'TestName'");
|
||||
|
||||
// // Console.WriteLine($"[Echo] Received: Id={idElement.GetInt32()}, Name={nameElement.GetString()}");
|
||||
// // });
|
||||
// //}
|
||||
// Console.WriteLine($"[Echo] Received: Id={idElement.GetInt32()}, Name={nameElement.GetString()}");
|
||||
// });
|
||||
//}
|
||||
|
||||
// //[TestMethod]
|
||||
// //public async Task SignalR_GetTestItems_ReturnsItemList()
|
||||
// //{
|
||||
// // await TestSignalREndpoint(GetTestItemsTag, null, "GetTestItems", response =>
|
||||
// // {
|
||||
// // Assert.IsNotNull(response, "Response should not be null");
|
||||
//[TestMethod]
|
||||
//public async Task SignalR_GetTestItems_ReturnsItemList()
|
||||
//{
|
||||
// await TestSignalREndpoint(GetTestItemsTag, null, "GetTestItems", response =>
|
||||
// {
|
||||
// Assert.IsNotNull(response, "Response should not be null");
|
||||
|
||||
// // using var jsonDoc = JsonDocument.Parse(response);
|
||||
// // var root = jsonDoc.RootElement;
|
||||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // // Ellen<65>rizz<7A>k, hogy t<>mb-e
|
||||
// // Assert.AreEqual(JsonValueKind.Array, root.ValueKind, "Response should be an array");
|
||||
// // Assert.IsTrue(root.GetArrayLength() > 0, "Array should have items");
|
||||
// // Ellenõrizzük, hogy tömb-e
|
||||
// Assert.AreEqual(JsonValueKind.Array, root.ValueKind, "Response should be an array");
|
||||
// Assert.IsTrue(root.GetArrayLength() > 0, "Array should have items");
|
||||
|
||||
// // Console.WriteLine($"[GetTestItems] Received {root.GetArrayLength()} items");
|
||||
// Console.WriteLine($"[GetTestItems] Received {root.GetArrayLength()} items");
|
||||
|
||||
// // // Ellen<65>rizz<7A>k az els<6C> elemet
|
||||
// // var firstItem = root[0];
|
||||
// // Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _),
|
||||
// // "Item should have 'Id' property");
|
||||
// // Assert.IsTrue(firstItem.TryGetProperty("Name", out _) || firstItem.TryGetProperty("name", out _),
|
||||
// // "Item should have 'Name' property");
|
||||
// // });
|
||||
// //}
|
||||
// // Ellenõrizzük az elsõ elemet
|
||||
// var firstItem = root[0];
|
||||
// Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _),
|
||||
// "Item should have 'Id' property");
|
||||
// Assert.IsTrue(firstItem.TryGetProperty("Name", out _) || firstItem.TryGetProperty("name", out _),
|
||||
// "Item should have 'Name' property");
|
||||
// });
|
||||
//}
|
||||
|
||||
// //#endregion
|
||||
//#endregion
|
||||
|
||||
// //#region EREDETI BUSINESS ENDPOINT TESZTEK - KIKOMMENTEZVE
|
||||
//#region EREDETI BUSINESS ENDPOINT TESZTEK - KIKOMMENTEZVE
|
||||
|
||||
// //// ===========================================
|
||||
// //// === Az al<61>bbi tesztek az eredeti 3 endpoint-ot tesztelik ===
|
||||
// //// === Vissza<7A>ll<6C>t<EFBFBD>shoz: t<>r<EFBFBD>ld a kommenteket <20>s regisztr<74>ld az endpoint-okat a Program.cs-ben ===
|
||||
// //// ===========================================
|
||||
//// ===========================================
|
||||
//// === Az alábbi tesztek az eredeti 3 endpoint-ot tesztelik ===
|
||||
//// === Visszaállításhoz: töröld a kommenteket és regisztráld az endpoint-okat a Program.cs-ben ===
|
||||
//// ===========================================
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetMeasuringUsers_ReturnsJson()
|
||||
// //// {
|
||||
// //// await TestSignalREndpoint(GetMeasuringUsersTag, null, "GetMeasuringUsers");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetMeasuringUsers_ReturnsJson()
|
||||
//// {
|
||||
//// await TestSignalREndpoint(GetMeasuringUsersTag, null, "GetMeasuringUsers");
|
||||
//// }
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetStockQuantityHistoryDtos_ReturnsJson()
|
||||
// //// {
|
||||
// //// await TestSignalREndpoint(GetStockQuantityHistoryDtosTag, null, "GetStockQuantityHistoryDtos");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetStockQuantityHistoryDtos_ReturnsJson()
|
||||
//// {
|
||||
//// await TestSignalREndpoint(GetStockQuantityHistoryDtosTag, null, "GetStockQuantityHistoryDtos");
|
||||
//// }
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetStockQuantityHistoryDtosByProductId_ReturnsJson()
|
||||
// //// {
|
||||
// //// // ProductId = 10
|
||||
// //// await TestSignalREndpoint(GetStockQuantityHistoryDtosByProductIdTag, 10, "GetStockQuantityHistoryDtosByProductId");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetStockQuantityHistoryDtosByProductId_ReturnsJson()
|
||||
//// {
|
||||
//// // ProductId = 10
|
||||
//// await TestSignalREndpoint(GetStockQuantityHistoryDtosByProductIdTag, 10, "GetStockQuantityHistoryDtosByProductId");
|
||||
//// }
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetShippingDocumentsByShippingId_ReturnsJson()
|
||||
// //// {
|
||||
// //// // ShippingId = 5
|
||||
// //// await TestSignalREndpoint(GetShippingDocumentsByShippingIdTag, 5, "GetShippingDocumentsByShippingId");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetShippingDocumentsByShippingId_ReturnsJson()
|
||||
//// {
|
||||
//// // ShippingId = 5
|
||||
//// await TestSignalREndpoint(GetShippingDocumentsByShippingIdTag, 5, "GetShippingDocumentsByShippingId");
|
||||
//// }
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetOrderDtoById_ReturnsJson()
|
||||
// //// {
|
||||
// //// // OrderId = 15
|
||||
// //// await TestSignalREndpoint(GetOrderDtoByIdTag, 15, "GetOrderDtoById");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetOrderDtoById_ReturnsJson()
|
||||
//// {
|
||||
//// // OrderId = 15
|
||||
//// await TestSignalREndpoint(GetOrderDtoByIdTag, 15, "GetOrderDtoById");
|
||||
//// }
|
||||
|
||||
// //// [TestMethod]
|
||||
// //// public async Task SignalR_GetStockTakingItemsById_ReturnsJson()
|
||||
// //// {
|
||||
// //// // StockTakingItemId = 200
|
||||
// //// await TestSignalREndpoint(GetStockTakingItemsByIdTag, 200, "GetStockTakingItemsById");
|
||||
// //// }
|
||||
//// [TestMethod]
|
||||
//// public async Task SignalR_GetStockTakingItemsById_ReturnsJson()
|
||||
//// {
|
||||
//// // StockTakingItemId = 200
|
||||
//// await TestSignalREndpoint(GetStockTakingItemsByIdTag, 200, "GetStockTakingItemsById");
|
||||
//// }
|
||||
|
||||
// //#endregion
|
||||
//#endregion
|
||||
|
||||
// //#region Helper Methods
|
||||
//#region Helper Methods
|
||||
|
||||
// //private async Task TestSignalREndpoint(int tag, object? parameter, string endpointName, Action<string?>? validateResponse = null)
|
||||
// //{
|
||||
// // var connection = new HubConnectionBuilder()
|
||||
// // .WithUrl(HubUrl)
|
||||
// // .Build();
|
||||
//private async Task TestSignalREndpoint(int tag, object? parameter, string endpointName, Action<string?>? validateResponse = null)
|
||||
//{
|
||||
// var connection = new HubConnectionBuilder()
|
||||
// .WithUrl(HubUrl)
|
||||
// .Build();
|
||||
|
||||
// // string? receivedJson = null;
|
||||
// // int receivedTag = -1;
|
||||
// // var responseReceived = new TaskCompletionSource<bool>();
|
||||
// string? receivedJson = null;
|
||||
// int receivedTag = -1;
|
||||
// var responseReceived = new TaskCompletionSource<bool>();
|
||||
|
||||
// // connection.On<int, byte[]>("ReceiveMessage", (responseTag, data) =>
|
||||
// // {
|
||||
// // receivedTag = responseTag;
|
||||
// // if (data != null && data.Length > 0)
|
||||
// // {
|
||||
// // receivedJson = Encoding.UTF8.GetString(data);
|
||||
// // }
|
||||
// // responseReceived.TrySetResult(true);
|
||||
// // });
|
||||
// connection.On<int, byte[]>("ReceiveMessage", (responseTag, data) =>
|
||||
// {
|
||||
// receivedTag = responseTag;
|
||||
// if (data != null && data.Length > 0)
|
||||
// {
|
||||
// receivedJson = Encoding.UTF8.GetString(data);
|
||||
// }
|
||||
// responseReceived.TrySetResult(true);
|
||||
// });
|
||||
|
||||
// // try
|
||||
// // {
|
||||
// // await connection.StartAsync();
|
||||
// // Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}");
|
||||
// try
|
||||
// {
|
||||
// await connection.StartAsync();
|
||||
// Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}");
|
||||
|
||||
// // // K<>sz<73>ts<74>k el a request data-t
|
||||
// // // Ha nincs param<61>ter, null-t k<>ld<6C>nk (nem <20>res byte t<>mb<6D>t!)
|
||||
// // byte[]? requestData = parameter != null
|
||||
// // ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
|
||||
// // : null;
|
||||
// // Készítsük el a request data-t
|
||||
// // Ha nincs paraméter, null-t küldünk (nem üres byte tömböt!)
|
||||
// byte[]? requestData = parameter != null
|
||||
// ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
|
||||
// : null;
|
||||
|
||||
// // // A Hub met<65>dus neve: OnReceiveMessage (3 param<61>ter: messageTag, messageBytes, requestId)
|
||||
// // await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
|
||||
// // A Hub metódus neve: OnReceiveMessage (3 paraméter: messageTag, messageBytes, requestId)
|
||||
// await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
|
||||
|
||||
// // var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
|
||||
// var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
|
||||
|
||||
// // if (completed == responseReceived.Task)
|
||||
// // {
|
||||
// // Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}");
|
||||
// // Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}...");
|
||||
// if (completed == responseReceived.Task)
|
||||
// {
|
||||
// Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}");
|
||||
// Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}...");
|
||||
|
||||
// // // Ellen<65>rizz<7A>k, hogy valid JSON-e (ha van adat)
|
||||
// // if (!string.IsNullOrEmpty(receivedJson))
|
||||
// // {
|
||||
// // try
|
||||
// // {
|
||||
// // using var jsonDoc = JsonDocument.Parse(receivedJson);
|
||||
// // Assert.IsTrue(
|
||||
// // jsonDoc.RootElement.ValueKind == JsonValueKind.Array ||
|
||||
// // jsonDoc.RootElement.ValueKind == JsonValueKind.Object ||
|
||||
// // jsonDoc.RootElement.ValueKind == JsonValueKind.Null,
|
||||
// // $"[{endpointName}] Response is not a valid JSON");
|
||||
// // Ellenõrizzük, hogy valid JSON-e (ha van adat)
|
||||
// if (!string.IsNullOrEmpty(receivedJson))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// using var jsonDoc = JsonDocument.Parse(receivedJson);
|
||||
// Assert.IsTrue(
|
||||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Array ||
|
||||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Object ||
|
||||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Null,
|
||||
// $"[{endpointName}] Response is not a valid JSON");
|
||||
|
||||
// // // Custom validation
|
||||
// // validateResponse?.Invoke(receivedJson);
|
||||
// // }
|
||||
// // catch (JsonException ex)
|
||||
// // {
|
||||
// // Assert.Fail($"[{endpointName}] Invalid JSON response: {ex.Message}");
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
// // else
|
||||
// // {
|
||||
// // Assert.AreEqual(HubConnectionState.Connected, connection.State,
|
||||
// // $"[{endpointName}] Connection was closed - check SANDBOX logs for DI errors");
|
||||
// // }
|
||||
// // }
|
||||
// // catch (Exception ex)
|
||||
// // {
|
||||
// // Assert.Fail($"[{endpointName}] SignalR error: {ex.Message}. Check SANDBOX logs for missing DI registrations.");
|
||||
// // }
|
||||
// // finally
|
||||
// // {
|
||||
// // if (connection.State == HubConnectionState.Connected)
|
||||
// // {
|
||||
// // await connection.StopAsync();
|
||||
// // }
|
||||
// // }
|
||||
// //}
|
||||
// // Custom validation
|
||||
// validateResponse?.Invoke(receivedJson);
|
||||
// }
|
||||
// catch (JsonException ex)
|
||||
// {
|
||||
// Assert.Fail($"[{endpointName}] Invalid JSON response: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Assert.AreEqual(HubConnectionState.Connected, connection.State,
|
||||
// $"[{endpointName}] Connection was closed - check SANDBOX logs for DI errors");
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Assert.Fail($"[{endpointName}] SignalR error: {ex.Message}. Check SANDBOX logs for missing DI registrations.");
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// if (connection.State == HubConnectionState.Connected)
|
||||
// {
|
||||
// await connection.StopAsync();
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// #endregion
|
||||
//}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# TestData
|
||||
|
||||
Demo models for Toon serialization testing.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`ToonTestData.cs`** — TestOrder, TestCustomer, TestOrderItem, TestProduct models with one-to-many relationships and back-references for testing complex object graph serialization.
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
namespace FruitBankHybrid.Shared.Tests.TestData;
|
||||
|
||||
// Demo entity-k a teszteléshez
|
||||
public class TestOrder
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CustomerId { get; set; }
|
||||
public TestCustomer? Customer { get; set; }
|
||||
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestCustomer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public List<TestOrder> Orders { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestOrderItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int OrderId { get; set; }
|
||||
public TestOrder? Order { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public TestProduct? Product { get; set; }
|
||||
}
|
||||
|
||||
public class TestProduct
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Services.SignalRs;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test-only factory for <see cref="FruitBankSignalRClient"/>. Builds a <c>HubConnectionBuilder</c>
|
||||
/// with the same connection settings a production <c>Program.cs</c> would use, wires a logger factory
|
||||
/// backed by a single <c>SignaRClientLogItemWriter</c> (test-unit AppType, Detail level),
|
||||
/// and uses <see cref="BinaryProtocolMode.AsyncSegment"/> for the protocol.
|
||||
/// </summary>
|
||||
internal static class TestSignalRClientFactory
|
||||
{
|
||||
public static FruitBankSignalRClient Create(string testCategoryName)
|
||||
{
|
||||
var logWriters = new List<IAcLogWriterClientBase>
|
||||
{
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName)
|
||||
};
|
||||
|
||||
Func<string, LoggerClient> loggerFactory =
|
||||
categoryName => new LoggerClient(categoryName, logWriters.ToArray());
|
||||
|
||||
var connectionOptions = new AcHubConnectionOptions
|
||||
{
|
||||
Url = $"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}",
|
||||
TransportMaxBufferSize = 30_000_000,
|
||||
ApplicationMaxBufferSize = 30_000_000,
|
||||
CloseTimeout = TimeSpan.FromSeconds(10),
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(60),
|
||||
ServerTimeout = TimeSpan.FromSeconds(180),
|
||||
SkipNegotiation = true,
|
||||
Transports = HttpTransportType.WebSockets,
|
||||
UseAutomaticReconnect = true,
|
||||
UseStatefulReconnect = true
|
||||
};
|
||||
|
||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||
|
||||
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOptions);
|
||||
hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment);
|
||||
|
||||
return new FruitBankSignalRClient(hubBuilder, loggerFactory);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,463 +0,0 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Domain.Payments;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
//1. "Headered List" (A biztonságos táblázatosítás)
|
||||
|
||||
//Az LLM-eknek nem kell minden sorban megismételni a mezőneveket, ha a lista elején egyszer definiálod a sorrendet.Ez nem találgatás, hanem egy lokális "szerződés".
|
||||
|
||||
//Hagyományos (pazarló):
|
||||
//Kódrészlet
|
||||
|
||||
//OrderItemDtos = [
|
||||
// OrderItemDto { Id = 120, Quantity = 10, ProductName = "Áfonya" }
|
||||
//OrderItemDto { Id = 121, Quantity = 5, ProductName = "Narancs" }
|
||||
//]
|
||||
|
||||
//Optimalizált(pontos és tömör) :
|
||||
//Kódrészlet
|
||||
|
||||
//OrderItemDtos: OrderItemDto[] = [
|
||||
// [Id, Quantity, ProductName]
|
||||
// [120, 10, "Áfonya"]
|
||||
// [121, 5, "Narancs"]
|
||||
//]
|
||||
|
||||
// Miért jó ez? Az LLM a fejléc alapján(mint egy CSV-nél) rendeli hozzá az értékeket a típushoz.Mivel a típus (OrderItemDto) ott van a definícióban, a szemantikai kapcsolat nem vész el.
|
||||
|
||||
//2. Típus-öröklődés a listákban
|
||||
|
||||
//Ha a @types részben már leírtad, hogy az OrderItemDto.ProductDto mezője egy ProductDto típust vár, akkor a @data részben felesleges kiírni a típusnevet minden egyes elemnél.
|
||||
|
||||
//Példa:
|
||||
//Kódrészlet
|
||||
|
||||
//// A 'ProductDto' elhagyható az objektum elől, mert a sémából tudja
|
||||
//ProductDto = {
|
||||
// Id = 1
|
||||
// Name = "Áfonya..."
|
||||
// GenericAttributes = [
|
||||
// { Id = 99, Key = "NetWeight", Value = "178.3" }
|
||||
// { Id = 100, Key = "GrossWeight", Value = "19" }
|
||||
// ]
|
||||
//}
|
||||
|
||||
//3. Alapértelmezett értékek elhagyása(Implicit Defaults)
|
||||
|
||||
//Ha egy mező értéke megegyezik a @types - ban definiált default-value-val, vagy null/0/false, akkor azt teljesen hagyd ki a @data részből.
|
||||
|
||||
// Szabály: Ami nincs ott, az az alapértelmezett.
|
||||
|
||||
// Token megtakarítás: A FruitBank példádban a GenericAttributes = < GenericAttributeDto[] > (count: 0)[] sorok rengeteg helyet foglalnak. Ha üres, egyszerűen ne küldd el a mezőt.
|
||||
|
||||
//4. String Table helyett: "Object Anchoring"
|
||||
|
||||
//használd az objektum-referenciákat (amit a @ProductDto:1 jelöléssel már el is kezdett a rendszered).
|
||||
|
||||
//Ha ugyanaz a Product szerepel 5 különböző rendelési tételnél, ne írd le ötször.
|
||||
|
||||
// Első alkalommal: ProductDto { ... }
|
||||
|
||||
//Minden további alkalommal: ProductDto = @ProductDto:1
|
||||
|
||||
//[ToonIgnore][ToonDataIgnore]
|
||||
[ToonDescription(Purpose = "Container model for Shipping, Order")]
|
||||
public class FullProcessModel
|
||||
{
|
||||
public List<Shipping> Shippings { get; set; }
|
||||
public List<OrderDto> Orders { get; set; }
|
||||
public List<PreOrder> PreOrders { get; set; }
|
||||
public List<StockTaking> StockTakings { get; set; }
|
||||
public List<EkaerHistory> EkaerHistories { get; set; }
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public sealed class ToonTests
|
||||
{
|
||||
private const int CustomerIdAasdDsserverCom = 6; //aasd@dsserver.com
|
||||
|
||||
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(FruitBankClientTests));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[TestMethod]
|
||||
public async Task GetAnalyzeStringInternCandidatesLog()
|
||||
{
|
||||
var orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
|
||||
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
//options.SetReferenceHandlingUnsafe(ReferenceHandlingMode.OnlyId);
|
||||
var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(orders, options);
|
||||
|
||||
Assert.IsNotNull(analysisLog);
|
||||
Assert.IsGreaterThan(0, analysisLog.Length);
|
||||
|
||||
// Print results sorted by occurrence count
|
||||
Console.WriteLine(analysisLog.ToString());
|
||||
Console.WriteLine();
|
||||
}
|
||||
#endif
|
||||
|
||||
[TestMethod]
|
||||
public async Task OrderDtoToToon()
|
||||
{
|
||||
var a = new FullProcessModel();
|
||||
a.Orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
a.Shippings = (await _signalRClient.GetShippings())!.Where(x=>x.Created > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
|
||||
var toon = AcToonSerializer.Serialize(a, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
//var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
// Note: @ref: only appears when the same object instance is referenced multiple times.
|
||||
// Data from separate API calls typically don't share object instances.
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMetaInfos()
|
||||
{
|
||||
var a = new FullProcessModel();
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||
Console.WriteLine(toon);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void ReferenceHandling_WithSharedReferences_ShouldOutputRefMarkers()
|
||||
{
|
||||
// Create a simple test container with shared references
|
||||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
|
||||
// Create a container that references the same ProductDto twice
|
||||
var container = new TestContainerWithSharedRefs
|
||||
{
|
||||
Product1 = sharedProduct,
|
||||
Product2 = sharedProduct // Same instance, should create @ref
|
||||
};
|
||||
|
||||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferenceHandling_WithSharedIIdReferences_ShouldOutputRefMarkers()
|
||||
{
|
||||
// Create a simple test container with shared references
|
||||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
var sharedProduct2 = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
|
||||
// Create a container that references the same ProductDto twice
|
||||
var container = new TestContainerWithSharedRefs
|
||||
{
|
||||
Product1 = sharedProduct,
|
||||
Product2 = sharedProduct2 // Same instance, should create @ref
|
||||
};
|
||||
|
||||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_ShouldNotContainList1OrGenericTypeNames()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
StringAssert.DoesNotMatch(toon, new System.Text.RegularExpressions.Regex(@"List`?1"), "A @meta.types vagy @types szekcióban nem szerepelhet List`1 vagy generikus típusnév.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_ShouldNotContainDuplicateTypeNames()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var typesLine = toon.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("types = ["));
|
||||
Assert.IsNotNull(typesLine, "Nem található types lista a @meta szekcióban.");
|
||||
var typeNames = typesLine.Substring(typesLine.IndexOf('[') + 1, typesLine.LastIndexOf(']') - typesLine.IndexOf('[') - 1)
|
||||
.Split(',').Select(x => x.Trim(' ', '"')).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
|
||||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
Assert.IsTrue(duplicates.Count == 0, $"A types listában duplikált típusnév található: {string.Join(", ", duplicates)}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_EachTypeShouldBeDefinedOnceInTypesSection()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var typeDefLines = toon.Split('\n').Where(x => x.TrimEnd().EndsWith(": \"Object of type") || x.TrimEnd().EndsWith(": enum") || x.TrimEnd().EndsWith(": \"Object of type "));
|
||||
var typeNames = typeDefLines.Select(x => x.Trim().Split(':')[0]).ToList();
|
||||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
Assert.IsTrue(duplicates.Count == 0, $"A @types szekcióban duplikált típusdefiníció található: {string.Join(", ", duplicates)}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_PropertyTypesShouldNotReferenceList1()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var lines = toon.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Trim().EndsWith(": List`1"))
|
||||
{
|
||||
Assert.Fail($"Property List`1 típusra hivatkozik: {line.Trim()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_PropertyDescriptions_ShouldNotBeRedundantOrMisleading()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>(FruitBankConstClient.DomainDescription);
|
||||
var lines = toon.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Trim().StartsWith("description:") && line.Contains("Collection of Object for"))
|
||||
{
|
||||
Assert.Fail($"Redundáns vagy félrevezető description: {line.Trim()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_NavigationMetadata_ShouldBeComplete()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeMetadata(FruitBankConstClient.DomainDescription, [typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]);
|
||||
Console.WriteLine(toon);
|
||||
Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n");
|
||||
|
||||
var lines = toon.Split('\n').Select(x => x.TrimEnd()).ToList();
|
||||
|
||||
// Ismerten egyirányú kapcsolatok - ezeknél nincs inverse property a másik oldalon
|
||||
// Customer: NopCommerce domain entity, nincs benne Orders kollekció
|
||||
// OrderNotes: OrderNote osztályban nincs Order navigation property
|
||||
// ProductDto: nincs benne OrderItems kollekció
|
||||
// GenericAttributes: polimorf kapcsolat ExpressionPredicate-tel, nincs inverse ÉS nincs egyértelmű FK
|
||||
// FONTOS: Csak az inverse-property hiányát engedjük! Az other-key-nek léteznie kell!
|
||||
var knownUnidirectionalNavigations = new HashSet<string>
|
||||
{
|
||||
"Customer",
|
||||
"OrderNotes",
|
||||
"ProductDto",
|
||||
"GenericAttributes",
|
||||
"ShippingDocumentFile",
|
||||
"Pallet"
|
||||
};
|
||||
|
||||
// GenericAttributes speciális eset - polimorf, nincs other-key sem
|
||||
var knownPolymorphicNavigations = new HashSet<string>
|
||||
{
|
||||
"GenericAttributes"
|
||||
};
|
||||
|
||||
// FK property-k NEM tartalmazhatnak foreign-key attribútumot
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (line.EndsWith("Id: int") || line.EndsWith("Id: int?"))
|
||||
{
|
||||
int j = i + 1;
|
||||
while (j < lines.Count)
|
||||
{
|
||||
if (lines[j].StartsWith(" ") && !lines[j].StartsWith(" "))
|
||||
break;
|
||||
|
||||
if (lines[j].StartsWith(" "))
|
||||
{
|
||||
var metaLine = lines[j].Trim();
|
||||
if (metaLine.StartsWith("foreign-key:"))
|
||||
{
|
||||
Assert.Fail($"FK property nem tartalmazhat foreign-key attribútumot: {line} -> {metaLine}");
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ FK property-k nem tartalmaznak foreign-key attribútumot\n");
|
||||
|
||||
// Számoljuk meg a hiányzó navigation metadatokat
|
||||
var missingMetadata = new List<string>();
|
||||
var skippedUnidirectional = new List<string>();
|
||||
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
|
||||
// Navigation property-k keresése
|
||||
if (line.Contains(": ") &&
|
||||
!line.Contains(": int") && !line.Contains(": string") &&
|
||||
!line.Contains(": DateTime") && !line.Contains(": decimal") &&
|
||||
!line.Contains(": bool") && !line.Contains(": Guid") &&
|
||||
!line.Contains(": double") && !line.Contains(": float") &&
|
||||
!line.Contains("description:") && !line.Contains("purpose:") &&
|
||||
!line.Contains("navigation:") && !line.Contains("foreign-key:") &&
|
||||
!line.Contains("table-name:") && !line.Contains("constraints:") &&
|
||||
!line.Contains("inverse-property:") && !line.Contains("other-key:") &&
|
||||
!line.Contains("primary-key:") && !line.Contains("examples:") &&
|
||||
!line.Contains("@meta") && !line.Contains("@types") &&
|
||||
!line.Contains("version") && !line.Contains("format") && !line.Contains("source-code-language") &&
|
||||
!line.Contains("underlying-type:") && !line.Contains("default-value:") && !line.Contains("values:"))
|
||||
{
|
||||
var propName = line.Split(':')[0].Trim();
|
||||
if (string.IsNullOrEmpty(propName) || propName == "types") continue;
|
||||
|
||||
// Következő sorok metadatainak összegyűjtése
|
||||
var metadata = new HashSet<string>();
|
||||
int j = i + 1;
|
||||
while (j < lines.Count && lines[j].StartsWith(" ") && lines[j].Trim().Contains(':'))
|
||||
{
|
||||
var metaLine = lines[j].Trim();
|
||||
if (metaLine.StartsWith("navigation:")) metadata.Add("navigation");
|
||||
if (metaLine.StartsWith("foreign-key:")) metadata.Add("foreign-key");
|
||||
if (metaLine.StartsWith("inverse-property:")) metadata.Add("inverse-property");
|
||||
if (metaLine.StartsWith("other-key:")) metadata.Add("other-key");
|
||||
j++;
|
||||
}
|
||||
|
||||
// Ha van navigation attribútum, ellenőrizzük a szükséges metadatokat
|
||||
if (metadata.Contains("navigation"))
|
||||
{
|
||||
var navLine = lines.Skip(i + 1).FirstOrDefault(x => x.Trim().StartsWith("navigation:"));
|
||||
if (navLine != null)
|
||||
{
|
||||
var isUnidirectional = knownUnidirectionalNavigations.Contains(propName);
|
||||
var isPolymorphic = knownPolymorphicNavigations.Contains(propName);
|
||||
|
||||
if (navLine.Contains("many-to-one"))
|
||||
{
|
||||
if (!metadata.Contains("foreign-key"))
|
||||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik foreign-key");
|
||||
|
||||
if (!metadata.Contains("inverse-property"))
|
||||
{
|
||||
if (isUnidirectional)
|
||||
skippedUnidirectional.Add($"{propName} (ManyToOne): egyirányú kapcsolat, nincs inverse");
|
||||
else
|
||||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik inverse-property");
|
||||
}
|
||||
}
|
||||
else if (navLine.Contains("one-to-many"))
|
||||
{
|
||||
// other-key: polimorf kapcsolatoknál nem kötelező
|
||||
if (!metadata.Contains("other-key"))
|
||||
{
|
||||
if (isPolymorphic)
|
||||
{
|
||||
skippedUnidirectional.Add($"{propName} (OneToMany): polimorf kapcsolat, nincs other-key");
|
||||
}
|
||||
else
|
||||
{
|
||||
// DEBUG: részletes info
|
||||
Console.WriteLine($"\n[DEBUG] {propName} (OneToMany) - other-key hiányzik!");
|
||||
|
||||
// Keressük meg a property típusát a Toon outputban
|
||||
var propTypePart = line.Split(':').LastOrDefault()?.Trim() ?? "";
|
||||
Console.WriteLine($" Property type: {propTypePart}");
|
||||
|
||||
// Ha List<X> formátum, keressük meg X-et
|
||||
if (propTypePart.StartsWith("List<") && propTypePart.EndsWith(">"))
|
||||
{
|
||||
var elementTypeName = propTypePart.Substring(5, propTypePart.Length - 6);
|
||||
Console.WriteLine($" Element type: {elementTypeName}");
|
||||
|
||||
// Keressük meg az element type definícióját
|
||||
var elementTypeDefIndex = lines.FindIndex(l => l.Trim().StartsWith($"{elementTypeName}:"));
|
||||
if (elementTypeDefIndex >= 0)
|
||||
{
|
||||
Console.WriteLine($" Element type definition found at line {elementTypeDefIndex}");
|
||||
// Listázzuk ki az element type property-jeit amik "Id"-re végződnek
|
||||
for (int k = elementTypeDefIndex + 1; k < lines.Count; k++)
|
||||
{
|
||||
var propLine = lines[k];
|
||||
if (!propLine.StartsWith(" ")) break; // Új típus definíció
|
||||
if (propLine.StartsWith(" ")) continue; // Metaadat, skip
|
||||
|
||||
var trimmed = propLine.Trim();
|
||||
if (trimmed.EndsWith(": int") && trimmed.Contains("Id"))
|
||||
{
|
||||
Console.WriteLine($" FK candidate: {trimmed}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
missingMetadata.Add($"{propName} (OneToMany): hiányzik other-key");
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.Contains("inverse-property"))
|
||||
{
|
||||
if (isUnidirectional)
|
||||
skippedUnidirectional.Add($"{propName} (OneToMany): egyirányú kapcsolat, nincs inverse");
|
||||
else
|
||||
missingMetadata.Add($"{propName} (OneToMany): hiányzik inverse-property");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedUnidirectional.Count > 0)
|
||||
{
|
||||
Console.WriteLine("EGYIRÁNYÚ/POLIMORF KAPCSOLATOK (nem hiba):");
|
||||
foreach (var skipped in skippedUnidirectional)
|
||||
{
|
||||
Console.WriteLine($" ℹ {skipped}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
if (missingMetadata.Count > 0)
|
||||
{
|
||||
Console.WriteLine("HIÁNYZÓ METAADATOK:");
|
||||
foreach (var missing in missingMetadata)
|
||||
{
|
||||
Console.WriteLine($" - {missing}");
|
||||
}
|
||||
Assert.Fail($"Hiányzó navigation metaadatok: {missingMetadata.Count} db");
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test helper class to verify reference handling with shared object instances.
|
||||
/// </summary>
|
||||
public class TestContainerWithSharedRefs
|
||||
{
|
||||
public ProductDto? Product1 { get; set; }
|
||||
public ProductDto? Product2 { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Test debugger script for JsonExtensionTests
|
||||
$projectPath = "H:\Applications\Mango\Source\FruitBankHybridApp"
|
||||
Set-Location $projectPath
|
||||
|
||||
Write-Host "Building test project..."
|
||||
dotnet build FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj -c Debug
|
||||
|
||||
Write-Host "`nRunning JsonExtensionTests..."
|
||||
# Use --no-build to avoid the MSBuild conflict
|
||||
dotnet test FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj `
|
||||
--no-build `
|
||||
-c Debug `
|
||||
--filter "ClassName=FruitBankHybrid.Shared.Tests.JsonExtensionTests" `
|
||||
2>&1 | Tee-Object -FilePath "test_results.txt"
|
||||
|
||||
Write-Host "`n=== Test Results ==="
|
||||
Get-Content "test_results.txt" | Select-String -Pattern "FAILED|PASSED|Error|Assert" | tail -50
|
||||
|
|
@ -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,23 +26,10 @@
|
|||
<Columns>
|
||||
<MgGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true"/>
|
||||
<DxGridDataColumn FieldName="PartnerId" Caption="Partner" Visible="@(!ParentDataItemIsPartner)" ReadOnly="@ParentDataItemIsPartner">
|
||||
<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)"
|
||||
<EditSettings>
|
||||
<DxComboBoxSettings Data="Partners"
|
||||
ValueFieldName="Id"
|
||||
TextFieldName="Name"
|
||||
DropDownBodyCssClass="dd-body-class"
|
||||
ListRenderMode="ListRenderMode.Entire"
|
||||
SearchMode="ListSearchMode.AutoSearch"
|
||||
|
|
@ -53,36 +40,8 @@
|
|||
<DxListEditorColumn FieldName="@nameof(Partner.Name)" />
|
||||
<DxListEditorColumn FieldName="@nameof(Partner.TaxId)" />
|
||||
</Columns>
|
||||
</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>
|
||||
</DxComboBoxSettings>
|
||||
</EditSettings>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Shipping.ShippingDate" Caption="Beérkezés" ReadOnly="true" />
|
||||
<DxGridDataColumn FieldName="ShippingId" Caption="Shipping" Visible="@(!ParentDataItemIsShipping)" ReadOnly="@ParentDataItemIsShipping">
|
||||
|
|
@ -226,9 +185,6 @@
|
|||
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());
|
||||
|
|
@ -267,13 +223,9 @@
|
|||
{
|
||||
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())
|
||||
|
|
@ -317,16 +269,6 @@
|
|||
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;
|
||||
|
|
@ -334,13 +276,15 @@
|
|||
|
||||
private async Task Callback(ToolbarItemClickEventArgs obj)
|
||||
{
|
||||
if (windowVisible) await windowRef.CloseAsync();
|
||||
else await windowRef.ShowAsync();
|
||||
if (windowVisible)
|
||||
await windowRef.CloseAsync();
|
||||
else
|
||||
await windowRef.ShowAsync();
|
||||
}
|
||||
|
||||
private void Callback2(WindowClosingEventArgs obj)
|
||||
{
|
||||
ReloadDataFromDb(true).Forget(_logger);
|
||||
ReloadDataFromDb(true).Forget();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
@using System.Collections.ObjectModel
|
||||
@using AyCode.Blazor.Components.Components.Grids
|
||||
@using AyCode.Core.Helpers
|
||||
@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>
|
||||
<GridCargoPartnerBase @ref="Grid"
|
||||
DataSource="CargoPartners"
|
||||
AutoSaveLayoutName="GridCargoPartner"
|
||||
SignalRClient="FruitBankSignalRClient"
|
||||
Logger="_logger"
|
||||
CssClass="@GridCss"
|
||||
ValidationEnabled="false"
|
||||
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
|
||||
|
||||
<DxGridDataColumn FieldName="Name" />
|
||||
<DxGridDataColumn FieldName="TaxId" />
|
||||
<DxGridDataColumn FieldName="CertificationNumber" />
|
||||
<DxGridDataColumn FieldName="@nameof(CargoPartner.Currency)" />
|
||||
<DxGridDataColumn FieldName="@nameof(CargoPartner.CountryCode)" />
|
||||
<DxGridDataColumn FieldName="PostalCode" />
|
||||
<DxGridDataColumn FieldName="@nameof(CargoPartner.Country)" />
|
||||
<DxGridDataColumn FieldName="State" />
|
||||
<DxGridDataColumn FieldName="County" />
|
||||
<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 cargoPartner = ((CargoPartner)context.DataItem);
|
||||
var shippings = new AcObservableCollection<Shipping>(); //cargoPartner?.Shippings ?? [];
|
||||
var cargoTrucks = cargoPartner?.CargoTrucks ?? [];
|
||||
|
||||
<DxTabs>
|
||||
<DxTabPage Text="Kamionok">
|
||||
@{
|
||||
var observableCargoTruck = new AcObservableCollection<CargoTruck>(cargoTrucks);
|
||||
<GridCargoTruck ParentDataItem="@cargoPartner" CargoTrucks="@observableCargoTruck"></GridCargoTruck>
|
||||
}
|
||||
</DxTabPage>
|
||||
|
||||
@* <DxTabPage Text="Szállítmányok">
|
||||
@{
|
||||
//var observableShippings = new AcObservableCollection<Shipping>(shippings);
|
||||
<GridShipping Shippings="@shippings" IsMasterGrid="false" />
|
||||
}
|
||||
</DxTabPage>
|
||||
*@ </DxTabs>
|
||||
}
|
||||
</DetailRowTemplate>
|
||||
<ToolbarTemplate>
|
||||
@if (IsMasterGrid)
|
||||
{
|
||||
<MgGridToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" />
|
||||
}
|
||||
</ToolbarTemplate>
|
||||
</GridCargoPartnerBase>
|
||||
</GridContent>
|
||||
</MgGridWithInfoPanel>
|
||||
|
||||
@code {
|
||||
//[Inject] public required ObjectLock ObjectLock { get; set; }
|
||||
[Inject] public required DatabaseClient Database { get; set; }
|
||||
|
||||
[Parameter] public bool IsMasterGrid { get; set; } = false;
|
||||
[Parameter] public AcObservableCollection<CargoPartner>? CargoPartners { get; set; }
|
||||
[Parameter] public AcObservableCollection<Shipping>? Shippings { get; set; }
|
||||
|
||||
|
||||
const string ExportFileName = "ExportResult";
|
||||
string GridSearchText = "";
|
||||
bool EditItemsEnabled { get; set; }
|
||||
int FocusedRowVisibleIndex { get; set; }
|
||||
public GridCargoPartnerBase Grid { get; set; }
|
||||
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
|
||||
|
||||
private int _activeTabIndex;
|
||||
private LoggerClient<GridCargoPartner> _logger;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_logger = new LoggerClient<GridCargoPartner>(LogWriters.ToArray());
|
||||
await ReloadDataFromDb(false);
|
||||
}
|
||||
|
||||
private async Task ReloadDataFromDb(bool forceReload = false)
|
||||
{
|
||||
if (!IsMasterGrid) return;
|
||||
|
||||
if (Grid == null) return;
|
||||
|
||||
using (await ObjectLock.GetSemaphore<CargoPartner>().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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
using AyCode.Core.Interfaces;
|
||||
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.Cargos;
|
||||
|
||||
public class GridCargoPartnerBase: FruitBankGridBase<CargoPartner>, IGrid
|
||||
{
|
||||
private bool _isFirstInitializeParameterCore;
|
||||
private bool _isFirstInitializeParameters;
|
||||
|
||||
public GridCargoPartnerBase() : base()
|
||||
{
|
||||
GetAllMessageTag = SignalRTags.GetCargoPartners;
|
||||
AddMessageTag = SignalRTags.AddCargoPartner;
|
||||
UpdateMessageTag = SignalRTags.UpdateCargoPartner;
|
||||
|
||||
//RemoveMessageTag = SignalRTags.;
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
@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>
|
||||
<GridCargoTruckBase @ref="Grid"
|
||||
DataSource="CargoTrucks"
|
||||
ParentDataItem="ParentDataItem"
|
||||
AutoSaveLayoutName="GridCargoTruck"
|
||||
SignalRClient="FruitBankSignalRClient"
|
||||
Logger="_logger"
|
||||
CssClass="@GridCss"
|
||||
ValidationEnabled="true"
|
||||
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
|
||||
|
||||
<DxGridDataColumn FieldName="@nameof(CargoTruck.LicencePlate)" />
|
||||
<DxGridDataColumn FieldName="@nameof(CargoTruck.CountryCode)" />
|
||||
<DxGridDataColumn FieldName="@nameof(CargoTruck.IsTrailer)" />
|
||||
|
||||
<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>
|
||||
@if (IsMasterGrid)
|
||||
{
|
||||
<MgGridToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" />
|
||||
}
|
||||
</ToolbarTemplate>
|
||||
</GridCargoTruckBase>
|
||||
</GridContent>
|
||||
</MgGridWithInfoPanel>
|
||||
|
||||
@code {
|
||||
//[Inject] public required ObjectLock ObjectLock { get; set; }
|
||||
[Inject] public required DatabaseClient Database { get; set; }
|
||||
|
||||
[Parameter] public AcObservableCollection<CargoTruck>? CargoTrucks { get; set; }
|
||||
|
||||
const string ExportFileName = "ExportResult";
|
||||
string GridSearchText = "";
|
||||
bool EditItemsEnabled { get; set; }
|
||||
int FocusedRowVisibleIndex { get; set; }
|
||||
public GridCargoTruckBase Grid { get; set; }
|
||||
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
|
||||
|
||||
[Parameter] public IId<int>? ParentDataItem { get; set; }
|
||||
|
||||
public bool IsMasterGrid => ParentDataItem == null;
|
||||
public bool ParentDataItemIsCargoPartner => (ParentDataItem is CargoPartner);
|
||||
|
||||
|
||||
private int _activeTabIndex;
|
||||
private LoggerClient<GridCargoTruck> _logger;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_logger = new LoggerClient<GridCargoTruck>(LogWriters.ToArray());
|
||||
await ReloadDataFromDb(false);
|
||||
}
|
||||
|
||||
private async Task ReloadDataFromDb(bool forceReload = false)
|
||||
{
|
||||
if (!IsMasterGrid) return;
|
||||
|
||||
if (Grid == null) return;
|
||||
|
||||
using (await ObjectLock.GetSemaphore<CargoTruck>().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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
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.Cargos;
|
||||
|
||||
public class GridCargoTruckBase: FruitBankGridBase<CargoTruck>, IGrid
|
||||
{
|
||||
private bool _isFirstInitializeParameterCore;
|
||||
private bool _isFirstInitializeParameters;
|
||||
|
||||
public GridCargoTruckBase() : base()
|
||||
{
|
||||
//GetAllMessageTag = SignalRTags.GetCargoTrucks;
|
||||
AddMessageTag = SignalRTags.AddCargoTruck;
|
||||
UpdateMessageTag = SignalRTags.UpdateCargoTruck;
|
||||
|
||||
//RemoveMessageTag = SignalRTags.;
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (GetAllMessageTag > 0) return;
|
||||
|
||||
if (IsMasterGrid) GetAllMessageTag = SignalRTags.GetCargoTrucks;
|
||||
else
|
||||
{
|
||||
if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id];
|
||||
|
||||
switch (ParentDataItem)
|
||||
{
|
||||
case ICargoPartner:
|
||||
GetAllMessageTag = SignalRTags.GetCargoTrucksByCargoPartnerId;
|
||||
if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(CargoTruck.CargoPartnerId);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
@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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
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<int>-et explicit partial-lal teljesíti (Id = ItemExternalId-ból számítva).
|
||||
/// </summary>
|
||||
public class GridEkaerDetailBase : FruitBankGridBase<TradeCardItemType>, IGrid
|
||||
{
|
||||
public GridEkaerDetailBase() : base()
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue