first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
@@ -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
});
}
}
}