488 lines
19 KiB
C#
488 lines
19 KiB
C#
using Fitness_core_BE.Exceptions;
|
|
using Fitness_core_BE.Interfaces;
|
|
using Fitness_core_BE.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Fitness_core_BE.Repos
|
|
{
|
|
public class AccessDeviceRepo : IAccessDeviceRepo
|
|
{
|
|
private readonly TenantDbContext _dbContext;
|
|
private readonly CentralDbContext _central;
|
|
|
|
public AccessDeviceRepo(TenantDbContext dbContext, CentralDbContext central)
|
|
{
|
|
_dbContext = dbContext;
|
|
_central = central;
|
|
}
|
|
|
|
public async Task<object> AddDeviceAsync(AccessDevice device)
|
|
{
|
|
var result = await _dbContext.AddAsync(device);
|
|
return result;
|
|
}
|
|
|
|
public async Task<AccessDevice> RemoveDeviceAsync(Guid clientId)
|
|
{
|
|
AccessDevice? device = await _dbContext.AccessDevices
|
|
.Where(ad => ad.FKClientId == clientId)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (device == null)
|
|
throw new NotFoundException($"{clientId} don't have a Device in the System");
|
|
|
|
device.IsActive = false;
|
|
|
|
_dbContext.Update(device);
|
|
|
|
return device;
|
|
|
|
}
|
|
|
|
public async Task<AccessDevice> GetDeviceByDId(Guid deviceId)
|
|
{
|
|
AccessDevice? device = await _dbContext.AccessDevices
|
|
.Where(d => d.DeviceId == deviceId && d.IsActive == true)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (device == null)
|
|
throw new NotFoundException($"{deviceId} is not found");
|
|
|
|
return device;
|
|
}
|
|
|
|
public Task<AccessDevice?> GetDeviceByIdAsync(
|
|
Guid deviceId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDevices
|
|
.FirstOrDefaultAsync(device => device.DeviceId == deviceId, ct);
|
|
}
|
|
|
|
public Task<AccessDevice?> GetDeviceByHardwareIdAsync(
|
|
Guid clientId,
|
|
string hardwareId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDevices.FirstOrDefaultAsync(device =>
|
|
device.FKClientId == clientId &&
|
|
device.HardwareId == hardwareId, ct);
|
|
}
|
|
|
|
public Task<AccessDevice?> GetActiveDeviceForClientAsync(
|
|
Guid clientId,
|
|
Guid deviceId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDevices.FirstOrDefaultAsync(device =>
|
|
device.DeviceId == deviceId &&
|
|
device.FKClientId == clientId &&
|
|
device.IsActive, ct);
|
|
}
|
|
|
|
public Task<List<AccessDevice>> GetAgentDevicesAsync(
|
|
Guid clientId,
|
|
Guid agentId,
|
|
IReadOnlyCollection<Guid> deviceIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDevices
|
|
.Where(device =>
|
|
device.FKClientId == clientId &&
|
|
device.FKAgentId == agentId &&
|
|
deviceIds.Contains(device.DeviceId))
|
|
.ToListAsync(ct);
|
|
}
|
|
public async Task<AccessDevice> GetDeviceByCId(Guid clientId)
|
|
{
|
|
AccessDevice? device = await _dbContext.AccessDevices
|
|
.Where(d => d.FKClientId == clientId && d.IsActive == true)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (device == null)
|
|
throw new NotFoundException($"{clientId} don't have a device in the system");
|
|
|
|
return device;
|
|
}
|
|
|
|
public async Task<CustomerAccess> AddCustomerAccessAsync(CustomerAccess customerAccess)
|
|
{
|
|
await _dbContext.CustomerAccess.AddAsync(customerAccess);
|
|
return customerAccess;
|
|
}
|
|
|
|
public async Task<CustomerAccess> GetByIdAsync(int deviceUserId)
|
|
{
|
|
CustomerAccess? record = await _dbContext.CustomerAccess
|
|
.FindAsync(deviceUserId);
|
|
|
|
if (record == null)
|
|
throw new NotFoundException($"{deviceUserId} cannot find in the system");
|
|
return record;
|
|
}
|
|
// Maps a device's employeeNo (== CustomerAccess.Id) back to its CustomerAccess,
|
|
// scoped to the device so the same employeeNo on a different panel can't collide.
|
|
// Returns null (rather than throwing) when there is no mapping for this device user.
|
|
public async Task<CustomerAccess?> GetByDeviceAndUserIdAsync(Guid deviceId, int deviceUserId)
|
|
{
|
|
return await _dbContext.CustomerAccess
|
|
.FirstOrDefaultAsync(c => c.FKDeviceId == deviceId && c.Id == deviceUserId);
|
|
}
|
|
|
|
public async Task<CustomerAccess> GetByCusGuidAsync(Guid CusId)
|
|
{
|
|
// No IsActive filter here: this is the lookup WhiteListMember/BlackListMember use
|
|
// to find the record whose IsActive flag they're about to flip. Filtering on
|
|
// IsActive == true made it impossible to ever find (and re-activate) a customer
|
|
// who had already been locked, since locking sets IsActive = false.
|
|
CustomerAccess? record = await _dbContext.CustomerAccess
|
|
.Where(c => c.FKCustomerId == CusId)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (record == null)
|
|
throw new NotFoundException($"record cannot find in the system");
|
|
return record;
|
|
}
|
|
|
|
public async Task<CustomerAccess> UpdateAsync(CustomerAccess customerAccess)
|
|
{
|
|
_dbContext.CustomerAccess.Update(customerAccess);
|
|
return customerAccess;
|
|
}
|
|
|
|
public async Task<List<AccessDevice>> GetDevicesByClientAsync(Guid clientId)
|
|
{
|
|
return await _dbContext.AccessDevices
|
|
.AsNoTracking()
|
|
.Where(d => d.FKClientId == clientId && d.IsActive == true)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<bool> DeviceBelongsToClientAsync(Guid deviceId, Guid clientId)
|
|
{
|
|
return await _dbContext.AccessDevices
|
|
.AnyAsync(d => d.DeviceId == deviceId && d.FKClientId == clientId);
|
|
}
|
|
|
|
public Task<Customer?> GetCustomerByIdAsync(
|
|
Guid customerId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _central.Customer
|
|
.FirstOrDefaultAsync(customer => customer.CusId == customerId, ct);
|
|
}
|
|
|
|
public async Task<Staff?> GetStaffByIdAsync(
|
|
Guid clientId,
|
|
Guid staffId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var staff = await _dbContext.Staff
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(s =>
|
|
s.StaffId == staffId &&
|
|
s.FKClientId == clientId, ct);
|
|
if (staff == null)
|
|
return null;
|
|
|
|
// Staff.User is central-bucket -- attach it in memory so callers reading
|
|
// staff.User.* still see the right data.
|
|
staff.User = await _central.Users.AsNoTracking().FirstOrDefaultAsync(u => u.UserId == staff.FKUserId, ct);
|
|
return staff;
|
|
}
|
|
|
|
// Access on a shared plan is all-or-nothing: the subscription is the unit that was
|
|
// paid for, so locking one person on a couple/group plan has to take their co-members
|
|
// with it, or the locked member simply walks in on their partner's fingerprint.
|
|
public async Task<List<Guid>> GetCoMembersOnSharedSubscriptionsAsync(
|
|
Guid clientId,
|
|
Guid customerId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var sharedSubscriptionIds = await _dbContext.SubscriptionMembers
|
|
.AsNoTracking()
|
|
.Where(member =>
|
|
member.FKCustomerId == customerId &&
|
|
member.IsActive &&
|
|
member.Subscription.IsActive &&
|
|
member.Subscription.CustomerPlan.FKClientId == clientId &&
|
|
member.Subscription.CustomerPlan.PlanScope == "Group")
|
|
.Select(member => member.FKSubscriptionId)
|
|
.Distinct()
|
|
.ToListAsync(ct);
|
|
|
|
if (sharedSubscriptionIds.Count == 0)
|
|
return new List<Guid>();
|
|
|
|
return await _dbContext.SubscriptionMembers
|
|
.AsNoTracking()
|
|
.Where(member =>
|
|
sharedSubscriptionIds.Contains(member.FKSubscriptionId) &&
|
|
member.FKCustomerId != customerId &&
|
|
member.IsActive)
|
|
.Select(member => member.FKCustomerId)
|
|
.Distinct()
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public Task<bool> IsActiveCustomerMemberAsync(
|
|
Guid clientId,
|
|
Guid customerId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.SubscriptionMembers.AnyAsync(member =>
|
|
member.FKCustomerId == customerId &&
|
|
member.IsActive &&
|
|
member.Subscription.IsActive &&
|
|
member.Subscription.CustomerPlan.FKClientId == clientId, ct);
|
|
}
|
|
|
|
public async Task<Guid?> GetLatestActiveClientIdForCustomerAsync(
|
|
Guid customerId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var clientId = await _dbContext.SubscriptionMembers
|
|
.Where(member =>
|
|
member.FKCustomerId == customerId &&
|
|
member.IsActive &&
|
|
member.Subscription.IsActive)
|
|
.OrderByDescending(member => member.Subscription.OnboardDate)
|
|
.Select(member => member.Subscription.CustomerPlan.FKClientId)
|
|
.FirstOrDefaultAsync(ct);
|
|
return clientId == Guid.Empty ? null : clientId;
|
|
}
|
|
|
|
public Task<AccessProfile?> GetAccessProfileAsync(
|
|
Guid clientId,
|
|
Guid userId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessProfiles
|
|
.Include(profile => profile.DeviceBindings)
|
|
.FirstOrDefaultAsync(profile =>
|
|
profile.FKClientId == clientId &&
|
|
profile.FKUserId == userId, ct);
|
|
}
|
|
|
|
public Task<AccessProfile?> GetAccessStatusProfileAsync(
|
|
Guid clientId,
|
|
Guid userId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessProfiles
|
|
.AsNoTracking()
|
|
.Include(profile => profile.DeviceBindings)
|
|
.ThenInclude(binding => binding.AccessDevice)
|
|
.FirstOrDefaultAsync(profile =>
|
|
profile.FKClientId == clientId &&
|
|
profile.FKUserId == userId, ct);
|
|
}
|
|
|
|
public Task<List<AccessProfile>> GetAccessProfilesByClientAsync(
|
|
Guid clientId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessProfiles
|
|
.Include(profile => profile.DeviceBindings)
|
|
.Where(profile => profile.FKClientId == clientId)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task AddAccessProfileAsync(
|
|
AccessProfile profile,
|
|
CancellationToken ct = default)
|
|
{
|
|
await _dbContext.AccessProfiles.AddAsync(profile, ct);
|
|
}
|
|
|
|
public Task<AccessDeviceBinding?> GetBindingAsync(
|
|
long accessProfileId,
|
|
Guid deviceId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDeviceBindings.FirstOrDefaultAsync(binding =>
|
|
binding.FKAccessProfileId == accessProfileId &&
|
|
binding.FKDeviceId == deviceId, ct);
|
|
}
|
|
|
|
public Task<AccessDeviceBinding?> GetBindingByEmployeeNoAsync(
|
|
Guid deviceId,
|
|
string employeeNo,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDeviceBindings
|
|
.Include(binding => binding.AccessProfile)
|
|
.FirstOrDefaultAsync(binding =>
|
|
binding.FKDeviceId == deviceId &&
|
|
binding.DeviceEmployeeNo == employeeNo, ct);
|
|
}
|
|
|
|
public Task<List<AccessDeviceBinding>> GetBindingsForDeviceAsync(
|
|
Guid deviceId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDeviceBindings
|
|
.Include(binding => binding.AccessProfile)
|
|
.Where(binding => binding.FKDeviceId == deviceId)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task AddBindingAsync(
|
|
AccessDeviceBinding binding,
|
|
CancellationToken ct = default)
|
|
{
|
|
await _dbContext.AccessDeviceBindings.AddAsync(binding, ct);
|
|
}
|
|
|
|
public Task<bool> IsDeviceEmployeeNoOccupiedAsync(
|
|
Guid deviceId,
|
|
string employeeNo,
|
|
Guid exceptUserId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDeviceBindings.AnyAsync(binding =>
|
|
binding.FKDeviceId == deviceId &&
|
|
binding.DeviceEmployeeNo == employeeNo &&
|
|
binding.AccessProfile.FKUserId != exceptUserId, ct);
|
|
}
|
|
|
|
public Task<List<UnlinkedDeviceEnrollment>> GetDuplicateUnlinkedEnrollmentsAsync(
|
|
Guid deviceId,
|
|
string employeeNo,
|
|
int deviceUserId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.UnlinkedDeviceEnrollments
|
|
.Where(enrollment =>
|
|
enrollment.FKDeviceId == deviceId &&
|
|
enrollment.LinkedAt == null &&
|
|
(enrollment.DeviceEmployeeNo == employeeNo ||
|
|
(enrollment.DeviceEmployeeNo == "" &&
|
|
enrollment.DeviceUserId == deviceUserId)))
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public Task<UnlinkedDeviceEnrollment?> GetUnlinkedEnrollmentByEmployeeNoAsync(
|
|
Guid deviceId,
|
|
string employeeNo,
|
|
CancellationToken ct = default)
|
|
{
|
|
var tracked = _dbContext.UnlinkedDeviceEnrollments.Local
|
|
.FirstOrDefault(enrollment =>
|
|
enrollment.FKDeviceId == deviceId &&
|
|
enrollment.DeviceEmployeeNo == employeeNo &&
|
|
enrollment.LinkedAt == null);
|
|
|
|
return tracked != null
|
|
? Task.FromResult<UnlinkedDeviceEnrollment?>(tracked)
|
|
: _dbContext.UnlinkedDeviceEnrollments.FirstOrDefaultAsync(enrollment =>
|
|
enrollment.FKDeviceId == deviceId &&
|
|
enrollment.DeviceEmployeeNo == employeeNo &&
|
|
enrollment.LinkedAt == null, ct);
|
|
}
|
|
|
|
public async Task AddUnlinkedEnrollmentAsync(
|
|
UnlinkedDeviceEnrollment enrollment,
|
|
CancellationToken ct = default)
|
|
{
|
|
await _dbContext.UnlinkedDeviceEnrollments.AddAsync(enrollment, ct);
|
|
}
|
|
|
|
public async Task<Guid?> GetCustomerIdForUserAtClientAsync(
|
|
Guid userId,
|
|
Guid clientId,
|
|
CancellationToken ct = default)
|
|
{
|
|
// Central lookup for the customer identity, then a tenant-scoped membership
|
|
// check -- these used to be a single cross-table query before the split.
|
|
var cusId = await _central.Customer
|
|
.Where(customer => customer.FKUserId == userId)
|
|
.Select(customer => (Guid?)customer.CusId)
|
|
.FirstOrDefaultAsync(ct);
|
|
if (cusId == null)
|
|
return null;
|
|
|
|
var belongs = await _dbContext.SubscriptionMembers.AnyAsync(member =>
|
|
member.FKCustomerId == cusId.Value &&
|
|
member.IsActive &&
|
|
member.Subscription.IsActive &&
|
|
member.Subscription.CustomerPlan.FKClientId == clientId, ct);
|
|
|
|
return belongs ? cusId : null;
|
|
}
|
|
|
|
public Task<bool> IsDeviceCommandableAsync(
|
|
Guid deviceId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return _dbContext.AccessDevices.AnyAsync(device =>
|
|
device.DeviceId == deviceId &&
|
|
device.IsActive &&
|
|
device.FKAgentId != null, ct);
|
|
}
|
|
|
|
public async Task UpsertUnlinkedEnrollmentAsync(Guid deviceId, int deviceUserId, string? reportedName)
|
|
{
|
|
// Check already-tracked-but-unsaved entities first: a single attendance-ingest
|
|
// batch can carry several events for the same not-yet-linked device user (e.g.
|
|
// multiple punches since the last sync), and SaveChanges only runs once after the
|
|
// whole batch. A DB-only lookup wouldn't see an earlier Add() in this same batch
|
|
// and would insert a duplicate row per repeated punch.
|
|
var existing = _dbContext.UnlinkedDeviceEnrollments.Local
|
|
.FirstOrDefault(e => e.FKDeviceId == deviceId && e.DeviceUserId == deviceUserId && e.LinkedAt == null)
|
|
?? await _dbContext.UnlinkedDeviceEnrollments
|
|
.FirstOrDefaultAsync(e => e.FKDeviceId == deviceId && e.DeviceUserId == deviceUserId && e.LinkedAt == null);
|
|
|
|
if (existing != null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(reportedName))
|
|
existing.DeviceReportedName = reportedName;
|
|
return;
|
|
}
|
|
|
|
await _dbContext.UnlinkedDeviceEnrollments.AddAsync(new UnlinkedDeviceEnrollment
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FKDeviceId = deviceId,
|
|
DeviceUserId = deviceUserId,
|
|
DeviceReportedName = reportedName,
|
|
DiscoveredAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
public async Task<List<UnlinkedDeviceEnrollment>> GetUnlinkedEnrollmentsByClientAsync(Guid clientId)
|
|
{
|
|
var enrollments = await _dbContext.UnlinkedDeviceEnrollments
|
|
.AsNoTracking()
|
|
.Include(e => e.AccessDevice)
|
|
.Where(e => e.LinkedAt == null && e.AccessDevice.FKClientId == clientId)
|
|
.OrderByDescending(e => e.DiscoveredAt)
|
|
.ToListAsync();
|
|
|
|
// Collapse any duplicate rows already sitting in the table (from before the
|
|
// upsert dedup fix above) so the same physical fingerprint doesn't show up
|
|
// multiple times in the link picker; keep the most recently discovered one.
|
|
return enrollments
|
|
.GroupBy(e => (
|
|
e.FKDeviceId,
|
|
EmployeeNo: string.IsNullOrWhiteSpace(e.DeviceEmployeeNo)
|
|
? e.DeviceUserId.ToString()
|
|
: e.DeviceEmployeeNo))
|
|
.Select(g => g.First())
|
|
.ToList();
|
|
}
|
|
|
|
public async Task<UnlinkedDeviceEnrollment?> GetUnlinkedEnrollmentByIdAsync(Guid id)
|
|
{
|
|
return await _dbContext.UnlinkedDeviceEnrollments
|
|
.Include(e => e.AccessDevice)
|
|
.FirstOrDefaultAsync(e => e.Id == id);
|
|
}
|
|
|
|
public async Task<bool> CustomerAccessIdExistsAsync(int id)
|
|
{
|
|
return await _dbContext.CustomerAccess.AnyAsync(c => c.Id == id);
|
|
}
|
|
}
|
|
}
|