1343 lines
61 KiB
C#
1343 lines
61 KiB
C#
using Fitness_core_BE.Domain;
|
|
using Fitness_core_BE.DTOs.Auth;
|
|
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 Fitness_core_BE.Models.IOs;
|
|
using System.Text.Json;
|
|
|
|
namespace Fitness_core_BE.Services
|
|
{
|
|
public class ClientService : IClientService
|
|
{
|
|
private readonly IClientRepository _clientRepository;
|
|
private readonly ICustomerRepository _customerRepository;
|
|
private readonly IUnitOfWork _uow;
|
|
private readonly IAuthService _authService;
|
|
private readonly ICustomerService _customerService;
|
|
private readonly ICustomerSubscriptionService _customerSubscriptionService;
|
|
private readonly IMealService _mealService;
|
|
private readonly IWorkoutService _workoutService;
|
|
private readonly UserRoleHelper _userRoleHelper;
|
|
private readonly IThirdPartyService _thirdPartyService;
|
|
private readonly IConfiguration _config;
|
|
private readonly ITermsRepository _termsRepository;
|
|
private readonly IPaymentService _paymentService;
|
|
private readonly IAccessDeviceService _accessDeviceService;
|
|
private readonly ICustomerInvoiceRepository _customerInvoiceRepository;
|
|
|
|
public ClientService(IClientRepository _clientRepository, ICustomerRepository customerRepository, IUnitOfWork uow, IAuthService authService, ICustomerService customerService, ICustomerSubscriptionService customerSubscriptionService, IMealService mealService, IWorkoutService workoutService, UserRoleHelper userRoleHelper, IThirdPartyService thirdPartyService, IConfiguration config, ITermsRepository termsRepository, IPaymentService paymentService, IAccessDeviceService accessDeviceService, ICustomerInvoiceRepository customerInvoiceRepository)
|
|
{
|
|
this._clientRepository = _clientRepository;
|
|
_customerRepository = customerRepository;
|
|
_uow = uow;
|
|
_authService = authService;
|
|
_customerService = customerService;
|
|
_customerSubscriptionService = customerSubscriptionService;
|
|
_mealService = mealService;
|
|
_workoutService = workoutService;
|
|
_userRoleHelper = userRoleHelper;
|
|
_thirdPartyService = thirdPartyService;
|
|
_config = config;
|
|
_termsRepository = termsRepository;
|
|
_paymentService = paymentService;
|
|
_accessDeviceService = accessDeviceService;
|
|
_customerInvoiceRepository = customerInvoiceRepository;
|
|
}
|
|
|
|
//V1 implementation
|
|
public async Task<object> GetAllCustomers(object payload, HttpContext httpContext)
|
|
{
|
|
return await _clientRepository.GetAllCustomers();
|
|
}
|
|
|
|
|
|
public async Task<object> GetCustomerDetailsById(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
var customer = await _clientRepository.GetCustomerById(customerId);
|
|
|
|
if (customer == null)
|
|
throw new KeyNotFoundException($"Customer with ID {customerId} not found");
|
|
|
|
customer.CustomerSubscriptions = null;
|
|
|
|
// SubscriptionMembers-based: also finds subscriptions where this customer
|
|
// is a group secondary (customer.CustomerSubscriptions only sees primaries).
|
|
var subscriptions = await _customerSubscriptionService.BuildSummariesForCustomerAsync(customerId);
|
|
|
|
// Legacy `plans` shape retained for backward compatibility.
|
|
var plans = subscriptions
|
|
.Select(s => new
|
|
{
|
|
customerPlanId = s.CustomerPlanId,
|
|
planName = s.PlanName,
|
|
planPrice = s.PlanPrice,
|
|
durationInDays = 0,
|
|
planScope = s.PlanScope,
|
|
isActive = s.IsActive
|
|
})
|
|
.ToList();
|
|
|
|
return new
|
|
{
|
|
customer,
|
|
plans,
|
|
subscriptions
|
|
};
|
|
}
|
|
|
|
// Aggregated member profile for the "More Info" view: basic details + targets +
|
|
// weight progress (already loaded on the customer) plus the plans they follow.
|
|
// Medical details are deliberately excluded — they require a password (GetCustomerMedical).
|
|
public async Task<object> GetCustomerFullDetails(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
var customer = await _clientRepository.GetCustomerById(customerId)
|
|
?? throw new KeyNotFoundException($"Customer with ID {customerId} not found");
|
|
customer.CustomerSubscriptions = null;
|
|
|
|
var subscriptions = await _customerSubscriptionService.BuildSummariesForCustomerAsync(customerId);
|
|
var workoutPlans = await _clientRepository.GetFollowedWorkoutPlansAsync(customerId);
|
|
var mealPlans = await _clientRepository.GetFollowedMealPlansAsync(customerId);
|
|
|
|
return new { customer, subscriptions, workoutPlans, mealPlans };
|
|
}
|
|
|
|
// Password-gated medical details. The owner re-enters their account password, which is
|
|
// verified against the auth service; only on success are BloodGroup/Disabilities returned.
|
|
public async Task<object> GetCustomerMedical(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
|
|
string password = DataExtractor.GetRequiredString(data, "password");
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
var verify = await _authService.VerifyPassword(new { password }, httpContext);
|
|
if (verify.StatusCode != 200 || !ExtractValid(verify.Data))
|
|
throw new UnauthorizedException("Password is incorrect.");
|
|
|
|
var health = await _clientRepository.GetCustomerHealthAsync(customerId);
|
|
return new
|
|
{
|
|
bloodGroup = health?.BloodGroup,
|
|
disabilities = health?.Disabilities
|
|
};
|
|
}
|
|
|
|
// The auth VerifyPassword returns { valid } (possibly wrapped in a data envelope).
|
|
private static bool ExtractValid(object? data)
|
|
{
|
|
if (data is not JsonElement el || el.ValueKind != JsonValueKind.Object)
|
|
return false;
|
|
if (el.TryGetProperty("valid", out var v) &&
|
|
(v.ValueKind == JsonValueKind.True || v.ValueKind == JsonValueKind.False))
|
|
return v.GetBoolean();
|
|
if (el.TryGetProperty("data", out var d) && d.ValueKind == JsonValueKind.Object &&
|
|
d.TryGetProperty("valid", out var v2) &&
|
|
(v2.ValueKind == JsonValueKind.True || v2.ValueKind == JsonValueKind.False))
|
|
return v2.GetBoolean();
|
|
return false;
|
|
}
|
|
|
|
//V1 implementation
|
|
public async Task<object> DeleteCustomerByClient (object payload, HttpContext httpContext)
|
|
{
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
|
|
|
// Revoke physical access at this gym first, while the membership is still active
|
|
// in the DB (SetCustomerAccessAsync requires an active membership at clientId to
|
|
// resolve which AccessProfile to touch). No profile exists if the member never
|
|
// completed device enrollment — nothing to revoke in that case.
|
|
try
|
|
{
|
|
await _accessDeviceService.SetCustomerAccessAsync(clientId, customerId, enabled: false, reason: "Removed from gym");
|
|
}
|
|
catch (NotFoundException)
|
|
{
|
|
}
|
|
|
|
// Client-initiated delete only deactivates this customer's subscriptions/plan
|
|
// assignments at THIS gym — the shared central Customer record (and any
|
|
// membership at another gym) must never be touched by a single client's delete.
|
|
var result = await _customerRepository.DeactivateCustomerForClient(customerId, clientId);
|
|
if (!result)
|
|
{
|
|
throw new KeyNotFoundException("Customer Not Found");
|
|
}
|
|
await _uow.CommitAsync();
|
|
|
|
return new { success = true, message = "Customer deleted successfully" };
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw new KeyNotFoundException("Error");
|
|
}
|
|
|
|
}
|
|
|
|
public async Task<object> ClientLogOut (object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
var authPayload = new { userId = userId };
|
|
|
|
AuthResponse authResponse = await _authService.LogOut(authPayload, httpContext);
|
|
if (!authResponse.Success)
|
|
{
|
|
throw new InvalidOperationException(authResponse.Message);
|
|
}
|
|
|
|
return new { success = true, message = "Logged out successfully", auth = authResponse };
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public async Task<object> GetClientList(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var clients = await _clientRepository.GetAllClientsAsync();
|
|
return clients;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
public async Task<object> GetClientById(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var client = await _clientRepository.GetClientByIdAsync(clientId);
|
|
|
|
if (client == null)
|
|
throw new KeyNotFoundException("Client not found");
|
|
|
|
return client;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> UpdateGymProfile(
|
|
UpdateGymProfileRequest request, HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var business = await _clientRepository.GetPrimaryBusinessByClientIdAsync(clientId)
|
|
?? throw new KeyNotFoundException("Gym business profile not found");
|
|
|
|
if (request.BusinessName != null) business.BusinessName = request.BusinessName.Trim();
|
|
if (request.Address != null) business.Address = request.Address.Trim();
|
|
if (request.City != null) business.City = request.City.Trim();
|
|
if (request.Description != null) business.Description = request.Description.Trim();
|
|
|
|
// AC + equipment live as JSON in Facilities; preserve whichever isn't sent.
|
|
if (request.HasAc.HasValue || request.Equipment != null)
|
|
{
|
|
var current = GymFacilities.Parse(business.Facilities);
|
|
var hasAc = request.HasAc ?? current.HasAc;
|
|
var equipment = request.Equipment == null
|
|
? current.Equipment
|
|
: request.Equipment
|
|
.Select(item => new EquipmentCount(item.Name, item.Count))
|
|
.ToList();
|
|
business.Facilities = GymFacilities.Serialize(hasAc, equipment);
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
var savedFacilities = GymFacilities.Parse(business.Facilities);
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Gym profile updated",
|
|
facilities = new
|
|
{
|
|
hasAc = savedFacilities.HasAc,
|
|
equipment = savedFacilities.Equipment
|
|
}
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Gym-owner view of their own gym's active Terms & Conditions (for editing).
|
|
public async Task<object> GetMyGymTerms(object payload, HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var terms = await _termsRepository.GetActiveByClientIdAsync(clientId);
|
|
if (terms == null)
|
|
return new { hasTerms = false };
|
|
|
|
return new { hasTerms = true, terms.Version, terms.Content, terms.CreatedAt };
|
|
}
|
|
|
|
// Publishes a new Terms version for the gym owner's own gym; deactivates the old one.
|
|
public async Task<object> PublishGymTerms(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
string content = DataExtractor.GetRequiredString(data, "content");
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var currentTerms = await _termsRepository.GetActiveForUpdateAsync(clientId);
|
|
foreach (var current in currentTerms)
|
|
current.IsActive = false;
|
|
|
|
var latestVersion = await _termsRepository.GetLatestVersionAsync(clientId);
|
|
var terms = new ClientTerms
|
|
{
|
|
ClientTermsId = Guid.NewGuid(),
|
|
FKClientId = clientId,
|
|
Version = latestVersion + 1,
|
|
Content = content,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
await _termsRepository.AddTermsAsync(terms);
|
|
await _uow.SaveChangesAsync();
|
|
return new { success = true, terms.Version, terms.CreatedAt };
|
|
}
|
|
|
|
public async Task<object> UploadGymLogo(IFormFile logo, HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
if (logo == null || logo.Length == 0)
|
|
throw new BadRequestException("No logo file provided.");
|
|
|
|
string extension = Path.GetExtension(logo.FileName).ToLowerInvariant();
|
|
string[] allowed = { ".jpg", ".jpeg", ".png", ".webp" };
|
|
if (!allowed.Contains(extension))
|
|
throw new BadRequestException("Logo must be an image (jpg, png, webp).");
|
|
if (logo.Length > 5 * 1024 * 1024)
|
|
throw new BadRequestException("Logo too large (max 5MB).");
|
|
|
|
string uploadsRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "Uploads", "gym-logos", clientId.ToString()));
|
|
if (!Directory.Exists(uploadsRoot))
|
|
Directory.CreateDirectory(uploadsRoot);
|
|
|
|
string filename = $"logo_{DateTime.UtcNow:yyyyMMdd_HHmmss}{extension}";
|
|
string filePath = Path.Combine(uploadsRoot, filename);
|
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
{
|
|
await logo.CopyToAsync(stream);
|
|
}
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var business = await _clientRepository.GetPrimaryBusinessByClientIdAsync(clientId)
|
|
?? throw new KeyNotFoundException("Gym business profile not found");
|
|
business.BusinessLogoId = $"/Uploads/gym-logos/{clientId}/{Uri.EscapeDataString(filename)}";
|
|
await _uow.CommitAsync();
|
|
return new { businessLogoId = business.BusinessLogoId };
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetGymLogo(HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var business = await _clientRepository.GetPrimaryBusinessByClientIdAsync(clientId);
|
|
var logo = await BusinessLogoHelper.ReadAsDataUrlAsync(business?.BusinessLogoId);
|
|
return new { logo };
|
|
}
|
|
|
|
// Sends an OTP to the owner's new email/phone before it replaces the current one.
|
|
public async Task<object> SendContactOtp(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
string identifier = DataExtractor.GetRequiredString(data, "identifier");
|
|
|
|
var authResponse = await _authService.SendOtp(new { identifier, newUser = true }, httpContext);
|
|
if (!authResponse.Success)
|
|
throw new BadRequestException($"Failed to send OTP: {authResponse.Message}");
|
|
|
|
return AuthOtpResponseParser.ToSendOtpResult(authResponse);
|
|
}
|
|
|
|
// Verifies the OTP, writes the new value to the auth DB (UpdateUser), then mirrors it
|
|
// onto the local Client record so the dashboard shows the change immediately.
|
|
public async Task<object> VerifyContactOtp(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
string type = DataExtractor.GetRequiredString(data, "type").ToLowerInvariant();
|
|
string identifier = DataExtractor.GetRequiredString(data, "identifier");
|
|
string referenceNumber = DataExtractor.GetRequiredString(data, "referenceNumber");
|
|
string otpCode = DataExtractor.GetRequiredString(data, "otpCode");
|
|
|
|
if (type != "email" && type != "phone")
|
|
throw new BadRequestException("Contact type must be 'email' or 'phone'.");
|
|
|
|
var verify = await _authService.VerifyOTP(new { referenceNumber, otpCode, newUser = true }, httpContext);
|
|
if (!verify.Success)
|
|
throw new BadRequestException($"OTP verification failed: {verify.Message}");
|
|
|
|
object updatePayload = type == "email" ? new { email = identifier } : new { mobileNumber = identifier };
|
|
var update = await _authService.UpdateUser(updatePayload, httpContext);
|
|
if (!update.Success)
|
|
throw new BadRequestException($"Failed to update account: {update.Message}");
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var client = await _clientRepository.GetClientEntityByIdAsync(clientId)
|
|
?? throw new KeyNotFoundException("Client not found");
|
|
if (type == "email") client.ClientEmail = identifier;
|
|
else client.ClientPhone = identifier;
|
|
await _uow.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
return new { success = true, message = type == "email" ? "Email updated" : "Phone number updated" };
|
|
}
|
|
|
|
|
|
|
|
public async Task<object> CreateCustomerWithPlans(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
var clientId = await _userRoleHelper.GetClientId(
|
|
DataExtractor.GetUserIdFromContext(httpContext));
|
|
var mobileNumber = DataExtractor.GetRequiredString(data, "mobileNumber");
|
|
var email = DataExtractor.GetOptionalString(data, "email");
|
|
var confirmLink = DataExtractor.GetOptionalBool(data, "confirmLink") ?? false;
|
|
|
|
// Reject outright if this gym already has this phone/email as an active member --
|
|
// re-adding them here would otherwise silently stack a duplicate subscription.
|
|
var ownGymDuplicate = await _customerRepository.FindActiveLinkByIdentifierAsync(mobileNumber) ??
|
|
(!string.IsNullOrWhiteSpace(email)
|
|
? await _customerRepository.FindActiveLinkByIdentifierAsync(email)
|
|
: null);
|
|
if (ownGymDuplicate != null)
|
|
throw new ArgumentException("This phone number or email is already used by another customer at your gym.");
|
|
|
|
var existingCustomer =
|
|
await _customerRepository.GetCustomerByIdentifier(mobileNumber) ??
|
|
(!string.IsNullOrWhiteSpace(email)
|
|
? await _customerRepository.GetCustomerByIdentifier(email)
|
|
: null);
|
|
|
|
// Not at this gym, but already a platform customer elsewhere -- don't auto-link;
|
|
// let staff confirm this is really the same person first. Nothing written yet.
|
|
if (existingCustomer != null && !confirmLink)
|
|
{
|
|
return new
|
|
{
|
|
success = true,
|
|
requiresConfirmation = true,
|
|
message = "This phone number or email already belongs to an existing HexaFitness customer. Add them as your member?",
|
|
existingCustomer = new
|
|
{
|
|
existingCustomer.CusId,
|
|
existingCustomer.CusName,
|
|
existingCustomer.PrimaryPH,
|
|
existingCustomer.Email
|
|
}
|
|
};
|
|
}
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
if (existingCustomer != null)
|
|
{
|
|
existingCustomer.IsActive = true;
|
|
if (existingCustomer.User != null)
|
|
{
|
|
existingCustomer.User.IsActive = true;
|
|
existingCustomer.User.UserName = DataExtractor.GetRequiredString(data, "userName");
|
|
existingCustomer.User.PhoneNumber = mobileNumber;
|
|
if (!string.IsNullOrWhiteSpace(email))
|
|
existingCustomer.User.Email = email;
|
|
}
|
|
|
|
existingCustomer.CusName = DataExtractor.GetRequiredString(data, "fullname");
|
|
existingCustomer.Gender = DataExtractor.GetRequiredString(data, "gender");
|
|
existingCustomer.BoD = DataExtractor.GetOptionalDateOfBirth(data, "birthday");
|
|
existingCustomer.Email = email;
|
|
existingCustomer.PrimaryPH = mobileNumber;
|
|
existingCustomer.NIC = DataExtractor.GetOptionalString(data, "nic");
|
|
existingCustomer.Address = DataExtractor.GetOptionalString(data, "address");
|
|
|
|
await _uow.SaveChangesAsync();
|
|
// The central Customer may predate this gym entirely (same person already
|
|
// a member elsewhere), in which case this tenant DB has no link row yet.
|
|
await _customerRepository.EnsureTenantCustomerLink(existingCustomer);
|
|
await _uow.SaveChangesAsync();
|
|
await _accessDeviceService.EnsureAccessProfileAsync(
|
|
clientId,
|
|
existingCustomer.User?.UserId ?? Guid.Empty,
|
|
"Customer",
|
|
existingCustomer.CusName ?? existingCustomer.User?.UserName ?? string.Empty);
|
|
|
|
Guid? existingSubscriptionId = null;
|
|
Guid? existingInvoiceId = null;
|
|
decimal? existingInvoiceAmount = null;
|
|
if ((data.ContainsKey("workoutPlanIds") && data["workoutPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
(data.ContainsKey("mealPlanIds") && data["mealPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
data.ContainsKey("customerPlanId"))
|
|
{
|
|
var assignResult = await _customerService.AssignPlansToCustomer(existingCustomer.CusId, payload, httpContext);
|
|
var subscriptionIdProp = assignResult?.GetType().GetProperty("subscriptionId");
|
|
existingSubscriptionId = subscriptionIdProp?.GetValue(assignResult) as Guid?;
|
|
var invoiceIdProp = assignResult?.GetType().GetProperty("invoiceId");
|
|
existingInvoiceId = invoiceIdProp?.GetValue(assignResult) as Guid?;
|
|
var invoiceAmountProp = assignResult?.GetType().GetProperty("invoiceAmount");
|
|
existingInvoiceAmount = invoiceAmountProp?.GetValue(assignResult) as decimal?;
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Customer already existed in core DB and was reactivated.",
|
|
customer = existingCustomer,
|
|
subscriptionId = existingSubscriptionId,
|
|
invoiceId = existingInvoiceId,
|
|
invoiceAmount = existingInvoiceAmount,
|
|
paymentLinkSent = false,
|
|
paymentLinkError = (string?)null,
|
|
auth = new
|
|
{
|
|
skipped = true
|
|
}
|
|
};
|
|
}
|
|
|
|
var user = new User
|
|
{
|
|
UserName = DataExtractor.GetRequiredString(data, "userName"),
|
|
PhoneNumber = mobileNumber,
|
|
Email = email,
|
|
IsActive = true,
|
|
CreateDate = DateTime.UtcNow
|
|
};
|
|
|
|
var customer = new Customer
|
|
{
|
|
CusName = DataExtractor.GetRequiredString(data, "fullname"),
|
|
Gender = DataExtractor.GetRequiredString(data, "gender"),
|
|
BoD = DataExtractor.GetOptionalDateOfBirth(data, "birthday"),
|
|
Email = email,
|
|
PrimaryPH = mobileNumber,
|
|
NIC = DataExtractor.GetOptionalString(data, "nic"),
|
|
Address = DataExtractor.GetOptionalString(data, "address"),
|
|
IsPrimaryPHVerfied = false,
|
|
User = user
|
|
};
|
|
|
|
var result = await _customerRepository.CreateCustomer(customer);
|
|
await _uow.SaveChangesAsync();
|
|
// Must precede AssignPlansToCustomer: the tenant-side subscription FKs point
|
|
// at this link row, so it has to be persisted before any of them are inserted.
|
|
await _customerRepository.EnsureTenantCustomerLink(result);
|
|
await _uow.SaveChangesAsync();
|
|
await _accessDeviceService.EnsureAccessProfileAsync(
|
|
clientId, user.UserId, "Customer", customer.CusName ?? user.UserName ?? string.Empty);
|
|
|
|
var tempPassword = Guid.NewGuid().ToString("N").Substring(0, 8);
|
|
|
|
AuthResponse authResponse;
|
|
try
|
|
{
|
|
|
|
Guid customerRoleId = Guid.Parse("08de6a11-9e9f-4401-8a10-685cc57b41ed");
|
|
Guid customerUserTypeId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
|
|
|
var authPayload = DeserializePayload.Deserialize(payload);
|
|
authPayload["userId"] = JsonSerializer.SerializeToElement(user.UserId);
|
|
authPayload["roleId"] = JsonSerializer.SerializeToElement(customerRoleId);
|
|
authPayload["userTypeId"] = JsonSerializer.SerializeToElement(customerUserTypeId);
|
|
authPayload["password"] = JsonSerializer.SerializeToElement(tempPassword);
|
|
authPayload["mobileNumber"] = JsonSerializer.SerializeToElement(user.PhoneNumber);
|
|
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}");
|
|
}
|
|
|
|
Guid? subscriptionId = null;
|
|
Guid? invoiceId = null;
|
|
decimal? invoiceAmount = null;
|
|
if ((data.ContainsKey("workoutPlanIds") && data["workoutPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
(data.ContainsKey("mealPlanIds") && data["mealPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
data.ContainsKey("customerPlanId"))
|
|
{
|
|
var assignResult = await _customerService.AssignPlansToCustomer(result.CusId, payload, httpContext);
|
|
|
|
// The FE opens the "add group members" step right after creating a
|
|
// member on a Group plan, so surface the new subscription's id.
|
|
var subscriptionIdProp = assignResult?.GetType().GetProperty("subscriptionId");
|
|
subscriptionId = subscriptionIdProp?.GetValue(assignResult) as Guid?;
|
|
var invoiceIdProp = assignResult?.GetType().GetProperty("invoiceId");
|
|
invoiceId = invoiceIdProp?.GetValue(assignResult) as Guid?;
|
|
var invoiceAmountProp = assignResult?.GetType().GetProperty("invoiceAmount");
|
|
invoiceAmount = invoiceAmountProp?.GetValue(assignResult) as decimal?;
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
|
|
// Send the member their app login (Gmail email + SMS with the mobile-app
|
|
// link, username and password). Non-critical: delivery failures must not
|
|
// roll back the created customer.
|
|
await SendCustomerWelcomeAsync(user.UserName, user.Email, user.PhoneNumber, tempPassword);
|
|
|
|
bool paymentLinkSent = false;
|
|
string? paymentLinkError = null;
|
|
if (invoiceId.HasValue && invoiceAmount is > 0)
|
|
{
|
|
try
|
|
{
|
|
await _paymentService.SendPaymentLink(invoiceId.Value, httpContext);
|
|
paymentLinkSent = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Account/subscription creation is complete. Delivery can be retried
|
|
// from the member invoice without rolling the member back.
|
|
paymentLinkError = ex.Message;
|
|
Console.WriteLine($"Automatic customer payment link failed (non-critical): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Customer created and plans assigned successfully",
|
|
customer = result,
|
|
subscriptionId,
|
|
invoiceId,
|
|
paymentLinkSent,
|
|
paymentLinkError,
|
|
auth = authResponse
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Delivers a newly created member's app login over both channels. Each channel is
|
|
// isolated so one failing (e.g. no email configured) still lets the other through,
|
|
// and any failure is swallowed — the member already exists at this point.
|
|
private async Task SendCustomerWelcomeAsync(string? userName, string? email, string? phoneNumber, string password)
|
|
{
|
|
var appLink = _config["App:MobileAppLink"] ?? "";
|
|
|
|
if (!string.IsNullOrWhiteSpace(email))
|
|
{
|
|
try
|
|
{
|
|
string html =
|
|
$"<p>Hi {userName},</p>" +
|
|
"<p>Your gym membership account has been created. Use the credentials below to log in to the Hexa Fitness mobile app:</p>" +
|
|
$"<p><b>Username:</b> {userName}<br/><b>Password:</b> {password}</p>" +
|
|
(string.IsNullOrWhiteSpace(appLink) ? "" : $"<p>Download the app: <a href='{appLink}'>{appLink}</a></p>") +
|
|
"<p>We recommend changing your password after your first login.</p>" +
|
|
"<p>— Hexa Fitness</p>";
|
|
await _thirdPartyService.SendEmailAsync(email!, "Your Hexa Fitness app login", html);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Customer welcome email failed (non-critical): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(phoneNumber))
|
|
{
|
|
try
|
|
{
|
|
string sms =
|
|
$"Welcome to Hexa Fitness!" +
|
|
(string.IsNullOrWhiteSpace(appLink) ? "" : $" Download: {appLink}");
|
|
await _thirdPartyService.SendSmsAsync(phoneNumber!, null, sms);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Customer welcome SMS failed (non-critical): {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public async Task<object> UpdateCustomerByClient(object payload, HttpContext httpContext)
|
|
{
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = DataExtractor.GetRequiredGuid(data, "customerId");
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
var customer = await _customerRepository.GetCustomerById(customerId);
|
|
if (customer == null)
|
|
throw new NotFoundException("Customer not found");
|
|
|
|
customer.CusName = DataExtractor.GetRequiredString(data, "fullname");
|
|
customer.Gender = DataExtractor.GetRequiredString(data, "gender");
|
|
customer.BoD = DataExtractor.GetOptionalDateOfBirth(data, "birthday");
|
|
customer.Email = DataExtractor.GetOptionalString(data, "email");
|
|
customer.PrimaryPH = DataExtractor.GetOptionalString(data, "phoneNumber");
|
|
customer.NIC = DataExtractor.GetOptionalString(data, "nic");
|
|
customer.Address = DataExtractor.GetOptionalString(data, "address");
|
|
|
|
var userName = DataExtractor.GetRequiredString(data, "userName");
|
|
if (customer.User != null)
|
|
{
|
|
customer.User.UserName = userName;
|
|
customer.User.Email = customer.Email;
|
|
customer.User.PhoneNumber = customer.PrimaryPH;
|
|
}
|
|
|
|
var result = await _customerRepository.UpdateCustomer(customerId, customer);
|
|
if (result == null)
|
|
throw new KeyNotFoundException("Customer not found");
|
|
|
|
// This gym's own TenantCustomerLink caches name/phone/email for list and payment
|
|
// views -- without this it keeps showing whatever was cached at link time, drifting
|
|
// from the central edit above. EnsureTenantCustomerLink is idempotent, so this is
|
|
// just a refresh when the link already exists.
|
|
await _customerRepository.EnsureTenantCustomerLink(result);
|
|
|
|
// A client-side profile edit must not silently drift from what the customer logs
|
|
// in with — push the same name/contact fields to their Auth login credential.
|
|
// Failure here throws (caught below), rolling back the local edit too, so the
|
|
// two never end up out of sync in either direction.
|
|
var authResponse = await _authService.AdminUpdateUser(new
|
|
{
|
|
userId = customer.FKUserId,
|
|
userName,
|
|
email = customer.Email,
|
|
mobileNumber = customer.PrimaryPH
|
|
}, httpContext);
|
|
if (authResponse.StatusCode != 200 || !authResponse.Success)
|
|
throw new InvalidOperationException($"Auth profile sync failed: {authResponse.Message}");
|
|
|
|
if ((data.ContainsKey("workoutPlanIds") && data["workoutPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
(data.ContainsKey("mealPlanIds") && data["mealPlanIds"].ValueKind == JsonValueKind.Array) ||
|
|
data.ContainsKey("customerPlanId"))
|
|
{
|
|
await _customerService.AssignPlansToCustomer(customerId, payload, httpContext);
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
return result;
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public async Task<object> AssignPlanToExistingCustomer(Guid customerId, object payload, HttpContext httpContext)
|
|
{
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
if (!data.ContainsKey("customerPlanId") || data["customerPlanId"].ValueKind == JsonValueKind.Null)
|
|
throw new ArgumentException("customerPlanId is required.");
|
|
|
|
return await _customerService.AssignPlansToCustomer(customerId, payload, httpContext);
|
|
}
|
|
|
|
// Switches a customer already on an Individual-scope plan to a different Individual-scope
|
|
// plan: deactivates their current subscription (and its members/subitems/installments)
|
|
// and starts a fresh one via CustomerSubscriptionFactory, mirroring how AssignPlansToCustomer
|
|
// builds a first subscription — but self-contained here rather than reusing that method,
|
|
// since it assumes the customer has no existing active subscription.
|
|
//
|
|
// Settles the old plan's billing rather than abandoning it:
|
|
// - old invoice still Pending/Partial -> mark it RolledOver and carry the unpaid
|
|
// balance onto a full-price invoice for the new plan (same mechanism the renewal
|
|
// sweep already uses for exactly this).
|
|
// - old invoice Paid, new plan costs more -> a single settlement invoice for just the
|
|
// difference (not the new plan's full price plus the difference).
|
|
// - old invoice Paid, new plan costs less -> the same settlement invoice, but negative:
|
|
// a "Credit" invoice the customer's next bill (any kind) will automatically deduct.
|
|
public async Task<object> ChangeCustomerMembershipPlan(Guid customerId, Guid newCustomerPlanId, HttpContext httpContext)
|
|
{
|
|
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var newPlan = await _clientRepository.GetCustomerPlanEntityForClientAsync(newCustomerPlanId, clientId)
|
|
?? throw new NotFoundException("Membership plan not found.");
|
|
if (newPlan.PlanScope != "Individual")
|
|
throw new BadRequestException("Change Membership only supports Individual-scope plans.");
|
|
|
|
var oldMembership = await _clientRepository.GetActiveMembershipForCustomerAsync(customerId);
|
|
if (oldMembership?.Subscription.FKCustomerPlanId == newCustomerPlanId)
|
|
throw new BadRequestException("This member is already on that plan.");
|
|
|
|
// Read the old subscription's billing state before ReplaceCustomerSubscriptionAsync
|
|
// deactivates it -- GetInvoiceBySubscriptionIdAsync still resolves correctly on an
|
|
// active subscription regardless of what happens to it a moment later.
|
|
var oldInvoice = oldMembership != null
|
|
? await _customerInvoiceRepository.GetInvoiceBySubscriptionIdAsync(oldMembership.FKSubscriptionId)
|
|
: null;
|
|
|
|
var subscription = new Models.CustomerSubscription
|
|
{
|
|
FKCustomerPlanId = newCustomerPlanId,
|
|
FKCustomerId = customerId,
|
|
OnboardDate = DateTime.UtcNow,
|
|
IsActive = true
|
|
};
|
|
var initialSubItem = new Models.CustomerSubItem
|
|
{
|
|
SubItemName = "Membership change",
|
|
IsActive = true
|
|
};
|
|
var aggregate = CustomerSubscriptionFactory.Create(subscription, newPlan, initialSubItem);
|
|
|
|
await _clientRepository.ReplaceCustomerSubscriptionAsync(oldMembership, aggregate);
|
|
await _uow.SaveChangesAsync();
|
|
|
|
// Sibling namespace Fitness_core_BE.Services.CustomerInvoice (CustomerInvoiceService.cs)
|
|
// outranks a bare `CustomerInvoice` here, so the model type needs its full name.
|
|
Fitness_core_BE.Models.CustomerInvoice invoice;
|
|
if (oldInvoice == null || oldInvoice.Status == "Pending" || oldInvoice.Status == "Partial")
|
|
{
|
|
decimal carriedOverBalance = 0m;
|
|
if (oldInvoice != null)
|
|
{
|
|
var approvedPaid = await _customerInvoiceRepository.GetApprovedPaymentTotalAsync(oldInvoice.CustomerInvoiceId);
|
|
carriedOverBalance = Math.Max(0m, oldInvoice.Amount - approvedPaid);
|
|
await _customerInvoiceRepository.UpdateCustomerInvoiceStatus(oldInvoice.CustomerInvoiceId, "RolledOver");
|
|
}
|
|
|
|
invoice = await _customerInvoiceRepository.CreateCustomerInvoiceFromSubscription(
|
|
subscription.SubscriptionId, chargePayment: true, carriedOverBalance);
|
|
}
|
|
else
|
|
{
|
|
// oldInvoice is Paid (any other status would mean the old subscription was
|
|
// never properly billed, which "shouldn't happen" for an active membership --
|
|
// treated the same as Paid here rather than adding a third branch for it).
|
|
var diff = newPlan.PlanPrice - oldInvoice.Amount;
|
|
invoice = await _customerInvoiceRepository.CreatePlanChangeInvoiceAsync(
|
|
subscription.SubscriptionId, "Membership change", diff);
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Membership plan changed",
|
|
subscriptionId = subscription.SubscriptionId,
|
|
invoiceId = invoice.CustomerInvoiceId,
|
|
invoiceAmount = invoice.Amount,
|
|
invoiceStatus = invoice.Status
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Drops the member back to "no plan": if they're the primary member of a subscription,
|
|
// the whole subscription (and every other member on it) is deactivated; otherwise only
|
|
// their own membership row is. No new subscription/invoice is created.
|
|
public async Task<object> UnassignCustomerMembership(Guid customerId, HttpContext httpContext)
|
|
{
|
|
await EnsureCustomerBelongsToCallerClient(customerId, httpContext);
|
|
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var membership = await _clientRepository.GetMembershipForCustomerAsync(customerId)
|
|
?? throw new BadRequestException("This member doesn't have a plan to unassign.");
|
|
|
|
// Toggle: the same button unassigns an assigned member and re-assigns them
|
|
// to the plan they were already on. The subscription itself is left active
|
|
// either way, so nothing has to be rebuilt to put them back.
|
|
var assigned = !membership.IsPlanAssigned;
|
|
|
|
// Unassigning the primary takes the whole group off the shared plan (it is one
|
|
// entitlement); a secondary only unassigns themselves.
|
|
await _clientRepository.SetMembershipPlanAssignedAsync(
|
|
membership.FKSubscriptionId,
|
|
assigned,
|
|
wholeSubscription: membership.IsPrimary,
|
|
membership.SubscriptionMemberId);
|
|
|
|
await _uow.SaveChangesAsync();
|
|
await _uow.CommitAsync();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
isPlanAssigned = assigned,
|
|
message = assigned ? "Membership assigned" : "Membership unassigned"
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetCustomersByClientId(object payload, HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var subscriptions = await _clientRepository.GetCustomersByClientIdAsync(clientId);
|
|
|
|
return subscriptions;
|
|
}
|
|
|
|
private async Task EnsureCustomerBelongsToCallerClient(Guid customerId, HttpContext httpContext)
|
|
{
|
|
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
|
if (!await _clientRepository.CustomerBelongsToClientAsync(customerId, clientId))
|
|
throw new ForbiddenException("This customer belongs to another gym.");
|
|
}
|
|
|
|
public async Task<object> GetCustomerPlansByClientId(object payload, HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plans = await _clientRepository.GetCustomerPlansByClientIdAsync(clientId);
|
|
return plans;
|
|
}
|
|
|
|
public async Task<object> RegisterClient(object payload, HttpContext httpContext)
|
|
{
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
|
|
// Helper to extract double values safely
|
|
double? GetDouble(string key)
|
|
{
|
|
if (data.ContainsKey(key) && data[key].ValueKind == JsonValueKind.Number)
|
|
return data[key].GetDouble();
|
|
if (data.ContainsKey(key) && data[key].ValueKind == JsonValueKind.String &&
|
|
double.TryParse(data[key].GetString(), out var result))
|
|
return result;
|
|
return null;
|
|
}
|
|
|
|
// Create User first
|
|
var user = new User
|
|
{
|
|
UserName = data.ContainsKey("ClientName") ? data["ClientName"].GetString() : "unknown",
|
|
Email = data.ContainsKey("ClientEmail") ? data["ClientEmail"].GetString() : null,
|
|
PhoneNumber = data.ContainsKey("ClientPhone") ? data["ClientPhone"].GetString() : null,
|
|
IsActive = true,
|
|
CreateDate = DateTime.UtcNow
|
|
};
|
|
|
|
var client = new Client
|
|
{
|
|
ClientName = data.ContainsKey("ClientName") ? data["ClientName"].GetString() : null,
|
|
ClientPhone = data.ContainsKey("ClientPhone") ? data["ClientPhone"].GetString() : null,
|
|
ClientEmail = data.ContainsKey("ClientEmail") ? data["ClientEmail"].GetString() : null,
|
|
User = user
|
|
};
|
|
|
|
await _clientRepository.RegisterClientAsync(client);
|
|
|
|
|
|
|
|
var clientBussiness = new ClientBusiness
|
|
{
|
|
FKClientId = client.ClientId,
|
|
BusinessType = data.ContainsKey("BusinessType") ? (Fitness_core_BE.Models.Enums.BusinessType)Enum.Parse(typeof(Fitness_core_BE.Models.Enums.BusinessType), data["BusinessType"].GetString() ?? "Individual") : Fitness_core_BE.Models.Enums.BusinessType.institute,
|
|
BusinessName = data.ContainsKey("BusinessName") ? data["BusinessName"].GetString() : null,
|
|
BusinessLogoId = data.ContainsKey("BusinessLogoId") ? data["BusinessLogoId"].GetString() : null,
|
|
Description = data.ContainsKey("Description") ? data["Description"].GetString() : null,
|
|
IsPysicalLocation = data.ContainsKey("IsPysicalLocation") ? data["IsPysicalLocation"].GetBoolean() : true,
|
|
Address = data.ContainsKey("Address") ? data["Address"].GetString() : null,
|
|
City = data.ContainsKey("City") ? data["City"].GetString() : null,
|
|
Facilities = data.ContainsKey("facilities") ? data["facilities"].GetString() : null,
|
|
CoachingMode = data.ContainsKey("CoachingMode") ? data["CoachingMode"].GetString() : null,
|
|
Expertise = data.ContainsKey("Expertise") ? data["Expertise"].GetString() : null,
|
|
Gender = data.ContainsKey("Gender") ? data["Gender"].GetString() : null,
|
|
ExperienceYears = data.ContainsKey("ExperienceYears") ? data["ExperienceYears"].GetString() : null,
|
|
SessionType = data.ContainsKey("SessionType") ? data["SessionType"].GetString() : null,
|
|
StoreType = data.ContainsKey("StoreType") ? data["StoreType"].GetString() : null,
|
|
ProductCategory = data.ContainsKey("ProductCategory") ? data["ProductCategory"].GetString() : null,
|
|
DeliveryAvailable = data.ContainsKey("DeliveryAvailable") ? data["DeliveryAvailable"].GetString() : null,
|
|
|
|
Latitude = GetDouble("latitude"),
|
|
Longitude = GetDouble("longitude"),
|
|
};
|
|
|
|
Guid userTypeId = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
|
Guid roleId = Guid.Parse("08de6a11-9e9f-4401-8a10-6859860b41ed");
|
|
Guid userId = user.UserId;
|
|
|
|
|
|
await _clientRepository.RegisterBussinessDetailAsync(clientBussiness);
|
|
await _uow.CommitAsync();
|
|
|
|
var authPayload = new { email = client.ClientEmail, userTypeId = userTypeId, roleId = roleId, userId};
|
|
|
|
AuthResponse authResponse;
|
|
try
|
|
{
|
|
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}");
|
|
}
|
|
|
|
return new
|
|
{
|
|
client,
|
|
auth = authResponse
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetPackageList(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var packages = await _clientRepository.GetPackageListAsync();
|
|
return packages;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetAllCustomerPlans(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plans = await _clientRepository.GetCustomerPlansByClientIdAsync(clientId);
|
|
return plans;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetCustomerPlanById(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerPlanId = DataExtractor.GetRequiredGuid(data, "customerPlanId");
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plan = await _clientRepository.GetCustomerPlanByIdForClientAsync(customerPlanId, clientId);
|
|
|
|
if (plan == null)
|
|
throw new NotFoundException($"Customer plan with ID {customerPlanId} not found");
|
|
|
|
return plan;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetMealPlansByClientId(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plans = await _clientRepository.GetMealPlansByClientIdAsync(clientId);
|
|
return plans;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetMealPlanById(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plan = await _clientRepository.GetMealPlanByIdForClientAsync(mealPlanId, clientId);
|
|
|
|
if (plan == null)
|
|
throw new NotFoundException($"Meal plan with ID {mealPlanId} not found");
|
|
|
|
if (plan.MealItems != null)
|
|
{
|
|
foreach (var item in plan.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 plan;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetWorkoutPlansByClientId(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plans = await _clientRepository.GetWorkoutPlansByClientIdAsync(clientId);
|
|
return plans;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetWorkoutPlanById(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var plan = await _clientRepository.GetWorkoutPlanByIdForClientAsync(workoutPlanId, clientId);
|
|
|
|
if (plan == null)
|
|
throw new Exceptions.NotFoundException($"Workout plan with ID {workoutPlanId} not found");
|
|
|
|
var transformedWorkoutItems = plan.WorkoutItems?.Select(item =>
|
|
{
|
|
object? exercisesObj = null;
|
|
if (!string.IsNullOrEmpty(item.Exercises))
|
|
{
|
|
try
|
|
{
|
|
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
|
|
}
|
|
catch
|
|
{
|
|
exercisesObj = item.Exercises;
|
|
}
|
|
}
|
|
|
|
return new
|
|
{
|
|
item.WorkoutItemId,
|
|
item.ExerciseDay,
|
|
item.Description,
|
|
Exercises = exercisesObj,
|
|
item.HowToDo,
|
|
item.DoURL,
|
|
item.WorkoutPlanId,
|
|
item.IsActive,
|
|
item.CreateDate,
|
|
item.UpdatedAt
|
|
};
|
|
}).ToList();
|
|
|
|
return new
|
|
{
|
|
plan.WorkoutPlanId,
|
|
plan.Name,
|
|
plan.Description,
|
|
plan.IsTemplate,
|
|
plan.WorkoutCategoryId,
|
|
plan.WorkoutCategoryName,
|
|
plan.ClientId,
|
|
plan.ClientName,
|
|
plan.IsActive,
|
|
plan.CreateDate,
|
|
WorkoutItems = transformedWorkoutItems,
|
|
AssignedCustomers = plan.AssignedCustomers
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetCardDetails(object payload, HttpContext httpContext)
|
|
{
|
|
try
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var cards = await _clientRepository.GetCardsByClientIdAsync(clientId);
|
|
return cards;
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetCustomerCards(object payload, HttpContext httpContext)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
Guid customerId = DataExtractor.GetRequiredGuid(data, "customerId");
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var cards = await _clientRepository.GetCardsByCustomerForClientAsync(customerId, clientId);
|
|
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
|
|
});
|
|
}
|
|
|
|
public async Task<object> GetLockPolicy(HttpContext httpContext)
|
|
{
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var client = await _clientRepository.GetClientEntityByIdAsync(clientId)
|
|
?? throw new KeyNotFoundException("Client not found");
|
|
|
|
return new { lockAfterDaysOverdue = client.LockAfterDaysOverdue };
|
|
}
|
|
|
|
public async Task<object> UpdateLockPolicy(int? lockAfterDaysOverdue, HttpContext httpContext)
|
|
{
|
|
if (lockAfterDaysOverdue is < 0)
|
|
throw new ArgumentException("lockAfterDaysOverdue cannot be negative.");
|
|
|
|
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
|
|
Guid clientId = await _userRoleHelper.GetClientId(userId);
|
|
|
|
var client = await _clientRepository.GetClientEntityByIdAsync(clientId)
|
|
?? throw new KeyNotFoundException("Client not found");
|
|
|
|
client.LockAfterDaysOverdue = lockAfterDaysOverdue;
|
|
await _uow.SaveChangesAsync();
|
|
|
|
return new { lockAfterDaysOverdue = client.LockAfterDaysOverdue };
|
|
}
|
|
}
|
|
}
|