Compare commits

..

No commits in common. "dbc7fd31d394db57083aac7a7878a851b15f70e8" and "7fc6c8a8ef44abfc4da3d1eb2d40ad71e260cb51" have entirely different histories.

31 changed files with 40 additions and 824 deletions

View File

@ -97,10 +97,7 @@
"PowerShell(Remove-Item \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Loggers\\\\FbLogItem.cs\" -Confirm:$false)",
"PowerShell(\"=== Server ===\")",
"PowerShell(\"=== Generated ===\")",
"PowerShell(\\(Get-Content \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Blazor\\\\AyCode.Blazor.Components\\\\docs\\\\MGGRID\\\\MGGRID_TODO.md\" | Measure-Object -Line\\).Lines)",
"PowerShell(\"=== FruitBank.Common ===\")",
"Bash(git check-ignore *)",
"PowerShell(dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common.Server\\\\FruitBank.Common.Server.csproj\" -v q --nologo 2>&1 | Select-String -Pattern \"error CS\" | Select-Object -First 20; \"--- CS hibak szama: ---\"; \\(dotnet build \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common.Server\\\\FruitBank.Common.Server.csproj\" -v q --nologo 2>&1 | Select-String -Pattern \"error CS\" | Measure-Object\\).Count)"
"PowerShell(\\(Get-Content \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Blazor\\\\AyCode.Blazor.Components\\\\docs\\\\MGGRID\\\\MGGRID_TODO.md\" | Measure-Object -Line\\).Lines)"
]
}
}

View File

@ -74,59 +74,6 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
history.Partner = partner;
}
public async Task<EkaerHistory> SendEkaerAsync(EkaerHistory ekaerHistory, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(ekaerHistory);
// Csak legenerált, blokkoló hiba nélküli (warninggal is) sor küldhető — a kliens is így gátol, de itt is védünk.
if (!ekaerHistory.Status.IsSubmittable() || string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc))
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = "[Error] A bejelentés nem küldhető be: nincs legenerált, érvényes XML — előbb Generate.";
return ekaerHistory;
}
// A warning-eredetet a küldés ELŐTT rögzítjük (siker esetén ez dönt Sent vs SentWithMissingData között).
var hadWarning = ekaerHistory.Status == EkaerStatus.GeneratedWithWarning;
try
{
// A TÁROLT tradeCard-ot küldjük (amit a Generate előállított + validált), create-műveletként.
var tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc);
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
var result = await _submitService.SubmitAsync([operation], cancellationToken).ConfigureAwait(false);
// Elvileg nem fordul elő (a submittable sort a Generate már validálta), de defenzíven kezeljük.
if (!result.IsValid)
{
ekaerHistory.Status = EkaerStatus.ValidationError;
ekaerHistory.ErrorText = string.Join(Environment.NewLine, result.ValidationErrors.Select(e => $"[Error] {e.ErrorMessage}"));
return ekaerHistory;
}
// Siker: a NAV visszaadta az EKÁER-számot (tcn). Warningos generálásból SentWithMissingData (pótlásra vár), egyébként Sent.
ekaerHistory.EkaerNumber = result.Response?.TradeCardOperationsResults.FirstOrDefault()?.TradeCardInfo?.Tcn;
ekaerHistory.SentDate = DateTime.UtcNow;
ekaerHistory.Status = hadWarning ? EkaerStatus.SentWithMissingData : EkaerStatus.Sent;
// Tiszta beküldés → nincs üzenet; warningnál a meglévő szöveg (pl. hiányzó rendszám) MARAD (a pótlandó adat jelzése).
if (!hadWarning) ekaerHistory.ErrorText = null;
return ekaerHistory;
}
catch (NavReportException ex)
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = $"[Error] NAV ({ex.ReasonCode ?? ex.HttpStatusCode?.ToString() ?? "hiba"}): {ex.Message}";
return ekaerHistory;
}
catch (Exception ex)
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = $"[Error] Beküldési hiba: {ex.Message}";
return ekaerHistory;
}
}
/// <summary>Depot-kapu (T-D8R4): a PartnerDepot az EKÁER-hez kötelező — a felrakodási hely a depot címe. Hiányzó
/// vagy be nem töltött telephelynél a leképezés ELŐTT ValidationError (nincs félrevezető, partner-központi-címes XML).
/// <c>null</c> = rendben, mehet a generálás.</summary>

View File

@ -46,15 +46,6 @@ public interface IFruitBankEkaerService
/// <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>
/// Beküldi egy legenerált (<see cref="EkaerStatus.Generated"/> / <see cref="EkaerStatus.GeneratedWithWarning"/>)
/// <see cref="EkaerHistory"/> TÁROLT tradeCard XML-jét a NAV-nak (create-művelet). A rekordot tölti: siker →
/// <see cref="EkaerStatus.Sent"/> (tiszta) vagy <see cref="EkaerStatus.SentWithMissingData"/> (warningos generálásból,
/// pl. hiányzó rendszám) + <c>EkaerNumber</c> (a NAV tcn) + <c>SentDate</c>; NAV-/transport-hiba →
/// <see cref="EkaerStatus.SendError"/> + <c>ErrorText</c>. NEM perzisztál — a mentés a hívó SignalR endpoint dolga.
/// </summary>
Task<EkaerHistory> SendEkaerAsync(EkaerHistory ekaerHistory, CancellationToken cancellationToken = default);
/// <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>

View File

@ -15,14 +15,10 @@ namespace FruitBank.Common.Server.Services.SignalRs;
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
{
private readonly IFruitBankDataControllerServer _fruitBankDataController;
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
{
_fruitBankDataController = fruitBankDataController;
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
SerializerOptions = new AcBinarySerializerOptions();
@ -34,32 +30,6 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
DynamicMethodRegistry.Register(stockSignalREndpointServer);
}
/// <summary>
/// ENV-announce (push-on-connect): minden új kapcsolat azonnal megkapja a szerver PROD/DEV besorolását
/// (ServerEnvironment tag). A kliens a tárolt értékével veti össze — eltérés (= a szerver restart közben
/// MÁSIK DB-re állt) esetén riaszt. Így a kliensnek NEM kell reconnectkor újra lekérdeznie ("csak az elején 1x").
/// Best-effort: hibája nem dönti be a kapcsolatot — az autoritatív kapu a kliens első-connect lekérdezése.
/// </summary>
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
try
{
var signalParams = new SignalParams
{
Status = SignalResponseStatus.Success,
DataSerializerType = AyCode.Core.Serializers.AcSerializerType.Binary
};
await Clients.Caller.OnReceiveMessage(SignalRTags.ServerEnvironment, null, signalParams, await _fruitBankDataController.IsProdEnvironment());
}
catch (Exception ex)
{
Logger.Error($"ServerEnvironment announce failed; ConnectionId: {Context.ConnectionId}", ex);
}
}
protected override void LogContextUserNameAndId()
{
return;

View File

@ -79,12 +79,12 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.GrossWeight), 1)")]
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
public double GrossWeight
{
get
{
return double.Round(OrderItemPallets.Sum(x => x.GrossWeight), 1);
return double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1);
//CommonHelper.To<double>(GenericAttributes.SingleOrDefault(x => x.Key == nameof(IMeasuringGrossWeight.GrossWeight))?.Value ?? "0");
}
set

