1639 lines
69 KiB
C#
1639 lines
69 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)
|
|
{
|
|
// 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 plan belongs to
|
|
// ChangeMembershipPlan, which explicitly deactivates the old one first.
|
|
var existingActive = await _customerSubscriptionRepository.GetActiveSubscriptionByCustomerId(customerId);
|
|
if (existingActive != null)
|
|
{
|
|
alreadyAssigned = true;
|
|
subscriptionId = existingActive.SubscriptionId;
|
|
var existingInvoice = await _customerInvoiceRepository.GetInvoiceBySubscriptionIdAsync(existingActive.SubscriptionId);
|
|
invoiceId = existingInvoice?.CustomerInvoiceId;
|
|
invoiceAmount = existingInvoice?.Amount;
|
|
}
|
|
else
|
|
{
|
|
// Not primary anywhere (checked above), but could still be an active
|
|
// secondary on someone else's group plan -- GetActiveSubscriptionByCustomerId
|
|
// can't see that (it only matches CustomerSubscription.FKCustomerId, the
|
|
// primary). One customer, one plan at a time either way.
|
|
var hasOtherActivePlan = await _customerSubscriptionRepository.HasAnyActiveSubscriptionAsync(customerId);
|
|
if (hasOtherActivePlan)
|
|
throw new ArgumentException("This customer already has an active plan. Remove them from it before assigning a new one.");
|
|
|
|
var customerPlanId = Guid.Parse(data["customerPlanId"].GetString()!);
|
|
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.GetActiveSubscriptionByCustomerId(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.GetActiveSubscriptionByCustomerId(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.GetActiveSubscriptionByCustomerId(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;
|
|
}
|
|
}
|
|
}
|
|
}
|