232 lines
10 KiB
C#
232 lines
10 KiB
C#
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<object> 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<object> 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<Guid> { 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<object> 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<object>(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<object> 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<object>(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<object> 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<object> 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 };
|
|
}
|
|
}
|
|
}
|