Add environment guard & EKÁER NAV submit; test safety

- Integration tests now fail fast if run against PROD, using a new TestServerGuard and async [TestInitialize] methods.
- Test config is unified via shared appsettings.json, loaded at assembly init.
- SignalR protocol extended: server announces environment on connect; client hydrates and verifies environment, logging changes.
- EKÁER grid supports NAV submission with async UI and status updates.
- Backend and protocol updated for SendEkaerToNav support.
- Minor UI and error handling improvements.
This commit is contained in:
Loretta 2026-07-13 19:00:13 +02:00
parent 7fc6c8a8ef
commit 42f5c86ce4
19 changed files with 346 additions and 23 deletions

View File

@ -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)"
]
}
}

View File

@ -74,6 +74,59 @@ 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,6 +46,15 @@ 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,10 +15,14 @@ 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*/,
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
{
_fruitBankDataController = fruitBankDataController;
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
SerializerOptions = new AcBinarySerializerOptions();
@ -30,6 +34,32 @@ 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

@ -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;
/// <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,6 +35,7 @@ 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,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;
/// <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,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

View File

@ -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

View File

@ -76,4 +76,10 @@
<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,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

View File

@ -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]

View File

@ -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);
}

View File

@ -0,0 +1,38 @@
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

@ -0,0 +1,21 @@
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,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

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="200" AllowSort="false" AllowGroup="false">
<DxGridDataColumn Caption="Műveletek" Width="290" AllowSort="false" AllowGroup="false">
<CellDisplayTemplate>
@{
var row = (EkaerHistory)context.DataItem;
@ -89,6 +89,12 @@
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>
@ -219,6 +225,12 @@
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;
@ -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;

View File

@ -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<List<MeasuringModel>?> GetMeasuringModels() => GetAllAsync<List<MeasuringModel>>(SignalRTags.GetMeasuringModels);
public Task<bool> IsProdEnvironment() => GetAllAsync<bool>(SignalRTags.IsProdEnvironment);
/// <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 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;
}
/// <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
#region Partner
public Task<List<Partner>?> GetPartners() => GetAllAsync<List<Partner>>(SignalRTags.GetPartners);
@ -82,6 +164,7 @@ 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

@ -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).