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