View File

@ -7,40 +7,21 @@ public static class FruitBankConstClient
{
public static string DefaultLocale = "en-US";
// DELEGÁT az AcEnv.BaseUrl-re (AyCode.Core framework, 2026-07): az érték EGY helyen, a kereten él
// (fail-fast: hidratálatlan olvasás exception). A hostok induláskor hidratálják az appsettings
// "AyCode:Urls:BaseUrl"-ből; az fbHub és a loggerHub URL-je is ebből származik (+/fbHub, +/loggerHub).
// Környezet-értékek: dev https://localhost:59579 | prod https://shop.fruitbank.hu | test https://fruitbank.mangoweb.hu | android http://10.0.2.2:59579
// A nop szerver base URL-je. Induláskor a host tölti fel az appsettings "AyCode:Urls:BaseUrl"-ből
// (ld. Program.cs / MauiProgram.cs). Az fbHub ÉS a loggerHub is EBBŐL az egy értékből származik (+/fbHub, +/loggerHub).
// Default: null — ha nincs beállítva, az OLVASÁS exception-t dob, nem megy tovább csendben hiányzó/rossz URL-lel.
// A környezet-váltás az appsettings-ben történik:
// dev: https://localhost:59579 | prod: https://shop.fruitbank.hu | test: https://fruitbank.mangoweb.hu | android: http://10.0.2.2:59579
private static string? _baseUrl;
public static string BaseUrl
{
get => AcEnv.BaseUrl;
set => AcEnv.BaseUrl = value;
get => !string.IsNullOrWhiteSpace(_baseUrl)
? _baseUrl
: throw new InvalidOperationException("FruitBankConstClient.BaseUrl nincs beállítva — a host indulásakor hidratáld az appsettings 'AyCode:Urls:BaseUrl' értékéből (Program.cs / MauiProgram.cs).");
set => _baseUrl = value;
}
// A szerver-környezet (PROD/DEV) — az érték KIZÁRÓLAG a szerver IsProdEnvironment() válaszából jöhet
// (a szerver a DB-katalógusból sorolja be magát, fail-closed módon), URL-alakból/configból SOHA.
// Az EGYETLEN értékadási pont: FruitBankSignalRClient (kapcsolat-felépüléskor hidratál, ld. EnsureEnvironmentAsync).
// Hidratálatlan olvasás → exception (fail fast); sikertelen lekérdezés → marad hidratálatlan (fail closed).
private static bool? _isProd;
public static bool IsProd
=> _isProd ?? throw new InvalidOperationException(
"FruitBankConstClient.IsProd nincs hidratálva — a FruitBankSignalRClient a kapcsolat felépülésekor tölti fel a szerver IsProdEnvironment() válaszából (await client.EnsureEnvironmentAsync()).");
/// <summary>Az IsProd TÉNYLEGES változásakor tüzel (első hidratálás + announce-detektált env-váltás) —
/// a megjelenítők (title-badge, UI-sáv) feliratkozási pontja. Az értékadás forrása változatlanul EGY hely.</summary>
public static event Action<bool>? IsProdChanged;
public static void SetIsProd(bool isProd)
{
var changed = _isProd != isProd;
_isProd = isProd;
if (changed) IsProdChanged?.Invoke(isProd);
}
/// <summary>True, ha az IsProd már hidratált — kapu-logikához (első-connect await) és a push-összevetéshez.</summary>
public static bool IsProdHydrated => _isProd != null;
public static string DefaultHubName = "fbHub";
public static string LoggerHubName = "loggerHub";

View File

@ -35,7 +35,6 @@ public interface IFruitBankDataControllerCommon
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId);
public Task<EkaerHistory?> SendEkaerToNav(int ekaerHistoryId);
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);

View File

@ -127,15 +127,11 @@ public class SignalRTags : AcSignalRTags
public const int CreateEkaerHistory = 191;
public const int CreateMissingEkaerHistories = 192;
public const int GetEkaerHistoryCount = 193;
public const int SendEkaerToNav = 194;
public const int AuthenticateUser = 195;
public const int RefreshToken = 200;
public const int IsProdEnvironment = 210;
/// <summary>Szerver→kliens ENV-announce (push-on-connect): minden új kapcsolat megkapja a szerver PROD/DEV
/// besorolását. A kliens BELSŐLEG kezeli (FruitBankSignalRClient — SetIsProd + eltérésnél riasztás), nem app-esemény.</summary>
public const int ServerEnvironment = 211;
#region SendToClient
public const int SendOrderChanged = 500;

View File

@ -27,12 +27,11 @@ namespace FruitBankHybrid.Shared.Tests
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
#region Shipping

View File

@ -12,12 +12,11 @@ namespace FruitBankHybrid.Shared.Tests
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests));
}
#region EkaerHistory / Create

View File

@ -76,10 +76,4 @@
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
</ItemGroup>
<!-- A KANONIKUS Shared appsettings.json linkelve — a teszt BaseUrl-hidratálása (TestEnvironment) ugyanabból
az egy forrásból, mint a hostoké; nincs külön teszt-config. -->
<ItemGroup>
<Content Include="..\FruitBankHybrid.Shared\appsettings.json" Link="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -18,12 +18,11 @@ namespace FruitBankHybrid.Shared.Tests
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankPartnerTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
#region Partner

View File

@ -186,12 +186,11 @@ public sealed class JsonExtensionTests
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(JsonExtensionTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
[TestMethod]

View File

@ -24,12 +24,11 @@ public sealed class OrderClientTests
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests));
}

View File

@ -1,38 +0,0 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using FruitBank.Common;
namespace FruitBankHybrid.Shared.Tests;
/// <summary>
/// Assembly-szintű környezet-hidratálás: a BaseUrl a KANONIKUS FruitBankHybrid.Shared\appsettings.json-ból jön
/// (a csproj Content-linkje másolja a teszt-outputba) — ugyanaz az egy forrás, mint a hostoknál; nincs külön
/// teszt-config és nincs hardcode. Hiányzó fájl/kulcs → azonnali exception (fail fast), pontosan mint a hostoknál.
/// A PROD-védelem NEM itt van: azt a TestServerGuard adja, a szerver saját IsProdEnvironment() bevallása alapján.
/// </summary>
internal static class TestEnvironment
{
[ModuleInitializer]
internal static void Init()
{
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (!File.Exists(path))
throw new InvalidOperationException(
$"appsettings.json nem található a teszt-outputban ({path}) — a csproj Content-linkje másolja a FruitBankHybrid.Shared kanonikus fájlját.");
// A kanonikus appsettings kommenteket is tartalmaz → engedékeny parse.
using var doc = JsonDocument.Parse(File.ReadAllText(path),
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true });
var baseUrl = doc.RootElement.TryGetProperty("AyCode", out var ayCode)
&& ayCode.TryGetProperty("Urls", out var urls)
&& urls.TryGetProperty("BaseUrl", out var url)
? url.GetString()
: null;
FruitBankConstClient.BaseUrl = baseUrl
?? throw new InvalidOperationException(
"Hiányzik az 'AyCode:Urls:BaseUrl' az appsettings-ből — a teszt-kliens hub-URL-je ebből származik.");
}
}

