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.DTOs.PaymentsEnablement; using Fitness_core_BE.DTOs.PaymentGateways; using Fitness_core_BE.Models; using Fitness_core_BE.Models.Enums; namespace Fitness_core_BE.Services { public class PaymentsEnablementService : IPaymentsEnablementService { private readonly IPaymentsEnablementRepository _paymentsRepository; private readonly IClientRepository _clientRepository; private readonly IPaymentServiceClient _paymentServiceClient; private readonly UserRoleHelper _userRoleHelper; private readonly IUnitOfWork _uow; public PaymentsEnablementService( IPaymentsEnablementRepository paymentsRepository, IClientRepository clientRepository, IPaymentServiceClient paymentServiceClient, UserRoleHelper userRoleHelper, IUnitOfWork uow) { _paymentsRepository = paymentsRepository; _clientRepository = clientRepository; _paymentServiceClient = paymentServiceClient; _userRoleHelper = userRoleHelper; _uow = uow; } public Task> GetTrackOptionsAsync() { return Task.FromResult(new List { new(PaymentTrack.OwnGateway.ToString(), new { description = "Use your own PayHere/Stripe merchant account.", feePercentage = 0 }), new(PaymentTrack.HexaFitness.ToString(), new { description = "Use HexaFitness's payment gateway; requires KYC before first payout.", feePercentage = 2.5 }) }); } public async Task SelectTrackAsync(HttpContext httpContext, SelectPaymentTrackRequest request) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); if (!Enum.TryParse(request.Track, true, out var track)) throw new BadRequestException($"Unknown payment track '{request.Track}'."); var client = await _clientRepository.GetClientEntityByIdAsync(clientId) ?? throw new NotFoundException("Client not found."); client.PaymentTrack = track; await _uow.SaveChangesAsync(); return new PaymentTrackSelectionResult(track.ToString(), track == PaymentTrack.HexaFitness); } public async Task> GetAvailableGatewaysAsync( CancellationToken ct = default) { return (await _paymentServiceClient.ListGatewaysAsync(ct)) .Where(gateway => gateway.IsEnabled) .OrderByDescending(gateway => gateway.IsDefault) .ThenBy(gateway => gateway.DisplayName) .ToList(); } public async Task SubmitKycAsync( HttpContext httpContext, SubmitGatewayOnboardingRequest request, CancellationToken ct = default) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); var gateway = (await GetAvailableGatewaysAsync(ct)) .FirstOrDefault(item => item.Id == request.GatewayId) ?? throw new BadRequestException("Select an available payment gateway."); var client = await _clientRepository.GetClientEntityByIdAsync(clientId) ?? throw new NotFoundException("Client not found."); var current = await _paymentsRepository.GetKycByClientIdAsync(clientId, ct); if (current?.KycStatus == KycStatus.Pending) throw new BadRequestException("A gateway KYC request is already pending review."); if (current?.KycStatus == KycStatus.Approved) throw new BadRequestException("Gateway KYC is already approved for this client."); await _uow.BeginAsync(); try { // Catalog gateways are platform-managed, so this follows the // HexaFitness payment track rather than client-owned credentials. client.PaymentTrack = PaymentTrack.HexaFitness; var kyc = new ClientPayoutKyc { Id = Guid.NewGuid(), FKClientId = clientId, SelectedGatewayId = gateway.Id, SelectedGatewayName = gateway.DisplayName, KycStatus = KycStatus.Pending, SubmittedAt = DateTime.UtcNow }; await _paymentsRepository.CreateKycAsync(kyc, ct); await _uow.CommitAsync(); return ToDto(kyc, client); } catch { await _uow.RollbackAsync(); throw; } } public async Task GetKycStatusAsync(HttpContext httpContext) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); var kyc = await _paymentsRepository.GetKycByClientIdAsync(clientId); return kyc is null ? new GatewayOnboardingStatusDto("NotSubmitted", null, null, null) : new GatewayOnboardingStatusDto( kyc.KycStatus.ToString(), kyc.SelectedGatewayId, kyc.SelectedGatewayName, kyc.RejectionReason); } public async Task GetWalletAsync(HttpContext httpContext) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); var wallet = await _paymentsRepository.GetWalletByClientIdAsync(clientId) ?? throw new NotFoundException("Wallet not found for this client."); var ledger = await _paymentsRepository.GetLedgerAsync(wallet.Id); return new WalletDto( wallet.Balance, wallet.Currency, ledger.Select(e => new WalletLedgerEntryDto(e.Id, e.EntryType.ToString(), e.Amount, e.Description, e.CreatedAt)).ToList(), wallet.LastPayoutDate); } public async Task GetPayoutScheduleAsync(HttpContext httpContext) { var userId = DataExtractor.GetUserIdFromContext(httpContext); var clientId = await _userRoleHelper.GetClientId(userId); var wallet = await _paymentsRepository.GetWalletByClientIdAsync(clientId) ?? throw new NotFoundException("Wallet not found for this client."); var today = DateTime.UtcNow.Date; var daysUntilTuesday = ((int)DayOfWeek.Tuesday - (int)today.DayOfWeek + 7) % 7; daysUntilTuesday = daysUntilTuesday == 0 ? 7 : daysUntilTuesday; var nextPayoutDate = today.AddDays(daysUntilTuesday); return new PayoutScheduleDto(nextPayoutDate, "Weekly (Tuesday)", wallet.Balance); } // NIC fields used to come from the multi-gym onboarding wizard's OwnerActivation record; // that flow no longer exists for a single already-real club (see PayoutKycReviewService's // ToDto for the same note) -- always null here now. private static ClientPayoutKycDto ToDto( ClientPayoutKyc kyc, Client client) => new( kyc.Id, kyc.FKClientId, client.ClientName, kyc.SelectedGatewayId, kyc.SelectedGatewayName, kyc.KycStatus.ToString(), kyc.SubmittedAt, kyc.ReviewedAt, kyc.RejectionReason, null, null, null); } }