using Fitness_core_BE.Infra.Tenancy; using Fitness_core_BE.Interfaces; namespace Fitness_core_BE.Services { /// /// Periodically locks (and un-locks) member fingerprint access based on each client's /// configurable Client.LockAfterDaysOverdue setting (req 7). Reuses the existing /// AccessDeviceService.LockCustomer/UnlockCustomer (BlackListMember/WhiteListMember) /// pipeline — this job only decides *who* should be locked, not *how*. /// /// This is a background service with no HttpContext, so TenantConnectionResolver's normal /// per-request tenant resolution can never apply here -- a DI-scoped TenantDbContext always /// resolves to the same single fallback schema regardless of which client is being checked. /// Every client's overdue-check therefore opens its OWN schema directly via /// TenantSchemaIterator instead of using the scope's injected TenantDbContext. /// /// The per-client algorithm itself lives in OverdueLockRunner so /// AdminBackgroundJobsController's manual "trigger now" endpoint can call the exact same /// code path instead of duplicating it -- this class only owns the 6-hour loop and the /// central-only client lookup. /// public class OverdueLockJob : BackgroundService { private static readonly TimeSpan Interval = TimeSpan.FromHours(6); private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public OverdueLockJob(IServiceScopeFactory scopeFactory, ILogger logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await RunOnceAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "OverdueLockJob run failed."); } try { await Task.Delay(Interval, stoppingToken); } catch (TaskCanceledException) { // Shutting down. } } } private async Task RunOnceAsync(CancellationToken ct) { using var scope = _scopeFactory.CreateScope(); var iterator = scope.ServiceProvider.GetRequiredService(); var runner = scope.ServiceProvider.GetRequiredService(); var now = DateTime.UtcNow; // Central-only lookup -- safe to use the scope's normal DI-resolved repository, // since GetClientsWithOverdueLockPolicyAsync only ever touches CentralDbContext. var bootstrapRepo = scope.ServiceProvider.GetRequiredService(); var clients = await bootstrapRepo.GetClientsWithOverdueLockPolicyAsync(ct); foreach (var client in clients) { try { await runner.RunForClientAsync(client, now, iterator, ct); } catch (Exception ex) { _logger.LogError(ex, "OverdueLockJob failed for client {ClientId}.", client.ClientId); } } } } }