first commit
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user