Files
Core_Be/Services/CustomerService.cs
T
HarithaRandunu 5d711bd13e Facility booking-mode redesign, multi-plan subscriptions, and Classes
Implements the facility-classes-booking-dashboard plan (backend):

- Facility.BookingMode (single Scheduled/AdHoc choice) replaced with
  independent SupportsClasses/SupportsBooking flags, so one facility
  can run a class and accept walk-in bookings; a running class now
  blocks walk-in booking for its window on both create and reschedule.
- CustomerPlan.GrantsGymAccess + CustomerPlanClass join table let a
  customer hold one gym-access plan plus any number of class-only
  plans. Replaced the old single-subscription resolution
  (GetActiveSubscriptionByCustomerId/HasAnyActiveSubscriptionAsync)
  with gym-access-aware and class-aware resolvers across booking
  creation/cancellation, plan assignment, and customer self-service.
  Fixed ChangeMembershipPlan/renewal plan-changes, which resolved "the
  subscription to replace" by most-recent-created and would have
  deactivated a member's class subscription instead of their gym one.
- New FacilityClass model: recurring class definitions (Daily/Weekly/
  Monthly/Custom) with lazy occurrence generation (8-week horizon,
  race-safe via a unique index) and proper unlimited-capacity handling.
  "Mark not held" replaces the old bare-cancel: cascades to cancel and
  refund every confirmed booking on the occurrence through the same
  core CancelBooking uses, and frees the slot for walk-in booking.
  Scheduling conflicts (a walk-in booking now overlapping a class) are
  surfaced for staff to resolve via the existing Reschedule flow.
- Renamed the pre-existing "AdHoc" booking-mode terminology to
  "WalkIn" throughout (routes, methods, DTOs, the stored BookingMode
  value) for a clearer, simpler name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-09 14:26:59 +05:30

1656 lines
70 KiB
C#

