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

247 lines
10 KiB
C#

using Fitness_core_BE.DTOs.PaymentGateways;
using Fitness_core_BE.DTOs.PlatformBilling;
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Helpers;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models;
using Microsoft.AspNetCore.Http;
namespace Fitness_core_BE.Services
{
// The gym owner's own card(s) for paying their HexaFitness platform subscription.
// Payment-Service is the source of truth for everything about a card (masked PAN, brand,
// expiry, default flag) — this service never caches those locally. The one thing Core
// keeps is ClientCard.CardToken, because ClientInvoice.FKClientCardId is a pre-existing
// int FK that needs a stable local row to point at; a ClientCard row is only ever created
// once Payment-Service has confirmed a card (see CompleteCardOnboardingAsync) — there's no
// local "pending" bookkeeping, since Payment-Service already tracks that itself.
public class ClientPaymentMethodService : IClientPaymentMethodService
{
private const string GatewayName = "DirectPay";
private readonly IPlatformBillingRepository _platformBillingRepository;
private readonly IPaymentsEnablementRepository _paymentsEnablementRepository;
private readonly IPaymentGatewayClient _paymentGatewayClient;
private readonly UserRoleHelper _userRoleHelper;
private readonly IUnitOfWork _uow;
public ClientPaymentMethodService(
IPlatformBillingRepository platformBillingRepository,
IPaymentsEnablementRepository paymentsEnablementRepository,
IPaymentGatewayClient paymentGatewayClient,
UserRoleHelper userRoleHelper,
IUnitOfWork uow)
{
_platformBillingRepository = platformBillingRepository;
_paymentsEnablementRepository = paymentsEnablementRepository;
_paymentGatewayClient = paymentGatewayClient;
_userRoleHelper = userRoleHelper;
_uow = uow;
}
public async Task<StartCardOnboardingResult> StartCardOnboardingAsync(
HttpContext httpContext,
StartCardOnboardingRequest request,
CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var client = await _platformBillingRepository.GetClientByIdAsync(clientId, ct)
?? throw new NotFoundException("Client not found.");
var isFirstCard = (await _platformBillingRepository.GetClientCardsAsync(clientId, ct)).Count == 0;
var response = await _paymentGatewayClient.OnboardCardAsync(
clientId,
new OnboardCardRequest
{
GatewayName = GatewayName,
CustomerId = clientId.ToString(),
CardLabel = string.IsNullOrWhiteSpace(request.CardLabel) ? "Gym billing card" : request.CardLabel,
CardholderName = client.ClientName ?? "Gym Owner",
IsDefault = isFirstCard,
ReturnUrl = request.ReturnUrl
},
ct);
if (response.Status == "Active")
{
// Synchronous gateway (no hosted redirect) — finalize immediately. DirectPay
// never takes this path today (always hosted/pending); kept for gateway-agnosticism.
await PersistConfirmedCardAsync(clientId, response.CardToken, isFirstCard, ct);
}
return new StartCardOnboardingResult
{
RedirectUrl = response.RedirectUrl ?? string.Empty,
SessionId = response.GatewayRef
};
}
public async Task<CardOnboardingStatusResult> CompleteCardOnboardingAsync(
HttpContext httpContext,
string sessionId,
CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
// No local lookup — Payment-Service already tracks the pending onboarding by
// (gatewayName, sessionId) and verifies it belongs to this platform.
var status = await _paymentGatewayClient.GetOnboardingStatusAsync(clientId, GatewayName, sessionId, ct);
if (status.Status != "Active")
{
return new CardOnboardingStatusResult { IsComplete = false };
}
var existingLocal = await _platformBillingRepository.GetClientCardByTokenAsync(clientId, status.CardToken, ct);
if (existingLocal is null)
{
var isFirstCard = (await _platformBillingRepository.GetClientCardsAsync(clientId, ct)).Count == 0;
await PersistConfirmedCardAsync(clientId, status.CardToken, isFirstCard, ct);
}
return new CardOnboardingStatusResult
{
IsComplete = true,
Card = new ClientCardDto
{
CardToken = status.CardToken,
MaskedPan = status.MaskedPan,
CardBrand = status.CardBrand,
ExpiryMonth = status.ExpiryMonth,
ExpiryYear = status.ExpiryYear,
IsDefault = status.IsDefault
}
};
}
public async Task<List<ClientCardDto>> ListCardsAsync(HttpContext httpContext, CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var cards = await _paymentGatewayClient.ListCardsAsync(clientId, clientId.ToString(), ct);
return cards
.Select(card => new ClientCardDto
{
CardToken = card.CardToken,
MaskedPan = card.MaskedPan,
CardBrand = card.CardBrand,
ExpiryMonth = card.ExpiryMonth,
ExpiryYear = card.ExpiryYear,
IsDefault = card.IsDefault
})
.ToList();
}
public async Task RemoveCardAsync(HttpContext httpContext, string cardToken, CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
await _paymentGatewayClient.DeactivateCardAsync(clientId, cardToken, ct);
// Best-effort local sync: only exists to keep ClientInvoice's FK anchor consistent
// with Payment-Service, not treated as the source of truth.
var local = await _platformBillingRepository.GetClientCardByTokenAsync(clientId, cardToken, ct);
if (local is not null)
{
await _uow.BeginAsync();
try
{
local.IsActive = false;
local.IsPrimary = false;
await _uow.CommitAsync();
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
}
public async Task<ClientCardDto> SetDefaultCardAsync(HttpContext httpContext, string cardToken, CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var result = await _paymentGatewayClient.SetDefaultCardAsync(clientId, cardToken, clientId.ToString(), ct);
await _uow.BeginAsync();
try
{
var localCards = await _platformBillingRepository.GetClientCardsAsync(clientId, ct);
foreach (var other in localCards)
{
other.IsPrimary = other.CardToken == cardToken;
}
await _uow.CommitAsync();
}
catch
{
await _uow.RollbackAsync();
throw;
}
var details = await _paymentGatewayClient.GetCardDetailsAsync(clientId, cardToken, ct);
return new ClientCardDto
{
CardToken = result.CardToken,
MaskedPan = details.MaskedPan,
CardBrand = details.CardBrand,
ExpiryMonth = details.ExpiryMonth,
ExpiryYear = details.ExpiryYear,
IsDefault = result.IsDefault
};
}
public async Task<TransactionHistoryResult> GetTransactionHistoryAsync(
HttpContext httpContext,
int page,
int pageSize,
CancellationToken ct = default)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var result = await _paymentGatewayClient.ListPaymentsAsync(clientId, cardToken: null, page, pageSize, ct);
return new TransactionHistoryResult
{
Total = result.Total,
Page = result.Page,
Items = result.Items
.Select(item => new TransactionHistoryItemDto
{
PaymentId = item.PaymentId,
AmountMinorUnits = item.AmountMinorUnits,
Currency = item.Currency,
Status = item.Status,
CreatedAtUtc = item.CreatedAtUtc
})
.ToList()
};
}
private async Task PersistConfirmedCardAsync(Guid clientId, string cardToken, bool isPrimary, CancellationToken ct)
{
await _uow.BeginAsync();
try
{
await _paymentsEnablementRepository.CreateClientCardAsync(
new ClientCard
{
FK_ClientId = clientId,
CardToken = cardToken,
IsActive = true,
IsPrimary = isPrimary
},
ct);
await _uow.CommitAsync();
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
}
}