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;
}
}