Files
Core_Be/Domain/CustomerSubscriptionFactory.cs
T
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

147 lines
6.5 KiB
C#

using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Models;
namespace Fitness_core_BE.Domain
{
public sealed record CustomerSubscriptionAggregate(
CustomerSubscription Subscription,
CustomerSubItem InitialSubItem,
SubscriptionMember PrimaryMember,
IReadOnlyList<SubscriptionInstallment> Installments);
public static class CustomerSubscriptionFactory
{
public static CustomerSubscriptionAggregate Create(
CustomerSubscription subscription,
CustomerPlan plan,
CustomerSubItem initialSubItem,
DateTime? nowUtc = null)
{
var now = nowUtc ?? DateTime.UtcNow;
subscription.SubscriptionId =
subscription.SubscriptionId == Guid.Empty
? Guid.NewGuid()
: subscription.SubscriptionId;
subscription.CreatedAt = now;
subscription.OnboardDate =
subscription.OnboardDate == default ? now : subscription.OnboardDate;
if (plan.DurationMode == "FixedRange")
{
if (plan.ValidTo == null || plan.ValidFrom == null)
throw new BadRequestException("Fixed-range plan is missing its validity dates.");
if (now > plan.ValidTo.Value)
throw new BadRequestException("This plan's validity period has already ended.");
if (subscription.OnboardDate < plan.ValidFrom.Value)
subscription.OnboardDate = plan.ValidFrom.Value;
subscription.ExpiryDate ??= plan.ValidTo.Value;
}
else if (subscription.ExpiryDate == null && plan.DurationInDays > 0)
{
subscription.ExpiryDate =
subscription.OnboardDate.AddDays(plan.DurationInDays);
}
// RenewalDate starts equal to ExpiryDate and is the only one of the two that ever
// advances again (on each approved renewal cycle) -- ExpiryDate is frozen from here on.
subscription.RenewalDate ??= subscription.ExpiryDate;
initialSubItem.CustomerSubItemId =
initialSubItem.CustomerSubItemId == Guid.Empty
? Guid.NewGuid()
: initialSubItem.CustomerSubItemId;
initialSubItem.FKCustomerSubscriptionId = subscription.SubscriptionId;
initialSubItem.CreatedAt = now;
var primaryMember = new SubscriptionMember
{
SubscriptionMemberId = Guid.NewGuid(),
FKSubscriptionId = subscription.SubscriptionId,
FKCustomerId = subscription.FKCustomerId,
IsPrimary = true,
JoinedAt = now,
IsActive = true
};
return new CustomerSubscriptionAggregate(
subscription,
initialSubItem,
primaryMember,
BuildInstallmentSchedule(subscription, plan, now));
}
// Internal (not private): reused by SubscriptionRenewalApprovalService to build a fresh
// installment schedule for a renewal cycle on a subscription that already exists (no new
// CustomerSubscription row involved, unlike Create above).
internal static IReadOnlyList<SubscriptionInstallment> BuildInstallmentSchedule(
CustomerSubscription subscription,
CustomerPlan plan,
DateTime now,
DateTime? anchorDate = null)
{
// Defaults to subscription.OnboardDate, exactly Create()'s existing behavior (a
// brand-new subscription's OnboardDate is always "now" for that cycle). The
// renewal-approval flow passes the approval moment explicitly instead: unlike the old
// renewal path, the same CustomerSubscription row now persists across many cycles, so
// OnboardDate stays frozen at the very first cycle and can no longer anchor a later
// cycle's due dates on its own.
var scheduleAnchor = anchorDate ?? subscription.OnboardDate;
var count = Math.Max(1, plan.InstallmentCount);
var onboardingFee = plan.HasOnboardingFee ? plan.OnboardingFeeAmount : 0m;
// Mirrors CreateCustomerInvoiceFromSubscription's discount base (plan price +
// onboarding fee) so the installment schedule shown to the client (and used to
// prefill "record payment") matches what the invoice actually charges. Only the
// plan-price portion absorbs the reduction -- discountAmount=0 when no discount was
// given reproduces today's undiscounted split exactly.
var discountAmount = DiscountCalculator.ComputeDiscountAmount(
plan.PlanPrice + onboardingFee, subscription.DiscountType, subscription.DiscountValue);
var discountedPlanPrice = Math.Max(0m, plan.PlanPrice - discountAmount);
var perInstallment = Math.Round(discountedPlanPrice / count, 2);
var rows = new List<SubscriptionInstallment>(count);
decimal allocated = 0m;
double? fixedRangeStepDays = null;
if (plan.DurationMode == "FixedRange" &&
plan.ValidTo != null &&
count > 1)
{
var windowDays =
(plan.ValidTo.Value - scheduleAnchor).TotalDays;
if (windowDays > 0)
fixedRangeStepDays = windowDays / count;
}
for (var index = 0; index < count; index++)
{
var membershipAmount =
index == count - 1
? discountedPlanPrice - allocated
: perInstallment;
allocated += membershipAmount;
rows.Add(new SubscriptionInstallment
{
SubscriptionInstallmentId = Guid.NewGuid(),
FKSubscriptionId = subscription.SubscriptionId,
InstallmentNo = index + 1,
DueDate = fixedRangeStepDays.HasValue
? scheduleAnchor.AddDays(
fixedRangeStepDays.Value * index)
: scheduleAnchor.AddMonths(index),
Amount = membershipAmount + (index == 0 ? onboardingFee : 0m),
PaidAmount = 0m,
Status = "Pending",
IsActive = true,
CreatedAt = now
});
}
return rows;
}
}
}