247 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|