diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1bc83d90..b0dab7b9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -97,7 +97,10 @@ "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(\\(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)" ] } } diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index 65c470fe..6391e1df 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -74,6 +74,59 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService history.Partner = partner; } + public async Task 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(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; + } + } + /// Depot-kapu (T-D8R4): a PartnerDepot az EKÁER-hez kötelező — a felrakodási hely a depot címe. Hiányzó /// vagy be nem töltött telephelynél a leképezés ELŐTT ValidationError (nincs félrevezető, partner-központi-címes XML). /// null = rendben, mehet a generálás. diff --git a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs index a6f9619f..f9323ced 100644 --- a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs @@ -46,6 +46,15 @@ public interface IFruitBankEkaerService /// Eldönti egy kimenő rendelés EKÁER-kötelezettségét (ugyanaz a logika, a rendelés tételeire). EkaerObligationResult EvaluateObligation(OrderDto order); + /// + /// Beküldi egy legenerált ( / ) + /// TÁROLT tradeCard XML-jét a NAV-nak (create-művelet). A rekordot tölti: siker → + /// (tiszta) vagy (warningos generálásból, + /// pl. hiányzó rendszám) + EkaerNumber (a NAV tcn) + SentDate; NAV-/transport-hiba → + /// + ErrorText. NEM perzisztál — a mentés a hívó SignalR endpoint dolga. + /// + Task SendEkaerAsync(EkaerHistory ekaerHistory, CancellationToken cancellationToken = default); + /// Beállítja a deklaráció () összegző mezőit (DB-mentes): ShippingDate + Partner. /// 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. diff --git a/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs b/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs index 36354f08..f9adab08 100644 --- a/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs +++ b/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs @@ -15,10 +15,14 @@ namespace FruitBank.Common.Server.Services.SignalRs; public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase> { - public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/, + private readonly IFruitBankDataControllerServer _fruitBankDataController; + + public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable logWriters) : base(configuration, new Logger(logWriters.ToArray())) { + _fruitBankDataController = fruitBankDataController; + EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog; SerializerOptions = new AcBinarySerializerOptions(); @@ -30,6 +34,32 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase + /// 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. + /// + 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; diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index e61eed2d..dc6ae370 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -22,6 +22,20 @@ public static class FruitBankConstClient 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())."); + + public static void SetIsProd(bool isProd) => _isProd = isProd; + + /// True, ha az IsProd már hidratált — kapu-logikához (első-connect await) és a push-összevetéshez. + public static bool IsProdHydrated => _isProd != null; + public static string DefaultHubName = "fbHub"; public static string LoggerHubName = "loggerHub"; diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index ffc7e4dc..89840b77 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -35,6 +35,7 @@ public interface IFruitBankDataControllerCommon public Task AddEkaerHistory(EkaerHistory ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); public Task GenerateEkaerXmlDocument(int ekaerHistoryId); + public Task SendEkaerToNav(int ekaerHistoryId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing); public Task CreateMissingEkaerHistories(DateTime fromDate); public Task GetEkaerHistoryCount(EkaerHistoryFilter filter); diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index 791cf2ab..990c19f6 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -127,11 +127,15 @@ 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; + /// 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. + public const int ServerEnvironment = 211; #region SendToClient public const int SendOrderChanged = 500; diff --git a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs index 1d2fe8fe..b9396fb1 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs @@ -27,11 +27,12 @@ namespace FruitBankHybrid.Shared.Tests private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty); [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); + + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } #region Shipping diff --git a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs index 5aecaa92..2be9ff6d 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs @@ -12,11 +12,12 @@ namespace FruitBankHybrid.Shared.Tests private FruitBankSignalRClient _signalRClient = null!; [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests)); + + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } #region EkaerHistory / Create diff --git a/FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj b/FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj index c778ae20..c7e0c01c 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj +++ b/FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj @@ -76,4 +76,10 @@ + + + + + diff --git a/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs index a3cc4214..7fdb86a5 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs @@ -18,11 +18,12 @@ namespace FruitBankHybrid.Shared.Tests private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty); [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankPartnerTests)); - _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } #region Partner diff --git a/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs b/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs index cd1ad43f..eef3bf77 100644 --- a/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs +++ b/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs @@ -186,11 +186,12 @@ public sealed class JsonExtensionTests private FruitBankSignalRClient _signalRClient = null!; [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + _signalRClient = TestSignalRClientFactory.Create(nameof(JsonExtensionTests)); - _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } [TestMethod] diff --git a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs index a711f963..46f0b8f4 100644 --- a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs @@ -24,11 +24,12 @@ public sealed class OrderClientTests private FruitBankSignalRClient _signalRClient = null!; [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests)); + + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } diff --git a/FruitBankHybrid.Shared.Tests/TestEnvironment.cs b/FruitBankHybrid.Shared.Tests/TestEnvironment.cs new file mode 100644 index 00000000..145f09cc --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/TestEnvironment.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using FruitBank.Common; + +namespace FruitBankHybrid.Shared.Tests; + +/// +/// 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. +/// +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."); + } +} diff --git a/FruitBankHybrid.Shared.Tests/TestServerGuard.cs b/FruitBankHybrid.Shared.Tests/TestServerGuard.cs new file mode 100644 index 00000000..335ab0ba --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/TestServerGuard.cs @@ -0,0 +1,21 @@ +using FruitBank.Common; +using FruitBankHybrid.Shared.Services.SignalRs; + +namespace FruitBankHybrid.Shared.Tests; + +/// +/// 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). +/// +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)"); + } +} diff --git a/FruitBankHybrid.Shared.Tests/ToonTests.cs b/FruitBankHybrid.Shared.Tests/ToonTests.cs index 7e0223aa..9008a692 100644 --- a/FruitBankHybrid.Shared.Tests/ToonTests.cs +++ b/FruitBankHybrid.Shared.Tests/ToonTests.cs @@ -99,11 +99,12 @@ public sealed class ToonTests private FruitBankSignalRClient _signalRClient = null!; [TestInitialize] - public void TestInit() + public async Task TestInit() { - if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + _signalRClient = TestSignalRClientFactory.Create(nameof(ToonTests)); - _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); + // Környezet-guard a szerver saját bevallása alapján (PROD / nem igazolható → exception, fail closed). + await TestServerGuard.EnsureNotProdAsync(_signalRClient); } #if DEBUG diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index 6f015740..b714a40a 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -73,7 +73,7 @@ - + @{ var row = (EkaerHistory)context.DataItem; @@ -89,6 +89,12 @@ Enabled="@(CanDownload(row))" Attributes="@(new Dictionary { ["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)" /> + } @@ -219,6 +225,12 @@ private static bool CanDownload(EkaerHistory ekaerHistory) => ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc); + private readonly HashSet _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; @@ -257,6 +269,50 @@ } } + 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; diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 02bea86d..2a7a4c5b 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -45,6 +45,14 @@ 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); } @@ -58,7 +66,81 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task?> GetMeasuringModels() => GetAllAsync>(SignalRTags.GetMeasuringModels); - public Task IsProdEnvironment() => GetAllAsync(SignalRTags.IsProdEnvironment); + /// + /// FAIL CLOSED: a nyers SignalResponseDataMessage úton kérdez — timeout/hiba esetén EXCEPTION, nem default(false). + /// (A sima GetAllAsync<bool> timeoutnál default-ot adna vissza, ami "DEV"-nek hazudná a nem-elérhető szervert.) + /// + public async Task IsProdEnvironment() + { + var response = await GetAllAsync(SignalRTags.IsProdEnvironment) + ?? throw new InvalidOperationException( + "IsProdEnvironment: nincs szerver-válasz (timeout/kapcsolat-hiba) — a környezet nem igazolható (fail closed)."); + + return response.GetResponseData(); + } + + #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 SEMMILYEN más művelet + // nem megy ki ezen a kapcsolaton. Hiba → a kiváltó kérés is elhasal (fail closed: env-tudás nélkül + // nem beszélünk a szerverrel). 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; + + _environmentHydration = HydrateEnvironmentAsync(); + await _environmentHydration; + } + + /// + /// 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). + /// + 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(); + + 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 #region Partner public Task?> GetPartners() => GetAllAsync>(SignalRTags.GetPartners); @@ -82,6 +164,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory); public Task GenerateEkaerXmlDocument(int ekaerHistoryId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, ekaerHistoryId); + public Task SendEkaerToNav(int ekaerHistoryId) => GetByIdAsync(SignalRTags.SendEkaerToNav, ekaerHistoryId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]); public Task CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync(SignalRTags.CreateMissingEkaerHistories, fromDate); public Task GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync(SignalRTags.GetEkaerHistoryCount, filter); diff --git a/FruitBankHybrid.Web/appsettings.json b/FruitBankHybrid.Web/appsettings.json index 0c0a21c5..366ba808 100644 --- a/FruitBankHybrid.Web/appsettings.json +++ b/FruitBankHybrid.Web/appsettings.json @@ -23,7 +23,6 @@ } ] } - }, "AcHubConnection": { // Az Url innen KIVÉVE: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik (kódban: +/fbHub, +/loggerHub).