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:
parent
7fc6c8a8ef
commit
42f5c86ce4
|
|
@ -97,7 +97,10 @@
|
||||||
"PowerShell(Remove-Item \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Loggers\\\\FbLogItem.cs\" -Confirm:$false)",
|
"PowerShell(Remove-Item \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Loggers\\\\FbLogItem.cs\" -Confirm:$false)",
|
||||||
"PowerShell(\"=== Server ===\")",
|
"PowerShell(\"=== Server ===\")",
|
||||||
"PowerShell(\"=== Generated ===\")",
|
"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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,59 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
||||||
history.Partner = partner;
|
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ó
|
/// <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).
|
/// 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>
|
/// <c>null</c> = rendben, mehet a generálás.</summary>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
/// <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);
|
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>.
|
/// <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 +
|
/// 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>
|
/// Customer.Company) a hívóé. Fallback NINCS — null/üres marad, ha hiányzik.</summary>
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,14 @@ namespace FruitBank.Common.Server.Services.SignalRs;
|
||||||
|
|
||||||
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
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)
|
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
|
||||||
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
||||||
{
|
{
|
||||||
|
_fruitBankDataController = fruitBankDataController;
|
||||||
|
|
||||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||||
SerializerOptions = new AcBinarySerializerOptions();
|
SerializerOptions = new AcBinarySerializerOptions();
|
||||||
|
|
||||||
|
|
@ -30,6 +34,32 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
||||||
DynamicMethodRegistry.Register(stockSignalREndpointServer);
|
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()
|
protected override void LogContextUserNameAndId()
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,20 @@ public static class FruitBankConstClient
|
||||||
set => _baseUrl = value;
|
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 DefaultHubName = "fbHub";
|
||||||
public static string LoggerHubName = "loggerHub";
|
public static string LoggerHubName = "loggerHub";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public interface IFruitBankDataControllerCommon
|
||||||
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
|
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
|
||||||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
|
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
|
||||||
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId);
|
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int ekaerHistoryId);
|
||||||
|
public Task<EkaerHistory?> SendEkaerToNav(int ekaerHistoryId);
|
||||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
||||||
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
|
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
|
||||||
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
|
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
|
||||||
|
|
|
||||||
|
|
@ -127,11 +127,15 @@ public class SignalRTags : AcSignalRTags
|
||||||
public const int CreateEkaerHistory = 191;
|
public const int CreateEkaerHistory = 191;
|
||||||
public const int CreateMissingEkaerHistories = 192;
|
public const int CreateMissingEkaerHistories = 192;
|
||||||
public const int GetEkaerHistoryCount = 193;
|
public const int GetEkaerHistoryCount = 193;
|
||||||
|
public const int SendEkaerToNav = 194;
|
||||||
|
|
||||||
public const int AuthenticateUser = 195;
|
public const int AuthenticateUser = 195;
|
||||||
public const int RefreshToken = 200;
|
public const int RefreshToken = 200;
|
||||||
|
|
||||||
public const int IsProdEnvironment = 210;
|
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
|
#region SendToClient
|
||||||
public const int SendOrderChanged = 500;
|
public const int SendOrderChanged = 500;
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,12 @@ namespace FruitBankHybrid.Shared.Tests
|
||||||
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
|
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
|
||||||
|
|
||||||
[TestInitialize]
|
[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));
|
_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
|
#region Shipping
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ namespace FruitBankHybrid.Shared.Tests
|
||||||
private FruitBankSignalRClient _signalRClient = null!;
|
private FruitBankSignalRClient _signalRClient = null!;
|
||||||
|
|
||||||
[TestInitialize]
|
[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));
|
_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
|
#region EkaerHistory / Create
|
||||||
|
|
|
||||||
|
|
@ -76,4 +76,10 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@ namespace FruitBankHybrid.Shared.Tests
|
||||||
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
|
private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty);
|
||||||
|
|
||||||
[TestInitialize]
|
[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
|
#region Partner
|
||||||
|
|
|
||||||
|
|
@ -186,11 +186,12 @@ public sealed class JsonExtensionTests
|
||||||
private FruitBankSignalRClient _signalRClient = null!;
|
private FruitBankSignalRClient _signalRClient = null!;
|
||||||
|
|
||||||
[TestInitialize]
|
[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]
|
[TestMethod]
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@ public sealed class OrderClientTests
|
||||||
private FruitBankSignalRClient _signalRClient = null!;
|
private FruitBankSignalRClient _signalRClient = null!;
|
||||||
|
|
||||||
[TestInitialize]
|
[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));
|
_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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -99,11 +99,12 @@ public sealed class ToonTests
|
||||||
private FruitBankSignalRClient _signalRClient = null!;
|
private FruitBankSignalRClient _signalRClient = null!;
|
||||||
|
|
||||||
[TestInitialize]
|
[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
|
#if DEBUG
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
|
|
||||||
<DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
|
<DxGridDataColumn FieldName="Created" ReadOnly="true" DisplayFormat="yyyy.MM.dd HH:mm" />
|
||||||
<DxGridDataColumn FieldName="Modified" 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>
|
<CellDisplayTemplate>
|
||||||
@{
|
@{
|
||||||
var row = (EkaerHistory)context.DataItem;
|
var row = (EkaerHistory)context.DataItem;
|
||||||
|
|
@ -89,6 +89,12 @@
|
||||||
Enabled="@(CanDownload(row))"
|
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." })"
|
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)" />
|
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>
|
</CellDisplayTemplate>
|
||||||
</DxGridDataColumn>
|
</DxGridDataColumn>
|
||||||
|
|
@ -219,6 +225,12 @@
|
||||||
private static bool CanDownload(EkaerHistory ekaerHistory)
|
private static bool CanDownload(EkaerHistory ekaerHistory)
|
||||||
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc);
|
=> 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.
|
// A szülő-oldal (fül-számlálók) értesítése egy státuszt érintő művelet után. Üres delegáltnál no-op.
|
||||||
private Task NotifyDataChanged() => OnDataChanged.HasDelegate ? OnDataChanged.InvokeAsync() : Task.CompletedTask;
|
private 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);
|
DateTime CreateFromDate { get; set; } = DateTime.Today.AddDays(-3);
|
||||||
private bool _creatingMissing;
|
private bool _creatingMissing;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||||
DataSerializerType = AcSerializerType.Binary,
|
DataSerializerType = AcSerializerType.Binary,
|
||||||
RawResponseData = data
|
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);
|
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<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<bool> 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
|
#region Partner
|
||||||
public Task<List<Partner>?> GetPartners() => GetAllAsync<List<Partner>>(SignalRTags.GetPartners);
|
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?> AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory);
|
||||||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, 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?> 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<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<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<EkaerCreateResult?>(SignalRTags.CreateMissingEkaerHistories, fromDate);
|
||||||
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync<int>(SignalRTags.GetEkaerHistoryCount, filter);
|
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync<int>(SignalRTags.GetEkaerHistoryCount, filter);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"AcHubConnection": {
|
"AcHubConnection": {
|
||||||
// Az Url innen KIVÉVE: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik (kódban: +/fbHub, +/loggerHub).
|
// Az Url innen KIVÉVE: az fbHub és a loggerHub is az AyCode:Urls:BaseUrl-ből származik (kódban: +/fbHub, +/loggerHub).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue