using System.Net.Http.Json; using System.Text.Json; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; namespace EntitlementBLL.Payment; /// PAY's own SourceDocType registry has no shared enum (confirmed via direct code /// read — TPAYORDER.SOURCEDOCTYPE is a plain int, PAYDAL.Constants.SourceDocTypeConstants is /// the new registry both sides must agree on). Kept as a local mirror constant here since /// Entitlement has no project reference to PAYDAL — the numeric value (100) must always match /// PAYDAL.Constants.SourceDocTypeConstants.EntitlementSubscription exactly. public static class PaySourceDocType { public const int EntitlementSubscription = 100; } public class PaySubscriptionClient : IPaySubscriptionClient { private readonly IHttpClientFactory _HttpClientFactory; private readonly ILogger _Logger; public PaySubscriptionClient(IHttpClientFactory httpClientFactory, ILogger logger) { _HttpClientFactory = httpClientFactory; _Logger = logger; } public async Task InitiatePaymentAsync(PayInitiatePaymentRequest req, LoginDTO login, CancellationToken ct) { var client = CreateClient(login); var body = new { SourceDocType = PaySourceDocType.EntitlementSubscription, SourceDocId = req.SubscriptionId, req.CustomerId, req.CustomerName, req.CustomerEmail, req.CustomerPhone, req.OrderAmt, req.OrderCurrency, req.GatewayCode, CouponCode = (string?)null, LoyaltyPointsToRedeem = 0m, LoyaltyProgramId = 0, LoyaltyEnrollmentId = 0, req.CallbackUrl }; var response = await client.PostAsJsonAsync("/Pay/Order/InitiatePayOrder", body, ct).ConfigureAwait(false); var raw = await ReadAndEnsureSuccessAsync(response, "InitiatePayOrder", ct).ConfigureAwait(false); using var doc = JsonDocument.Parse(raw); if (!doc.RootElement.TryGetProperty("Body", out var payBody) || payBody.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"PAY InitiatePayOrder succeeded but returned no Body: {raw}"); var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var payOrderId = payBody.TryGetProperty("Order", out var orderEl) && orderEl.TryGetProperty("PayOrderId", out var idEl) ? idEl.GetInt32() : 0; var checkoutUrl = payBody.TryGetProperty("CheckoutUrl", out var urlEl) ? urlEl.GetString() : null; if (payOrderId <= 0) throw new InvalidOperationException($"PAY InitiatePayOrder succeeded but no PayOrderId could be parsed from: {raw}"); return new PayInitiateResultDTO { PayOrderId = payOrderId, CheckoutUrl = checkoutUrl }; } public async Task GetPayOrderAsync(int payOrderId, LoginDTO login, CancellationToken ct) { var client = CreateClient(login); var response = await client.GetAsync($"/Pay/Order/GetPayOrder?PayOrderId={payOrderId}", ct).ConfigureAwait(false); var raw = await ReadAndEnsureSuccessAsync(response, "GetPayOrder", ct).ConfigureAwait(false); using var doc = JsonDocument.Parse(raw); if (!doc.RootElement.TryGetProperty("Body", out var body)) return null; // GetPayOrder's BLL/DAL path double-serializes (JsonConvert.SerializeObject at the DAL // layer, then wrapped again by CreateSuccessResponse) — Body can arrive as a JSON string // needing a second parse, not a plain object (same quirk this engagement already found // and documented for DevAdmin's GetReplicate, §41). JsonElement orderEl = body.ValueKind == JsonValueKind.String ? JsonDocument.Parse(body.GetString() ?? "null").RootElement : body; if (orderEl.ValueKind != JsonValueKind.Object) return null; var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; return orderEl.Deserialize(opts); } public async Task> GetPaymentHistoryAsync(int subscriptionId, LoginDTO login, CancellationToken ct) { var client = CreateClient(login); var response = await client.GetAsync( $"/Pay/Order/GetPayOrdersBySourceDoc?SourceDocType={PaySourceDocType.EntitlementSubscription}&SourceDocId={subscriptionId}", ct) .ConfigureAwait(false); var raw = await ReadAndEnsureSuccessAsync(response, "GetPayOrdersBySourceDoc", ct).ConfigureAwait(false); using var doc = JsonDocument.Parse(raw); if (!doc.RootElement.TryGetProperty("Body", out var body)) return Array.Empty(); JsonElement listEl = body.ValueKind == JsonValueKind.String ? JsonDocument.Parse(body.GetString() ?? "[]").RootElement : body; if (listEl.ValueKind != JsonValueKind.Array) return Array.Empty(); var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; return listEl.Deserialize>(opts) ?? new List(); } // ── Helpers ────────────────────────────────────────────────────────────── private HttpClient CreateClient(LoginDTO callerLogin) { var client = _HttpClientFactory.CreateClient("Pay"); // PAY resolves DatabaseName from TenantId internally (§7.3 — it's a shared // platform-level module, not per-tenant-database) — only ClientId/UserId carry over, // same convention SqlWorkbenchClient already established for its own reverse direction. var payLogin = new LoginDTO { ClientId = callerLogin.ClientId, UserId = callerLogin.UserId }; client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(payLogin)); return client; } private async Task ReadAndEnsureSuccessAsync(HttpResponseMessage response, string operation, CancellationToken ct) { var raw = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { _Logger.LogError("PAY {Operation} returned {StatusCode}: {Body}", operation, response.StatusCode, raw); throw new InvalidOperationException($"PAY {operation} failed ({(int)response.StatusCode}): {ExtractErrorBody(raw) ?? raw}"); } var errorBody = ExtractErrorBody(raw); if (!string.IsNullOrEmpty(errorBody)) { _Logger.LogError("PAY {Operation} returned 200 with an ErrorBody: {ErrorBody}", operation, errorBody); throw new InvalidOperationException($"PAY {operation} failed: {errorBody}"); } return raw; } private static string? ExtractErrorBody(string rawJson) { try { using var doc = JsonDocument.Parse(rawJson); if (doc.RootElement.TryGetProperty("ErrorBody", out var err) && err.ValueKind == JsonValueKind.String) return err.GetString(); } catch { /* not JSON, or no ErrorBody property — treated as success by the caller */ } return null; } }