using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using PayRollBLL.SAP.Audit; using PayRollBLL.SAP.Builders; using PayRollBLL.SAP.Client; using PayRollDAL.CustomeCode.SAP; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace PayRollBLL.SAP { public class ITDeclarationSapBLL : IITDeclarationSapBLL { private readonly IITDeclarationSapDAL _dal; private readonly ISapApiClient _sapClient; private readonly ISapDeclarationAuditLog _auditLog; private readonly ILogger _logger; public ITDeclarationSapBLL( IITDeclarationSapDAL dal, ISapApiClient sapClient, ISapDeclarationAuditLog auditLog, ILogger logger) { _dal = dal; _sapClient = sapClient; _auditLog = auditLog; _logger = logger; } public async Task> GenerateSapPayloadsAsync( int declarationId, int employeeId, int periodId, int infotype, string date, string remarks, LoginDTO login, CancellationToken ct) { if (declarationId == 0 && employeeId == 0 && periodId == 0) throw new ArgumentException( "At least one filter (DeclarationId, EmployeeId, or PeriodId) must be provided."); // Normalize envelope date to dd.MM.yyyy regardless of how the caller passed it date = NormalizeSapDate(date); var rows = (await _dal.GetDeclarationsAsync( declarationId, employeeId, periodId, login, ct) .ConfigureAwait(false)).ToList(); if (rows.Count == 0) { _logger.LogInformation( "No declarations found for SAP payload generation " + "(DeclarationId={DeclarationId}, EmployeeId={EmployeeId}, PeriodId={PeriodId})", declarationId, employeeId, periodId); return new Dictionary(); } var builders = infotype == 0 ? SapInfotypeBuilderFactory.GetAll() : new[] { SapInfotypeBuilderFactory.Get(infotype) }; var result = new Dictionary(); foreach (var builder in builders) { string json = builder.Build(rows, date, remarks); if (!string.IsNullOrEmpty(json)) result[builder.Infotype] = json; } return result; } public async Task> PushToSapAsync( int declarationId, int employeeId, int periodId, int infotype, string date, string remarks, LoginDTO login, CancellationToken ct) { if (declarationId == 0 && employeeId == 0 && periodId == 0) throw new ArgumentException( "At least one filter (DeclarationId, EmployeeId, or PeriodId) must be provided."); date = NormalizeSapDate(date); var builders = infotype == 0 ? SapInfotypeBuilderFactory.GetAll() : new[] { SapInfotypeBuilderFactory.Get(infotype) }; var results = new List(); var rows = (await _dal.GetDeclarationsAsync( declarationId, employeeId, periodId, login, ct) .ConfigureAwait(false)).ToList(); // ── No declaration record found ────────────────────────────────────────── if (rows.Count == 0) { string noDecMsg = $"No IT Declaration record found for the given filters " + $"(DeclarationId={declarationId}, EmployeeId={employeeId}, PeriodId={periodId}). " + "Verify the declaration exists and is not cancelled."; _logger.LogInformation("SAP push skipped — {Reason}", noDecMsg); foreach (var b in builders) results.Add(NoDataResult(b, noDecMsg)); return results; } // ── Post each infotype ─────────────────────────────────────────────────── foreach (var builder in builders) { bool anySent = false; // BuildAll returns one SapSubtypePayload per sub-type (e.g. 584 returns two). foreach (var sub in builder.BuildAll(rows, date, remarks)) { anySent = true; // ── Audit: log what we are about to send ───────────────────────── _auditLog.LogRequest( sub.Infotype, declarationId, employeeId, periodId, sub.EndpointPath, sub.Description, sub.Json); var apiResult = await _sapClient .PostAsync(sub.EndpointPath, sub.MsgNavProperty, sub.Json, ct) .ConfigureAwait(false); // ── Audit: log what SAP returned ───────────────────────────────── _auditLog.LogResponse( sub.Infotype, sub.EndpointPath, sub.Description, apiResult.StatusCode, apiResult.IsSuccess, apiResult.Message, apiResult.ResponseBody); JsonElement? sentPayload = null; try { sentPayload = JsonDocument.Parse(sub.Json).RootElement.Clone(); } catch { } results.Add(new SapPushResultDTO( sub.Infotype, sub.Description, sub.EndpointPath, apiResult.IsSuccess, apiResult.StatusCode, apiResult.ResponseBody, apiResult.Message, sentPayload)); _logger.LogInformation( "SAP push infotype {Infotype} ({Description}): success={Success}, status={Status}, message={Message}", sub.Infotype, sub.Description, apiResult.IsSuccess, apiResult.StatusCode, apiResult.Message); } // ── No payload built for this infotype — explain why ───────────────── if (!anySent) { string noDataMsg = string.IsNullOrEmpty(builder.EndpointPath) ? $"Infotype {builder.Infotype} ({builder.DisplayName}): " + "SAP service endpoint is not yet configured — pending confirmation from the SAP team. " + "Nothing was posted." : $"Infotype {builder.Infotype} ({builder.DisplayName}): " + "No matching declaration detail records found for this employee / period. " + "Nothing was posted to SAP."; _logger.LogInformation( "SAP push infotype {Infotype} — no payload generated. {Reason}", builder.Infotype, noDataMsg); // Always write to audit log so every call is fully traceable _auditLog.LogRequest(builder.Infotype, declarationId, employeeId, periodId, builder.EndpointPath, builder.DisplayName, "(no matching data — nothing sent to SAP)"); _auditLog.LogResponse(builder.Infotype, builder.EndpointPath, builder.DisplayName, 0, false, noDataMsg, string.Empty); results.Add(NoDataResult(builder, noDataMsg)); } } return results; } private static SapPushResultDTO NoDataResult(ISapInfotypeBuilder builder, string message) => new(builder.Infotype, builder.DisplayName, builder.EndpointPath, false, 0, string.Empty, message, null); // Accepts any common date format and returns dd.MM.yyyy as required by SAP envelope. // Handles: "01-Apr-2025", "01/04/2025", "2025-04-01", "01.04.2025" (passthrough). private static string NormalizeSapDate(string date) { if (string.IsNullOrWhiteSpace(date)) return DateTime.Today.ToString("dd.MM.yyyy"); string[] formats = { "dd-MMM-yyyy", "dd/MM/yyyy", "yyyy-MM-dd", "dd.MM.yyyy", "d-MMM-yyyy", "d/M/yyyy" }; return DateTime.TryParseExact(date, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var parsed) ? parsed.ToString("dd.MM.yyyy") : date; // unknown format — pass through as-is } } }