first commit
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models;
|
||||
using Fitness_core_BE.Infra.UoW;
|
||||
using Fitness_core_BE.DTOs.CustomerPlan;
|
||||
using System.Text.Json;
|
||||
using Fitness_core_BE.Helpers;
|
||||
|
||||
|
||||
namespace Fitness_core_BE.Services.CustomerPlanService
|
||||
{
|
||||
public class CustomerPlanService : ICustomerPlanService
|
||||
{
|
||||
private const string ScopeIndividual = "Individual";
|
||||
private const string ScopeGroup = "Group";
|
||||
private const string ModeRolling = "Rolling";
|
||||
private const string ModeFixedRange = "FixedRange";
|
||||
private const string RolloverNone = "NoRollover";
|
||||
private const string RolloverGrace = "GracePeriod";
|
||||
private const string BillingPrepaid = "Prepaid";
|
||||
private const string BillingPostpaid = "Postpaid";
|
||||
|
||||
private readonly ICustomerPlanRepository _customerPlanRepository;
|
||||
private readonly IPlaceRepo _placeRepo;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
|
||||
public CustomerPlanService(
|
||||
ICustomerPlanRepository customerPlanRepository,
|
||||
IPlaceRepo placeRepo,
|
||||
IUnitOfWork uow,
|
||||
UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_customerPlanRepository = customerPlanRepository;
|
||||
_placeRepo = placeRepo;
|
||||
_uow = uow;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
|
||||
public async Task<object> GetAllCustomerPlans(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
bool includeInactive = data != null &&
|
||||
data.ContainsKey("includeInactive") &&
|
||||
data["includeInactive"].GetBoolean();
|
||||
|
||||
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
||||
Guid contextClientId = await _userRoleHelper.GetClientId(userId);
|
||||
|
||||
Guid? requestedClientId = null;
|
||||
if (data != null && data.ContainsKey("FKClientId") && data["FKClientId"].ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
requestedClientId = Guid.Parse(data["FKClientId"].GetString()!);
|
||||
}
|
||||
|
||||
var clientId = requestedClientId ?? contextClientId;
|
||||
|
||||
var plans = await _customerPlanRepository.GetAllCustomerPlans(includeInactive, clientId);
|
||||
|
||||
var placeIdsByPlan = await _placeRepo.GetPlaceIdsByPlanIdsAsync(plans.Select(p => p.CustomerPlanId).ToList());
|
||||
var allPlaceIds = placeIdsByPlan.Values.SelectMany(ids => ids).Distinct().ToList();
|
||||
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(allPlaceIds);
|
||||
|
||||
return plans.Select(p =>
|
||||
{
|
||||
var placeIds = placeIdsByPlan.GetValueOrDefault(p.CustomerPlanId) ?? new List<Guid>();
|
||||
return MapToDto(p, p.Client?.ClientName, placeIds, placeNames);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> GetCustomerPlanById(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
if (!data!.ContainsKey("customerPlanId"))
|
||||
{
|
||||
throw new KeyNotFoundException("customerPlanId is required");
|
||||
}
|
||||
|
||||
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
|
||||
var plan = await _customerPlanRepository.GetCustomerPlanById(customerPlanId);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Customer Plan not found");
|
||||
}
|
||||
|
||||
return await MapToDtoWithPlacesAsync(plan, plan.Client?.ClientName);
|
||||
}
|
||||
|
||||
public async Task<object> CreateCustomerPlan(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
if (!data!.ContainsKey("planName"))
|
||||
{
|
||||
throw new KeyNotFoundException("planName is required");
|
||||
}
|
||||
|
||||
if (!data.ContainsKey("planPrice"))
|
||||
{
|
||||
throw new KeyNotFoundException("planPrice is required");
|
||||
}
|
||||
|
||||
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
||||
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
||||
|
||||
var customerPlan = new Models.CustomerPlan
|
||||
{
|
||||
FKClientId = clientId,
|
||||
PlanName = data["planName"].GetString(),
|
||||
PlanDescription = data.ContainsKey("planDescription")
|
||||
? data["planDescription"].GetString()
|
||||
: null,
|
||||
PlanPrice = data["planPrice"].GetDecimal(),
|
||||
HasOnboardingFee = data.ContainsKey("hasOnboardingFee") && data["hasOnboardingFee"].GetBoolean(),
|
||||
OnboardingFeeAmount = data.ContainsKey("onboardingFeeAmount") ? data["onboardingFeeAmount"].GetDecimal() : 0m,
|
||||
PlanScope = data.ContainsKey("planScope") ? data["planScope"].GetString() ?? ScopeIndividual : ScopeIndividual,
|
||||
MemberLimit = data.ContainsKey("memberLimit") ? data["memberLimit"].GetInt32() : 1,
|
||||
DurationMode = data.ContainsKey("durationMode") ? data["durationMode"].GetString() ?? ModeRolling : ModeRolling,
|
||||
DurationInDays = data.ContainsKey("durationInDays") ? data["durationInDays"].GetInt32() : 0,
|
||||
ValidFrom = DataExtractor.GetOptionalDateTime(data, "validFrom"),
|
||||
ValidTo = DataExtractor.GetOptionalDateTime(data, "validTo"),
|
||||
AllowSplitPayments = data.ContainsKey("allowSplitPayments") && data["allowSplitPayments"].GetBoolean(),
|
||||
InstallmentCount = data.ContainsKey("installmentCount") ? data["installmentCount"].GetInt32() : 0,
|
||||
SessionsPerCycle = data.ContainsKey("sessionsPerCycle") && data["sessionsPerCycle"].ValueKind != JsonValueKind.Null
|
||||
? data["sessionsPerCycle"].GetInt32()
|
||||
: null,
|
||||
SessionRolloverPolicy = data.ContainsKey("sessionRolloverPolicy") ? data["sessionRolloverPolicy"].GetString() ?? RolloverNone : RolloverNone,
|
||||
RolloverGraceDays = data.ContainsKey("rolloverGraceDays") ? data["rolloverGraceDays"].GetInt32() : 0,
|
||||
BillingTiming = data.ContainsKey("billingTiming") ? data["billingTiming"].GetString() ?? BillingPrepaid : BillingPrepaid
|
||||
};
|
||||
|
||||
NormalizeAndValidate(customerPlan);
|
||||
|
||||
var result = await _customerPlanRepository.CreateCustomerPlan(customerPlan);
|
||||
await _uow.SaveChangesAsync();
|
||||
|
||||
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
|
||||
if (placeIds != null)
|
||||
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
// Fetch the created plan with client details
|
||||
var createdPlan = await _customerPlanRepository.GetCustomerPlanById(result.CustomerPlanId);
|
||||
|
||||
return await MapToDtoWithPlacesAsync(result, createdPlan?.Client?.ClientName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> UpdateCustomerPlan(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
if (!data!.ContainsKey("customerPlanId"))
|
||||
{
|
||||
throw new KeyNotFoundException("customerPlanId is required");
|
||||
}
|
||||
|
||||
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
|
||||
|
||||
var existingPlan = await _customerPlanRepository.GetCustomerPlanById(customerPlanId);
|
||||
if (existingPlan == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Customer Plan not found");
|
||||
}
|
||||
|
||||
// Merge: start from existing values, override only the provided keys,
|
||||
// then re-validate the combined configuration.
|
||||
if (data.ContainsKey("planName")) existingPlan.PlanName = data["planName"].GetString();
|
||||
if (data.ContainsKey("planDescription")) existingPlan.PlanDescription = data["planDescription"].GetString();
|
||||
if (data.ContainsKey("planPrice")) existingPlan.PlanPrice = data["planPrice"].GetDecimal();
|
||||
if (data.ContainsKey("hasOnboardingFee")) existingPlan.HasOnboardingFee = data["hasOnboardingFee"].GetBoolean();
|
||||
if (data.ContainsKey("onboardingFeeAmount")) existingPlan.OnboardingFeeAmount = data["onboardingFeeAmount"].GetDecimal();
|
||||
if (data.ContainsKey("planScope")) existingPlan.PlanScope = data["planScope"].GetString() ?? existingPlan.PlanScope;
|
||||
if (data.ContainsKey("memberLimit")) existingPlan.MemberLimit = data["memberLimit"].GetInt32();
|
||||
if (data.ContainsKey("durationMode")) existingPlan.DurationMode = data["durationMode"].GetString() ?? existingPlan.DurationMode;
|
||||
if (data.ContainsKey("durationInDays")) existingPlan.DurationInDays = data["durationInDays"].GetInt32();
|
||||
if (data.ContainsKey("validFrom")) existingPlan.ValidFrom = DataExtractor.GetOptionalDateTime(data, "validFrom");
|
||||
if (data.ContainsKey("validTo")) existingPlan.ValidTo = DataExtractor.GetOptionalDateTime(data, "validTo");
|
||||
if (data.ContainsKey("allowSplitPayments")) existingPlan.AllowSplitPayments = data["allowSplitPayments"].GetBoolean();
|
||||
if (data.ContainsKey("installmentCount")) existingPlan.InstallmentCount = data["installmentCount"].GetInt32();
|
||||
if (data.ContainsKey("sessionsPerCycle"))
|
||||
existingPlan.SessionsPerCycle = data["sessionsPerCycle"].ValueKind == JsonValueKind.Null ? null : data["sessionsPerCycle"].GetInt32();
|
||||
if (data.ContainsKey("sessionRolloverPolicy")) existingPlan.SessionRolloverPolicy = data["sessionRolloverPolicy"].GetString() ?? existingPlan.SessionRolloverPolicy;
|
||||
if (data.ContainsKey("rolloverGraceDays")) existingPlan.RolloverGraceDays = data["rolloverGraceDays"].GetInt32();
|
||||
if (data.ContainsKey("billingTiming")) existingPlan.BillingTiming = data["billingTiming"].GetString() ?? existingPlan.BillingTiming;
|
||||
if (data.ContainsKey("isActive")) existingPlan.IsActive = data["isActive"].GetBoolean();
|
||||
|
||||
NormalizeAndValidate(existingPlan);
|
||||
|
||||
var result = await _customerPlanRepository.UpdateCustomerPlan(existingPlan);
|
||||
|
||||
var placeIds = DataExtractor.GetOptionalGuidList(data, "placeIds");
|
||||
if (placeIds != null)
|
||||
await _placeRepo.SetPlanPlacesAsync(result.CustomerPlanId, placeIds);
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
var updatedPlan = await _customerPlanRepository.GetCustomerPlanById(result.CustomerPlanId);
|
||||
|
||||
return await MapToDtoWithPlacesAsync(result, updatedPlan?.Client?.ClientName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> SetCustomerPlanStatus(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
if (!data!.ContainsKey("customerPlanId"))
|
||||
{
|
||||
throw new KeyNotFoundException("customerPlanId is required");
|
||||
}
|
||||
|
||||
if (!data.ContainsKey("isActive"))
|
||||
{
|
||||
throw new KeyNotFoundException("isActive is required");
|
||||
}
|
||||
|
||||
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
|
||||
var isActive = data["isActive"].GetBoolean();
|
||||
|
||||
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
||||
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
||||
|
||||
// Scoped to the caller's gym, so a plan id from another gym reads as
|
||||
// "not found" rather than being toggled.
|
||||
var plan = await _customerPlanRepository.SetCustomerPlanActive(customerPlanId, clientId, isActive);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Customer Plan not found");
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return await MapToDtoWithPlacesAsync(plan, plan.Client?.ClientName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> DeleteCustomerPlan(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
if (!data!.ContainsKey("customerPlanId"))
|
||||
{
|
||||
throw new KeyNotFoundException("customerPlanId is required");
|
||||
}
|
||||
|
||||
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
|
||||
|
||||
// Check if hard delete is requested (default is soft delete)
|
||||
bool softDelete = !data.ContainsKey("hardDelete") || !data["hardDelete"].GetBoolean();
|
||||
|
||||
var result = await _customerPlanRepository.DeleteCustomerPlan(customerPlanId, softDelete);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
throw new KeyNotFoundException("Customer Plan not found");
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = softDelete
|
||||
? "Customer Plan deactivated successfully"
|
||||
: "Customer Plan deleted successfully",
|
||||
softDelete = softDelete
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the cross-field rules of the plan configuration and fills
|
||||
/// derived defaults (member limit, installment count, cleared date range).
|
||||
/// Throws ArgumentException on invalid combinations.
|
||||
/// </summary>
|
||||
private static void NormalizeAndValidate(Models.CustomerPlan plan)
|
||||
{
|
||||
// Scope
|
||||
if (plan.PlanScope != ScopeIndividual && plan.PlanScope != ScopeGroup)
|
||||
throw new ArgumentException($"planScope must be \"{ScopeIndividual}\" or \"{ScopeGroup}\"");
|
||||
|
||||
if (plan.PlanScope == ScopeGroup)
|
||||
{
|
||||
if (plan.MemberLimit < 2)
|
||||
throw new ArgumentException("memberLimit must be at least 2 for Group plans (couple = 2)");
|
||||
}
|
||||
else
|
||||
{
|
||||
plan.MemberLimit = 1;
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (plan.DurationMode != ModeRolling && plan.DurationMode != ModeFixedRange)
|
||||
throw new ArgumentException($"durationMode must be \"{ModeRolling}\" or \"{ModeFixedRange}\"");
|
||||
|
||||
int effectiveDays;
|
||||
if (plan.DurationMode == ModeRolling)
|
||||
{
|
||||
if (plan.DurationInDays < 1)
|
||||
throw new ArgumentException("durationInDays (>= 1) is required for Rolling plans");
|
||||
plan.ValidFrom = null;
|
||||
plan.ValidTo = null;
|
||||
effectiveDays = plan.DurationInDays;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (plan.ValidFrom == null || plan.ValidTo == null)
|
||||
throw new ArgumentException("validFrom and validTo are required for FixedRange plans");
|
||||
if (plan.ValidTo <= plan.ValidFrom)
|
||||
throw new ArgumentException("validTo must be after validFrom");
|
||||
plan.DurationInDays = 0;
|
||||
effectiveDays = (int)(plan.ValidTo.Value - plan.ValidFrom.Value).TotalDays;
|
||||
}
|
||||
|
||||
if (plan.PlanPrice < 0)
|
||||
throw new ArgumentException("planPrice must be a positive value");
|
||||
|
||||
if (plan.OnboardingFeeAmount < 0)
|
||||
throw new ArgumentException("onboardingFeeAmount must be zero or greater");
|
||||
if (!plan.HasOnboardingFee)
|
||||
plan.OnboardingFeeAmount = 0m;
|
||||
else if (plan.OnboardingFeeAmount <= 0)
|
||||
throw new ArgumentException("onboardingFeeAmount must be greater than zero when hasOnboardingFee is true");
|
||||
|
||||
// Split payments: mostly-equal installments (e.g. Rs. 6000 / 6 months -> 6 x Rs. 1000).
|
||||
if (plan.AllowSplitPayments)
|
||||
{
|
||||
if (plan.InstallmentCount < 2)
|
||||
{
|
||||
// Default: one installment per month of the plan's duration.
|
||||
plan.InstallmentCount = Math.Max(2, (int)Math.Round(effectiveDays / 30.0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
plan.InstallmentCount = 1;
|
||||
}
|
||||
|
||||
// Session-count / rollover / billing timing (new SRS requirements).
|
||||
if (plan.SessionRolloverPolicy != RolloverNone && plan.SessionRolloverPolicy != RolloverGrace)
|
||||
throw new ArgumentException($"sessionRolloverPolicy must be \"{RolloverNone}\" or \"{RolloverGrace}\"");
|
||||
|
||||
if (plan.SessionsPerCycle.HasValue)
|
||||
{
|
||||
if (plan.SessionsPerCycle.Value < 1)
|
||||
throw new ArgumentException("sessionsPerCycle must be at least 1 when set");
|
||||
|
||||
if (plan.SessionRolloverPolicy == RolloverGrace && plan.RolloverGraceDays < 1)
|
||||
throw new ArgumentException("rolloverGraceDays must be at least 1 when sessionRolloverPolicy is \"GracePeriod\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
// No session allowance on this plan (unlimited-access, existing gym behavior) --
|
||||
// rollover settings are meaningless without a per-cycle count to roll over.
|
||||
plan.SessionRolloverPolicy = RolloverNone;
|
||||
plan.RolloverGraceDays = 0;
|
||||
}
|
||||
|
||||
if (plan.BillingTiming != BillingPrepaid && plan.BillingTiming != BillingPostpaid)
|
||||
throw new ArgumentException($"billingTiming must be \"{BillingPrepaid}\" or \"{BillingPostpaid}\"");
|
||||
}
|
||||
|
||||
private async Task<CustomerPlanDto> MapToDtoWithPlacesAsync(Models.CustomerPlan plan, string? clientName)
|
||||
{
|
||||
var placeIds = await _placeRepo.GetPlaceIdsForPlanAsync(plan.CustomerPlanId);
|
||||
var placeNames = await _placeRepo.GetPlaceNamesByIdsAsync(placeIds);
|
||||
return MapToDto(plan, clientName, placeIds, placeNames);
|
||||
}
|
||||
|
||||
private static CustomerPlanDto MapToDto(
|
||||
Models.CustomerPlan plan,
|
||||
string? clientName,
|
||||
List<Guid> placeIds,
|
||||
Dictionary<Guid, string> placeNames) => new()
|
||||
{
|
||||
CustomerPlanId = plan.CustomerPlanId,
|
||||
FKClientId = plan.FKClientId,
|
||||
ClientName = clientName,
|
||||
PlanName = plan.PlanName,
|
||||
PlanDescription = plan.PlanDescription,
|
||||
PlanPrice = plan.PlanPrice,
|
||||
HasOnboardingFee = plan.HasOnboardingFee,
|
||||
OnboardingFeeAmount = plan.OnboardingFeeAmount,
|
||||
DurationInDays = plan.DurationInDays,
|
||||
PlanScope = plan.PlanScope,
|
||||
MemberLimit = plan.MemberLimit,
|
||||
DurationMode = plan.DurationMode,
|
||||
ValidFrom = plan.ValidFrom,
|
||||
ValidTo = plan.ValidTo,
|
||||
AllowSplitPayments = plan.AllowSplitPayments,
|
||||
InstallmentCount = plan.InstallmentCount,
|
||||
SessionsPerCycle = plan.SessionsPerCycle,
|
||||
SessionRolloverPolicy = plan.SessionRolloverPolicy,
|
||||
RolloverGraceDays = plan.RolloverGraceDays,
|
||||
BillingTiming = plan.BillingTiming,
|
||||
IsActive = plan.IsActive,
|
||||
CreatedAt = plan.CreatedAt,
|
||||
PlaceIds = placeIds,
|
||||
PlaceNames = placeIds.Select(id => placeNames.GetValueOrDefault(id, "")).Where(n => n != "").ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user