using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using FastEndpoints; using FrameworkBLL.Promotion; using GB5Shared.Deployment; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.DTO.Promotion; using GB5Shared.FastEndPoint; using Microsoft.Extensions.Configuration; using static FrameworkDAL.Constant.Constant; namespace FrameworkSL.Endpoints.Promotion { // ============================================================ // ExportPromotionPackage — Tier 1 (manual carry) package dispatch. // // Synchronous, direct BLL call — NOT routed through GOP. A file-download // response needs its bytes immediately; GOP execution is deliberately // async/queued (GopQueueBLL.SubmitExecution enqueues and returns, a // background worker runs it later), which is the wrong fit for "stream me // a file right now." See PromotionExportNodeExecutor's own header comment // and the plan's Transport section for why Export and Ingest differ here. // // A package may batch several promotion candidates (req.Items) into one // encrypted file, per the user's explicit ask. // ============================================================ public class ExportPromotionPackage : BaseEndpoint< ExportPromotionPackage.ExportPromotionPackageParameters, ResponseStandardDTO> { private readonly IPromotionBLL _PromotionBLL; private readonly IConfiguration _Configuration; private readonly IGB5Environment _GB5Environment; public ExportPromotionPackage(IPromotionBLL promotionBLL, IConfiguration configuration, IGB5Environment gb5Environment) { _PromotionBLL = promotionBLL; _Configuration = configuration; _GB5Environment = gb5Environment; } public override void Configure() { Post("/Promotion/ExportPromotionPackage"); AllowAnonymous(); } public record ExportPromotionPackageParameters( [property: FromHeader] string Login, [property: FromBody] PromotionExportRequestDTO Request ); protected override async Task> ExecuteAsync( ExportPromotionPackageParameters req, LoginDTO LoginDTO, CancellationToken ct) { try { if (req.Request.Items.Count == 0) throw new ArgumentException("At least one item is required to export a package."); if (string.IsNullOrWhiteSpace(req.Request.DestinationEnvironmentCode)) throw new ArgumentException("DestinationEnvironmentCode is required."); // Defaults to this deployment's own tier code (Part A, GB5Shared.Deployment) — // the appsettings key is now an optional override only, for the rare case of // re-running a historical promotion under a different origin code. No longer // throws if unset (previously shipped as the literal "CHANGE_ME_PER_ENVIRONMENT" // placeholder, a "ships broken until hand-edited" footgun). var configuredOverride = _Configuration["Promotion:OriginEnvironmentCode"]; var originEnvironmentCode = string.IsNullOrWhiteSpace(configuredOverride) ? _GB5Environment.GopEnvironmentCode : configuredOverride; var bundles = new List(req.Request.Items.Count); foreach (var item in req.Request.Items) { var bundle = await _PromotionBLL.BuildExportBundleAsync( item.EntityTypeCode, item.OriginEntityId, LoginDTO, ct).ConfigureAwait(false); bundle.OriginEnvironmentCode = originEnvironmentCode; bundles.Add(bundle); } var packageBytes = await _PromotionBLL.BuildPackageAsync( bundles, req.Request.DestinationEnvironmentCode, LoginDTO, ct).ConfigureAwait(false); var fileName = $"promotion-package-{DateTime.UtcNow:yyyyMMddHHmmss}.gbpromo"; using var ms = new MemoryStream(packageBytes); await HttpContext.Response.SendStreamAsync( ms, fileName, ms.Length, "application/octet-stream", cancellation: ct); // Return value unused once SendStreamAsync has written the response. return null!; } catch (Exception ex) { return await GB5Shared.ResponseStandard.Response.CreateExceptionError( ex, CacheKeyLevel.NOT_REQUIRED, LoginDTO, ex.Message, 500); } } } }