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
+146
View File
@@ -0,0 +1,146 @@
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;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace Fitness_core_BE.Domain
{
public static class DiscountCalculator
{
public static decimal ComputeDiscountAmount(decimal subtotal, string? discountType, decimal? discountValue)
{
if (string.IsNullOrEmpty(discountType) || discountValue is not decimal value || value <= 0 || subtotal <= 0)
return 0m;
var discount = discountType switch
{
"Percentage" => subtotal * Math.Clamp(value, 0m, 100m) / 100m,
"Flat" => Math.Clamp(value, 0m, subtotal),
_ => 0m
};
return Math.Round(discount, 2);
}
}
}