View File

@ -1,21 +0,0 @@
using FruitBank.Common;
using FruitBankHybrid.Shared.Services.SignalRs;
namespace FruitBankHybrid.Shared.Tests;
/// <summary>
/// Szerver-környezet guard a szerver-routolt tesztekhez: a döntés KIZÁRÓLAG a szerver saját bevallásán
/// (IsProdEnvironment — a szerver a DB-katalógusából sorolja be magát, fail-closed) alapul; URL-alakból
/// SOHA nem következtetünk. PROD vagy nem-igazolható környezet → exception, a teszt el sem indul (fail closed).
/// </summary>
internal static class TestServerGuard
{
public static async Task EnsureNotProdAsync(FruitBankSignalRClient client)
{
// Timeout / kapcsolat-hiba / szerver-hiba → a hidratálás kivétele ide propagál → a teszt bukik.
await client.EnsureEnvironmentAsync();
if (FruitBankConstClient.IsProd)
throw new InvalidOperationException("PROD szerveren NEM tesztelünk! (a szerver IsProdEnvironment() == true)");
}
}

View File

@ -99,12 +99,11 @@ public sealed class ToonTests
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public async Task TestInit()
public void TestInit()
{
_signalRClient = TestSignalRClientFactory.Create(nameof(ToonTests));
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
// Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed).
await TestServerGuard.EnsureNotProdAsync(_signalRClient);
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
#if DEBUG

View File

@ -73,7 +73,7 @@
<DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
<DxGridDataColumn Caption="Műveletek" Width="290" AllowSort="false" AllowGroup="false">
<DxGridDataColumn Caption="Műveletek" Width="200" AllowSort="false" AllowGroup="false">
<CellDisplayTemplate>
@{
var row = (EkaerHistory)context.DataItem;
@ -89,12 +89,6 @@
Enabled="@(CanDownload(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = CanDownload(row) ? "A generált XML letöltése fájlként (kézi NAV-beadáshoz)" : "Hibás bejelentés nem tölthető le — javítsd az adatokat és generáld újra." })"
Click="async () => await OnDownloadClick(row)" />
<DxButton Text="Beküldés"
SizeMode="SizeMode.Small"
RenderStyle="ButtonRenderStyle.Success"
Enabled="@(CanSend(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = CanSend(row) ? "A legenerált bejelentés beküldése a NAV-nak" : "Csak legenerált, hibátlan bejelentés küldhető be." })"
Click="async () => await OnSendClick(row)" />
}
</CellDisplayTemplate>
</DxGridDataColumn>
@ -225,12 +219,6 @@
private static bool CanDownload(EkaerHistory ekaerHistory)
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc);
private readonly HashSet<int> _sendingIds = [];
// Ugyanaz az előfeltétel, mint a letöltésnél (legenerálva, blokkoló hiba nélkül) + nincs épp folyamatban küldés.
private bool CanSend(EkaerHistory ekaerHistory)
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc) && !_sendingIds.Contains(ekaerHistory.Id);
// A szülő-oldal (fül-számlálók) értesítése egy státuszt érintő művelet után. Üres delegáltnál no-op.
private Task NotifyDataChanged() => OnDataChanged.HasDelegate ? OnDataChanged.InvokeAsync() : Task.CompletedTask;
@ -269,50 +257,6 @@
}
}
private async Task OnSendClick(EkaerHistory ekaerHistory)
{
if (!CanSend(ekaerHistory)) return;
// A beküldés valós, kifelé irányuló művelet (a NAV-nál rögzít) — megerősítés kötelező.
var confirmed = await DialogService.ShowConfirmBoxAsync(
"EKÁER beküldés",
$"Biztosan beküldöd a NAV-nak? (#{ekaerHistory.Id} — {ekaerHistory.Partner})",
MessageBoxRenderStyle.Warning);
if (!confirmed) return;
if (!_sendingIds.Add(ekaerHistory.Id)) return;
await OnBusyChanged.InvokeAsync(true); // Loading panel BE — a NAV-hívás lassú lehet
try
{
var updated = await FruitBankSignalRClient.SendEkaerToNav(ekaerHistory.Id);
if (updated == null)
{
_logger.Error($"SendEkaerToNav null választ adott; EkaerHistory #{ekaerHistory.Id}");
return;
}
// A sor frissítése helyben — a grid ugyanazt a példányt mutatja.
ekaerHistory.Status = updated.Status;
ekaerHistory.ErrorText = updated.ErrorText;
ekaerHistory.EkaerNumber = updated.EkaerNumber;
ekaerHistory.SentDate = updated.SentDate;
ekaerHistory.Modified = updated.Modified;
}
catch (Exception ex)
{
_logger.Error($"SendEkaerToNav hiba; EkaerHistory #{ekaerHistory.Id}", ex);
}
finally
{
_sendingIds.Remove(ekaerHistory.Id);
await OnBusyChanged.InvokeAsync(false); // Loading panel KI
Grid?.Reload();
await NotifyDataChanged(); // a fül-számlálók frissítése (a sor átkerülhet az „Elküldött" tabba)
}
}
DateTime CreateFromDate { get; set; } = DateTime.Today.AddDays(-3);
private bool _creatingMissing;

View File

@ -1,8 +1,6 @@
using AyCode.Core.Consts;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using FruitBank.Common;
using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions;
using DevExpress.Blazor;
@ -11,7 +9,6 @@ using FruitBank.Common.Models;
using FruitBank.Common.Models.SignalRs;
using FruitBank.Common.SignalRs;
using FruitBankHybrid.Shared.Pages;
using FruitBankHybrid.Shared.Services;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Mango.Nop.Core.Loggers;
@ -26,7 +23,6 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
[Inject] public required LoggedInModel LoggedInModel { get; set; }
[Inject] IToastNotificationService ToastService { get; set; }
[Inject] public required FruitBankSignalRClient FruitBankSignalRClient { get; set; }
[Inject] public required IAppTitleService AppTitleService { get; set; }
private EventCallback RefreshMainLayoutEventCallback => EventCallback.Factory.Create(this, RefreshMainLayout);
private NavMenu _navMenu = null!;
@ -51,16 +47,8 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
LoggedInModel.GetRolesFunc = FruitBankSignalRClient.GetCustomerRolesByCustomerId;
FruitBankSignalRClient.OnMessageReceived += SignalRClientOnMessageReceived;
// Env-badge a címsorba (a nop-szerver Console.Title mintája): azonnal, ha az IsProd már hidratált,
// és feliratkozva a változásra (első hidratálás / announce-detektált env-váltás → riasztó title-frissítés).
// A hidratálást nem itt indítjuk — az első SignalR-hívás connect-kapuja végzi (pontosan 1 hívás).
FruitBankConstClient.IsProdChanged += OnIsProdChanged;
if (FruitBankConstClient.IsProdHydrated) OnIsProdChanged(FruitBankConstClient.IsProd);
}
private void OnIsProdChanged(bool isProd) => AppTitleService.SetEnvironmentBadgeAsync(isProd).Forget();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
@ -144,6 +132,5 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
public void Dispose()
{
FruitBankSignalRClient.OnMessageReceived -= SignalRClientOnMessageReceived;
FruitBankConstClient.IsProdChanged -= OnIsProdChanged;
}
}

