Files
Core_Be/Services/StaffService.cs
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

420 lines
18 KiB
C#

using Fitness_core_BE.DTOs.Auth;
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Helpers;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.DTOs.Staff;
using System.Text.Json;
namespace Fitness_core_BE.Services.Staff
{
public class StaffService : IStaffService
{
private readonly IStaffRepository _staffRepository;
private readonly IUnitOfWork _uow;
private readonly UserRoleHelper _userRoleHelper;
private readonly IAuthService _authService;
private readonly IAccessDeviceService _accessDeviceService;
private readonly IGymRoleRepository _gymRoleRepository;
public StaffService(IStaffRepository staffRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper, IAuthService authService, IAccessDeviceService accessDeviceService, IGymRoleRepository gymRoleRepository)
{
_staffRepository = staffRepository;
_uow = uow;
_userRoleHelper = userRoleHelper;
_authService = authService;
_accessDeviceService = accessDeviceService;
_gymRoleRepository = gymRoleRepository;
}
public async Task<object> GetAllStaff(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
bool includeInactive = data != null &&
data.ContainsKey("includeInactive") &&
data["includeInactive"].GetBoolean();
var staffList = await _staffRepository.GetAllStaff(includeInactive);
return await MapStaffListAsync(staffList);
}
public async Task<object> GetStaffById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("staffId"))
{
throw new KeyNotFoundException("staffId is required");
}
var staffId = Guid.Parse(data["staffId"].GetString()!);
var staff = await _staffRepository.GetStaffById(staffId);
if (staff == null)
{
throw new KeyNotFoundException("Staff not found");
}
var callerClientId = await _userRoleHelper.GetClientId(
DataExtractor.GetUserIdFromContext(httpContext));
if (staff.FKClientId != callerClientId)
throw new ForbiddenException("This staff member does not belong to your gym.");
return await MapStaffAsync(staff);
}
public async Task<object> GetStaffByClientId(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("clientId"))
{
throw new KeyNotFoundException("clientId is required");
}
var clientId = Guid.Parse(data["clientId"].GetString()!);
var callerClientId = await _userRoleHelper.GetClientId(
DataExtractor.GetUserIdFromContext(httpContext));
if (clientId != callerClientId)
throw new ForbiddenException("You cannot view another gym's staff.");
bool includeInactive = data.ContainsKey("includeInactive") &&
data["includeInactive"].GetBoolean();
var staffList = await _staffRepository.GetStaffByClientId(clientId, includeInactive);
return await MapStaffListAsync(staffList);
}
public async Task<object> CreateStaff(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid clientId = await _userRoleHelper.GetClientId(userId);
if (!data!.ContainsKey("userName"))
{
throw new KeyNotFoundException("userName is required");
}
if (!data.ContainsKey("email"))
{
throw new KeyNotFoundException("email is required");
}
if (!data.ContainsKey("phoneNumber"))
{
throw new KeyNotFoundException("phoneNumber is required");
}
var user = new User
{
UserName = data["userName"].GetString()!,
Email = data["email"].GetString()!,
PhoneNumber = data["phoneNumber"].GetString()!
};
// Optional -- must belong to this same client, same check AssignStaffRole
// already does for the post-creation assignment path. Left unset, the new
// staff member has no dashboard access until a role is assigned later.
Guid? gymRoleId = null;
if (data.ContainsKey("gymRoleId") && data["gymRoleId"].ValueKind != JsonValueKind.Null
&& Guid.TryParse(data["gymRoleId"].GetString(), out var parsedRoleId))
{
var role = await _gymRoleRepository.GetRoleById(parsedRoleId);
if (role == null || role.FKClientId != clientId)
throw new BadRequestException("That role doesn't belong to your gym.");
gymRoleId = parsedRoleId;
}
var staff = new Models.Staff
{
FKUserId = userId,
FKClientId = clientId,
StaffPosition = data.ContainsKey("staffPosition") ? data["staffPosition"].GetString() : null,
Department = data.ContainsKey("department") ? data["department"].GetString() : null,
FKGymRoleId = gymRoleId,
};
var result = await _staffRepository.CreateStaff(staff, user);
await _uow.SaveChangesAsync();
await _accessDeviceService.EnsureAccessProfileAsync(
clientId, user.UserId, "Staff", user.UserName ?? string.Empty);
AuthResponse authResponse;
try
{
var authPayload = DeserializePayload.Deserialize(payload);
Guid roleId = Guid.Parse("08de6a11-9e9f-4401-8a10-6854567b41ed");
Guid userTypeId = Guid.Parse("00000000-0000-0000-0000-000000000003");
authPayload["userId"] = JsonSerializer.SerializeToElement(user.UserId);
authPayload["roleId"] = JsonSerializer.SerializeToElement(roleId);
authPayload["userTypeId"] = JsonSerializer.SerializeToElement(userTypeId);
// The client portal's request uses "phoneNumber", but Core-Hex-Auth's
// RegisterUser only reads "mobileNumber" — without this the auth User row
// was silently created with a null phone number.
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}");
}
await _uow.CommitAsync();
// Newly created staff have no password of their own yet — email them a
// "set your password" link (reuses the auth service's reset-link flow).
// Non-critical: a failure here must not roll back the created staff member.
try
{
await _authService.ForgotPassword(
new { identifier = user.Email, useResetLink = true, welcome = true },
httpContext);
}
catch (Exception ex)
{
Console.WriteLine($"Staff set-password email failed (non-critical): {ex.Message}");
}
return new
{
staff = await MapStaffAsync(result, user),
auth = authResponse
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateStaff(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("staffId"))
{
throw new KeyNotFoundException("staffId is required");
}
var staffId = Guid.Parse(data["staffId"].GetString()!);
// Get existing staff first
var existingStaff = await _staffRepository.GetStaffById(staffId);
if (existingStaff == null)
{
throw new KeyNotFoundException("Staff not found");
}
var callerClientId = await _userRoleHelper.GetClientId(
DataExtractor.GetUserIdFromContext(httpContext));
if (existingStaff.FKClientId != callerClientId)
throw new ForbiddenException("This staff member does not belong to your gym.");
// Create update object with only provided fields
var updateStaff = new Models.Staff
{
StaffId = staffId,
StaffPosition = data.ContainsKey("staffPosition") ? data["staffPosition"].GetString() : null,
Department = data.ContainsKey("department") ? data["department"].GetString() : null,
IsActive = data.ContainsKey("isActive") ? data["isActive"].GetBoolean() : existingStaff.IsActive,
User = new User
{
UserName = data.ContainsKey("userName") ? data["userName"].GetString() : null,
Email = data.ContainsKey("email") ? data["email"].GetString() : null,
PhoneNumber = data.ContainsKey("phoneNumber") ? data["phoneNumber"].GetString() : null
}
};
var result = await _staffRepository.UpdateStaff(updateStaff);
await _uow.CommitAsync();
return await MapStaffAsync(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteStaff(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data!.ContainsKey("staffId"))
{
throw new KeyNotFoundException("staffId is required");
}
var staffId = Guid.Parse(data["staffId"].GetString()!);
await EnsureStaffBelongsToCallerAsync(staffId, httpContext);
// Check if hard delete is requested (default is soft delete)
bool softDelete = !data.ContainsKey("hardDelete") || !data["hardDelete"].GetBoolean();
var result = await _staffRepository.DeleteStaff(staffId, softDelete);
if (!result)
{
throw new KeyNotFoundException("Staff not found");
}
await _uow.CommitAsync();
return new
{
success = true,
message = softDelete
? "Staff deactivated successfully"
: "Staff deleted successfully",
softDelete = softDelete
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetPayRate(Guid staffId, HttpContext httpContext)
{
await EnsureStaffBelongsToCallerAsync(staffId, httpContext);
var rate = await _staffRepository.GetActivePayRateAsync(staffId);
return rate == null
? null!
: new StaffPayRateDto { Id = rate.Id, RateType = rate.RateType, Amount = rate.Amount, EffectiveFrom = rate.EffectiveFrom };
}
public async Task<object> UpsertPayRate(Guid staffId, string rateType, decimal amount, HttpContext httpContext)
{
await EnsureStaffBelongsToCallerAsync(staffId, httpContext);
if (rateType != "Monthly" && rateType != "Hourly")
throw new BadRequestException("rateType must be 'Monthly' or 'Hourly'.");
if (amount <= 0)
throw new BadRequestException("amount must be greater than zero.");
var currentRate = await _staffRepository.GetActivePayRateAsync(staffId);
if (currentRate != null)
currentRate.IsActive = false;
var rate = await _staffRepository.AddPayRateAsync(new Models.StaffPayRate
{
Id = Guid.NewGuid(),
FKStaffId = staffId,
RateType = rateType,
Amount = amount,
EffectiveFrom = DateTime.UtcNow,
IsActive = true
});
await _uow.SaveChangesAsync();
return new StaffPayRateDto { Id = rate.Id, RateType = rate.RateType, Amount = rate.Amount, EffectiveFrom = rate.EffectiveFrom };
}
public async Task<object> GetPayments(Guid staffId, HttpContext httpContext)
{
await EnsureStaffBelongsToCallerAsync(staffId, httpContext);
var payments = await _staffRepository.GetPaymentsAsync(staffId);
return payments.Select(p => new StaffPaymentDto { Id = p.Id, Amount = p.Amount, Note = p.Note, PaidAt = p.PaidAt }).ToList();
}
public async Task<object> RecordPayment(Guid staffId, decimal amount, string? note, HttpContext httpContext)
{
await EnsureStaffBelongsToCallerAsync(staffId, httpContext);
if (amount <= 0)
throw new BadRequestException("amount must be greater than zero.");
var payment = new Models.StaffPayment
{
Id = Guid.NewGuid(),
FKStaffId = staffId,
Amount = amount,
Note = note,
PaidAt = DateTime.UtcNow,
FKPaidByUserId = DataExtractor.GetUserIdFromContext(httpContext)
};
await _staffRepository.AddPaymentAsync(payment);
await _uow.SaveChangesAsync();
return new StaffPaymentDto { Id = payment.Id, Amount = payment.Amount, Note = payment.Note, PaidAt = payment.PaidAt };
}
private async Task EnsureStaffBelongsToCallerAsync(Guid staffId, HttpContext httpContext)
{
var userId = DataExtractor.GetUserIdFromContext(httpContext);
var clientId = await _userRoleHelper.GetClientId(userId);
var staff = await _staffRepository.GetStaffById(staffId)
?? throw new KeyNotFoundException("Staff not found");
if (staff.FKClientId != clientId)
throw new ForbiddenException("This staff member does not belong to your gym.");
}
private async Task<StaffDto> MapStaffAsync(Models.Staff staff, User? userOverride = null)
{
var user = userOverride ?? staff.User;
var access = await _accessDeviceService.GetAccessStatusAsync(
staff.FKClientId, staff.FKUserId);
return new StaffDto
{
StaffId = staff.StaffId,
UserId = staff.FKUserId,
UserName = user?.UserName,
Email = user?.Email,
PhoneNumber = user?.PhoneNumber,
ClientId = staff.FKClientId,
ClientName = staff.Client?.ClientName,
GymRoleId = staff.FKGymRoleId,
GymRoleName = staff.GymRole?.Name,
StaffPosition = staff.StaffPosition,
Department = staff.Department,
HireDate = staff.HireDate,
IsActive = staff.IsActive,
CreatedAt = staff.CreatedAt,
AccessProfileId = access?.AccessProfileId,
AccessNumber = access?.AccessNumber,
AccessProvisionStatus = access?.ProvisionStatus ?? "NotConfigured",
ActualAccessState = access?.ActualAccessState ?? "Unknown",
DesiredAccessState = access?.DesiredAccessState ?? "Enabled",
FingerprintCount = access?.FingerprintCount ?? 0,
AccessLastError = access?.LastError
};
}
private async Task<List<StaffDto>> MapStaffListAsync(IEnumerable<Models.Staff> staff)
{
var result = new List<StaffDto>();
foreach (var member in staff)
result.Add(await MapStaffAsync(member));
return result;
}
}
}