using FastEndpoints; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.FastEndPoint; using GB5Shared.Telemetry; using PayRollBLL.SAP; using System; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace PayRollSL.EndPoints.SAP { /// /// GET /SAP/PushITDeclarationToSap /// Pushes IT Declaration data to the SAP HR OData API (infotypes 580–586). /// /// HTTP 200 — every SAP infotype posting was accepted (IsSuccess = true for all results). /// HTTP 400 — one or more postings were rejected or had no data to post. /// HTTP 500 — unexpected server-side exception. /// /// Pass Infotype = 0 to push all applicable infotypes in one call. /// The Body array always contains one entry per infotype processed, with full detail /// on what was sent and what SAP returned — even for failures. /// public class PushITDeclarationToSap : BaseEndpoint> { private readonly IITDeclarationSapBLL _bll; public PushITDeclarationToSap(IITDeclarationSapBLL bll) => _bll = bll; public override void Configure() { Get("/SAP/PushITDeclarationToSap"); AllowAnonymous(); } public record PushITDeclarationToSapParameters( [property: FromHeader] string Login, [property: QueryParam] int DeclarationId, [property: QueryParam] int EmployeeId, [property: QueryParam] int PeriodId, [property: QueryParam] int Infotype, // 0 = push all 580–586 [property: QueryParam] string? Date, // dd.MM.yyyy; defaults to today [property: QueryParam] string? Remarks ); protected override string? GetCacheKey(PushITDeclarationToSapParameters req, LoginDTO login) => null; // HandleAsync is overridden directly so the HTTP status code accurately reflects the SAP outcome. // The base-class HandleAsync always calls SendAsync without a status code (defaults to 200), // which gives a misleading success signal even when SAP rejects the payload. // This override sends 200 only when every infotype posting is confirmed successful by SAP. public override async Task HandleAsync(PushITDeclarationToSapParameters req, CancellationToken ct) { using var activity = GB5ActivitySources.Endpoints.StartActivity( nameof(PushITDeclarationToSap), ActivityKind.Server); var login = GetLoginDTOFromRequest(req); if (login != null) login.RequestUrl = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.Path}"; string date = string.IsNullOrWhiteSpace(req.Date) ? DateTime.Today.ToString("dd.MM.yyyy") : req.Date; try { var results = await _bll.PushToSapAsync( req.DeclarationId, req.EmployeeId, req.PeriodId, req.Infotype, date, req.Remarks ?? string.Empty, login!, ct); bool allSucceeded = results.Count > 0 && results.All(r => r.IsSuccess); if (allSucceeded) { activity?.SetTag("gb5.success", true); activity?.SetTag("http.response.status_code", 200); activity?.SetStatus(ActivityStatusCode.Ok); var response = await GB5Shared.ResponseStandard.Response .CreateSuccessResponse(results, CacheKeyLevel.NOT_REQUIRED, login!); await Send.ResponseAsync(response, statusCode: 200, cancellation: ct); } else { string errorSummary = BuildErrorSummary(results); activity?.SetTag("gb5.success", false); activity?.SetTag("gb5.app.error", errorSummary); activity?.SetTag("http.response.status_code", 400); activity?.SetStatus(ActivityStatusCode.Error, errorSummary); Logger.LogWarning( "[PushITDeclarationToSap] SAP push completed with failures. {Summary}", errorSummary); var response = new ResponseStandardDTO { Status = GB5Shared.DTO.Framework.Enum.FrameworkEnumDTO.ResponseStatus.Failed, Body = results, ErrorBody = errorSummary, ErrorInnerException = string.Empty, }; await Send.ResponseAsync(response, statusCode: 400, cancellation: ct); } } catch (OperationCanceledException) when (ct.IsCancellationRequested) { activity?.SetTag("gb5.success", false); activity?.SetStatus(ActivityStatusCode.Ok); await Send.ResponseAsync(new ResponseStandardDTO { Status = GB5Shared.DTO.Framework.Enum.FrameworkEnumDTO.ResponseStatus.Failed, ErrorBody = "The request was cancelled by the client.", ErrorInnerException = string.Empty, }, statusCode: 499, cancellation: CancellationToken.None); } catch (Exception ex) { Logger.LogError(ex, "[PushITDeclarationToSap] {ExceptionType}: {Message}", ex.GetType().Name, ex.Message); activity?.SetTag("gb5.success", false); activity?.SetTag("gb5.error.type", ex.GetType().Name); activity?.SetTag("gb5.error.message", ex.Message); activity?.SetTag("http.response.status_code", 500); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); await Send.ResponseAsync(new ResponseStandardDTO { Status = GB5Shared.DTO.Framework.Enum.FrameworkEnumDTO.ResponseStatus.Failed, ErrorBody = ex.Message, ErrorInnerException = ex.InnerException?.Message ?? string.Empty, }, statusCode: 500, cancellation: ct); } } // ExecuteAsync satisfies the abstract contract of BaseEndpoint. // All execution is handled by HandleAsync above; this method is never called. protected override Task> ExecuteAsync( PushITDeclarationToSapParameters req, LoginDTO login, CancellationToken ct) => Task.FromResult(new ResponseStandardDTO()); private static string BuildErrorSummary( System.Collections.Generic.List results) { if (results.Count == 0) return "No SAP posting results were returned. " + "Verify that at least one filter parameter (DeclarationId, EmployeeId, or PeriodId) is provided."; var failed = results.Where(r => !r.IsSuccess).ToList(); if (failed.Count == 0) return string.Empty; // should not reach here — caller checked allSucceeded if (failed.Count == 1) return $"Infotype {failed[0].Infotype} ({failed[0].Description}): {failed[0].Message}"; return $"{failed.Count} SAP posting(s) failed — " + string.Join("; ", failed.Select(r => $"{r.Description}: {r.Message}")); } } }