View File

@ -1,11 +0,0 @@
namespace FruitBankHybrid.Shared.Services;
/// <summary>
/// Platform címsor-szolgáltatás az env-badge-hez — a nop-szerver Console.Title mintájának kliens-párja
/// ("[PROD]/[DEV] FruitBank"). Host-onkénti implementáció (az IFormFactor/ISecureCredentialService mintára):
/// browser document.title / MAUI Window.Title / szerver-prerender no-op.
/// </summary>
public interface IAppTitleService
{
Task SetEnvironmentBadgeAsync(bool isProd);
}

View File

@ -45,14 +45,6 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
DataSerializerType = AcSerializerType.Binary,
RawResponseData = data
};
// Szerver ENV-announce (push-on-connect) — kliens-belső kezelés, nem megy tovább app-eseményként.
if (messageTag == SignalRTags.ServerEnvironment)
{
HandleServerEnvironmentAnnounce(responseDataMessage);
return;
}
await OnMessageReceived(messageTag, responseDataMessage);
}
@ -66,85 +58,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
public Task<List<MeasuringModel>?> GetMeasuringModels() => GetAllAsync<List<MeasuringModel>>(SignalRTags.GetMeasuringModels);
/// <summary>
/// FAIL CLOSED: a nyers SignalResponseDataMessage úton kérdez — timeout/hiba esetén EXCEPTION, nem default(false).
/// (A sima GetAllAsync&lt;bool&gt; timeoutnál default-ot adna vissza, ami "DEV"-nek hazudná a nem-elérhető szervert.)
/// </summary>
public async Task<bool> IsProdEnvironment()
{
var response = await GetAllAsync<SignalResponseDataMessage>(SignalRTags.IsProdEnvironment)
?? throw new InvalidOperationException(
"IsProdEnvironment: nincs szerver-válasz (timeout/kapcsolat-hiba) — a környezet nem igazolható (fail closed).");
return response.GetResponseData<bool>();
}
#region Server environment (IsProd) hidratálás az EGYETLEN értékadási pont a FruitBankConstClient.IsProd-hoz
private Task? _environmentHydration;
// ELSŐ felépülés: HARD GATE — a StartConnection bevárja a hidratálást, addig a kiváltó kérés nem megy ki
// (fail closed: env-tudás nélkül nem beszélünk a szerverrel). PONTOSAN EGY wire-hívás: ha a hidratálás már
// fut (pl. a teszt-guard EnsureEnvironmentAsync-je váltotta ki magát a connectet), nem indítunk másodikat —
// és nem is várjuk meg (önmagát várná: promise-ciklus lenne), az elindítója úgyis await-eli.
// Reconnect: NINCS újra-lekérdezés ("csak az elején 1x") — az env-váltást (szerver-restart MÁSIK DB-vel)
// a szerver push-on-connect announce-a jelzi (ServerEnvironment tag).
protected override async Task OnConnectionEstablished()
{
if (FruitBankConstClient.IsProdHydrated) return;
if (_environmentHydration is { IsCompleted: false }) return;
_environmentHydration = HydrateEnvironmentAsync();
await _environmentHydration;
}
/// <summary>
/// A szerver-környezet (FruitBankConstClient.IsProd) hidratálásának bevárása — teszt-guardok / UI ezt await-eli.
/// Ha az érték már megvan (gate vagy announce töltötte), azonnal kész; hibás korábbi kísérletnél újrapróbál,
/// és ha az is bukik, a kivétel a hívóra megy (fail closed).
/// </summary>
public Task EnsureEnvironmentAsync()
{
if (FruitBankConstClient.IsProdHydrated) return Task.CompletedTask;
return _environmentHydration is { IsFaulted: false, IsCanceled: false } running
? running
: _environmentHydration = HydrateEnvironmentAsync();
}
private async Task HydrateEnvironmentAsync()
{
try
{
var isProd = await IsProdEnvironment();
FruitBankConstClient.SetIsProd(isProd);
Logger.Info($"Server environment: {(isProd ? "PROD" : "DEV")}");
}
catch (Exception ex)
{
Logger.Error($"Server environment hydration FAILED — FruitBankConstClient.IsProd hidratálatlan marad (fail closed); {ex.Message}", ex);
throw;
}
}
// A szerver MINDEN kapcsolat-felépüléskor bemondja a környezetét (push-on-connect announce).
// Első alkalommal ez is hidratálhat (verseny a gate-lekérdezéssel — azonos értéket írnak, ártalmatlan);
// KÉSŐBB az összevetés a lényeg: eltérés = a szerver restart közben MÁSIK DB-re állt → hangos riasztás.
private void HandleServerEnvironmentAnnounce(SignalResponseDataMessage responseDataMessage)
{
var isProd = responseDataMessage.GetResponseData<bool>();
if (FruitBankConstClient.IsProdHydrated && FruitBankConstClient.IsProd != isProd)
Logger.Error($"SERVER ENVIRONMENT CHANGED: {(FruitBankConstClient.IsProd ? "PROD" : "DEV")} → {(isProd ? "PROD" : "DEV")} — a szerver újraindult MÁSIK adatbázissal!");
else if (!FruitBankConstClient.IsProdHydrated)
Logger.Info($"Server environment (announce): {(isProd ? "PROD" : "DEV")}");
FruitBankConstClient.SetIsProd(isProd);
}
#endregion Server environment
public Task<bool> IsProdEnvironment() => GetAllAsync<bool>(SignalRTags.IsProdEnvironment);
#region Partner
public Task<List<Partner>?> GetPartners() => GetAllAsync<List<Partner>>(SignalRTags.GetPartners);
@ -168,7 +82,6 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory);
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.GenerateEkaerXmlDocument, ekaerHistoryId);
public Task<EkaerHistory?> SendEkaerToNav(int ekaerHistoryId) => GetByIdAsync<EkaerHistory?>(SignalRTags.SendEkaerToNav, ekaerHistoryId);
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<EkaerCreateResult?>(SignalRTags.CreateMissingEkaerHistories, fromDate);
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync<int>(SignalRTags.GetEkaerHistoryCount, filter);

View File

@ -18,8 +18,8 @@ using Microsoft.Extensions.Options;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is a BaseUrl-ből származik
// (a FruitBankConstClient.BaseUrl az AcEnv.BaseUrl keret-állapotra delegál — fail-fast).
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik.
// A loggerHub writer a FruitBankConstClient.BaseUrl-t olvassa, ezért induláskor onnan hidratáljuk.
FruitBankConstClient.BaseUrl = builder.Configuration["AyCode:Urls:BaseUrl"]
?? throw new InvalidOperationException("Hiányzik az 'AyCode:Urls:BaseUrl' az appsettings-ből — a hub-ok URL-je ebből származik.");
@ -28,7 +28,6 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres
// Add device-specific services used by the FruitBankHybrid.Shared project
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, WebSecureCredentialService>();
builder.Services.AddSingleton<IAppTitleService, WebAppTitleService>();
#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();

