Files
Core_Be/Services/PaymentGatewayClient.cs
T
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

179 lines
8.6 KiB
C#

using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Fitness_core_BE.DTOs.PaymentGateways;
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
namespace Fitness_core_BE.Services
{
public class PaymentGatewayClient : IPaymentGatewayClient
{
private readonly IClientRepository _clientRepository;
private readonly IPaymentServiceClient _paymentServiceClient;
private readonly IUnitOfWork _uow;
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
public PaymentGatewayClient(
IClientRepository clientRepository,
IPaymentServiceClient paymentServiceClient,
IUnitOfWork uow,
IHttpClientFactory httpClientFactory,
IConfiguration configuration)
{
_clientRepository = clientRepository;
_paymentServiceClient = paymentServiceClient;
_uow = uow;
_httpClient = httpClientFactory.CreateClient();
_baseUrl = (configuration["PaymentService:BaseUrl"] ?? "http://127.0.0.1:5120").TrimEnd('/');
}
public async Task<OnboardCardResult> OnboardCardAsync(Guid clientId, OnboardCardRequest request, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Post, "/api/v1/cards/onboard", request, ct);
return Deserialize<OnboardCardResult>(body);
}
public async Task<CardDetailsResult> GetCardDetailsAsync(Guid clientId, string cardToken, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Get, $"/api/v1/cards/{Uri.EscapeDataString(cardToken)}", null, ct);
return Deserialize<CardDetailsResult>(body);
}
public async Task<IReadOnlyCollection<CardDetailsResult>> ListCardsAsync(Guid clientId, string customerId, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Get, "/api/v1/cards", null, ct,
query: $"customerId={Uri.EscapeDataString(customerId)}");
return Deserialize<List<CardDetailsResult>>(body);
}
public async Task DeactivateCardAsync(Guid clientId, string cardToken, CancellationToken ct = default)
{
await SendAsync(clientId, HttpMethod.Delete, $"/api/v1/cards/{Uri.EscapeDataString(cardToken)}", null, ct);
}
public async Task<SetDefaultCardResult> SetDefaultCardAsync(Guid clientId, string cardToken, string customerId, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Patch, $"/api/v1/cards/{Uri.EscapeDataString(cardToken)}/default",
new { customerId }, ct);
return Deserialize<SetDefaultCardResult>(body);
}
public async Task<OnboardCardResult> GetOnboardingStatusAsync(Guid clientId, string gatewayName, string sessionId, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Get,
$"/api/v1/cards/onboarding/{Uri.EscapeDataString(gatewayName)}/{Uri.EscapeDataString(sessionId)}", null, ct);
return Deserialize<OnboardCardResult>(body);
}
public async Task<AttemptPaymentResult> AttemptPaymentAsync(Guid clientId, AttemptPaymentRequest request, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Post, "/api/v1/payments/attempt", request, ct);
return Deserialize<AttemptPaymentResult>(body);
}
public async Task<RefundPaymentResult> RefundPaymentAsync(Guid clientId, Guid paymentId, bool isVoid, CancellationToken ct = default)
{
var body = await SendAsync(clientId, HttpMethod.Post, $"/api/v1/refunds/{paymentId}", new { isVoid }, ct);
return Deserialize<RefundPaymentResult>(body);
}
public async Task<PaymentListResult> ListPaymentsAsync(Guid clientId, string? cardToken, int page, int pageSize, CancellationToken ct = default)
{
var queryParts = new List<string> { $"page={page}", $"pageSize={pageSize}" };
if (!string.IsNullOrWhiteSpace(cardToken))
{
queryParts.Add($"cardToken={Uri.EscapeDataString(cardToken)}");
}
var body = await SendAsync(clientId, HttpMethod.Get, "/api/v1/payments", null, ct, query: string.Join("&", queryParts));
return Deserialize<PaymentListResult>(body);
}
private static T Deserialize<T>(string body)
{
return JsonSerializer.Deserialize<T>(body, JsonOptions)
?? throw new ServiceUnavailableException("Payment-Service returned an empty response.");
}
/// <summary>
/// Mints this client's Payment-Service Platform the first time it's needed, storing the
/// ApiKey/HmacSecret on Client since Payment-Service only ever returns them once.
/// </summary>
private async Task<(string ApiKey, string HmacSecret)> EnsureCredentialsAsync(Guid clientId, CancellationToken ct)
{
var client = await _clientRepository.GetClientEntityByIdAsync(clientId, ct)
?? throw new NotFoundException($"Client {clientId} not found.");
if (string.IsNullOrEmpty(client.ClientPaymentApiKey) || string.IsNullOrEmpty(client.ClientPaymentHmacSecret))
{
var (platformId, apiKey, hmacSecret) = await _paymentServiceClient.ProvisionPlatformAsync(
clientId, client.ClientName ?? "Gym", ct);
client.FKPaymentPlatformId = platformId;
client.ClientPaymentApiKey = apiKey;
client.ClientPaymentHmacSecret = hmacSecret;
await _uow.SaveChangesAsync(ct);
}
return (client.ClientPaymentApiKey!, client.ClientPaymentHmacSecret!);
}
private async Task<string> SendAsync(Guid clientId, HttpMethod method, string path, object? body, CancellationToken ct, string? query = null)
{
var (apiKey, hmacSecret) = await EnsureCredentialsAsync(clientId, ct);
var bodyJson = body is null ? string.Empty : JsonSerializer.Serialize(body);
var timestamp = DateTimeOffset.UtcNow.ToString("o");
// Signed over the path only — Payment-Service's HmacValidationMiddleware signs
// against Request.Path.Value, which never includes the query string.
var signature = ComputeSignature(hmacSecret, method.Method, path, timestamp, bodyJson);
var url = string.IsNullOrEmpty(query) ? $"{_baseUrl}{path}" : $"{_baseUrl}{path}?{query}";
using var httpRequest = new HttpRequestMessage(method, url);
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequest.Headers.Add("X-Api-Key", apiKey);
httpRequest.Headers.Add("X-Timestamp", timestamp);
httpRequest.Headers.Add("X-Signature", signature);
if (body is not null)
{
httpRequest.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
}
HttpResponseMessage response;
try
{
response = await _httpClient.SendAsync(httpRequest, ct);
}
catch (HttpRequestException ex)
{
throw new ServiceUnavailableException("Could not reach Payment-Service.", ex);
}
var responseBody = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
throw new ServiceUnavailableException($"Payment-Service call failed ({(int)response.StatusCode}): {responseBody}");
}
return responseBody;
}
// Must match Payment-Service's Middleware/HmacValidationMiddleware.ComputeSignature
// exactly: METHOD\nPATH\nTIMESTAMP\nBODY, HMACSHA256, hex lowercase.
private static string ComputeSignature(string secret, string method, string path, string timestamp, string body)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var payload = $"{method}\n{path}\n{timestamp}\n{body}";
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
}