first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
+752
View File
@@ -0,0 +1,752 @@
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 Fitness_core_BE.Models.Enums;
namespace Fitness_core_BE.Services
{
public class PlatformBillingService : IPlatformBillingService
{
// The gateway a gym's saved ClientCard was onboarded through — see
// ClientPaymentMethodService, which uses the same constant for onboarding.
private const string GatewayName = "DirectPay";
private readonly IPlatformBillingRepository _repository;
private readonly IClientStatRepository _clientStatRepository;
private readonly IAccessDeviceRepo _accessDeviceRepo;
private readonly IThirdPartyService _thirdPartyService;
private readonly IPaymentGatewayClient _paymentGatewayClient;
private readonly IPaymentRepository _paymentRepository;
private readonly PlatformInvoicePaymentTokenHelper _paymentTokenHelper;
private readonly IConfiguration _config;
private readonly IUnitOfWork _uow;
public PlatformBillingService(
IPlatformBillingRepository repository,
IClientStatRepository clientStatRepository,
IAccessDeviceRepo accessDeviceRepo,
IThirdPartyService thirdPartyService,
IPaymentGatewayClient paymentGatewayClient,
IPaymentRepository paymentRepository,
PlatformInvoicePaymentTokenHelper paymentTokenHelper,
IConfiguration config,
IUnitOfWork uow)
{
_repository = repository;
_clientStatRepository = clientStatRepository;
_accessDeviceRepo = accessDeviceRepo;
_thirdPartyService = thirdPartyService;
_paymentGatewayClient = paymentGatewayClient;
_paymentRepository = paymentRepository;
_paymentTokenHelper = paymentTokenHelper;
_config = config;
_uow = uow;
}
public async Task<List<PricingPlanDto>> GetPricingPlansAsync(CancellationToken ct = default)
{
var plans = await _repository.GetPricingPlansAsync(ct);
return plans.Select(ToDto).ToList();
}
public async Task<PricingPlanDto> UpsertPricingPlanAsync(UpsertPricingPlanRequest request, CancellationToken ct = default)
{
await _uow.BeginAsync();
try
{
ClientPackage plan;
if (request.ClientPackageId.HasValue)
{
plan = await _repository.GetPackageByIdAsync(
request.ClientPackageId.Value, ct)
?? throw new NotFoundException("Pricing plan not found");
}
else
{
plan = new ClientPackage { ClientPackageId = Guid.NewGuid() };
await _repository.AddPackageAsync(plan, ct);
}
plan.PackageName = request.PackageName;
plan.PackageDescription = request.PackageDescription;
plan.PerMemberRate = request.PerMemberRate;
plan.MinimumMonthlyBill = request.MinimumMonthlyBill;
plan.MonthlyCap = request.MonthlyCap;
plan.SetupFee = request.SetupFee;
plan.IncludedMemberCount = request.IncludedMemberCount;
plan.MaxBillableMemberCount = request.MaxBillableMemberCount;
plan.OwnGatewayFeePercent = request.OwnGatewayFeePercent;
plan.OwnGatewayFeeFixed = request.OwnGatewayFeeFixed;
plan.OurGatewayFeePercent = request.OurGatewayFeePercent;
plan.OurGatewayFeeFixed = request.OurGatewayFeeFixed;
plan.PayoutDayOfMonth = request.PayoutDayOfMonth is >= 1 and <= 28 ? request.PayoutDayOfMonth : 1;
plan.PackagePrice = request.MinimumMonthlyBill;
if (request.IsDefault)
{
// Only one plan may be default at a time.
var currentDefaults = await _repository
.GetOtherDefaultPackagesAsync(plan.ClientPackageId, ct);
foreach (var d in currentDefaults)
d.IsDefault = false;
}
plan.IsDefault = request.IsDefault;
await _uow.CommitAsync();
return ToDto(plan);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<DevicePricingDto> GetDevicePricingAsync(CancellationToken ct = default)
{
var config = await GetDeviceConfigOrDefaultAsync(ct);
return new DevicePricingDto
{
DeviceRentalFeePerMonth = config.DeviceRentalFeePerMonth,
DeviceInstallationFee = config.DeviceInstallationFee,
DeviceConfigureExistingFee = config.DeviceConfigureExistingFee
};
}
public async Task<DevicePricingDto> UpdateDevicePricingAsync(DevicePricingDto request, CancellationToken ct = default)
{
await _uow.BeginAsync();
try
{
var config = await _repository.GetDevicePricingConfigAsync(ct);
if (config == null)
{
config = CreateDefaultDeviceConfig();
await _repository.AddDevicePricingConfigAsync(config, ct);
}
config.DeviceRentalFeePerMonth = request.DeviceRentalFeePerMonth;
config.DeviceInstallationFee = request.DeviceInstallationFee;
config.DeviceConfigureExistingFee = request.DeviceConfigureExistingFee;
config.UpdatedAt = DateTime.UtcNow;
await _uow.CommitAsync();
return request;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<Models.ClientInvoice> AssignPricingPlanAsync(Guid clientId, AssignPricingPlanRequest request, CancellationToken ct = default)
{
await _uow.BeginAsync();
try
{
var plan = await _repository.GetPackageByIdAsync(
request.ClientPackageId, ct)
?? throw new NotFoundException("Pricing plan not found");
var client = await _repository.GetClientByIdAsync(clientId, ct)
?? throw new NotFoundException("Client not found");
var existingActive = await _repository
.GetActiveClientSubscriptionsAsync(clientId, ct);
foreach (var s in existingActive)
s.IsActive = false;
await _repository.AddClientSubscriptionAsync(new ClientSubscription
{
SubscriptionId = Guid.NewGuid(),
FKClientId = clientId,
FKClientPackageId = request.ClientPackageId,
OnboardDate = DateTime.UtcNow,
IsActive = true
}, ct);
if (!string.IsNullOrWhiteSpace(request.DeviceBillingMode))
{
var existingDeviceBillings = await _repository
.GetActiveDeviceBillingsAsync(clientId, ct);
foreach (var d in existingDeviceBillings)
d.IsActive = false;
await _repository.AddDeviceBillingAsync(new ClientDeviceBilling
{
Id = Guid.NewGuid(),
FKClientId = clientId,
Mode = request.DeviceBillingMode,
AgreedAmount = request.DeviceAgreedAmount ?? 0,
SetupFee = request.DeviceSetupFee ?? 0,
IsActive = true,
AssignedAt = DateTime.UtcNow
}, ct);
}
await _uow.SaveChangesAsync(ct);
// Combined onboarding payment: (machine full price OR setup-only for rental) +
// setup fee + minimum-member-subscription fee + gateway fee (may be 0).
var minMemberFee = plan.MinimumMonthlyBill;
var gatewayFee = ComputeGatewayFee(client, plan, minMemberFee);
var extraItems = new List<(string Name, string Description, decimal Amount)>();
if (!string.IsNullOrWhiteSpace(request.DeviceBillingMode))
{
if (request.DeviceBillingMode == "FullPurchase")
{
extraItems.Add(("Access device (full purchase)", "One-time device purchase", request.DeviceAgreedAmount ?? 0));
}
if ((request.DeviceSetupFee ?? 0) > 0)
{
extraItems.Add(("Device setup fee", "One-time installation/setup", request.DeviceSetupFee ?? 0));
}
}
if (minMemberFee > 0)
{
extraItems.Add(("Minimum member subscription fee", $"{plan.PackageName ?? "Standard"} plan minimum bill", minMemberFee));
}
if (gatewayFee > 0)
{
extraItems.Add(("Gateway fee", client.PaymentTrack == Models.Enums.PaymentTrack.OwnGateway ? "Own gateway fee" : "HexaFitness gateway fee", gatewayFee));
}
var invoice = await CreatePlatformInvoiceAsync(clientId,
"Plan setup",
$"Setup for the {plan.PackageName ?? "Standard"} plan",
plan.SetupFee,
extraItems,
ct);
await _uow.CommitAsync();
return invoice;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<PlatformFeeBreakdownDto> ComputeMonthlyFeeAsync(Guid clientId, CancellationToken ct = default)
{
var client = await _repository.GetClientByIdAsync(clientId, ct)
?? throw new NotFoundException("Client not found");
var plan = await ResolvePlanForClientAsync(clientId, ct);
var activeMembers = await _clientStatRepository.GetTotalCustomerCountAsync(clientId, ct);
var billableMembers = plan.MaxBillableMemberCount.HasValue
? Math.Min(activeMembers, plan.MaxBillableMemberCount.Value)
: activeMembers;
var overageMembers = Math.Max(0, billableMembers - plan.IncludedMemberCount);
decimal baseFee = plan.MinimumMonthlyBill + overageMembers * plan.PerMemberRate;
if (plan.MonthlyCap.HasValue)
baseFee = Math.Min(baseFee, plan.MonthlyCap.Value);
var deviceFee = await ComputeDeviceFeeAsync(clientId, ct);
var gatewayFee = ComputeGatewayFee(client, plan, baseFee);
return new PlatformFeeBreakdownDto
{
ClientId = clientId,
PlanName = plan.PackageName,
ActiveMemberCount = activeMembers,
PerMemberRate = plan.PerMemberRate,
MinimumMonthlyBill = plan.MinimumMonthlyBill,
MonthlyCap = plan.MonthlyCap,
BaseFee = baseFee,
DeviceCount = deviceFee.DeviceCount,
DeviceFee = deviceFee.Fee,
GatewayFee = gatewayFee,
TotalFee = baseFee + deviceFee.Fee + gatewayFee
};
}
public async Task<Models.ClientInvoice> GenerateMonthlyInvoiceAsync(Guid clientId, CancellationToken ct = default)
{
var breakdown = await ComputeMonthlyFeeAsync(clientId, ct);
await _uow.BeginAsync();
try
{
var extraItems = new List<(string, string, decimal)>();
if (breakdown.DeviceCount > 0)
extraItems.Add(("Access device rental", $"{breakdown.DeviceCount} unit(s)", breakdown.DeviceFee));
if (breakdown.GatewayFee > 0)
extraItems.Add(("Gateway fee", "Platform gateway fee for this billing period", breakdown.GatewayFee));
var invoice = await CreatePlatformInvoiceAsync(clientId,
$"Platform fee — {breakdown.ActiveMemberCount} active member(s)",
$"{breakdown.PlanName ?? "Standard"} plan — Rs. {breakdown.PerMemberRate}/member (min Rs. {breakdown.MinimumMonthlyBill})",
breakdown.BaseFee,
extraItems,
ct);
await _uow.CommitAsync();
return invoice;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<Models.ClientInvoice> GenerateOnboardingInvoiceAsync(Guid clientId, CancellationToken ct = default)
{
var plan = await ResolvePlanForClientAsync(clientId, ct);
var devicePricing = await GetDeviceConfigOrDefaultAsync(ct);
var hasDevice = await _repository.HasActiveDeviceAsync(clientId, ct);
await _uow.BeginAsync();
try
{
var invoice = await CreatePlatformInvoiceAsync(clientId,
"Onboarding & setup fee",
$"One-time setup fee for the {plan.PackageName ?? "Standard"} plan",
plan.SetupFee,
hasDevice
? new[] { ("Access device installation", "One-time installation fee", devicePricing.DeviceInstallationFee) }
: Array.Empty<(string, string, decimal)>(),
ct);
await _uow.CommitAsync();
return invoice;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<List<Models.ClientInvoice>> RunMonthlyBillingForAllClientsAsync(CancellationToken ct = default)
{
var startOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
var clientIds = await _repository
.GetMonthlyBillingCandidatesAsync(startOfMonth, ct);
var results = new List<Models.ClientInvoice>();
foreach (var clientId in clientIds)
{
// Skip clients already billed for the current period, and clients with no card on file yet
// (activation not completed) — nothing meaningful to invoice for those.
results.Add(await GenerateMonthlyInvoiceAsync(clientId, ct));
}
return results;
}
// Closes the loop RunMonthlyBillingForAllClientsAsync leaves open: generates any
// invoices newly due this period, then attempts (or re-polls) a real Payment-Service
// charge for every still-outstanding platform invoice that has a card on file.
// AttemptPaymentAsync is idempotent per invoice (IdempotencyKey below), so simply
// re-calling it on a later run is a safe, correct way to both charge new invoices and
// poll ones still Processing from a prior run — no separate local "pending" bookkeeping.
public async Task<SubscriptionChargeRunResult> RunAndChargeSubscriptionBillingAsync(CancellationToken ct = default)
{
var result = new SubscriptionChargeRunResult();
var generated = await RunMonthlyBillingForAllClientsAsync(ct);
result.InvoicesGenerated = generated.Count;
var outstanding = await _repository.GetInvoicesByStatusAsync("Outstanding", newestFirst: false, ct);
foreach (var invoice in outstanding)
{
var balance = invoice.Amount - invoice.PaidAmount;
if (balance <= 0)
continue;
var clientId = invoice.ClientSubscription.FKClientId;
var card = await _repository.GetClientCardAsync(clientId, ct);
if (card is null || string.IsNullOrEmpty(card.CardToken))
{
result.SkippedNoCard++;
continue;
}
// The most recent charge attempt already recorded for this invoice, if any.
// Payment-Service ties AmountMinorUnits to the IdempotencyKey permanently (a
// mismatched amount on replay throws), so a fresh charge always uses the
// invoice's current balance, but re-polling an in-flight one must reuse the
// exact amount it was created with.
var existingTxn = (await _paymentRepository.GetPaymentsByClientInvoiceIdAsync(invoice.ClientInvoiceId))
.Where(t => t.RefType == "platform-subscription")
.OrderByDescending(t => t.PayDate)
.FirstOrDefault();
if (existingTxn?.PayStatus == "Failed")
{
// Payment-Service payments can never leave Failed — left Pending for the
// existing manual pay-link fallback (SendClientPaymentLinkAsync) rather than
// retried automatically forever.
result.SkippedPreviouslyFailed++;
continue;
}
decimal chargeAmount;
if (existingTxn?.PayStatus == "Processing")
{
if (existingTxn.PayAmount != balance)
{
// The balance moved since the in-flight attempt was created (e.g. a
// manual partial payment) — retrying would either overcharge or hit
// Payment-Service's idempotency-conflict check. Needs manual review.
result.SkippedAmountMismatch++;
continue;
}
chargeAmount = existingTxn.PayAmount;
}
else
{
chargeAmount = balance;
}
AttemptPaymentResult attempt;
try
{
attempt = await _paymentGatewayClient.AttemptPaymentAsync(clientId, new AttemptPaymentRequest
{
IdempotencyKey = $"platform-invoice-{invoice.ClientInvoiceId}",
GatewayName = GatewayName,
CardToken = card.CardToken,
AmountMinorUnits = chargeAmount * 100m,
Currency = "LKR",
Description = $"HexaFitness platform invoice {invoice.ClientInvoiceId}"
}, ct);
}
catch (Exception ex)
{
// Non-fatal for the rest of the run: this invoice is retried (idempotently)
// next run, but the caller needs to see the run wasn't fully clean.
Console.WriteLine($"Subscription charge attempt failed for invoice {invoice.ClientInvoiceId}: {ex.Message}");
result.Errors++;
continue;
}
result.ChargesAttempted++;
await _uow.BeginAsync();
try
{
if (existingTxn is null || existingTxn.PaymentReferenceId != attempt.PaymentId.ToString())
{
await _paymentRepository.CreatePaymentAttemptAsync(new PaymentTransaction
{
PayDate = attempt.CreatedAtUtc,
PayStatus = attempt.Status,
PayAmount = chargeAmount,
FK_ClientInvoiceId = invoice.ClientInvoiceId,
FK_ClientId = clientId,
BeneficiaryType = BeneficiaryType.Client,
RefType = "platform-subscription",
PaymentReferenceId = attempt.PaymentId.ToString(),
IsActive = true,
IsApproved = attempt.Status == "Completed"
});
}
else
{
existingTxn.PayStatus = attempt.Status;
existingTxn.IsApproved = attempt.Status == "Completed";
await _paymentRepository.UpdatePaymentAttemptAsync(existingTxn);
}
if (attempt.Status == "Completed")
{
invoice.PaidAmount += chargeAmount;
invoice.Status = invoice.PaidAmount >= invoice.Amount ? "Paid" : "Partial";
result.Paid++;
}
else if (attempt.Status == "Failed")
{
result.Failed++;
}
else
{
result.StillProcessing++;
}
await _uow.CommitAsync();
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
return result;
}
public async Task<List<PendingPlatformInvoiceDto>> GetPendingInvoicesAsync(CancellationToken ct = default)
{
var pending = await _repository
.GetInvoicesByStatusAsync("Outstanding", newestFirst: false, ct);
return pending.Select(ToPendingDto).ToList();
}
public async Task<List<PendingPlatformInvoiceDto>> GetCompletedInvoicesAsync(CancellationToken ct = default)
{
var completed = await _repository
.GetInvoicesByStatusAsync("Paid", newestFirst: true, ct);
return completed.Select(ToPendingDto).ToList();
}
public async Task<PendingPlatformInvoiceDto> RecordClientInvoicePaymentAsync(Guid clientInvoiceId, decimal amount, CancellationToken ct = default)
{
if (amount <= 0)
throw new BadRequestException("Payment amount must be greater than zero.");
await _uow.BeginAsync();
try
{
var invoice = await _repository.GetInvoiceWithClientAsync(
clientInvoiceId, includeBusinesses: true, ct)
?? throw new NotFoundException("Platform invoice not found");
var balance = invoice.Amount - invoice.PaidAmount;
if (balance <= 0)
throw new BadRequestException("This invoice is already fully paid.");
invoice.PaidAmount += Math.Min(amount, balance);
invoice.Status = invoice.PaidAmount >= invoice.Amount ? "Paid" : "Partial";
await _uow.CommitAsync();
var client = invoice.ClientSubscription.Client;
if (client != null)
await NotifyPaymentActivityAsync(client, invoice, sendPaymentLink: invoice.Status != "Paid");
return ToPendingDto(invoice);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> SendClientPaymentLinkAsync(Guid clientInvoiceId, CancellationToken ct = default)
{
var invoice = await _repository.GetInvoiceWithClientAsync(
clientInvoiceId, includeBusinesses: false, ct)
?? throw new NotFoundException("Platform invoice not found");
var client = invoice.ClientSubscription.Client
?? throw new NotFoundException("Client not found for this invoice");
if (invoice.Amount - invoice.PaidAmount <= 0)
throw new BadRequestException("This invoice is already fully paid.");
await NotifyPaymentActivityAsync(client, invoice, sendPaymentLink: true);
return new { sent = true };
}
private async Task NotifyPaymentActivityAsync(Client client, Models.ClientInvoice invoice, bool sendPaymentLink)
{
var balance = invoice.Amount - invoice.PaidAmount;
var message = invoice.Status == "Paid"
? $"HexaFitness: payment of Rs. {invoice.PaidAmount:N2} received. Thank you!"
: $"HexaFitness: Rs. {balance:N2} due on your account.";
if (sendPaymentLink && balance > 0)
{
var token = _paymentTokenHelper.Generate(invoice.ClientInvoiceId);
var baseUrl = (_config["App:FrontendBaseUrl"] ?? string.Empty).TrimEnd('/');
var link = $"{baseUrl}/pay-platform/{token}";
message += $" Pay online: {link}";
}
if (!string.IsNullOrWhiteSpace(client.ClientPhone))
{
try
{
await _thirdPartyService.SendSmsAsync(client.ClientPhone!, message: message);
}
catch (Exception ex)
{
Console.WriteLine($"Platform invoice SMS failed (non-critical): {ex.Message}");
}
}
}
private async Task<(int DeviceCount, decimal Fee)> ComputeDeviceFeeAsync(Guid clientId, CancellationToken ct)
{
var devices = await _accessDeviceRepo.GetDevicesByClientAsync(clientId);
var activeDeviceCount = devices.Count(d => d.IsActive);
var deviceBilling = await _repository
.GetLatestActiveDeviceBillingAsync(clientId, ct);
if (deviceBilling != null)
{
// Rental contributes a recurring monthly fee; a full purchase is one-time
// (already billed at assignment time) and adds nothing to the recurring fee.
var fee = deviceBilling.Mode == "Rental" ? deviceBilling.AgreedAmount : 0;
return (activeDeviceCount, fee);
}
// No per-client agreement recorded yet: fall back to the global default rate.
var devicePricing = await GetDeviceConfigOrDefaultAsync(ct);
return (activeDeviceCount, activeDeviceCount * devicePricing.DeviceRentalFeePerMonth);
}
private static decimal ComputeGatewayFee(Client client, ClientPackage plan, decimal baseAmount)
{
var isOwnGateway = client.PaymentTrack == Models.Enums.PaymentTrack.OwnGateway;
var percent = isOwnGateway ? plan.OwnGatewayFeePercent : plan.OurGatewayFeePercent;
var fixedFee = isOwnGateway ? plan.OwnGatewayFeeFixed : plan.OurGatewayFeeFixed;
return Math.Round(baseAmount * percent / 100m, 2) + fixedFee;
}
private async Task<Models.ClientInvoice> CreatePlatformInvoiceAsync(
Guid clientId,
string primaryItemName,
string primaryItemDescription,
decimal primaryItemAmount,
IEnumerable<(string Name, string Description, decimal Amount)> extraItems,
CancellationToken ct)
{
var client = await _repository.GetClientByIdAsync(clientId, ct)
?? throw new NotFoundException("Client not found");
var subscription = await _repository
.GetActiveClientSubscriptionAsync(clientId, ct);
if (subscription == null)
{
var defaultPlan = await _repository.GetDefaultPackageAsync(ct);
if (defaultPlan == null)
{
defaultPlan = CreateDefaultPlan();
await _repository.AddPackageAsync(defaultPlan, ct);
}
subscription = new ClientSubscription
{
SubscriptionId = Guid.NewGuid(),
FKClientId = clientId,
FKClientPackageId = defaultPlan.ClientPackageId,
OnboardDate = DateTime.UtcNow,
IsActive = true
};
await _repository.AddClientSubscriptionAsync(subscription, ct);
}
var clientCard = await _repository.GetClientCardAsync(clientId, ct)
?? throw new BadRequestException("No card on file for this gym yet — cannot generate an invoice.");
var items = new List<(string Name, string Description, decimal Amount)>
{
(primaryItemName, primaryItemDescription, primaryItemAmount)
};
items.AddRange(extraItems);
var invoice = new Models.ClientInvoice
{
ClientInvoiceId = Guid.NewGuid(),
FKClientSubscriptionId = subscription.SubscriptionId,
FKUserId = client.FKUserId,
FKClientCardId = clientCard.CardId,
Amount = items.Sum(i => i.Amount),
PaidAmount = 0,
Status = "Pending",
ClientInvoiceDate = DateTime.UtcNow
};
var invoiceItems = items
.Select(item => new ClientInvoiceItem
{
ClientInvoiceItemId = Guid.NewGuid(),
FKClientInvoiceId = invoice.ClientInvoiceId,
ItemName = item.Name,
ItemDescription = item.Description,
ItemPrice = item.Amount,
CreatedAt = DateTime.UtcNow
})
.ToList();
await _repository.AddInvoiceAsync(invoice, invoiceItems, ct);
return invoice;
}
private async Task<ClientPackage> ResolvePlanForClientAsync(Guid clientId, CancellationToken ct)
{
return await _repository.GetActivePackageForClientAsync(clientId, ct)
?? await _repository.GetDefaultPackageAsync(ct)
?? CreateDefaultPlan();
}
private static ClientPackage CreateDefaultPlan()
{
return new ClientPackage
{
ClientPackageId = Guid.NewGuid(),
PackageName = "Standard",
PackageDescription = "Default platform pricing plan",
PerMemberRate = 0,
MinimumMonthlyBill = 0,
MonthlyCap = null,
SetupFee = 0,
IsDefault = true,
IsActive = true
};
}
private async Task<DevicePricingConfig> GetDeviceConfigOrDefaultAsync(CancellationToken ct)
{
return await _repository.GetDevicePricingConfigAsync(ct)
?? CreateDefaultDeviceConfig();
}
private static DevicePricingConfig CreateDefaultDeviceConfig() =>
new() { DevicePricingConfigId = Guid.NewGuid() };
private static PricingPlanDto ToDto(ClientPackage plan) => new()
{
ClientPackageId = plan.ClientPackageId,
PackageName = plan.PackageName,
PackageDescription = plan.PackageDescription,
PerMemberRate = plan.PerMemberRate,
MinimumMonthlyBill = plan.MinimumMonthlyBill,
MonthlyCap = plan.MonthlyCap,
SetupFee = plan.SetupFee,
IncludedMemberCount = plan.IncludedMemberCount,
MaxBillableMemberCount = plan.MaxBillableMemberCount,
OwnGatewayFeePercent = plan.OwnGatewayFeePercent,
OwnGatewayFeeFixed = plan.OwnGatewayFeeFixed,
OurGatewayFeePercent = plan.OurGatewayFeePercent,
OurGatewayFeeFixed = plan.OurGatewayFeeFixed,
PayoutDayOfMonth = plan.PayoutDayOfMonth,
IsDefault = plan.IsDefault,
IsActive = plan.IsActive
};
private static PendingPlatformInvoiceDto ToPendingDto(Models.ClientInvoice invoice)
{
var now = DateTime.UtcNow;
return new PendingPlatformInvoiceDto
{
ClientInvoiceId = invoice.ClientInvoiceId,
ClientId = invoice.ClientSubscription.FKClientId,
ClientName = invoice.ClientSubscription.Client?.ClientName,
City = invoice.ClientSubscription.Client?.ClientBusinesses?.FirstOrDefault()?.City,
Amount = invoice.Amount,
PaidAmount = invoice.PaidAmount,
Status = invoice.Status,
ClientInvoiceDate = invoice.ClientInvoiceDate,
DaysOverdue = Math.Max(0, (int)(now - invoice.ClientInvoiceDate).TotalDays)
};
}
}
}