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 AddDeviceAsync(AccessDevice device) { var result = await _dbContext.AddAsync(device); return result; } public async Task 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 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 GetDeviceByIdAsync( Guid deviceId, CancellationToken ct = default) { return _dbContext.AccessDevices .FirstOrDefaultAsync(device => device.DeviceId == deviceId, ct); } public Task GetDeviceByHardwareIdAsync( Guid clientId, string hardwareId, CancellationToken ct = default) { return _dbContext.AccessDevices.FirstOrDefaultAsync(device => device.FKClientId == clientId && device.HardwareId == hardwareId, ct); } public Task GetActiveDeviceForClientAsync( Guid clientId, Guid deviceId, CancellationToken ct = default) { return _dbContext.AccessDevices.FirstOrDefaultAsync(device => device.DeviceId == deviceId && device.FKClientId == clientId && device.IsActive, ct); } public Task> GetAgentDevicesAsync( Guid clientId, Guid agentId, IReadOnlyCollection deviceIds, CancellationToken ct = default) { return _dbContext.AccessDevices .Where(device => device.FKClientId == clientId && device.FKAgentId == agentId && deviceIds.Contains(device.DeviceId)) .ToListAsync(ct); } public async Task 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 AddCustomerAccessAsync(CustomerAccess customerAccess) { await _dbContext.CustomerAccess.AddAsync(customerAccess); return customerAccess; } public async Task 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 GetByDeviceAndUserIdAsync(Guid deviceId, int deviceUserId) { return await _dbContext.CustomerAccess .FirstOrDefaultAsync(c => c.FKDeviceId == deviceId && c.Id == deviceUserId); } public async Task 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 UpdateAsync(CustomerAccess customerAccess) { _dbContext.CustomerAccess.Update(customerAccess); return customerAccess; } public async Task> GetDevicesByClientAsync(Guid clientId) { return await _dbContext.AccessDevices .AsNoTracking() .Where(d => d.FKClientId == clientId && d.IsActive == true) .ToListAsync(); } public async Task DeviceBelongsToClientAsync(Guid deviceId, Guid clientId) { return await _dbContext.AccessDevices .AnyAsync(d => d.DeviceId == deviceId && d.FKClientId == clientId); } public Task GetCustomerByIdAsync( Guid customerId, CancellationToken ct = default) { return _central.Customer .FirstOrDefaultAsync(customer => customer.CusId == customerId, ct); } public async Task 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> 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(); 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 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 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 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 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> 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 GetBindingAsync( long accessProfileId, Guid deviceId, CancellationToken ct = default) { return _dbContext.AccessDeviceBindings.FirstOrDefaultAsync(binding => binding.FKAccessProfileId == accessProfileId && binding.FKDeviceId == deviceId, ct); } public Task 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> 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 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> 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 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(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 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 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> 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 GetUnlinkedEnrollmentByIdAsync(Guid id) { return await _dbContext.UnlinkedDeviceEnrollments .Include(e => e.AccessDevice) .FirstOrDefaultAsync(e => e.Id == id); } public async Task CustomerAccessIdExistsAsync(int id) { return await _dbContext.CustomerAccess.AnyAsync(c => c.Id == id); } } }