using System; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using GB5Shared.Auth.Jwt; using GB5Shared.Telemetry; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace RecruitmentBLL.Integration { /// public class DxpPartyIntegrationService : IDxpPartyIntegrationService { // Must equal DXPBLL.Auth.DXPJwtService's private SigningKeyVaultPath constant — a token // signed at any other path would never validate under DXP's own JwtBearer scheme. private const string SigningKeyVaultPath = "dxp/jwt-signing-key"; // Sentinel "system" caller — matches DXPDAL.Common.DXPSystemContext.GetSystemLogin's own // default parameter value. GetSystemLogin never validates this against MDXPUSER, it only // embeds it into the resulting LoginDTO.UserId, so no real DXP user account is required. private const int SystemDxpUserId = -1; // Same "trailing integer token" convention DxpAuthIntegrationService.TrailingIdRegex // already relies on for SuccessResponse.SaveSuccessMessage-style strings. private static readonly Regex TrailingIdRegex = new(@"(-?\d+)\s*$", RegexOptions.Compiled); private readonly IHttpClientFactory _HttpClientFactory; private readonly IJwtAccessTokenIssuer _JwtIssuer; private readonly IConfiguration _Configuration; private readonly ILogger _Logger; public DxpPartyIntegrationService( IHttpClientFactory httpClientFactory, IJwtAccessTokenIssuer jwtIssuer, IConfiguration configuration, ILogger logger) { _HttpClientFactory = httpClientFactory; _JwtIssuer = jwtIssuer; _Configuration = configuration; _Logger = logger; } public async Task SavePartyAsync(int dxpPartyId, string legalName, byte partyTypeCode, CancellationToken ct) { try { GB5Trace.Step("dxp-save-party", new { dxpPartyId, legalName, partyTypeCode }); var token = await MintSystemTokenAsync(ct).ConfigureAwait(false); var client = _HttpClientFactory.CreateClient("DXPModule"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // PartyDTO shape (DXPDAL.Party.PartyDTO) posted at top level — SaveParty is a // DXPBaseEndpoint, no Login header/wrapper (caller identity comes // from the Bearer token, not the body). DxpPartyId=0 -> DXP creates a new Party // and assigns the AutoNumber id; nonzero -> DXP updates that Party's LegalName. var payload = new { DxpPartyId = dxpPartyId, LegalName = legalName, PartyTypeCode = partyTypeCode }; var response = await client.PostAsJsonAsync("DXP/Party/SaveParty", payload, ct).ConfigureAwait(false); var raw = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { var error = ExtractErrorBody(raw) ?? $"DXP returned {(int)response.StatusCode}"; _Logger.LogWarning("DXP SaveParty failed for {LegalName}: {StatusCode} {Error}", legalName, response.StatusCode, error); return new DxpPartyOutcome { Success = false, Error = error }; } var body = ExtractBody(raw); var resultPartyId = ExtractTrailingId(body) ?? (dxpPartyId != 0 ? dxpPartyId : -1); return new DxpPartyOutcome { Success = true, DxpPartyId = resultPartyId }; } catch (Exception ex) { GB5Trace.MarkFailed("dxp-save-party-failed", ex); _Logger.LogError(ex, "DXP SavePartyAsync failed for {LegalName}", legalName); return new DxpPartyOutcome { Success = false, Error = ex.Message }; } } // ── Helpers ────────────────────────────────────────────────────────────── private async Task MintSystemTokenAsync(CancellationToken ct) { var accessTokenMinutes = _Configuration.GetValue("DXPJwt:AccessTokenMinutes", 15); var issuer = _Configuration["DXPJwt:Issuer"] ?? "GB5-DXP"; var audience = _Configuration["DXPJwt:Audience"] ?? "GB5-DXP-Portal"; var claims = new DxpSystemAccessTokenClaims { DxpUserId = SystemDxpUserId }; var issued = await _JwtIssuer .IssueAsync(claims, SigningKeyVaultPath, issuer, audience, accessTokenMinutes, ct) .ConfigureAwait(false); return issued.AccessToken; } // Same permissive-parse idiom as DxpAuthIntegrationService's Extract* helpers. private static string? ExtractBody(string raw) { try { using var doc = JsonDocument.Parse(raw); if (doc.RootElement.TryGetProperty("Body", out var bodyElement)) return bodyElement.ValueKind == JsonValueKind.String ? bodyElement.GetString() : bodyElement.GetRawText(); return null; } catch { return null; } } private static string? ExtractErrorBody(string raw) { try { using var doc = JsonDocument.Parse(raw); return doc.RootElement.TryGetProperty("ErrorBody", out var errorElement) ? errorElement.GetString() : null; } catch { return null; } } private static int? ExtractTrailingId(string? body) { if (body is null) return null; var match = TrailingIdRegex.Match(body); return match.Success && int.TryParse(match.Groups[1].Value, out var id) ? id : null; } } }