using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using Fitness_core_BE.Infra.UoW; using System.Text.Json; using Fitness_core_BE.Helpers; namespace Fitness_core_BE.Services.CustomerInvoice { public class CustomerInvoiceService : ICustomerInvoiceService { private readonly ICustomerInvoiceRepository _customerInvoiceRepository; private readonly IPaymentRepository _paymentRepository; private readonly IUnitOfWork _uow; private readonly UserRoleHelper _userRoleHelper; public CustomerInvoiceService(ICustomerInvoiceRepository customerInvoiceRepository, IPaymentRepository paymentRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper) { _customerInvoiceRepository = customerInvoiceRepository; _paymentRepository = paymentRepository; _uow = uow; _userRoleHelper = userRoleHelper; } public async Task CreateCustomerInvoiceFromSubscription(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("subscriptionId")) { throw new KeyNotFoundException("subscriptionId is required"); } var subscriptionId = Guid.Parse(data["subscriptionId"].GetString()!); var result = await _customerInvoiceRepository.CreateCustomerInvoiceFromSubscription(subscriptionId); await _uow.CommitAsync(); return result; } catch { await _uow.RollbackAsync(); throw; } } public async Task GetCustomerInvoiceById(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerInvoiceId")) { throw new KeyNotFoundException("customerInvoiceId is required"); } var customerInvoiceId = Guid.Parse(data["customerInvoiceId"].GetString()!); var customerInvoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId); if (customerInvoice == null) { throw new KeyNotFoundException("CustomerInvoice not found"); } var paid = await _paymentRepository.GetApprovedPaidAmountAsync(customerInvoice.CustomerInvoiceId); var carriedForward = await _customerInvoiceRepository.GetCarriedForwardAmountsAsync( new List { customerInvoice.CustomerInvoiceId }); carriedForward.TryGetValue(customerInvoice.CustomerInvoiceId, out var carriedForwardAmount); var link = customerInvoice.CustomerSubscription?.TenantCustomerLink; return new { customerInvoice.CustomerInvoiceId, customerInvoice.FKCustomerSubscriptionId, customerInvoice.FKClientId, customerInvoice.FKUserId, customerInvoice.Amount, // The portion of Amount that's a prior cycle's unpaid balance rolled forward // (vs. this cycle's own charge) -- see SubscriptionRenewalRunner/BuildAndAddInvoice. CarriedForwardAmount = carriedForwardAmount, customerInvoice.Status, customerInvoice.CustomerInvoiceDate, PaidAmount = paid, Balance = customerInvoice.Amount - paid, // Flattened for callers that don't want to reach through // CustomerSubscription.TenantCustomerLink for basic display fields. CustomerName = link?.DisplayName, CustomerPhone = link?.PrimaryPhoneCached, customerInvoice.CustomerSubscription }; } public async Task GetCustomerInvoicesByCustomerId(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 invoices = await _customerInvoiceRepository.GetCustomerInvoicesByCustomerId(customerId); // Same enriched shape as GetCustomerInvoicesByClientId -- this now backs a public // "view previous invoices" route the frontend renders as MemberInvoice rows. var result = new List(invoices.Count); foreach (var invoice in invoices) { var paid = await _paymentRepository.GetApprovedPaidAmountAsync(invoice.CustomerInvoiceId); var link = invoice.CustomerSubscription?.TenantCustomerLink; result.Add(new { invoice.CustomerInvoiceId, invoice.FKCustomerSubscriptionId, invoice.FKClientId, invoice.Amount, invoice.Status, invoice.IsActive, invoice.CustomerInvoiceDate, PaidAmount = paid, Balance = invoice.Amount - paid, CustomerName = link?.DisplayName, CustomerPhone = link?.PrimaryPhoneCached, FKCustomerId = invoice.CustomerSubscription?.FKCustomerId, invoice.CustomerSubscription }); } return result; } public async Task GetCustomerInvoicesByClientId(object payload, HttpContext httpContext) { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); Guid clientId = await _userRoleHelper.GetClientId(userId); // scope=="latest": one row per customer (their latest approved invoice) instead of // every invoice they've ever had -- used by the regular invoices tab now that the // renewal-approval workflow can leave older invoices (Paid/RolledOver/Cancelled) // sitting alongside the current one. Default (no scope) keeps today's full-history // behavior for any other existing caller of this endpoint. var data = DeserializePayload.Deserialize(payload); var scope = data != null && data.ContainsKey("scope") ? data["scope"].GetString() : null; var invoices = scope == "latest" ? await _customerInvoiceRepository.GetLatestApprovedInvoicePerCustomerAsync(clientId) : await _customerInvoiceRepository.GetCustomerInvoicesByClientId(clientId); // Enrich each invoice with its approved paid total and outstanding balance, // preserving the nested shape the frontend reads. var result = new List(invoices.Count); foreach (var invoice in invoices) { var paid = await _paymentRepository.GetApprovedPaidAmountAsync(invoice.CustomerInvoiceId); var link = invoice.CustomerSubscription?.TenantCustomerLink; result.Add(new { invoice.CustomerInvoiceId, invoice.FKCustomerSubscriptionId, invoice.FKClientId, invoice.Amount, invoice.Status, invoice.IsActive, invoice.CustomerInvoiceDate, PaidAmount = paid, Balance = invoice.Amount - paid, // Flattened for callers that don't want to reach through // CustomerSubscription.TenantCustomerLink for basic display fields. CustomerName = link?.DisplayName, CustomerPhone = link?.PrimaryPhoneCached, FKCustomerId = invoice.CustomerSubscription?.FKCustomerId, invoice.CustomerSubscription }); } return result; } public async Task UpdateCustomerInvoiceStatus(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerInvoiceId") || !data.ContainsKey("status")) { throw new KeyNotFoundException("customerInvoiceId and status are required"); } var customerInvoiceId = Guid.Parse(data["customerInvoiceId"].GetString()!); var status = data["status"].GetString()!; // Validate status var validStatuses = new[] { "Pending", "Partial", "Paid", "Failed", "Cancelled", "Refunded", "RolledOver" }; if (!validStatuses.Contains(status)) { throw new InvalidOperationException($"Invalid status. Valid statuses are: {string.Join(", ", validStatuses)}"); } var result = await _customerInvoiceRepository.UpdateCustomerInvoiceStatus(customerInvoiceId, status); await _uow.CommitAsync(); return result; } catch { await _uow.RollbackAsync(); throw; } } public async Task CalculateCustomerInvoiceTotal(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data!.ContainsKey("customerInvoiceId")) { throw new KeyNotFoundException("customerInvoiceId is required"); } var customerInvoiceId = Guid.Parse(data["customerInvoiceId"].GetString()!); var customerInvoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId); var total = customerInvoice?.Amount ?? 0; return new { Total = total }; } } }