first commit
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Infra.Tenancy;
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Fitness_core_BE.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Platform-Admin-only visibility into every background job, and a manual "run it now"
|
||||
// trigger for each -- one home for this instead of a new controller per job. Each job's
|
||||
// actual algorithm stays in its own runner class (SubscriptionRenewalRunner,
|
||||
// OverdueLockRunner); this controller only reuses those, same as the hosted services do.
|
||||
// Same [CustomMiddleware][TenantResolution] gating as the rest of AdminController.
|
||||
[ApiController]
|
||||
[Route("api/admin/background-jobs")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public class AdminBackgroundJobsController : ControllerBase
|
||||
{
|
||||
private const string RenewalJobName = "SubscriptionRenewalJob";
|
||||
|
||||
private readonly TenantSchemaIterator _iterator;
|
||||
private readonly SubscriptionRenewalRunner _renewalRunner;
|
||||
private readonly OverdueLockRunner _overdueLockRunner;
|
||||
private readonly IJobRunHistory _history;
|
||||
private readonly IOverdueLockRepository _overdueLockRepository;
|
||||
// Normal DI-resolved CentralDbContext is fine here (unlike in a background job) --
|
||||
// this runs inside a real HTTP request, and central is one fixed database regardless
|
||||
// of tenant, so there's no per-request tenant resolution involved either way.
|
||||
private readonly CentralDbContext _central;
|
||||
|
||||
public AdminBackgroundJobsController(
|
||||
TenantSchemaIterator iterator,
|
||||
SubscriptionRenewalRunner renewalRunner,
|
||||
OverdueLockRunner overdueLockRunner,
|
||||
IJobRunHistory history,
|
||||
IOverdueLockRepository overdueLockRepository,
|
||||
CentralDbContext central)
|
||||
{
|
||||
_iterator = iterator;
|
||||
_renewalRunner = renewalRunner;
|
||||
_overdueLockRunner = overdueLockRunner;
|
||||
_history = history;
|
||||
_overdueLockRepository = overdueLockRepository;
|
||||
_central = central;
|
||||
}
|
||||
|
||||
[HttpGet("renewal/runs")]
|
||||
public async Task<ActionResult<ApiResponse<IReadOnlyList<JobRunRecord>>>> GetRenewalRuns(
|
||||
[FromQuery] int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
return Response(await _history.GetRecentAsync(_central, RenewalJobName, take, ct));
|
||||
}
|
||||
|
||||
// Runs a full sweep across every provisioned tenant schema immediately, rather than
|
||||
// waiting for the next daily (01:00 Sri Lanka time) tick. Uses the exact same
|
||||
// SubscriptionRenewalRunner the hosted service itself calls -- no separate/duplicated
|
||||
// renewal logic. Optional clientId scopes the sweep to one gym (e.g. re-running just for
|
||||
// a client that failed, or testing against one tenant without touching every other gym).
|
||||
[HttpPost("renewal/trigger")]
|
||||
public async Task<ActionResult<ApiResponse<List<SubscriptionRenewalRunResult>>>> TriggerRenewalNow(
|
||||
[FromQuery] Guid? clientId, CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var results = new List<SubscriptionRenewalRunResult>();
|
||||
|
||||
var targets = clientId.HasValue
|
||||
? SingleClientAsync(clientId.Value, ct)
|
||||
: _iterator.GetProvisionedRegistrationsAsync(ct);
|
||||
|
||||
await foreach (var (targetClientId, schemaName) in targets)
|
||||
{
|
||||
TenantSchemaContext? schemaCtx = null;
|
||||
try
|
||||
{
|
||||
schemaCtx = _iterator.OpenSchema(targetClientId, schemaName);
|
||||
var result = await _renewalRunner.RunForSchemaAsync(schemaCtx, now, ct);
|
||||
results.Add(result);
|
||||
|
||||
await _history.RecordAsync(schemaCtx.Central, new JobRunRecord(
|
||||
RenewalJobName, targetClientId, now,
|
||||
result.Failed > 0 ? "PartialFailure" : "Success",
|
||||
result.Renewed,
|
||||
result.Failed > 0 ? string.Join("; ", result.Notes) : null), ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add(new SubscriptionRenewalRunResult(targetClientId, 0, 0, 1, new[] { ex.Message }));
|
||||
schemaCtx ??= _iterator.OpenSchema(targetClientId, schemaName);
|
||||
await _history.RecordAsync(schemaCtx.Central, new JobRunRecord(RenewalJobName, targetClientId, now, "Failed", 0, ex.Message), ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (schemaCtx != null)
|
||||
await schemaCtx.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return Response(results);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<(Guid ClientId, string SchemaName)> SingleClientAsync(
|
||||
Guid clientId, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
await using var schemaCtx = await _iterator.OpenSchemaForClientAsync(clientId, ct);
|
||||
if (schemaCtx == null)
|
||||
yield break;
|
||||
yield return (schemaCtx.ClientId, schemaCtx.SchemaName);
|
||||
}
|
||||
|
||||
// Runs the overdue-lock sweep across every client with a lock policy immediately, rather
|
||||
// than waiting for the next 6-hour tick. Uses the exact same OverdueLockRunner the
|
||||
// hosted service itself calls. No run-history endpoint yet -- OverdueLockJob doesn't
|
||||
// record to IJobRunHistory today; that's a separate follow-up if it's ever needed.
|
||||
[HttpPost("overdue-lock/trigger")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> TriggerOverdueLockNow(CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var clients = await _overdueLockRepository.GetClientsWithOverdueLockPolicyAsync(ct);
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var client in clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _overdueLockRunner.RunForClientAsync(client, now, _iterator, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add($"{client.ClientId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return Response<object>(new
|
||||
{
|
||||
clientsChecked = clients.Count,
|
||||
failed = failures.Count,
|
||||
failures
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user