using System.Text.Json; using GB5Shared.DTO.Framework.Login; using SwBLL.ClientDatabase; using SwBLL.ClientDatabaseKey; using SwBLL.DdlScript; using SwBLL.Provisioning; using SwBLL.UpgradePackage; namespace SwBLL.OfflineRollout; public class OfflineRolloutExportBLL : IOfflineRolloutExportBLL { private readonly IUpgradePackageBLL _UpgradePackageBLL; private readonly IDdlScriptBLL _DdlScriptBLL; private readonly IClientDatabaseKeyBLL _KeyBLL; private readonly IRolloutPackageCrypto _Crypto; private readonly IClientDatabaseBLL _ClientDatabaseBLL; private readonly IProvisioningBLL _ProvisioningBLL; public OfflineRolloutExportBLL( IUpgradePackageBLL upgradePackageBLL, IDdlScriptBLL ddlScriptBLL, IClientDatabaseKeyBLL keyBLL, IRolloutPackageCrypto crypto, IClientDatabaseBLL clientDatabaseBLL, IProvisioningBLL provisioningBLL) { _UpgradePackageBLL = upgradePackageBLL; _DdlScriptBLL = ddlScriptBLL; _KeyBLL = keyBLL; _Crypto = crypto; _ClientDatabaseBLL = clientDatabaseBLL; _ProvisioningBLL = provisioningBLL; } public async Task ExportOfflinePackageAsync(int clientDbId, int packageId, LoginDTO loginDTO, CancellationToken ct) { // Real GB5 ClientDbIds are large negative AutoNumber-allocated values (e.g. -1370001001), // never small positive ones — a "clientDbId <= 0" guard would reject every genuine // client database, the same live-caught bug class already fixed in // EntitlementBLL.ClientEntitlementSnapshotBLL.GetSnapshotAsync. Only reject the // genuinely-missing/default case. if (clientDbId == 0) throw new ArgumentException("ClientDbId is required.", nameof(clientDbId)); if (packageId <= 0) throw new ArgumentException("PackageId is required.", nameof(packageId)); var package = await _UpgradePackageBLL.GetById(packageId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"UpgradePackage {packageId} not found."); if (package.PkgStatus != 2) // 2 = Released, same convention as GetProvisioningChain's own filter throw new InvalidOperationException($"UpgradePackage {packageId} is not Released — only Released packages can be exported."); var key = await _KeyBLL.GetActiveByClientDb(clientDbId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Active public key registered for ClientDbId {clientDbId} — register one via " + "ClientDatabaseKeyBLL.RegisterKeyAsync before exporting an offline package to it."); var lines = await _UpgradePackageBLL.GetDdlLines(packageId, loginDTO, ct).ConfigureAwait(false); var bundle = new OfflinePackageBundleDTO { PackageId = package.PackageId, PackageCode = package.PackageCode, DbModelId = package.DbModelId, PackageContentType = package.PackageContentType, ReleaseVersion = package.ReleaseVersion, }; foreach (var line in lines.OrderBy(l => l.Sequence)) { var script = await _DdlScriptBLL.GetById(line.DdlScriptId, loginDTO, ct).ConfigureAwait(false); if (script is null) continue; bundle.Scripts.Add(new OfflinePackageScriptDTO { ObjectName = line.ObjectName, ObjectType = script.ObjectType, SqlScript = script.SqlScript, Checksum = script.Checksum, Sequence = line.Sequence, }); } var plaintext = JsonSerializer.SerializeToUtf8Bytes(bundle); return await _Crypto.EncryptPackageAsync(plaintext, key.PublicKeyPem, ct).ConfigureAwait(false); } public async Task CheckForUpdatesAsync(int clientDbId, LoginDTO loginDTO, CancellationToken ct) { // Real GB5 ClientDbIds are large negative AutoNumber-allocated values (e.g. -1370001001), // never small positive ones — a "clientDbId <= 0" guard would reject every genuine // client database, the same live-caught bug class already fixed in // EntitlementBLL.ClientEntitlementSnapshotBLL.GetSnapshotAsync. Only reject the // genuinely-missing/default case. if (clientDbId == 0) throw new ArgumentException("ClientDbId is required.", nameof(clientDbId)); var clientDb = await _ClientDatabaseBLL.GetById(clientDbId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"ClientDatabase {clientDbId} not found."); var currentVersion = await _ProvisioningBLL.GetCurrentVersion(clientDbId, loginDTO, ct).ConfigureAwait(false); // Universal-scope chain only — an on-prem/BYOC install checking in for itself has no // caller-supplied scope context (partner/industry/country) beyond its own DbModel. var chain = await _UpgradePackageBLL.GetProvisioningChain(clientDb.DbModelId, null, loginDTO, ct).ConfigureAwait(false); var latest = chain.FirstOrDefault(c => c.ScopeType == 0); // Universal bool updateAvailable = latest is not null && latest.PackageId != currentVersion.MostRecentlyAppliedPackageId; return new CheckForUpdatesResultDTO { ClientDbId = clientDbId, UpdateAvailable = updateAvailable, LatestPackageId = latest?.PackageId, LatestPackageCode = latest?.PackageCode, CurrentReleaseVersion = currentVersion.CurrentReleaseVersion, }; } }