View File

@ -1,11 +0,0 @@
using FruitBankHybrid.Shared.Services;
using Microsoft.JSInterop;
namespace FruitBankHybrid.Web.Client.Services;
/// <summary>Böngésző-tab title env-badge: "[PROD]/[DEV] FruitBank" (a nop-szerver Console.Title mintája).</summary>
public class WebAppTitleService(IJSRuntime js) : IAppTitleService
{
public async Task SetEnvironmentBadgeAsync(bool isProd)
=> await js.InvokeVoidAsync("eval", $"document.title = '{(isProd ? "[PROD]" : "[DEV]")} FruitBank'");
}

View File

@ -17,8 +17,7 @@ using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is a BaseUrl-ből származik
// (a FruitBankConstClient.BaseUrl az AcEnv.BaseUrl keret-állapotra delegál — fail-fast).
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik.
FruitBankConstClient.BaseUrl = builder.Configuration["AyCode:Urls:BaseUrl"]
?? throw new InvalidOperationException("Hiányzik az 'AyCode:Urls:BaseUrl' az appsettings-ből — a hub-ok URL-je ebből származik.");
@ -28,7 +27,6 @@ builder.Services.AddMvc();
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, ServerSecureCredentialService>();
builder.Services.AddSingleton<IAppTitleService, ServerAppTitleService>();
builder.Services.AddSingleton<IAcLogWriterBase, ConsoleLogWriter>();

View File

@ -1,9 +0,0 @@
using FruitBankHybrid.Shared.Services;
namespace FruitBankHybrid.Web.Services;
/// <summary>No-op: a szerver-host (prerender) nem birtokol címsort — az env-badge a WASM/MAUI kliensé.</summary>
public class ServerAppTitleService : IAppTitleService
{
public Task SetEnvironmentBadgeAsync(bool isProd) => Task.CompletedTask;
}

View File

@ -23,6 +23,7 @@
}
]
}
},
"AcHubConnection": {
// Az Url innen KIVÉVE: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik (kódban: +/fbHub, +/loggerHub).

View File

@ -47,8 +47,7 @@ namespace FruitBankHybrid
}
}
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is a BaseUrl-ből származik
// (a FruitBankConstClient.BaseUrl az AcEnv.BaseUrl keret-állapotra delegál — fail-fast).
// Hub-URL-ek EGY forrásból: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik.
FruitBankConstClient.BaseUrl = builder.Configuration["AyCode:Urls:BaseUrl"]
?? throw new InvalidOperationException("Hiányzik az 'AyCode:Urls:BaseUrl' az appsettings-ből — a hub-ok URL-je ebből származik.");
@ -76,7 +75,6 @@ namespace FruitBankHybrid
// Add device-specific services used by the FruitBankHybrid.Shared project
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, MauiSecureCredentialService>();
builder.Services.AddSingleton<IAppTitleService, MauiAppTitleService>();
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));

View File

@ -1,21 +0,0 @@
using FruitBankHybrid.Shared.Services;
namespace FruitBankHybrid.Services;
/// <summary>
/// MAUI ablak-cím env-badge: "[PROD]/[DEV] FruitBank" — Windows desktopon látszik;
/// Android/iOS-en nincs futásidőben írható ablakcím (ott hatástalan, majd egy UI-badge fedheti).
/// </summary>
public class MauiAppTitleService : IAppTitleService
{
public Task SetEnvironmentBadgeAsync(bool isProd)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var window = Application.Current?.Windows.FirstOrDefault();
if (window != null) window.Title = $"{(isProd ? "[PROD]" : "[DEV]")} FruitBank";
});
return Task.CompletedTask;
}
}

View File

@ -1,324 +0,0 @@
# Wholesale Framework — tervezési összefoglaló (handoff-mag)
> **Status:** Proposed (Draft) — 2026-07-14
> **Jelleg:** Ez NEM ADR és NEM végleges doksi. Provizórikus gondolkodási összefoglaló, ideiglenesen a
> FruitBankHybridApp `docs/` mappájában. A tárgyalt Wholesale framework-repo **még nem létezik**, a neve
> **változhat**, és **semmilyen döntés nem végleges**. Amint a repo létrejön, ez a fájl a forrása a rendes
> struktúrának (ADR-ek + repo-doksik) — lásd a záró "Mi lesz ebből" szakaszt.
> **Előzmény:** két korábbi session terv-összegzése (app-oldal + nop-plugin-oldal) + ez a session konszolidációja.
---
## 1. Cél és kiváltó ok
A FruitBank (zöldség-gyümölcs nagyker, nopCommerce-plugin + Blazor Hybrid app) mellé indul a **VeggieVoyage**
(veggievoyage.hu), egy közel azonos nagyker-rendszer apró eltérésekkel. Emellett **több további zöldséges nagyker
érdeklődő** is van a rendszerre. Cél: egy **közös nagyker-framework réteg**, amelyből minden nagyker-termék
származik, hogy egy új terméknél a lehető legkevesebbet kelljen újraírni.
Jelenleg (2026-07-14) csak a FruitBank-oldal létezik; sem a Wholesale-repo/könyvtár, sem a VeggieVoyage-projekt
nincs még létrehozva.
---
## 2. Architektúra-döntés: base-class minta (maximalista pull-up)
**Öröklés-alapú, NEM típus-átköltöztetés.** A közös rétegbe **szinte minden osztály felkerül** — abstract vagy
virtual formában (base class-ok, virtual method-ok/property-k). A konkrét (gyakran szinte üres) záró osztályok a
termék-repóban élnek és származnak a base-ből.
- **Amit lehet, a közös rétegben implementálunk** (virtual default üzleti logika); ami termék-specifikus, az a
származtatottban override-ol.
- **Az eltérés a fejlesztés közben derül ki, nem előre.** Default: minden felmegy közös virtual defaultként. Amikor
a VeggieVoyage (vagy egy későbbi termék) fejlesztése **bizonyítja**, hogy valami mégsem közös, akkor az a member
lemegy ("demote") a záró rétegbe. Amíg csak egy termék használ egy membert, a lehúzása senkit nem tör.
- Ez a **meglévő házi minta** kiterjesztése: `MgStockTakingItem<TStockTaking, TProduct> : MgEntityBase`,
`PartnerBase → CargoPartner`, `MeasuringItemPalletBase → 3 pallet`, `AcConst` abstract-constants minta.
### Miért ez nyert (a mozgatásos alternatívával szemben)
| Aggály | Feloldás |
|---|---|
| Tábla-nevek (`fbShipping`…) | `[Table(Name=…)]` a **származtatott** osztályon, termék-konstansból — nincs migráció, nincs runtime-konfig |
| AcBinary wire-compat (`SignalDataType` = AssemblyQualifiedName) | Konkrét típusok **helyben maradnak** a termék-assembly-ben → AQN változatlan, nincs lockstep-kényszer |
| `AcBinaryForcedInit` (AssemblyLoadContext-quirk) | Generált szerializálók a konkrét típusokra → meglévő ForceRegister érintetlen |
| LinqToDB `NameCompatibility` | Termék-pluginban marad, konkrét típusokra |
**Következmény:** a FruitBank-oldal minden pull-up lépésnél **fordítás-azonos** marad; a VeggieVoyage viszont
"majdnem készen" indul, mert a scaffold záró osztályai szinte üresek.
### Új DB + új plugin a VeggieVoyage-nek
- A VeggieVoyage **teljesen új adatbázist** kap → a `[Table(Name=…)]` (identitás) így is a záró rétegen marad; a
base hordozza az összes `[Column]`-t, a tábla-NÉV termék-konstans. A `vv`/`fb` prefix szabad termék-döntés, nem
framework-ügy.
- A VeggieVoyage **új nopCommerce-plugin** is → nem csak az üzleti logikát, hanem a **plugin-infrastruktúrát** is
base-formában ki kell szervezni: install/uninstall, locale-resource telepítés, admin-menü regisztráció,
settings-defaultok, schedule task-ok, `MgwPluginStartupBase` virtual DI-regisztrációkkal. A Veggie-pluginban
ideálisan csak `plugin.json` (SystemName), a záró osztályok és a konstansok maradnak.
- **Zöldmezős schema-generálás:** a friss install-nak a teljes sémát egy menetben kell felhúznia üres DB-n (a
FruitBank táblái évek alatt, lépésenként jöttek létre). Ellenőrizendő, hogy a meglévő install/tábla-létrehozó út
(MgDbContext-vonal) teljes és determinisztikus-e nulláról — GenericAttribute-kulcsok és seed-adatok is.
---
## 3. Rétegzés / célstruktúra
Rétegtérkép a legáltalánosabbtól (legmélyebb) a legspecifikusabbig:
```
L0 AyCode.Core (Ac*) + AyCode.Blazor — tech-framework, domain-mentes, bármely projekt
SignalR, AcBinary + SourceGenerator, logolás, NAV/EKÁER-protokoll, MgGrid/komponens-base-ek
L2 Mango.Nop Libraries (Mg*) — nop-generikus, nop-VERZIÓFÜGGŐ (4.70 mappa alatt)
L2.5 Mango.Nop.Wholesale.Core / .Services — nagyker-domain, nop-VERZIÓFÜGGŐ, prefix: Mgw
(Libraries alatt, 4.70) entitás-bázisok, DbTable/DbContext-bázisok, endpoint-bázisok, EKÁER-riport
L2.5 Wholesale.Common / .Common.Server — nagyker-domain, nop-verzió-FÜGGETLEN (ÚJ repo)
domain-modell, interfészek, enumok, DTO-bázisok, SignalR-tag core-tartomány, mérés-helperek,
service-bázisok (a Mango.Nop DLL-jei UTÁN build-el — pont mint ma a Common-ok)
L2.5 Wholesale.Shared (Blazor, ÚJ repo) — grid-/komponens-base-ek, SignalR-kliens base
L3 Termékek: FruitBank / VeggieVoyage / … — vékony záró osztályok + identitás + termék-delta
```
### Kétotthonos base-réteg — a besorolás kritériuma
A "hova kerüljön" kritérium **NEM az, hogy használ-e Mango.Nop-ot** (a Common-ok ma is jogosan használják),
hanem hogy **nop-verziófüggő-e**:
- **nop-verziófüggő** (Mg*/linq2db/nopCommerce-entitásokra épül, a nop belső API-ját tükrözi) → `Mango.Nop.Wholesale`
(Libraries, verziómappa alatt).
- **nop-verzió-független** nagyker-domain → `Wholesale.Common` / `.Common.Server` (új repo).
> Fontos: a mai `FruitBank.Common.Server` "nopCommerce integration"-t is tartalmaz. Pull-upnál a nop-ot érintő
> részek a `Mango.Nop.Wholesale.Services`-be mennek, nem a `Wholesale.Common.Server`-be. A kritérium szigorúan
> "nop-verziófüggő?", nem projektnév.
### Build-lánc (interleaved — ma is így épül, csak beékelődik két réteg)
```
AyCode → Mango.Nop Libraries → Mango.Nop.Wholesale.Core (entitás-bázisok)
→ Wholesale.Common / .Common.Server (domain- és service-bázisok, Mgw.Core-ra épülve)
→ Mango.Nop.Wholesale.Services (DbTable/DbContext/endpoint-bázisok, Wholesale.Common-ra épülve)
→ termékek (HybridApp + plugin)
```
Ezért a `Mango.Nop.Wholesale` **Core/Services szétvágása kötelező**, nem ízlés: a Core a Wholesale.Common ALATT,
a Services FELETTE — különben körkörös függőség.
### Per-app bin-profil
A referencia-minta már ma per-app build-profilos: az AyCode és Mango.Nop.Core DLL-jei
`bin\FruitBank\$(Configuration)\net9.0`-ból jönnek (HintPath). Lesz `bin\VeggieVoyage\…` is. TFM-ek: Common=net9.0
(WASM-kliens is fogyasztja, szerver-only függőség nélkül), Shared=net10.0. A `FruitBankHybrid.Shared.Common`
projekt ma üres váz — felhasználható.
---
## 4. Placement-kánon (a legfontosabb szabály)
**A kód a legmélyebb rétegbe kerül, amelynek a domain-hatóköre még jogosan magába foglalja a fogalmat.**
Két teszt együtt dönt:
1. **Kimenő-függőség teszt (szükséges, objektív):** egy típus akkor *mehet* mélyebbre, ha a függőségei engedik.
AyCode-ba csak az kerülhet, ami *nulla* nagyker/nop fogalomra hivatkozik (a `using`-ok és hivatkozott típusok
döntik el, nem érzés). Ez adja meg, milyen mélyre **mehetne** a kód.
2. **Hatókör-kohézió teszt (elégséges, logikai):** hova **való** a fogalom. Ez megállíthatja a kódot, mielőtt a
függőségei alapján túl mélyre süllyedne.
**Kanonikus példa — tálca/tare-aritmetika:** tiszta `decimal`-matematika, nulla domain-függőséggel *lefordulna*
AyCode.Core-ban. Mégis a **Wholesale**-be való, mert a fogalom hatóköre "nagyker-mérés". Core-ba nem azért nem
kerül, mert nem fordulna, hanem mert a Core hatóköre (*minden* projekt) tágabb, mint a fogalom valódi közönsége.
### Két hibamód
- **Túl mélyre** (nagyker-fogalom Core-ban): a univerzális réteg beszennyeződik, a nagyker-fejlesztő nem is ott
keresi. Ez a *csábító* hiba ("ez matek, mehet Core-ba"). **Fordul, csak koszol** → csendes hiba, ezt kell fogni.
- **Túl sekélyen** (a termékben): redundancia — a következő zöldséges projekt újraírja. Ezt a framework szünteti meg.
Iránytű: a redundancia-kerülés **lefelé** (általánosabb felé) tol; a hatókör-kohézió a **fék**. A hiba-irány
aszimmetrikus: túl mélyre tett domain-kód *nem fordul* (a rendszer önvédő); túl magasra tett specifikus kód
*fordul, csak koszol*. Ezért a default "a legmélyebb, ami fordul", és a **feljebb-tétel** az, ami indoklást igényel.
### Redundancia-ellensúly (hogy a DRY ne forduljon visszájára)
"Ne írd le kétszer ugyanazt a **fogalmat**" — NEM "vonj össze mindent, ami hasonlít". Ha két dolog csak *véletlenül*
hasonlít (nem ugyanaz a fogalom) és közös base-be húzod, hamis csatolást hozol létre — a legrosszabb fajta
redundancia-"javítás". A **rule-of-two/N** a védőkorlát: legalább két valódi fogyasztó bizonyítsa, hogy a közösség
valódi és nem esetleges.
### Identitás soha nem megy fel (checklist a záró réteghez)
- `[Table(Name=…)]` tábla-nevek (termék-konstansból)
- `[AcBinarySerializable]` konkrét típusok
- SignalRTag termék-tartománybeli értékei
- plugin SystemName, route-URL-ek, locale-fájlok, branding
- appsettings **értékek** (nem a séma)
### Hol él a kánon (doc-layering)
A placement-kánon kánonja a legmélyebb rétegben (AyCode.Core) él; minden alsóbb réteg copilot-instructions.md-je
erre **mutat** + a saját befogadási kritériumát mondja ki (AyCode = "domain-mentes"; Wholesale = "nagyker-domain,
nop-verzió-független"; Mango.Nop = "nop-verziófüggő"). A doksi vigyen 3-4 **levezetett** határeset-példát (a
tare-képlet a kanonikus) — az absztrakt szabály nem elég horgony, a *gondolkodásmódot* a példák tanítják.
---
## 5. Zöldség-kalibráció + jövőbeli továbbbontás (elhalasztott)
Az érdeklődők **egyelőre mind zöldséges nagykerek**, közel azonos igénnyel. Ezért a Wholesale-réteg **mai valódi
hatóköre "zöldséges nagyker"**, nem "általános nagyker" — a "zöldséges-specifikus vs. általános-nagyker" hasítás
**jövőbeli, szándékosan még meg nem tett rétegvágás**.
Következmények:
- **A zöldséges-specifikusság ma NEM tolja fel a kódot a termékbe.** Egy kimondottan zöldséges logika (érettség,
osztályozás, zöldség-mérés) jogosan a framework-rétegé — bármely másik zöldséges nagyker újra használná. Óvakodni
attól, hogy "ez túl zöldség-specifikus, hagyjuk a termékben" — ez **kiéheztetné a framework-öt**.
- **NE annotáld előre** a "zöldséges vs. általános" varratot — nincs második nem-zöldséges domain, ami bizonyítaná,
tehát bármilyen jelölés spekuláció (ugyanaz a korai-triázs-csapda). A jövőbeli *nem*-zöldséges nagyker fogja
felfedni a varratot, ugyanúgy, ahogy a Veggie-scaffold felfedi a termék-hardcode-ot.
- Ha egyszer jön nem-zöldséges nagyker: ugyanaz a mechanikus pull-up egy szinttel lejjebb (pl. `Trade`/`Distribution`
réteg), a Wholesale maga válik vékony leszármazottá. Mivel minden abstract/virtual, ez is fordítás-azonos lesz.
- A rétegnév lehet jövő-álló ("Wholesale", prefix `Mgw`), de a doksinak ki kell mondania a mai zöldség-kalibrációt,
hogy egy jövőbeli session ne nézze design-hibának és ne kezdjen kéretlenül általánosítani.
---
## 6. nopCommerce-verzió probléma + karantén-stratégia
**Kiváltó félelem:** a nopCommerce design sok hibája miatt gyakran nop-kódot kell másolni/átírni → nagyon nehéz a
FruitBank upgrade-je. A VeggieVoyage viszont már **új nop-verzióval** készülne. Kockázat: ha a catalog-táblák
változtak, az gond lehet.
**A rétegzés maga a válasz — csak explicit szabályként kell kimondani:**
1. **A verziófüggés karanténban van.** A `Mango.Nop Libraries` + `Mango.Nop.Wholesale` definíció szerint
nop-verziófüggő (ezért él verziómappa alatt). Új nop-verziónál **ez a két réteg forkol** (`4.90/Libraries`,
`4.90/…Wholesale`), és a per-app bin-profil ezt kiszolgálja: a FruitBank a 4.80-as fork ellen build-el, a
VeggieVoyage a 4.90-es ellen — **ugyanabból a Wholesale-forrásból**. Feltétel: a Mg*/Mgw típusok **publikus
felülete** forrás-kompatibilis maradjon a forkok között. A Mango.Nop réteg így **anti-korrupciós réteg**: a nop
churn a fork belsejében marad, a Wholesale nem látja.
2. **Másolt/átírt nop-kód** (a tényleges upgrade-fájdalom): jól megjelölt helyre (pl. `NopOverrides/` a verziófüggő
rétegben), mindegyik mellé kötelező jegyzet: *miért kellett, melyik nop-fájlból, mit kell újranézni port-nál*.
Így a 4.90-port checklist lesz, nem régészet.
3. **Catalog-tábla drift:** (a) minden catalog-hozzáférés a verziófüggő rétegben, a Wholesale csak interfészt/DTO-t
lát; (b) a **GenericAttribute-minta** (IsMeasurable, Tare, AverageWeight, IncomingQuantity, NetWeight mind GA-ban,
nem catalog-oszlopban) verzió-rezisztens — a GA a nop legstabilabb mechanizmusa. A kitettség arra szűkül, amit
tényleg a Product/Order/Customer **natív oszlopaiból** olvastok.
4. **A két nagy műveletet ne egyszerre.** Előbb pull-up a **mostani** nop-verzión (FruitBank fordítás-azonos, a
Veggie-scaffold ugyanazon a verzión bizonyítja a framework-öt), **utána** a verziófüggő réteg portja az új
verzióra. Egyszerre futtatva minden hiba kétesélyes lenne ("framework-bug vagy port-bug?").
5. **Reframe:** a VeggieVoyage **kifizeti a FruitBank upgrade-adósságát**. Ha a Veggie kikényszeríti a
Mango.Nop(+Wholesale) új-verziós portját, a FruitBank későbbi upgrade-je már csak "bin-profil átállítás a kész
forkra + termékrétegbeli maradék".
**Konkrét vizsgálat, amivel a catalog-félelem végessé tehető** (még nem futott le):
- (a) **Kitettség-térkép:** végigmenni a plugin + Libraries kódon, mely nop-entitásokat/service-eket/natív
oszlopokat használtok közvetlenül.
- (b) **Séma-diff** a cél nop-verzióval, a ténylegesen használt táblákra (a nop upgrade-migrációiból).
→ Cél nop-verzió megadása után elvégezhető.
---
## 7. Eltérés-taxonómia: settings vs. override (rule-of-N)
Több terméknél a "rule-of-two" **rule-of-N** lesz. Ökölszabály:
- **parametrikus eltérés → settings/config** (EkaerSettings-minta, DI mögé)
- **strukturális eltérés → virtual override**
- **identitás → abstract const a záró rétegen**
Ha ugyanazt a metódust minden termék máshogy override-olja, az annak a jele, hogy **paraméternek** kellett volna
lennie. Minél több megy settings-be, annál kevesebb kódot kell egy új terméknél megírni. Nem kell előre kitalált
settings-taxonómia — **demote-to-settings on evidence**, ugyanaz a workflow, mint a kódnál: minden fent marad közös
defaultként, és az első valódi termék-eltérésnél dől el, override vagy setting lesz.
**Demote-ablak szűkül N-nel:** amíg egy termék használ egy membert, a lehúzása fájdalommentes; a 2. fogyasztónál már
közös és fent marad. Ezért a Veggie-fázisban agresszíven demote-olni.
**Termék-scaffold = template.** A Veggie "új termék felhúzása" maga is termék: minden scaffold-lépést checklistként
dokumentálni a Wholesale-repo doksijába — a 3. terméknél már ez A termék-létrehozási folyamat. (Nem kell most
dotnet-template; a jól dokumentált Veggie-scaffold az első draft.)
**Framework-evolúció N fogyasztóval:** minden wholesale-változás N terméket érint. Per-app bin-profil jó izoláció
(mindenki a saját build-jét pinneli), de wire-formátumot (AcBinary) és base-séma `[Column]`-okat érintő változásnál
`CONSUMERS.md` szerinti végigfuttatás kell (P1).
**SignalRTag-tartomány kiosztás** (hogy két termék soha ne ütközzön közös infrastruktúrán): közös core-tartomány +
termék-tartományok, pl. FruitBank 09999, VeggieVoyage 1000019999, 3. termék 20000+…
---
## 8. Pilotban igazolandó mechanizmusok (a maximalista minta ezekre tesz fel mindent)
Az első pull-up ELŐTT explicit tesztelendő:
1. **`[Association]` keresztreferenciák öröklésnél** — interfész-kulcsú társítás
(`OtherKey = nameof(IShipping.Id)`) VAGY a navigációk a leszármazottban maradnak (base = skalárok + FK int-ek),
entitásonként döntve.
2. **AcBinary öröklött property-kkel** — a `MeasuringItemPalletBase` minta szerint; az első költözőnél explicit teszt.
3. **`[SignalR]` attribútum öröklött kontroller-metóduson** — az AyCode dispatcher végpont-felderítése lássa a
base-metódusokat.
4. **Razor `@typeparam` a DevExpress template-ekkel** — hol megy, hol marad vékony app-razor.
---
## 9. Migrációs fázisterv ("pull-up", lépésenként, FruitBank végig fordítás-azonos)
0. Skeleton repo + per-app bin-profil bekötés + mechanikus bázis-kiemelés (interfészek, enumok, konstans-bázisok) —
compile+wire-identical.
1. **Pilot:** a legkisebb/legtisztább gráf végigvitele (jelölt: Cargo — legkisebb; vagy EKÁER-lánc — legfrissebb).
Itt bizonyítjuk a 8. szakasz mechanizmusait.
2. Data-réteg bázisok (DbContext/DbTable).
3. Service-ek domain-enként (mérés → preorder → cargo → invoicing), FruitBank-osztályok vékony leszármazottá.
4. VeggieVoyage-plugin scaffold záró-osztályokkal — itt bukik ki a maradék hardcode.
5. Blazor Shared (grid-/komponens-base-ek).
6. Verziófüggő réteg portja az új nop-verzióra (a pull-up UTÁN, külön lépés).
---
## 10. Meta-protokoll teendők (amikor a repo létrejön)
- Új repo: `.github/copilot-instructions.md` + `@repo` blokk, **repo-prefix** (self-service a `REPO_PREFIXES.md`
szerint; korábbi javaslatok: nop-oldali libre `MGNOPWHOLE`, a nop-független Wholesale repóra pl. `MGWHOLE`
eldöntendő), layer-szám.
- **Class-prefix:** `Mgw` (Mango Wholesale) — collision-check lefutott, tiszta (a `Ws`, `Wh/Whs`, `Nk` elvetve).
A doksi tisztázza: a prefix jövő-álló, de a mai tartalom zöldség-kalibrált.
- **Doc-layering:** a defining layerrel a doksik is költöznek — a FruitBank-plugin `PREORDER/` doksija + a
`PREO`/`EKAER`/`ORDD` topic-kódok a wholesale-registry-be; a termék-pluginban pointer + delta marad.
- **`CONSUMERS.md`** a Wholesale-repóban (FruitBank, VeggieVoyage, …).
- **`LLMP-DEC` bejegyzés** az új repo létéről (workspace-szignifikáns).
- **ADR-ek** a végleges struktúrában (lásd lent).
---
## 11. Nyitott kérdések
- Wholesale-repo **neve** és pontos szeparációja (egy repo Common + Common.Server + Shared, vagy több?).
- **Cél nop-verzió** a VeggieVoyage-hez (ez indítja a 6/(b) séma-diffet).
- Repo-**prefix**(ek) véglegesítése.
- Generic-paraméter-robbanás mély gráfoknál (`MgwShippingDocumentBase<TShipping, TPartner, TShippingItem, >`) —
a házi minta elfogadta, de ez az ára; entitásonként figyelni.
- `Mango.Nop` vs `Wholesale` határ entitásonként: ami bármely nop-projektnek jó → Mango.Nop; ami nagyker-domain →
Wholesale.
---
## 12. Mi lesz ebből (amint van repo)
Ez a fájl a **forrás**; a rendes struktúrába szétszedve:
- **ADR-ek** (`<repo>/docs/adr/NNNN-*.md`, Nygard-stílus):
- `base-class-pull-up-vs-type-move` (2. szakasz)
- `wholesale-layer-split-and-nop-version-quarantine` (3. + 6. szakasz)
- `placement-canon` (4. szakasz) — vagy CONVENTIONS.md-be, a repo szokása szerint
- **Repo-doksik:** ARCHITECTURE (rétegtérkép + build-lánc), CONVENTIONS (placement-kánon + tare-példa +
zöldség-kalibráció), CONSUMERS.md, termék-scaffold checklist.
- **LLMP-DEC** sor: az új repo léte + a protokoll-relevanciák.
- A FruitBank-oldali doc-layering pointer-ek frissítése.
Ez a `WHOLESALE_FRAMEWORK_PLAN.md` a szétszedés után **archiválható vagy törölhető** (a tartalma a végleges
helyekre kerül).

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
# Architecture Decision Records (ADR)
Architektúra-szintű döntések durable rekordjai (Nygard-stílus): egy fájl = egy döntés, `NNNN-<slug>.md` néven. Minden ADR: kontextus → döntés → következmények → alternatívák. A `Status` a Nygard 4-értékű szótár: `Proposed` / `Accepted` / `Superseded by ADR-XXXX` / `Rejected` (ez KÜLÖN a `_ISSUES.md`/`_TODO.md` 3-értékű `Open`/`InProgress`/`Closed` szótárától).
> Formátum + folyamat: `adr-author` skill (AyCode.Core `.github/skills/adr-author/`), sablon: `references/ADR_TEMPLATE.md`.
## Index
| ADR | Cím | Status |
|---|---|---|
| [0001](0001-non-measurable-item-derived-weight.md) | Nem mérendő tétel súlya — derivált pallet-mérés a termék átlagsúlyából és tárájából | Proposed (2026-07-14) |