// =============================================================================
// GB5 Framework — ReportExecutionResult
// Namespace : GB5Shared.DTO.ReportOrchestration
// Purpose : Unified result returned by IQueryOrchestrator.ExecuteAsync().
// Covers sync (data/stream ready), async (SysJob submitted),
// and error outcomes. The caller (API endpoint / Angular client)
// inspects ResultType to determine how to respond to the browser.
// =============================================================================
using GB5Shared.Enums.ReportOrchestration;
namespace GB5Shared.DTO.ReportOrchestration
{
// -------------------------------------------------------------------------
// ResultType — discriminates the three possible outcomes
// -------------------------------------------------------------------------
///
/// Discriminates the three possible outcomes of an orchestrated report call.
///
public enum ReportResultType : byte
{
///
/// Report executed synchronously. Data or export stream is ready.
/// Caller should stream the result to the browser immediately.
///
SyncCompleted = 0,
///
/// Report submitted as SysJob. Execution is pending.
/// Caller should return SysJobId to the Angular client.
/// Angular shows "My Reports" inbox; SignalR delivers completion.
///
AsyncSubmitted = 1,
///
/// Execution failed. Error details are in ErrorMessage / Exception.
/// Caller should return an appropriate error response.
///
Failed = 2
}
// -------------------------------------------------------------------------
// ReportExecutionResult
// -------------------------------------------------------------------------
///
/// Unified result returned by .
/// Inspect before accessing other properties.
///
public sealed class ReportExecutionResult
{
// ---------------------------------------------------------------------
// DISCRIMINATOR
// ---------------------------------------------------------------------
/// Outcome of the orchestrated execution.
public ReportResultType ResultType { get; private init; }
// ---------------------------------------------------------------------
// SYNC RESULT FIELDS (valid when ResultType = SyncCompleted)
// ---------------------------------------------------------------------
///
/// The export format of the produced output.
/// Valid when ResultType = SyncCompleted.
///
public ExportFormat ExportFormat { get; private init; }
///
/// Flat data rows for Grid format, or raw data before export processing.
/// Each row is a dictionary of fieldName → value matching MREPORTVSFIELDS.
/// Valid when ResultType = SyncCompleted AND ExportFormat = Grid.
/// Null for binary export formats (PDF, Excel, CSV) — use ExportStream.
///
public IReadOnlyList>? GridData { get; private init; }
///
/// Total row count before any paging.
/// Valid when ResultType = SyncCompleted AND ExportFormat = Grid.
///
public int TotalRows { get; private init; }
///
/// Binary export stream for PDF / Excel / CSV.
/// Valid when ResultType = SyncCompleted AND ExportFormat ≠ Grid.
/// Caller must dispose after streaming to response.
/// Null for Grid format — use GridData.
///
public Stream? ExportStream { get; private init; }
///
/// MIME type for the export stream.
/// e.g. "application/pdf", "application/vnd.openxmlformats-..."
/// Valid when ExportStream is not null.
///
public string? ContentType { get; private init; }
///
/// Suggested file name for browser download (without path).
/// e.g. "SalesAnalysis_2025-01.xlsx"
/// Valid when ExportStream is not null.
///
public string? FileName { get; private init; }
///
/// True when the result was served from cache (not re-executed).
/// Informational only — behaviour is identical to a fresh result.
///
public bool IsFromCache { get; private init; }
// ---------------------------------------------------------------------
// ASYNC RESULT FIELDS (valid when ResultType = AsyncSubmitted)
// ---------------------------------------------------------------------
///
/// The TSYSJOB.SYSJOBID submitted by the orchestrator.
/// Valid when ResultType = AsyncSubmitted.
/// Return this to the Angular client so it can track progress
/// via SignalR or "My Reports" polling.
///
public int SysJobId { get; private init; } = -1;
///
/// Human-readable message to display to the user while the job runs.
/// Valid when ResultType = AsyncSubmitted.
/// e.g. "Your report is being generated. You will be notified when ready."
///
public string? AsyncMessage { get; private init; }
// ---------------------------------------------------------------------
// ERROR FIELDS (valid when ResultType = Failed)
// ---------------------------------------------------------------------
///
/// User-friendly error description.
/// Valid when ResultType = Failed.
/// Never expose raw exception messages to the client.
///
public string? ErrorMessage { get; private init; }
///
/// Error code for structured error handling by the Angular client.
/// e.g. "DATASOURCE_UNAVAILABLE", "TIMEOUT", "ACCESS_DENIED".
/// Valid when ResultType = Failed.
///
public string? ErrorCode { get; private init; }
///
/// The underlying exception — available server-side for logging.
/// Never serialise to the client response.
/// Valid when ResultType = Failed.
///
public Exception? Exception { get; private init; }
// ---------------------------------------------------------------------
// DIAGNOSTICS — always populated
// ---------------------------------------------------------------------
///
/// Correlation ID from .
/// Include in all error responses so support can correlate server logs.
///
public Guid CorrelationId { get; private init; }
///
/// UTC timestamp when execution completed (or was submitted for async).
///
public DateTime CompletedAtUtc { get; private init; } = DateTime.UtcNow;
///
/// Wall-clock time taken from context creation to result.
/// Includes DB resolution, query, and export.
///
public TimeSpan ElapsedTime { get; private init; }
///
/// The DataSourceType that was actually used for this execution.
/// Useful for the Angular client to display "Data from: Archive DB".
///
public DataSourceType DataSourceUsed { get; private init; }
// ---------------------------------------------------------------------
// FACTORY METHODS — the only way to construct results
// Use these in the orchestrator implementation; no public constructor.
// ---------------------------------------------------------------------
///
/// Creates a successful sync result carrying flat Grid data.
///
public static ReportExecutionResult SyncGrid(
IReadOnlyList> data,
int totalRows,
DataSourceType dataSourceUsed,
Guid correlationId,
TimeSpan elapsed,
bool fromCache = false)
=> new()
{
ResultType = ReportResultType.SyncCompleted,
ExportFormat = ExportFormat.Grid,
GridData = data,
TotalRows = totalRows,
IsFromCache = fromCache,
DataSourceUsed = dataSourceUsed,
CorrelationId = correlationId,
ElapsedTime = elapsed
};
///
/// Creates a successful sync result carrying a binary export stream.
///
public static ReportExecutionResult SyncExport(
Stream stream,
string contentType,
string fileName,
ExportFormat format,
DataSourceType dataSourceUsed,
Guid correlationId,
TimeSpan elapsed)
=> new()
{
ResultType = ReportResultType.SyncCompleted,
ExportFormat = format,
ExportStream = stream,
ContentType = contentType,
FileName = fileName,
DataSourceUsed = dataSourceUsed,
CorrelationId = correlationId,
ElapsedTime = elapsed
};
///
/// Creates an async submission result (SysJob queued successfully).
///
public static ReportExecutionResult AsyncQueued(
int sysJobId,
DataSourceType dataSourceUsed,
Guid correlationId,
TimeSpan elapsed,
string? message = null)
=> new()
{
ResultType = ReportResultType.AsyncSubmitted,
SysJobId = sysJobId,
AsyncMessage = message ?? "Your report is being generated. You will be notified when it is ready.",
DataSourceUsed = dataSourceUsed,
CorrelationId = correlationId,
ElapsedTime = elapsed
};
///
/// Creates a failure result. Exception is retained for server-side
/// logging; never expose it to the client directly.
///
public static ReportExecutionResult Failure(
string errorMessage,
string errorCode,
Guid correlationId,
TimeSpan elapsed,
Exception? ex = null)
=> new()
{
ResultType = ReportResultType.Failed,
ErrorMessage = errorMessage,
ErrorCode = errorCode,
Exception = ex,
CorrelationId = correlationId,
ElapsedTime = elapsed
};
}
}