using Fitness_core_BE.DTOs.Auth;
using Fitness_core_BE.Domain;
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 System.Text.Json;
using System.Globalization;
using Fitness_core_BE.Models.IOs;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Fitness_core_BE.Services.CustomerService
{
public class CustomerService : ICustomerService
{
private readonly ICustomerRepository _customerRepository;
private readonly IUnitOfWork _uow;
private readonly IAuthService _authService;
private readonly IWorkoutRepository _workoutRepository;
private readonly IMealRepository _mealRepository;
private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository;
private readonly ICustomerInvoiceRepository _customerInvoiceRepository;
private readonly UserRoleHelper _userRoleHelper;
private readonly IClientRepository _clientRepository;
private readonly IFeedService _feedService;
private readonly IAccessDeviceService _accessDeviceService;
private readonly IAccessDeviceRepo _accessDeviceRepo;
private readonly ITermsRepository _termsRepository;
private readonly ICustomerPlanRepository _customerPlanRepository;
// Customer/UserDocuments are central-bucket; CustomerSubscriptions/CustomerInvoices
// (used directly below, not through a repository) are tenant-bucket.
private readonly CentralDbContext _dbContext;
private readonly TenantDbContext _tenantDbContext;
private readonly ICustomerNotificationRepository _customerNotificationRepository;
public CustomerService(
ICustomerRepository customerRepository,
IUnitOfWork uow,
IAuthService authService,
IFeedService feedService,
IWorkoutRepository workoutRepository,
IMealRepository mealRepository,
ICustomerSubscriptionRepository customerSubscriptionRepository,
ICustomerInvoiceRepository customerInvoiceRepository,
UserRoleHelper userRoleHelper,
IClientRepository clientRepository,
IAccessDeviceService accessDeviceService,
IAccessDeviceRepo accessDeviceRepo,
ITermsRepository termsRepository,
ICustomerPlanRepository customerPlanRepository,
CentralDbContext dbContext,
TenantDbContext tenantDbContext,
ICustomerNotificationRepository customerNotificationRepository)
{
_customerRepository = customerRepository;
_uow = uow;
_authService = authService;
_feedService = feedService;
_workoutRepository = workoutRepository;
_mealRepository = mealRepository;
_customerSubscriptionRepository = customerSubscriptionRepository;
_customerInvoiceRepository = customerInvoiceRepository;
_userRoleHelper = userRoleHelper;
_clientRepository = clientRepository;
_accessDeviceService = accessDeviceService;
_accessDeviceRepo = accessDeviceRepo;
_termsRepository = termsRepository;
_customerPlanRepository = customerPlanRepository;
_dbContext = dbContext;
_tenantDbContext = tenantDbContext;
_customerNotificationRepository = customerNotificationRepository;
}
// Pre-OTP login check. Anonymous — given a phone/email, reports whether it belongs to
// an existing customer (active or not — an inactive identity still counts as "already
// has an account") and whether they've already set a fitness goal (so the app can send
// a client-created customer through goal-setting on their first login).
public async Task<object> CustomerExists(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var identifier = DataExtractor.GetRequiredString(data, "identifier");
var customer = await _customerRepository.GetCustomerByIdentifier(identifier);
if (customer == null)
return new { exists = false, hasGoal = false };
var latestTarget = await _customerRepository.GetLatestCustomerTargetAsync(customer.CusId);
return new { exists = true, hasGoal = latestTarget != null };
}
// Verification gate helper (used by the mobile controller to block actionable
// features until the customer verifies email or mobile).
public async Task<bool> IsCustomerVerified(HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
return await _customerRepository.IsverifiedCus(cusId);
}
// V1 implementation
public async Task<object> CreateCustomer(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var user = new User
{
UserName = DataExtractor.GetRequiredString(data, "fullname"),
PhoneNumber = DataExtractor.GetRequiredString(data, "mobileNumber"),
IsActive = true,
CreateDate = DateTime.UtcNow
};
var customer = new Customer
{
CusName = DataExtractor.GetRequiredString(data, "fullname"),
// The device ID ("deviceName") and the real Expo push token ("pushTokenId")
// are two different values the app sends — this column is for the push
// token, which is what PushNotificationService needs to actually send pushes.
PushTokenId = DataExtractor.GetOptionalStringByKeys(data, "pushTokenId", "PushTokenId"),
Gender = DataExtractor.GetRequiredString(data, "gender"),
BoD = DataExtractor.GetRequiredDateOfBirth(data, "bod"),
PrimaryPH = DataExtractor.GetRequiredString(data, "mobileNumber"),
IsPrimaryPHVerfied = false,
User = user
};
var result = await _customerRepository.CreateCustomer(customer);
await _uow.SaveChangesAsync();
// Anchor row for this tenant's customer FKs -- without it the first
// tenant-side write for this customer (subscription, card, attendance)
// fails the FK constraint.
await _customerRepository.EnsureTenantCustomerLink(result);
await _uow.SaveChangesAsync();
AuthResponse authResponse;
try
{
var authPayload = DeserializePayload.Deserialize(payload);
authPayload["userId"] = JsonSerializer.SerializeToElement(user.UserId);
authResponse = await _authService.RegisterUser(authPayload, httpContext);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Auth registration failed: {ex.Message}");
}
if (authResponse.StatusCode != 200 || !authResponse.Success)
{
throw new InvalidOperationException($"Auth registration failed: {authResponse.Message}");
}
await _uow.CommitAsync();
return new
{
customer = result,
auth = authResponse
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// V1 implementation
public async Task<object> CreateCusProgressRec(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid uId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(uId);
var customer = await _customerRepository.GetCustomerById(cusId)
?? throw new NotFoundException("Customer not found");
decimal? targetWeight = DataExtractor.GetOptionalDecimalByKeys(data, "targetWeight");
decimal? timePeriodMonths = DataExtractor.GetOptionalDecimalByKeys(data, "timePeriod");
if (timePeriodMonths == null)
throw new ArgumentException("Field 'timePeriod' is required.");
var createdAt = DateTime.UtcNow;
var currentWeight = data.ContainsKey("currentWeight")
? DataExtractor.GetRequiredDecimal(data, "currentWeight")
: DataExtractor.GetRequiredDecimal(data, "Weight");
var currentHeight = data.ContainsKey("currentHeight")
? DataExtractor.GetRequiredDecimal(data, "currentHeight")
: DataExtractor.GetRequiredDecimal(data, "Height");
var goal = DataExtractor.GetRequiredStringByKeys(data, "goal");
if (targetWeight == null && !CustomerProgressHelper.IsMaintainGoal(goal))
throw new ArgumentException("Field 'targetWeight' is required.");
var targetWeightValue = targetWeight ?? currentWeight;
var age = CustomerProgressHelper.GetProgressPredictionAge(data, customer, createdAt);
var gender = CustomerProgressHelper.GetProgressPredictionGender(data, customer);
var activityLevel = DataExtractor.GetRequiredStringByKeys(data, "activityLevel");
var predictions = CustomerProgressHelper.BuildProgressPredictions(
currentWeight,
targetWeightValue,
currentHeight,
goal,
timePeriodMonths.Value,
age,
gender,
activityLevel);
var latestTarget = await _customerRepository.GetLatestCustomerTargetAsync(cusId);
var customerTarget = CustomerProgressHelper.BuildCustomerTarget(
cusId,
currentWeight,
currentHeight,
targetWeightValue,
goal,
timePeriodMonths.Value,
predictions,
latestTarget,
createdAt);
await _uow.BeginAsync();
try
{
var createdTarget = await _customerRepository.CreateCustomerTargetAsync(customerTarget);
if (createdTarget == null)
throw new Exception("Something went wrong");
await _uow.SaveChangesAsync();
await _customerRepository.UpdateWeeklyProgressAsync(
cusId,
customerTarget.CustomerTargetId,
currentWeight,
createdAt);
await _uow.CommitAsync();
return new { message = "Progress record saved", customerTargetId = customerTarget.CustomerTargetId };
}
catch (Exception ex)
{
await _uow.RollbackAsync();
throw new Exception("Progress record saving failed");
}
}
// V1 implementation
public async Task<object> GetCustomerProgress(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var groupedProgress = await _customerRepository.GetCustomerProgressGroupedByGoalAsync(cusId);
if (groupedProgress == null || !groupedProgress.Any())
{
return new { message = "No progress records found" };
}
var result = groupedProgress.ToDictionary(
group => group.Key,
group => group.Value
.Select(CustomerProgressHelper.BuildCustomerProgressResponse)
.ToList());
return result;
}
//V1 implementation
public async Task<object> GetWeeklyProgressForCustomer(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
Guid customerTargetId =DataExtractor.GetRequiredGuid(data, "customerTargetId");
return await _customerRepository.GetWeeklyProgressForCustomerAsync(cusId, customerTargetId);
}
//V1 implementation
public async Task<object> UpdateCusProgressRec(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid uId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(uId);
var height = DataExtractor.GetOptionalDecimalByKeys(
data,
"height",
"Height",
"currentHeight",
"CurrentHeight");
var weight = DataExtractor.GetOptionalDecimalByKeys(
data,
"weight",
"Weight",
"currentWeight",
"CurrentWeight");
if (height == null && weight == null)
throw new ArgumentException("At least one of 'height' or 'weight' is required.");
if (height <= 0)
throw new ArgumentException("Field 'height' must be greater than zero.");
if (weight <= 0)
throw new ArgumentException("Field 'weight' must be greater than zero.");
DateTime? progressDate = null;
if (data.ContainsKey("progressDate"))
progressDate = DataExtractor.GetOptionalDateTime(data, "progressDate");
else if (data.ContainsKey("ProgressDate"))
progressDate = DataExtractor.GetOptionalDateTime(data, "ProgressDate");
await _uow.BeginAsync();
try
{
var updatedTarget = await _customerRepository.GetLatestCustomerTargetAsync(cusId)
?? throw new NotFoundException("Customer target not found");
var isChanged =
(height.HasValue && updatedTarget.CurrentHeight != height.Value) ||
(weight.HasValue && updatedTarget.CurrentWeight != weight.Value);
if (height.HasValue)
updatedTarget.CurrentHeight = height.Value;
if (weight.HasValue)
{
updatedTarget.CurrentWeight = weight.Value;
await _customerRepository.UpdateWeeklyProgressAsync(
cusId,
updatedTarget.CustomerTargetId,
weight.Value,
progressDate);
// Re-project the goal trajectory from this new weight to the (unchanged)
// target, over whatever's left of the original plan window — best-effort,
// must never break the core weight-update if the target's data is unusual.
try
{
if (updatedTarget.TargetWeight.HasValue &&
decimal.TryParse(updatedTarget.TimePeriod, NumberStyles.Any, CultureInfo.InvariantCulture, out var totalMonths))
{
var totalWeeksAvailable = (double)totalMonths * 4.33;
var elapsedWeeks = Math.Max(0.0, (DateTime.UtcNow - updatedTarget.CreatedAt).TotalDays / 7.0);
var remainingWeeks = Math.Max(1.0, totalWeeksAvailable - elapsedWeeks);
var rebuiltWeights = CustomerProgressHelper.RebuildPredictedWeights(
weight.Value,
updatedTarget.TargetWeight.Value,
updatedTarget.Goal,
remainingWeeks);
updatedTarget.PredictedWeightsJson = JsonSerializer.Serialize(rebuiltWeights);
}
}
catch
{
// Leave the existing PredictedWeightsJson in place if re-projection fails.
}
}
updatedTarget.IsChanged = updatedTarget.IsChanged || isChanged;
await _uow.CommitAsync();
return new
{
message = "Measurements updated",
customerTargetId = updatedTarget.CustomerTargetId,
currentHeight = updatedTarget.CurrentHeight,
currentWeight = updatedTarget.CurrentWeight,
isChanged = updatedTarget.IsChanged
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> AssignPlansToCustomer(Guid customerId, object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (data!.ContainsKey("workoutPlanIds") && data["workoutPlanIds"].ValueKind == JsonValueKind.Array)
{
var workoutPlanIds = data["workoutPlanIds"].EnumerateArray()
.Select(x => Guid.Parse(x.GetString()!))
.ToList();
foreach (var workoutPlanId in workoutPlanIds)
{
var customerWorkout = new CustomerWorkout
{
FKCustomerId = customerId,
FKWorkoutPlanId = workoutPlanId,
AssignedDate = DateTime.UtcNow,
IsActive = true
};
await _workoutRepository.AssignWorkoutToCustomerAsync(customerWorkout);
}
}
if (data.ContainsKey("mealPlanIds") && data["mealPlanIds"].ValueKind == JsonValueKind.Array)
{
var mealPlanIds = data["mealPlanIds"].EnumerateArray()
.Select(x => Guid.Parse(x.GetString()!))
.ToList();
foreach (var mealPlanId in mealPlanIds)
{
var customerMeal = new Models.CustomerMeal
{
FKCustomerId = customerId,
FKMealPlanId = mealPlanId,
AssignedDate = DateTime.UtcNow,
IsActive = true
};
await _mealRepository.AssignMealToCustomerAsync(customerMeal);
}
}
Guid? subscriptionId = null;
Guid? invoiceId = null;
decimal? invoiceAmount = null;
bool alreadyAssigned = false;
// The controller always sends the customerPlanId key (null when the
// member is created without a plan — e.g. a group secondary added later).
if (data.ContainsKey("customerPlanId") && data["customerPlanId"].ValueKind != JsonValueKind.Null)
{
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
// A class-linked plan (created from the Classes tab's "Manage Plan" action)
// stacks freely with other class-only plans and with one gym-access plan --
// only a second GYM-ACCESS subscription is blocked. See
// CustomerPlan.GrantsGymAccess.
var grantsGymAccess = await _customerSubscriptionRepository.PlanGrantsGymAccessAsync(customerPlanId);
// Guard against duplicate active subscriptions at this gym: callers like
// the create-existing-customer and edit-customer flows submit whatever plan
// is currently shown on the form, not necessarily a deliberate "assign a new
// plan" action, so re-submitting an already-assigned plan must not stack a
// second active subscription. Assigning a genuinely different GYM plan
// belongs to ChangeMembershipPlan, which explicitly deactivates the old one
// first; a genuinely different CLASS plan is just a new, additional
// subscription (handled below) -- it's only a duplicate of itself that's
// blocked here.
var existingActive = grantsGymAccess
? await _customerSubscriptionRepository.GetActiveGymAccessSubscriptionAsync(customerId)
: await _customerSubscriptionRepository.GetActiveSubscriptionForPlanAsync(customerId, customerPlanId);
if (existingActive != null)
{
alreadyAssigned = true;
subscriptionId = existingActive.SubscriptionId;
var existingInvoice = await _customerInvoiceRepository.GetInvoiceBySubscriptionIdAsync(existingActive.SubscriptionId);
invoiceId = existingInvoice?.CustomerInvoiceId;
invoiceAmount = existingInvoice?.Amount;
}
else
{
if (grantsGymAccess)
{
// Not primary anywhere (checked above), but could still be an active
// secondary on someone else's group plan -- GetActiveGymAccessSubscriptionAsync
// can't see that (it only matches CustomerSubscription.FKCustomerId, the
// primary). One customer, one GYM-ACCESS plan at a time; class-only
// plans are unrestricted.
var hasOtherActiveGymPlan = await _customerSubscriptionRepository.HasActiveGymAccessPlanAsync(customerId);
if (hasOtherActiveGymPlan)
throw new ArgumentException("This customer already has an active plan. Remove them from it before assigning a new one.");
}
var onboardDate = DataExtractor.GetOptionalDateTime(data, "onboardDate");
var chargePayment = DataExtractor.GetOptionalBool(data, "chargePayment") ?? true;
var subscription = new Models.CustomerSubscription
{
FKCustomerPlanId = customerPlanId,
FKCustomerId = customerId,
OnboardDate = onboardDate ?? DateTime.UtcNow,
ExpiryDate = DataExtractor.GetOptionalDateTime(data, "expiryDate"),
IsActive = true,
DiscountType = DataExtractor.GetOptionalString(data, "discountType"),
DiscountValue = DataExtractor.GetOptionalDecimal(data, "discountValue")
};
var initialSubItem = new Models.CustomerSubItem
{
SubItemName = data.ContainsKey("subItemName") ? data["subItemName"].GetString() : "Initial Subscription",
SubItemDescription = data.ContainsKey("subItemDescription") ? data["subItemDescription"].GetString() : null,
SubItemPrice = data.ContainsKey("subItemPrice") ? data["subItemPrice"].GetDecimal() : 0,
IsActive = true
};
var plan = await _customerPlanRepository.GetCustomerPlanById(customerPlanId)
?? throw new NotFoundException("Customer plan not found.");
var aggregate = CustomerSubscriptionFactory.Create(
subscription, plan, initialSubItem);
await _customerSubscriptionRepository.AddCustomerSubscriptionAsync(
aggregate.Subscription,
aggregate.InitialSubItem,
aggregate.PrimaryMember,
aggregate.Installments);
subscriptionId = subscription.SubscriptionId;
// Persist the subscription first: invoice creation re-queries it from
// the DB and needs the invoice from day one for balance tracking.
await _uow.SaveChangesAsync();
var invoice = await _customerInvoiceRepository.CreateCustomerInvoiceFromSubscription(
subscription.SubscriptionId, chargePayment);
invoiceId = invoice.CustomerInvoiceId;
invoiceAmount = invoice.Amount;
}
}
await _uow.CommitAsync();
return new
{
success = true,
message = alreadyAssigned
? "Customer already has an active plan at this gym; no new subscription created."
: "Plans assigned successfully to customer",
subscriptionId,
invoiceId,
invoiceAmount
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateCustomer(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
//if (!data!.ContainsKey("customerId"))
//{
// throw new KeyNotFoundException("customerId is required");
//}
var userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid customerId = await _userRoleHelper.GetCusId(userId);
Customer oldCus = await _customerRepository.GetCustomerById(customerId);
if (oldCus == null)
throw new NotFoundException("customer Not Found");
// Partial update: the mobile profile lets the customer edit one section (or one
// field) at a time, so only overwrite fields that were actually supplied and
// preserve everything else. GetOptionalString returns null for absent/empty.
oldCus.CusName = DataExtractor.GetOptionalString(data, "fullname") ?? oldCus.CusName;
oldCus.Gender = DataExtractor.GetOptionalString(data, "gender") ?? oldCus.Gender;
oldCus.PrimaryPH = DataExtractor.GetOptionalString(data, "primaryPH") ?? oldCus.PrimaryPH;
oldCus.SecondaryPH = DataExtractor.GetOptionalString(data, "secondaryPH") ?? oldCus.SecondaryPH;
oldCus.NIC = DataExtractor.GetOptionalString(data, "NIC") ?? oldCus.NIC;
oldCus.Address = DataExtractor.GetOptionalString(data, "address") ?? oldCus.Address;
oldCus.Email = DataExtractor.GetOptionalString(data, "email") ?? oldCus.Email;
if (data.ContainsKey("bod") && data["bod"].ValueKind != JsonValueKind.Null)
oldCus.BoD = DataExtractor.GetRequiredDateOfBirth(data, "bod");
var result = await _customerRepository.UpdateCustomer(customerId, oldCus);
if (result == null)
{
throw new KeyNotFoundException("Customer not found");
}
await _uow.CommitAsync();
return result;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteCustomer(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("customerId"))
{
throw new KeyNotFoundException("customerId is required");
}
var customerId = Guid.Parse(data["customerId"].GetString()!);
// TODO: Remove static ID after testing
// var customerId = Guid.Parse("d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a");
var result = await _customerRepository.DeleteCustomer(customerId);
if (!result)
{
throw new KeyNotFoundException("Customer not found");
}
await _uow.CommitAsync();
return new { success = true, message = "Customer deleted successfully" };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetCustomerMealPlan(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
Guid? ActiveClientId = await _customerRepository.GetCustomerClientId(cusId);
if (ActiveClientId == null)
{
MealPlan? clientMeal = await _customerRepository.GetActiveMealPlanAsync(ActiveClientId, cusId);
if (clientMeal == null)
return new { message = "No active meal plan found for you" };
return clientMeal;
}
else
{
MealPlan? cusMeal = await _customerRepository.GetActiveMealPlanAsync(null, cusId);
if (cusMeal == null)
return new { message = "No active meal plan found for you" };
return cusMeal;
}
}
public async Task<object> GetCustomerMealPlanById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var mealPlanId = DataExtractor.GetRequiredGuid(data, "mealPlanId");
var mealPlan = await _customerRepository.GetMealPlanByIdForCustomerAsync(mealPlanId);
if (mealPlan == null)
throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found");
if (mealPlan.MealItems != null)
{
foreach (var item in mealPlan.MealItems)
{
var itemAsString = item.MealItems as string;
if (!string.IsNullOrEmpty(itemAsString))
{
try
{
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
}
catch
{
// Keep as string if parsing fails
}
}
}
}
return mealPlan;
}
public async Task<object> GetFollowedMealPlans(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var mealPlans = await _customerRepository.GetFollowedMealPlansByCustomerIdAsync(cusId);
foreach (var mealPlan in mealPlans)
{
if (mealPlan.MealItems != null)
{
foreach (var item in mealPlan.MealItems)
{
var itemAsString = item.MealItems as string;
if (!string.IsNullOrEmpty(itemAsString))
{
try
{
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
}
catch
{
// Keep as string if parsing fails
}
}
}
}
}
return mealPlans;
}
public async Task<object> GetCustomerWorkOutPlan(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
Guid? ActiveClientId = await _customerRepository.GetCustomerClientId(cusId);
if (ActiveClientId == null)
{
WorkoutPlan? clientWorkOut = await _customerRepository.GetActiveWorkOutAsync(ActiveClientId, cusId);
if (clientWorkOut == null)
return new { message = "No active WorkOut plan found for you" };
return clientWorkOut;
}
else
{
WorkoutPlan? cusWorkOut = await _customerRepository.GetActiveWorkOutAsync(null, cusId);
if (cusWorkOut == null)
return new { message = "No active WorkOut plan found for you" };
return cusWorkOut;
}
}
public async Task<object> GetCustomerWorkoutPlanById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var workoutPlanId = DataExtractor.GetRequiredGuid(data, "workoutPlanId");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var workoutPlan = await _customerRepository.GetWorkoutPlanByIdForCustomerAsync(workoutPlanId, cusId);
if (workoutPlan == null)
throw new KeyNotFoundException($"Workout plan with ID {workoutPlanId} not found");
return new
{
workoutPlan.WorkoutPlanId,
workoutPlan.Name,
workoutPlan.Description,
workoutPlan.IsTemplate,
workoutPlan.WorkoutCategoryId,
workoutPlan.WorkoutCategoryName,
workoutPlan.ClientId,
workoutPlan.ClientName,
workoutPlan.IsActive,
workoutPlan.CreateDate,
WorkoutItems = workoutPlan.WorkoutItems,
AssignedCustomers = workoutPlan.AssignedCustomers
};
}
// All of the customer's own AI-generated meal plans (not just the currently-followed
// one) — backs the mobile "AI Plans" tab, distinct from the templates tab.
public async Task<object> GetMyMealPlans(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
return await _customerRepository.GetMyMealPlansByCustomerIdAsync(cusId);
}
public async Task<object> GetFollowedWorkoutPlans(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var workoutPlans = await _customerRepository.GetFollowedWorkoutPlansByCustomerIdAsync(cusId);
var transformedPlans = workoutPlans.Select(plan => new
{
plan.WorkoutPlanId,
plan.Name,
plan.Description,
plan.IsTemplate,
plan.WorkoutCategoryId,
plan.WorkoutCategoryName,
plan.ClientId,
plan.ClientName,
plan.IsActive,
plan.CreateDate,
WorkoutItems = plan.WorkoutItems,
AssignedCustomers = plan.AssignedCustomers
}).ToList();
return transformedPlans;
}
// All of the customer's own AI-generated workout plans — backs the mobile "AI Plans"
// tab, distinct from the templates tab.
public async Task<object> GetMyWorkoutPlans(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
return await _customerRepository.GetMyWorkoutPlansByCustomerIdAsync(cusId);
}
public async Task<object> GetClientDataById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid clientBizID = DataExtractor.GetRequiredGuid(data, "clientBizID");
var biz = (Models.ClientBusiness)await _customerRepository.GetClientBizDataAsync(clientBizID);
// Facilities may hold either the newer structured JSON (AC + named/counted
// equipment) or older plain comma-separated text — GymFacilities.Parse handles
// both, so the mobile gym-details screen can render real equipment instead of
// guessing by splitting on commas.
var facilities = Helpers.GymFacilities.Parse(biz.Facilities);
return new
{
biz.Bid,
biz.FKClientId,
biz.BusinessType,
biz.BusinessName,
biz.BusinessLogoId,
biz.Description,
biz.IsPysicalLocation,
biz.Address,
biz.City,
biz.Facilities,
hasAc = facilities.HasAc,
equipment = facilities.Equipment,
biz.CoachingMode,
biz.Expertise,
biz.Gender,
biz.ExperienceYears,
biz.SessionType,
biz.StoreType,
biz.ProductCategory,
biz.DeliveryAvailable,
biz.Latitude,
biz.Longitude,
biz.IsActive,
Client = biz.Client
};
}
public async Task<object> GetClientGyms(object payload, HttpContext httpContext)
{
var gyms = await _clientRepository.GetActiveGymsAsync();
if (gyms == null || !gyms.Any())
throw new KeyNotFoundException("No gyms found");
//var result = gyms.Select(g => new ClientGymResponse
//{
// Id = g.Bid,
// Name = g.BusinessName,
// Latitude = g.Latitude,
// Longitude = g.Longitude,
// IsActive = g.IsActive
//}).ToList();
return gyms;
}
// QR gym onboarding step 1: list the scanned gym's membership plans so the customer
// can pick one before joining.
public async Task<object> GetGymPlans(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid clientId = DataExtractor.GetRequiredGuid(data, "clientId");
return await _clientRepository.GetCustomerPlansByClientIdAsync(clientId);
}
// QR gym onboarding step 2: create the subscription linking this customer to the
// scanned gym via the chosen plan — same association path (Customer -> CustomerSubscription
// -> CustomerPlan -> Client) that client-initiated member creation already uses.
public async Task<object> JoinGym(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid customerPlanId = DataExtractor.GetRequiredGuid(data, "customerPlanId");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
return await AssignPlansToCustomer(cusId, new { customerPlanId }, httpContext);
}
public async Task<object> GetGymTerms(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid clientId = DataExtractor.GetRequiredGuid(data, "clientId");
var terms = await _termsRepository.GetActiveByClientIdAsync(clientId);
if (terms == null)
return new { hasTerms = false };
return new
{
hasTerms = true,
terms.Version,
terms.Content,
terms.CreatedAt
};
}
public async Task<object> AcceptGymTerms(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid clientId = DataExtractor.GetRequiredGuid(data, "clientId");
int version = DataExtractor.GetRequiredInt(data, "version");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var existing = await _termsRepository.GetAcceptanceAsync(cusId, clientId, version);
if (existing == null)
{
await _termsRepository.AddAcceptanceAsync(new CustomerTermsAcceptance
{
CustomerTermsAcceptanceId = Guid.NewGuid(),
FKCusId = cusId,
FKClientId = clientId,
Version = version,
AcceptedAt = DateTime.UtcNow
});
await _uow.SaveChangesAsync();
}
return new { success = true };
}
// Renders the gym's active Terms & Conditions as a downloadable PDF (same QuestPDF
// approach as ReceiptService.GeneratePdf). Returns null when the gym has none published.
public async Task<byte[]?> GetGymTermsPdf(Guid clientId)
{
var terms = await _termsRepository.GetActiveByClientIdAsync(clientId);
if (terms == null)
return null;
var document = QuestPDF.Fluent.Document.Create(container =>
{
container.Page(page =>
{
page.Size(QuestPDF.Helpers.PageSizes.A4);
page.Margin(36);
page.DefaultTextStyle(x => x.FontSize(11).FontColor(QuestPDF.Helpers.Colors.Grey.Darken3));
page.Header().Column(col =>
{
col.Item().Text("Terms & Conditions").FontSize(18).Bold();
col.Item().Text($"Version {terms.Version} — {terms.CreatedAt:yyyy-MM-dd}").FontSize(9).FontColor(QuestPDF.Helpers.Colors.Grey.Medium);
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(QuestPDF.Helpers.Colors.Grey.Lighten2);
});
page.Content().PaddingVertical(12).Text(terms.Content);
});
});
return document.GeneratePdf();
}
public async Task<object> CreateCusHealthRec(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid uId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(uId);
CustomerHealth customerHealth = new CustomerHealth
{
BloodGroup = DataExtractor.GetRequiredString(data, "bloodGroup"),
Disabilities = DataExtractor.GetRequiredString(data, "disabilities"),
FKCusId = cusId,
IsActive = true
};
await _uow.BeginAsync();
try
{
var customerhelth = await _customerRepository.CreateCusHealthRecAsync(customerHealth);
if (customerhelth == null)
throw new Exception("Something went wrong");
await _uow.CommitAsync();
return new { message = "Health record saved" };
}
catch (Exception ex)
{
await _uow.RollbackAsync();
throw new Exception("Health record saving failed");
}
}
public async Task<object> UpdateCusHealthRec(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid uId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(uId);
CustomerHealth oldCusHealth = await _customerRepository.GetCusHealthRec(cusId);
if(oldCusHealth == null)
{
var response = await CreateCusHealthRec(payload, httpContext);
return response;
}
oldCusHealth.BloodGroup = DataExtractor.GetRequiredString(data, "bloodGroup");
oldCusHealth.Disabilities = DataExtractor.GetRequiredString(data, "disabilities");
oldCusHealth.FKCusId = cusId;
await _uow.BeginAsync();
try
{
var customerhelth = await _customerRepository.UpdateCusHealthRecAsync(oldCusHealth);
if (customerhelth == null)
throw new Exception("Something went wrong");
await _uow.CommitAsync();
return new { message = "Health record Updated" };
}
catch (Exception ex)
{
await _uow.RollbackAsync();
throw new Exception("Health record Updating failed");
}
}
public async Task<object> UpdateCustomerTargetNutritions(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var predictedNutritionsJson = CustomerProgressHelper.GetPredictedNutritionsJson(data);
await _uow.BeginAsync();
try
{
var updatedTarget = await _customerRepository.UpdateLatestCustomerTargetNutritionsAsync(
cusId,
predictedNutritionsJson);
await _uow.CommitAsync();
return new
{
message = "Target nutritions updated",
customerTargetId = updatedTarget.CustomerTargetId,
isChanged = updatedTarget.IsChanged,
predictedNutritions = DeserializePayload.DeserializeJson<Dictionary<string, string>>(updatedTarget.PredictedNutritionsJson)
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateWeeklyProgress(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
Guid customerTargetId = data.ContainsKey("customerTargetId")
? DataExtractor.GetRequiredGuid(data, "customerTargetId")
: data.ContainsKey("targetId")
? DataExtractor.GetRequiredGuid(data, "targetId")
: DataExtractor.GetRequiredGuid(data, "progressId");
decimal weight = DataExtractor.GetRequiredDecimal(data, "weight");
DateTime? recordedAt = DataExtractor.GetOptionalDateTime(data, "recordedAt");
await _uow.BeginAsync();
try
{
var weeklyProgress = await _customerRepository.UpdateWeeklyProgressAsync(cusId, customerTargetId, weight, recordedAt);
await _uow.CommitAsync();
return weeklyProgress;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetCusBasicDetails(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var customerData = await _customerRepository.GetCustomerById(cusId);
if (customerData == null)
return new NotFoundException("Customer not Found");
return customerData;
}
public async Task<object> GetCusHealthDetails(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var customerData = await _customerRepository.GetCusHealthRec(cusId);
if (customerData == null)
return new { message = "no health records" };
return customerData;
}
public async Task<object> GetCusProgressDetails(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var customerData = await _customerRepository.GetCusProgressRec(cusId);
if (customerData == null)
return new { message = "no progress records" };
return customerData
.Select(CustomerProgressHelper.BuildCustomerProgressResponse)
.ToList();
}
public async Task<object> CreateFeed(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
bool isVerified = await _customerRepository.IsverifiedCus(cusId);
if (!isVerified)
//throw new UnauthorizedException("only for verified users");
return new { message = "only for verified users" };
try
{
var FeedResults = await _feedService.CreateFeed(payload, httpContext);
return FeedResults;
}
catch (Exception ex)
{
throw new Exception(ex.Message, ex);
}
}
public async Task<object> FollowMealPlan(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var data = DeserializePayload.Deserialize(payload);
var mealPlanId = DataExtractor.GetRequiredGuid(data, "mealPlanId");
await _uow.BeginAsync();
try
{
var result = await _customerRepository.FollowMealPlan(cusId, mealPlanId);
await _uow.CommitAsync();
return result;
}
catch (Exception ex)
{
await _uow.RollbackAsync();
throw new Exception($"Failed to follow meal plan: {ex.Message}");
}
}
public async Task<object> FollowWorkOutPlan(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var data = DeserializePayload.Deserialize(payload);
Guid workOutPlanId = DataExtractor.GetRequiredGuid(data, "workOutPlan");
await _uow.BeginAsync();
try
{
var result = await _customerRepository.FollowWorkoutPlan(cusId, workOutPlanId);
await _uow.CommitAsync();
return result;
}
catch (Exception ex)
{
await _uow.RollbackAsync();
throw new Exception($"Failed to follow Workout plan: {ex.Message}");
}
}
public async Task<object> SetWorkoutExerciseCompletion(Guid workoutPlanId, Guid exerciseId, bool completed, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var customerWorkout = await _customerRepository.GetActiveCustomerWorkoutAsync(cusId, workoutPlanId);
if (customerWorkout == null)
throw new KeyNotFoundException("You are not following or assigned to this workout plan.");
await _uow.BeginAsync();
try
{
await _customerRepository.SetExerciseCompletionAsync(customerWorkout.CustomerWorkoutId, exerciseId, completed);
await _uow.CommitAsync();
return new { workoutPlanId, exerciseId, completed };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetMyNotifications(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var notifications = await _customerNotificationRepository.GetByCustomerIdAsync(cusId);
return notifications.Select(n => new
{
notificationId = n.CustomerNotificationId,
title = n.Title,
body = n.Body,
notificationType = n.NotificationType,
routeName = n.RouteName,
routeParams = string.IsNullOrEmpty(n.RouteParamsJson)
? (JsonElement?)null
: JsonSerializer.Deserialize<JsonElement>(n.RouteParamsJson),
isRead = n.IsRead,
createdAt = n.CreatedAt
});
}
public async Task<object> MarkNotificationRead(Guid notificationId, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var notification = await _customerNotificationRepository.GetByIdAsync(notificationId, cusId);
if (notification == null)
throw new KeyNotFoundException("Notification not found.");
if (notification.IsRead)
return new { notificationId, isRead = true };
await _uow.BeginAsync();
try
{
notification.IsRead = true;
await _uow.CommitAsync();
return new { notificationId, isRead = true };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdatePushToken(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
string pushToken = DataExtractor.GetRequiredString(data, "pushTokenId");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
await _uow.BeginAsync();
try
{
var customer = await _dbContext.Customer.FirstOrDefaultAsync(c => c.CusId == cusId);
if (customer == null)
throw new KeyNotFoundException("Customer not found.");
customer.PushTokenId = pushToken;
await _uow.CommitAsync();
return new { success = true };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateProfileImage(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
int documentId = DataExtractor.GetRequiredInt(data, "documentId");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
await _uow.BeginAsync();
try
{
// Only accept a document this same user uploaded, so a customer can't point
// their profile picture at an arbitrary document id belonging to someone else.
var document = await _dbContext.UserDocuments
.FirstOrDefaultAsync(d => d.DocumentId == documentId && d.UserId == userId);
if (document == null)
throw new KeyNotFoundException("Document not found.");
var customer = await _dbContext.Customer.FirstOrDefaultAsync(c => c.CusId == cusId);
if (customer == null)
throw new KeyNotFoundException("Customer not found.");
customer.FKProfileImageDocId = documentId;
await _uow.CommitAsync();
return new { profileImageDocId = documentId };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetCardDetails(object payload, HttpContext httpContext)
{
try
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var cards = await _customerRepository.GetCardsByCustomerIdAsync(cusId);
return cards.Select(c => new
{
jobId = c.JobId,
cardId = c.CardId,
cardNumber = c.CardNumber,
cardType = c.CardType,
nickName = c.NickName,
isPrimary = c.IsPrimary,
consentGiven = c.ConsentGiven
});
}
catch
{
throw;
}
}
// Member self-serve "add card": explicit consent is required up front so a saved card
// can later be used for auto-deduct (see SetAutoCharge). No real PayHere/Stripe SDK is
// wired up anywhere in this backend yet — cardToken is an opaque client-side value, same
// staged convention ActivationService already uses for the platform's own billing.
public async Task<object> SaveCard(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
string cardToken = DataExtractor.GetRequiredString(data, "cardToken");
bool consent = DataExtractor.GetOptionalBool(data, "consent") ?? false;
if (!consent)
throw new ArgumentException("Consent is required to save a card for auto-deduct");
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
Guid clientId = await _userRoleHelper.GetClientIdByCusId(cusId);
var existing = await _customerRepository.GetCardsByCustomerIdAsync(cusId);
var card = new CustomerCards
{
FK_CustomerId = cusId,
FK_ClientId = clientId,
CardId = cardToken,
CardNumber = DataExtractor.GetOptionalString(data, "cardLast4"),
CardType = DataExtractor.GetOptionalString(data, "cardType"),
NickName = DataExtractor.GetOptionalString(data, "nickName"),
IsActive = true,
IsPrimary = existing.Count == 0,
ConsentGiven = true,
ConsentAt = DateTime.UtcNow
};
var result = await _customerRepository.AddCardAsync(card);
await _uow.CommitAsync();
return new { success = true, message = "Card saved", card = result };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// Lets the mobile app find the caller's active subscription id to target SetAutoCharge.
public async Task<object> GetMySubscription(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var subscription = await _customerSubscriptionRepository.GetActiveGymAccessSubscriptionAsync(cusId);
if (subscription == null)
return new { hasSubscription = false };
return new
{
hasSubscription = true,
subscriptionId = subscription.SubscriptionId,
planName = subscription.CustomerPlan?.PlanName,
autoChargeEnabled = subscription.AutoChargeEnabled,
autoChargeCardId = subscription.FKAutoChargeCardId
};
}
// Member opts a subscription in/out of auto-deduct against one of their consented cards.
public async Task<object> SetAutoCharge(Guid subscriptionId, object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
bool enabled = DataExtractor.GetOptionalBool(data, "enabled") ?? false;
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(subscriptionId)
?? throw new NotFoundException("Subscription not found");
if (subscription.FKCustomerId != cusId)
throw new UnauthorizedException("This subscription does not belong to you");
if (enabled)
{
Guid cardId = DataExtractor.GetRequiredGuid(data, "cardId");
var card = await _customerRepository.GetCardByIdAsync(cardId)
?? throw new NotFoundException("Card not found");
if (card.FK_CustomerId != cusId || !card.ConsentGiven)
throw new UnauthorizedException("This card can't be used for auto-deduct");
subscription.AutoChargeEnabled = true;
subscription.FKAutoChargeCardId = cardId;
}
else
{
subscription.AutoChargeEnabled = false;
subscription.FKAutoChargeCardId = null;
}
await _uow.SaveChangesAsync();
await _uow.CommitAsync();
return new
{
success = true,
subscriptionId,
autoChargeEnabled = subscription.AutoChargeEnabled,
autoChargeCardId = subscription.FKAutoChargeCardId
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetMyGym(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var customerSubscription = await _customerSubscriptionRepository.GetActiveGymAccessSubscriptionAsync(cusId);
if (customerSubscription != null)
{
var customerPlan = await _customerPlanRepository
.GetCustomerPlanById(customerSubscription.FKCustomerPlanId);
if (customerPlan != null)
{
var client = await _clientRepository.GetClientByIdAsync(customerPlan.FKClientId);
if (client != null)
{
var clientBusiness = await _clientRepository.GetPrimaryBusinessByClientIdAsync(client.ClientId);
var logo = await BusinessLogoHelper.ReadAsDataUrlAsync(clientBusiness?.BusinessLogoId);
var facilities = GymFacilities.Parse(clientBusiness?.Facilities);
return new
{
success = true,
clientId = client.ClientId,
clientName = client.ClientName,
clientPhone = client.ClientPhone,
businessName = clientBusiness?.BusinessName,
logo,
address = clientBusiness?.Address,
city = clientBusiness?.City,
hasAc = facilities.HasAc,
equipment = facilities.Equipment,
planName = customerPlan.PlanName,
planDescription = customerPlan.PlanDescription,
planPrice = customerPlan.PlanPrice
};
}
else
{
return new
{
success = true,
planDescription = customerPlan.PlanDescription,
planPrice = customerPlan.PlanPrice
};
}
}
}
return new { success = false, message = "No active gym found" };
}
catch
{
throw;
}
}
// Lets the customer leave their own gym (the primary member on their own
// subscription can never be removed via the gym-owner-side RemoveMemberFromSubscription,
// so this is a separate, customer-initiated path).
public async Task<object> UnfollowMyGym(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
var subscription = await _customerSubscriptionRepository.GetActiveGymAccessSubscriptionAsync(cusId);
if (subscription == null)
throw new KeyNotFoundException("You don't have an active gym to unfollow.");
await _uow.BeginAsync();
try
{
subscription.IsActive = false;
var primaryMember = await _customerSubscriptionRepository
.GetActiveSubscriptionMemberByCustomerAsync(subscription.SubscriptionId, cusId);
if (primaryMember != null)
primaryMember.IsActive = false;
await _uow.CommitAsync();
return new { success = true, message = "You have left the gym." };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> MyPayment(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid cusId = await _userRoleHelper.GetCusId(userId);
// Get all customer subscriptions for this customer first
var customerSubscriptionIds = await _tenantDbContext.CustomerSubscriptions
.AsNoTracking()
.Where(cs => cs.FKCustomerId == cusId)
.Select(cs => cs.SubscriptionId)
.ToListAsync();
if (!customerSubscriptionIds.Any())
{
return new { success = false, message = "No subscriptions found" };
}
// Get invoices with related data
var customerInvoices = await _tenantDbContext.CustomerInvoices
.AsNoTracking()
.Include(ci => ci.CustomerSubscription)
.ThenInclude(cs => cs.SubscriptionInstallments)
.Where(ci => customerSubscriptionIds.Contains(ci.FKCustomerSubscriptionId))
.OrderByDescending(ci => ci.CustomerInvoiceDate)
.ToListAsync();
if (customerInvoices != null && customerInvoices.Any())
{
var paymentList = customerInvoices.Select(invoice => new
{
customerInvoiceId = invoice.CustomerInvoiceId,
amount = invoice.Amount,
status = invoice.Status,
invoiceDate = invoice.CustomerInvoiceDate,
subscription = invoice.CustomerSubscription != null ? new
{
subscriptionId = invoice.CustomerSubscription.SubscriptionId,
onboardDate = invoice.CustomerSubscription.OnboardDate,
expiryDate = invoice.CustomerSubscription.ExpiryDate,
renewalDate = invoice.CustomerSubscription.RenewalDate,
isActive = invoice.CustomerSubscription.IsActive,
installments = invoice.CustomerSubscription.SubscriptionInstallments
?.OrderBy(si => si.InstallmentNo)
.Select(si => new
{
installmentNo = si.InstallmentNo,
dueDate = si.DueDate,
amount = si.Amount,
paidAmount = si.PaidAmount,
status = si.Status
}).ToList()
} : null
}).ToList();
return new
{
success = true,
payments = paymentList
};
}
return new { success = false, message = "No payments found" };
}
catch
{
throw;
}
}
}
}