using Fitness_core_BE.Domain; using Fitness_core_BE.Helpers; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.DTOs.CustomerSubscription; using System.Text.Json; namespace Fitness_core_BE.Services.CustomerSubscription { public class CustomerSubscriptionService : ICustomerSubscriptionService { private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository; private readonly ICustomerInvoiceRepository _customerInvoiceRepository; private readonly IUnitOfWork _uow; private readonly UserRoleHelper _userRoleHelper; public CustomerSubscriptionService( ICustomerSubscriptionRepository customerSubscriptionRepository, ICustomerInvoiceRepository customerInvoiceRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper) { _customerSubscriptionRepository = customerSubscriptionRepository; _customerInvoiceRepository = customerInvoiceRepository; _uow = uow; _userRoleHelper = userRoleHelper; } public async Task CreateCustomerSubscription(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerId")) { throw new KeyNotFoundException("customerId is required"); } if (!data.ContainsKey("customerPlanId")) { throw new KeyNotFoundException("customerPlanId is required"); } var customerId = Guid.Parse(data["customerId"].GetString()!); var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!); // Get plan details for creating the initial sub-item var plan = await _customerSubscriptionRepository .GetPlanByIdAsync(customerPlanId); if (plan == null) { throw new KeyNotFoundException("Customer Plan not found"); } // Create subscription var subscription = new Models.CustomerSubscription { FKCustomerId = customerId, FKCustomerPlanId = customerPlanId, OnboardDate = DataExtractor.GetOptionalDateTime(data, "onboardDate") ?? DateTime.UtcNow, ExpiryDate = DataExtractor.GetOptionalDateTime(data, "expiryDate"), IsActive = true }; // Create initial sub-item representing the main plan var initialSubItem = new CustomerSubItem { SubItemName = plan.PlanName, SubItemDescription = plan.PlanDescription, SubItemPrice = plan.PlanPrice, IsActive = true }; var aggregate = CustomerSubscriptionFactory.Create( subscription, plan, initialSubItem); await _customerSubscriptionRepository.AddCustomerSubscriptionAsync( aggregate.Subscription, aggregate.InitialSubItem, aggregate.PrimaryMember, aggregate.Installments); await _uow.CommitAsync(); return new { subscriptionId = subscription.SubscriptionId, customerId = subscription.FKCustomerId, customerPlanId = subscription.FKCustomerPlanId, onboardDate = subscription.OnboardDate, expiryDate = subscription.ExpiryDate, isActive = subscription.IsActive, message = "Customer subscription created successfully with initial plan item" }; } catch { await _uow.RollbackAsync(); throw; } } public async Task GetCustomerSubscriptionById(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("subscriptionId")) { throw new KeyNotFoundException("subscriptionId is required"); } var subscriptionId = Guid.Parse(data["subscriptionId"].GetString()!); var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(subscriptionId); if (subscription == null) { throw new KeyNotFoundException("Customer Subscription not found"); } // Get sub-items var subItems = (await _customerSubscriptionRepository.GetSubItemsAsync( subscriptionId, activeOnly: false, newestFirst: false)) .Select(ToSubItemDto) .ToList(); return new CustomerSubscriptionDto { SubscriptionId = subscription.SubscriptionId, CustomerId = subscription.FKCustomerId, CustomerName = subscription.TenantCustomerLink?.DisplayName, CustomerPlanId = subscription.FKCustomerPlanId, PlanName = subscription.CustomerPlan?.PlanName, PlanPrice = subscription.CustomerPlan?.PlanPrice ?? 0, DurationInDays = subscription.CustomerPlan?.DurationInDays ?? 0, OnboardDate = subscription.OnboardDate, ExpiryDate = subscription.ExpiryDate, RenewalDate = subscription.RenewalDate, IsActive = subscription.IsActive, CreatedAt = subscription.CreatedAt, DiscountType = subscription.DiscountType, DiscountValue = subscription.DiscountValue, SubItems = subItems }; } public async Task GetCustomerSubscriptionsByCustomerId(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerId")) { throw new KeyNotFoundException("customerId is required"); } var customerId = Guid.Parse(data["customerId"].GetString()!); var subscriptions = await _customerSubscriptionRepository.GetCustomerSubscriptionsByCustomerId(customerId); return subscriptions.Select(s => new { subscriptionId = s.SubscriptionId, customerPlanId = s.FKCustomerPlanId, planName = s.CustomerPlan?.PlanName, planPrice = s.CustomerPlan?.PlanPrice, durationInDays = s.CustomerPlan?.DurationInDays, onboardDate = s.OnboardDate, expiryDate = s.ExpiryDate, renewalDate = s.RenewalDate, isActive = s.IsActive, createdAt = s.CreatedAt }).ToList(); } public async Task GetActiveSubscriptionByCustomerId(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerId")) { throw new KeyNotFoundException("customerId is required"); } var customerId = Guid.Parse(data["customerId"].GetString()!); var subscription = await _customerSubscriptionRepository.GetActiveGymAccessSubscriptionAsync(customerId); if (subscription == null) { return new { message = "No active subscription found for this customer" }; } // Get sub-items var subItems = (await _customerSubscriptionRepository.GetSubItemsAsync( subscription.SubscriptionId, activeOnly: false, newestFirst: false)) .Select(ToSubItemDto) .ToList(); return new CustomerSubscriptionDto { SubscriptionId = subscription.SubscriptionId, CustomerId = subscription.FKCustomerId, CustomerName = subscription.TenantCustomerLink?.DisplayName, CustomerPlanId = subscription.FKCustomerPlanId, PlanName = subscription.CustomerPlan?.PlanName, PlanPrice = subscription.CustomerPlan?.PlanPrice ?? 0, DurationInDays = subscription.CustomerPlan?.DurationInDays ?? 0, OnboardDate = subscription.OnboardDate, ExpiryDate = subscription.ExpiryDate, RenewalDate = subscription.RenewalDate, IsActive = subscription.IsActive, CreatedAt = subscription.CreatedAt, DiscountType = subscription.DiscountType, DiscountValue = subscription.DiscountValue, SubItems = subItems }; } // ---------- Group membership & summaries (client dashboard) ---------- public async Task GetSubscriptionSummary(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); var subscriptionId = Guid.Parse(data!["subscriptionId"].GetString()!); var subscription = await LoadAuthorizedSubscription(subscriptionId, httpContext); return await BuildSummaryAsync(subscription); } public async Task AddMemberToSubscription(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var subscriptionId = Guid.Parse(data!["subscriptionId"].GetString()!); var customerId = Guid.Parse(data["customerId"].GetString()!); var subscription = await LoadAuthorizedSubscription(subscriptionId, httpContext); var plan = subscription.CustomerPlan; if (plan == null || plan.PlanScope != "Group") throw new ArgumentException("Members can only be added to Group plans"); var activeCount = await _customerSubscriptionRepository.GetActiveMemberCountAsync(subscriptionId); if (activeCount >= plan.MemberLimit) throw new ArgumentException($"This plan allows at most {plan.MemberLimit} members"); var customerExists = await _customerSubscriptionRepository .IsActiveCustomerAsync(customerId); if (!customerExists) throw new KeyNotFoundException("Customer not found"); var alreadyMember = await _customerSubscriptionRepository .GetActiveSubscriptionMemberByCustomerAsync(subscriptionId, customerId); if (alreadyMember != null) throw new ArgumentException("This customer is already a member of the subscription"); // One customer, one GYM-ACCESS plan at a time -- catches both "already primary // on their own plan" and "already a secondary on a different group", neither of // which the check above sees (it only looks at this one subscription). Group // plans are ordinary gym plans (no class links), so this is always the // gym-access check in practice; left as such rather than a generic // HasAnyActiveSubscriptionAsync so a class-only subscription elsewhere never // blocks joining a group. var hasOtherActiveGymPlan = await _customerSubscriptionRepository.HasActiveGymAccessPlanAsync(customerId); if (hasOtherActiveGymPlan) throw new ArgumentException("This customer already has an active plan. Remove them from it before adding them to another."); var member = await _customerSubscriptionRepository.AddSubscriptionMemberAsync(new SubscriptionMember { FKSubscriptionId = subscriptionId, FKCustomerId = customerId, IsPrimary = false }); await _uow.CommitAsync(); return new { success = true, subscriptionMemberId = member.SubscriptionMemberId, memberCount = activeCount + 1, memberLimit = plan.MemberLimit }; } catch { await _uow.RollbackAsync(); throw; } } public async Task RemoveMemberFromSubscription(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var subscriptionId = Guid.Parse(data!["subscriptionId"].GetString()!); var subscriptionMemberId = Guid.Parse(data["subscriptionMemberId"].GetString()!); await LoadAuthorizedSubscription(subscriptionId, httpContext); var member = await _customerSubscriptionRepository.GetSubscriptionMemberAsync(subscriptionMemberId); if (member == null || member.FKSubscriptionId != subscriptionId || !member.IsActive) throw new KeyNotFoundException("Subscription member not found"); if (member.IsPrimary) throw new ArgumentException("The primary member cannot be removed from a subscription"); member.IsActive = false; await _uow.CommitAsync(); return new { success = true }; } catch { await _uow.RollbackAsync(); throw; } } /// Typed variant used by ClientService.GetCustomerDetailsById (covers group secondaries). public async Task> BuildSummariesForCustomerAsync(Guid customerId) { var subscriptions = await _customerSubscriptionRepository.GetSubscriptionsForCustomerAsync(customerId); var summaries = new List(subscriptions.Count); foreach (var subscription in subscriptions) { summaries.Add(await BuildSummaryAsync(subscription)); } return summaries; } public async Task> GetExtraFeesAsync(Guid subscriptionId, HttpContext httpContext) { await LoadAuthorizedSubscription(subscriptionId, httpContext); return (await _customerSubscriptionRepository.GetSubItemsAsync( subscriptionId, activeOnly: true, newestFirst: true)) .Select(ToSubItemDto) .ToList(); } public async Task AddExtraFeeAsync(Guid subscriptionId, AddExtraFeeRequest request, HttpContext httpContext) { await LoadAuthorizedSubscription(subscriptionId, httpContext); if (string.IsNullOrWhiteSpace(request.Name)) throw new ArgumentException("A name is required for the extra fee."); if (request.Amount <= 0) throw new ArgumentException("Amount must be greater than zero."); if (request.Frequency != "OneTime" && request.Frequency != "Recurring") throw new ArgumentException("Frequency must be 'OneTime' or 'Recurring'."); await _uow.BeginAsync(); try { var subItem = new CustomerSubItem { CustomerSubItemId = Guid.NewGuid(), FKCustomerSubscriptionId = subscriptionId, SubItemName = request.Name, SubItemDescription = request.Description, SubItemPrice = request.Amount, Frequency = request.Frequency, IsActive = true, CreatedAt = DateTime.UtcNow }; await _customerSubscriptionRepository.AddSubItemAsync(subItem); // Charged immediately as its own invoice — the subscription's original // invoice is idempotent and won't retroactively pick up items added later. var invoice = await _customerInvoiceRepository.CreateSupplementalInvoiceAsync( subscriptionId, request.Name, request.Description, request.Amount); await _uow.CommitAsync(); return new { customerSubItemId = subItem.CustomerSubItemId, customerInvoiceId = invoice.CustomerInvoiceId, amount = invoice.Amount, frequency = subItem.Frequency }; } catch { await _uow.RollbackAsync(); throw; } } private async Task LoadAuthorizedSubscription(Guid subscriptionId, HttpContext httpContext) { var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(subscriptionId); if (subscription == null) throw new KeyNotFoundException("Subscription not found"); Guid userId = DataExtractor.GetUserIdFromContext(httpContext); Guid clientId = await _userRoleHelper.GetClientId(userId); if (subscription.CustomerPlan == null || subscription.CustomerPlan.FKClientId != clientId) throw new UnauthorizedAccessException("This subscription does not belong to your gym"); return subscription; } private async Task BuildSummaryAsync(Models.CustomerSubscription subscription) { var plan = subscription.CustomerPlan; var invoice = await _customerSubscriptionRepository .GetLatestInvoiceAsync(subscription.SubscriptionId); decimal invoiceAmount = invoice?.Amount ?? plan?.PlanPrice ?? 0m; decimal paid = 0m; if (invoice != null) { paid = await _customerSubscriptionRepository .GetApprovedPaymentTotalAsync(invoice.CustomerInvoiceId); } var members = (subscription.SubscriptionMembers ?? new List()) .Where(m => m.IsActive) .OrderByDescending(m => m.IsPrimary) .ThenBy(m => m.JoinedAt) .Select(m => new SubscriptionMemberDto { SubscriptionMemberId = m.SubscriptionMemberId, CustomerId = m.FKCustomerId, CusName = m.TenantCustomerLink?.DisplayName, Email = m.TenantCustomerLink?.EmailCached, PrimaryPH = m.TenantCustomerLink?.PrimaryPhoneCached, IsPrimary = m.IsPrimary, JoinedAt = m.JoinedAt, IsActive = m.IsActive, IsPlanAssigned = m.IsPlanAssigned }) .ToList(); var installments = (subscription.SubscriptionInstallments ?? new List()) .Where(i => i.IsActive) .OrderBy(i => i.InstallmentNo) .Select(i => new SubscriptionInstallmentDto { SubscriptionInstallmentId = i.SubscriptionInstallmentId, InstallmentNo = i.InstallmentNo, DueDate = i.DueDate, Amount = i.Amount, PaidAmount = i.PaidAmount, Status = i.Status }) .ToList(); AutoChargeCardDto? autoChargeCard = null; if (subscription.FKAutoChargeCardId.HasValue) { var card = await _customerSubscriptionRepository .GetCardByIdAsync(subscription.FKAutoChargeCardId.Value); if (card != null) { autoChargeCard = new AutoChargeCardDto { CardId = card.JobId, CardLast4 = card.CardNumber, CardType = card.CardType }; } } return new SubscriptionSummaryDto { SubscriptionId = subscription.SubscriptionId, CustomerPlanId = subscription.FKCustomerPlanId, PlanName = plan?.PlanName, PlanScope = plan?.PlanScope ?? "Individual", MemberLimit = plan?.MemberLimit ?? 1, DurationMode = plan?.DurationMode ?? "Rolling", AllowSplitPayments = plan?.AllowSplitPayments ?? false, PlanPrice = plan?.PlanPrice ?? 0m, OnboardDate = subscription.OnboardDate, ExpiryDate = subscription.ExpiryDate, RenewalDate = subscription.RenewalDate, IsActive = subscription.IsActive, CustomerInvoiceId = invoice?.CustomerInvoiceId, InvoiceAmount = invoiceAmount, PaidAmount = paid, Balance = invoiceAmount - paid, PaymentStatus = paid >= invoiceAmount && invoiceAmount > 0 ? "Paid" : paid > 0 ? "Partial" : "Pending", Members = members, Installments = installments, AutoChargeEnabled = subscription.AutoChargeEnabled, AutoChargeCard = autoChargeCard }; } private static CustomerSubItemDto ToSubItemDto(CustomerSubItem item) => new() { CustomerSubItemId = item.CustomerSubItemId, SubItemName = item.SubItemName, SubItemDescription = item.SubItemDescription, SubItemPrice = item.SubItemPrice, Frequency = item.Frequency, IsActive = item.IsActive, CreatedAt = item.CreatedAt }; } } // add function to add new sub-item to existing subscription, which can be used for add-on purchases