first commit
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Fitness_core_BE.Services.FunctionHandler;
|
||||
using Fitness_core_BE.Services.Document;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.Text.Json;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using Fitness_core_BE.DTOs;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Main API Controller for handling various operations
|
||||
/// </summary>
|
||||
[Route("api")]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[EnableCors("AllowAll")]
|
||||
public class APIController : ControllerBase
|
||||
{
|
||||
private readonly TicketFuncHandler _ticketFuncHandler;
|
||||
private readonly ActivityLogFuncHandler _activityLogFuncHandler;
|
||||
private readonly ClientFuncHandler _clientFuncHandler;
|
||||
private readonly StaffFunctionHandler _staffFuncHandler;
|
||||
private readonly ClientInvoiceFunctionHandler _clientInvoiceFuncHandler;
|
||||
private readonly CustomerFunctionHandler _customerFuncHandler;
|
||||
private readonly CustomerInvoiceFunctionHandler _customerInvoiceFuncHandler;
|
||||
private readonly CustomerPlanFunctionHandler _customerPlanFuncHandler;
|
||||
private readonly CustomerSubscriptionFunctionHandler _customerSubscriptionFuncHandler;
|
||||
private readonly MealFunctionHandler _mealFuncHandler;
|
||||
private readonly AuthFuncHandler _authFuncHandler;
|
||||
private readonly WorkoutFunctionHandler _workoutFuncHandler;
|
||||
private readonly DocumentFunctionHandler _documentFuncHandler;
|
||||
private readonly ChatFunctionHandler _chatFuncHandler;
|
||||
private readonly FeedFuncHandler _feedFuncHandler;
|
||||
private readonly AccessDeviceFunctionHandler _accessDeviceFuncHandler;
|
||||
private readonly AttendanceFunctionHandler _attendanceFuncHandler;
|
||||
private readonly DocumentFunctionHandler _docHandler;
|
||||
private readonly IDocumentService _docService;
|
||||
private readonly UserRegisterHandler _userRegisterHandler;
|
||||
private readonly ClientStatFuncHandler _clientStatFuncHandler;
|
||||
private readonly PaymentFunctionHandler _paymentFuncHandler;
|
||||
|
||||
public APIController(
|
||||
TicketFuncHandler ticketFuncHandler,
|
||||
ActivityLogFuncHandler activityLogFuncHandler,
|
||||
ClientFuncHandler clientFuncHandler,
|
||||
AuthFuncHandler authFuncHandler,
|
||||
StaffFunctionHandler staffFuncHandler,
|
||||
ClientInvoiceFunctionHandler clientInvoiceFuncHandler,
|
||||
CustomerFunctionHandler customerFuncHandler,
|
||||
CustomerInvoiceFunctionHandler customerInvoiceFuncHandler,
|
||||
CustomerPlanFunctionHandler customerPlanFuncHandler,
|
||||
CustomerSubscriptionFunctionHandler customerSubscriptionFuncHandler,
|
||||
MealFunctionHandler mealFuncHandler,
|
||||
WorkoutFunctionHandler workoutFuncHandler,
|
||||
DocumentFunctionHandler documentFuncHandler,
|
||||
FeedFuncHandler feedFuncHandler,
|
||||
AccessDeviceFunctionHandler accessDeviceFuncHandler,
|
||||
AttendanceFunctionHandler attendanceFuncHandler,
|
||||
ChatFunctionHandler chatFunctionHandler,
|
||||
DocumentFunctionHandler docHandler,
|
||||
IDocumentService docService,
|
||||
UserRegisterHandler userRegisterHandler,
|
||||
ClientStatFuncHandler clientStatFuncHandler,
|
||||
PaymentFunctionHandler paymentFuncHandler)
|
||||
{
|
||||
_ticketFuncHandler = ticketFuncHandler;
|
||||
_activityLogFuncHandler = activityLogFuncHandler;
|
||||
_clientFuncHandler = clientFuncHandler;
|
||||
_staffFuncHandler = staffFuncHandler;
|
||||
_clientInvoiceFuncHandler = clientInvoiceFuncHandler;
|
||||
_customerFuncHandler = customerFuncHandler;
|
||||
_customerInvoiceFuncHandler = customerInvoiceFuncHandler;
|
||||
_customerPlanFuncHandler = customerPlanFuncHandler;
|
||||
_customerSubscriptionFuncHandler = customerSubscriptionFuncHandler;
|
||||
_mealFuncHandler = mealFuncHandler;
|
||||
_authFuncHandler = authFuncHandler;
|
||||
_workoutFuncHandler = workoutFuncHandler;
|
||||
_documentFuncHandler = documentFuncHandler;
|
||||
_feedFuncHandler = feedFuncHandler;
|
||||
_accessDeviceFuncHandler = accessDeviceFuncHandler;
|
||||
_attendanceFuncHandler = attendanceFuncHandler;
|
||||
_chatFuncHandler = chatFunctionHandler;
|
||||
_docHandler = docHandler;
|
||||
_docService = docService;
|
||||
_userRegisterHandler = userRegisterHandler;
|
||||
_clientStatFuncHandler = clientStatFuncHandler;
|
||||
_paymentFuncHandler = paymentFuncHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check API health status
|
||||
/// </summary>
|
||||
/// <returns>API status and current timestamp</returns>
|
||||
/// <response code="200">Returns API status</response>
|
||||
[HttpGet("status")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IActionResult GetStatus()
|
||||
{
|
||||
return Ok(new { status = "API is running", timestamp = System.DateTime.UtcNow });
|
||||
}
|
||||
|
||||
[HttpPost("ticket")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteTicket([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _ticketFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("activitylog")]
|
||||
public async Task<IActionResult> ExecuteActivityLog([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _activityLogFuncHandler.ExecuteAsync(request.FunctionName, request.Payload, HttpContext);
|
||||
return Ok(new ApiResponse<object>
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "Success",
|
||||
Data = response
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("client")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteClient([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _clientFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("auth")]
|
||||
public async Task<IActionResult> ExecuteAuth([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _authFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("VerifyOTP")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ForgotPassword([FromBody] ApiRequest request)
|
||||
{
|
||||
request.FunctionName = "VerifyOTP";
|
||||
var response = await _authFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("refreshToken")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RefreshToken([FromBody] ApiRequest request)
|
||||
{
|
||||
request.FunctionName = "RefreshToken";
|
||||
var response = await _authFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
// Anonymous so logout still clears cookies even if the access token has expired.
|
||||
[HttpPost("logout")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout([FromBody] ApiRequest? request)
|
||||
{
|
||||
var logoutRequest = request ?? new ApiRequest();
|
||||
logoutRequest.FunctionName = "LogOut";
|
||||
var response = await _authFuncHandler.Execute(logoutRequest, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("staff")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteStaff([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _staffFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("clientinvoice")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteClientInvoice([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _clientInvoiceFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("customer")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteCustomer([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _customerFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("customerinvoice")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteCustomerInvoice([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _customerInvoiceFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("customerplan")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteCustomerPlan([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _customerPlanFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("customersubscription")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteCustomerSubscription([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _customerSubscriptionFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("meal")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteMeal([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _mealFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("workout")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteWorkout([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _workoutFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("document")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteDocument([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _documentFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("feed")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteFeed([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _feedFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("accessdevice")]
|
||||
public async Task<IActionResult> ExecuteAccessDevice([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _accessDeviceFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("attendance")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteAttendance([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _attendanceFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("chat")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteChat([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _chatFuncHandler.Execute(request);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
[HttpPost("initChat")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> InitChat([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _chatFuncHandler.Execute(request);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("payment")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecutePayment([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _paymentFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("clientstat")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteClientStat([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _clientStatFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> ExecuteRegsiter([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _userRegisterHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("docService/{docId}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetDocument(string docId)
|
||||
{
|
||||
if (!int.TryParse(docId, out int documentId))
|
||||
return BadRequest("Invalid Document ID format.");
|
||||
|
||||
|
||||
DocumentDownloadModel result = await _docService.GetFileBytesById(documentId);
|
||||
|
||||
if (result == null)
|
||||
return NotFound("Document not found");
|
||||
|
||||
try
|
||||
{
|
||||
byte[] fileBytes = result.FileData;
|
||||
string fileName = result.FileName;
|
||||
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
if (!provider.TryGetContentType(fileName, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
return File(fileBytes, contentType, fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest($"Error processing document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Fitness_core_BE.DTOs.PlatformBilling;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Platform billing (HexaFitness -> gym): pricing configuration and invoicing.
|
||||
// The gym-owner side of this same concern lives in ClientInvoiceController.
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public class AdminBillingController : ControllerBase
|
||||
{
|
||||
private readonly IPlatformBillingService _platformBilling;
|
||||
|
||||
public AdminBillingController(IPlatformBillingService platformBilling)
|
||||
{
|
||||
_platformBilling = platformBilling;
|
||||
}
|
||||
|
||||
[HttpGet("pricing-plans")]
|
||||
public async Task<ActionResult<ApiResponse<List<PricingPlanDto>>>> ListPricingPlans()
|
||||
{
|
||||
return Response(await _platformBilling.GetPricingPlansAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPut("pricing-plans")]
|
||||
public async Task<ActionResult<ApiResponse<PricingPlanDto>>> UpsertPricingPlan([FromBody] UpsertPricingPlanRequest request)
|
||||
{
|
||||
return Response(await _platformBilling.UpsertPricingPlanAsync(request, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpGet("device-pricing")]
|
||||
public async Task<ActionResult<ApiResponse<DevicePricingDto>>> GetDevicePricing()
|
||||
{
|
||||
return Response(await _platformBilling.GetDevicePricingAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPut("device-pricing")]
|
||||
public async Task<ActionResult<ApiResponse<DevicePricingDto>>> UpdateDevicePricing([FromBody] DevicePricingDto request)
|
||||
{
|
||||
return Response(await _platformBilling.UpdateDevicePricingAsync(request, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPost("clients/{id:guid}/pricing-plan")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> AssignPricingPlan(Guid id, [FromBody] AssignPricingPlanRequest request)
|
||||
{
|
||||
var invoice = await _platformBilling.AssignPricingPlanAsync(id, request, HttpContext.RequestAborted);
|
||||
return Response<object>(new { assigned = true, clientInvoiceId = invoice.ClientInvoiceId, amount = invoice.Amount });
|
||||
}
|
||||
|
||||
[HttpGet("clients/{id:guid}/platform-fee")]
|
||||
public async Task<ActionResult<ApiResponse<PlatformFeeBreakdownDto>>> GetPlatformFee(Guid id)
|
||||
{
|
||||
return Response(await _platformBilling.ComputeMonthlyFeeAsync(id, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpGet("invoices/pending")]
|
||||
public async Task<ActionResult<ApiResponse<List<PendingPlatformInvoiceDto>>>> ListPendingInvoices()
|
||||
{
|
||||
return Response(await _platformBilling.GetPendingInvoicesAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpGet("invoices/completed")]
|
||||
public async Task<ActionResult<ApiResponse<List<PendingPlatformInvoiceDto>>>> ListCompletedInvoices()
|
||||
{
|
||||
return Response(await _platformBilling.GetCompletedInvoicesAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{id:guid}/payments")]
|
||||
public async Task<ActionResult<ApiResponse<PendingPlatformInvoiceDto>>> RecordInvoicePayment(Guid id, [FromBody] RecordClientInvoicePaymentRequest request)
|
||||
{
|
||||
return Response(await _platformBilling.RecordClientInvoicePaymentAsync(id, request.Amount, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{id:guid}/send-payment-link")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendInvoicePaymentLink(Guid id)
|
||||
{
|
||||
return Response(await _platformBilling.SendClientPaymentLinkAsync(id, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPost("billing/run-monthly")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RunMonthlyBilling()
|
||||
{
|
||||
var invoices = await _platformBilling.RunMonthlyBillingForAllClientsAsync(HttpContext.RequestAborted);
|
||||
return Response<object>(new { generated = invoices.Count });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Fitness_core_BE.DTOs.PaymentsEnablement;
|
||||
using Fitness_core_BE.Exceptions;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IPayoutKycReviewService _kycReview;
|
||||
private readonly IAdminClientDirectoryService _clientDirectory;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
private readonly IAdminSuggestionService _suggestions;
|
||||
|
||||
public AdminController(
|
||||
IPayoutKycReviewService kycReview,
|
||||
IAdminClientDirectoryService clientDirectory,
|
||||
UserRoleHelper userRoleHelper,
|
||||
IAdminSuggestionService suggestions)
|
||||
{
|
||||
_kycReview = kycReview;
|
||||
_clientDirectory = clientDirectory;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
_suggestions = suggestions;
|
||||
}
|
||||
|
||||
[HttpGet("payout-kyc")]
|
||||
public async Task<ActionResult<ApiResponse<List<ClientPayoutKycDto>>>> ListKyc([FromQuery] string? status)
|
||||
{
|
||||
return Response(await _kycReview.ListAsync(status));
|
||||
}
|
||||
|
||||
[HttpGet("payout-kyc/{clientId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<ClientPayoutKycDto>>> GetKyc(Guid clientId)
|
||||
{
|
||||
return Response(await _kycReview.GetByClientIdAsync(clientId));
|
||||
}
|
||||
|
||||
[HttpPatch("payout-kyc/{clientId:guid}/status")]
|
||||
public async Task<ActionResult<ApiResponse<ClientPayoutKycDto>>> ReviewKyc(Guid clientId, [FromBody] ReviewPayoutKycRequest request)
|
||||
{
|
||||
return Response(await _kycReview.ReviewAsync(clientId, request, HttpContext));
|
||||
}
|
||||
|
||||
[HttpGet("clients")]
|
||||
public async Task<ActionResult<ApiResponse<PagedResult<ClientSummary>>>> ListClients([FromQuery] ClientDirectoryQuery query)
|
||||
{
|
||||
return Response(await _clientDirectory.ListAsync(query));
|
||||
}
|
||||
|
||||
[HttpGet("clients/{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetClient(Guid id)
|
||||
{
|
||||
return Response(await _clientDirectory.GetByIdAsync(id));
|
||||
}
|
||||
|
||||
[HttpPost("clients/{id:guid}/pairing-code")]
|
||||
public async Task<ActionResult<ApiResponse<PairingCodeResult>>> GeneratePairingCode(Guid id)
|
||||
{
|
||||
return Response(await _clientDirectory.GeneratePairingCodeAsync(id));
|
||||
}
|
||||
|
||||
[HttpPost("clients/{id:guid}/resend-welcome-email")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ResendWelcomeEmail(Guid id)
|
||||
{
|
||||
await _clientDirectory.ResendWelcomeEmailAsync(id, HttpContext);
|
||||
return Response<object>(new { sent = true });
|
||||
}
|
||||
|
||||
[HttpGet("clients/{id:guid}/access-devices")]
|
||||
public async Task<ActionResult<ApiResponse<AdminClientDevicesResult>>> GetAccessDevices(Guid id)
|
||||
{
|
||||
return Response(await _clientDirectory.GetDevicesAsync(id));
|
||||
}
|
||||
|
||||
// Platform plans are global templates (no FKClientId). Mobile customers see them
|
||||
// in Suggestions in addition to templates created by their own gym.
|
||||
[HttpGet("suggestions")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ListSuggestions()
|
||||
=> Response(await _suggestions.ListAsync());
|
||||
|
||||
[HttpPost("suggestions/workouts")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateWorkoutSuggestion([FromBody] AdminWorkoutSuggestionRequest request)
|
||||
=> Response(await _suggestions.CreateWorkoutAsync(request, HttpContext));
|
||||
|
||||
[HttpPost("suggestions/meals")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateMealSuggestion([FromBody] AdminMealSuggestionRequest request)
|
||||
=> Response(await _suggestions.CreateMealAsync(request, HttpContext));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Fitness_core_BE.DTOs.PaymentGateways;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Payment gateway catalog (proxied to Payment-Service; see PaymentServiceClient).
|
||||
// This is platform-admin's side of gateway registration — the gym-owner side
|
||||
// (payout track/KYC/wallet enablement) lives in PaymentsEnablementController.
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public class AdminPaymentGatewayController : ControllerBase
|
||||
{
|
||||
private readonly IPaymentServiceClient _paymentServiceClient;
|
||||
|
||||
public AdminPaymentGatewayController(IPaymentServiceClient paymentServiceClient)
|
||||
{
|
||||
_paymentServiceClient = paymentServiceClient;
|
||||
}
|
||||
|
||||
[HttpGet("payment-gateways")]
|
||||
public async Task<ActionResult<ApiResponse<List<GatewayDefinitionDto>>>> ListPaymentGateways()
|
||||
{
|
||||
return Response(await _paymentServiceClient.ListGatewaysAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPost("payment-gateways")]
|
||||
public async Task<ActionResult<ApiResponse<GatewayDefinitionDto>>> UpsertPaymentGateway([FromBody] UpsertGatewayDefinitionRequest request)
|
||||
{
|
||||
return Response(await _paymentServiceClient.UpsertGatewayAsync(request, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPut("payment-gateways/{id:guid}/enabled")]
|
||||
public async Task<ActionResult<ApiResponse<GatewayDefinitionDto>>> SetPaymentGatewayEnabled(Guid id, [FromBody] SetGatewayEnabledRequest request)
|
||||
{
|
||||
return Response(await _paymentServiceClient.SetGatewayEnabledAsync(id, request.IsEnabled, HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpPut("payment-gateways/platforms/{platformId:guid}/credentials/{gatewayName}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpsertPlatformGatewayCredential(
|
||||
Guid platformId, string gatewayName, [FromBody] UpsertPlatformGatewayCredentialRequest request)
|
||||
{
|
||||
request.PlatformId = platformId;
|
||||
request.GatewayName = gatewayName;
|
||||
await _paymentServiceClient.UpsertPlatformCredentialAsync(request, HttpContext.RequestAborted);
|
||||
return Response<object>(new { saved = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Fitness_core_BE.Auth;
|
||||
using Fitness_core_BE.DTOs.Agent;
|
||||
using Fitness_core_BE.Exceptions;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Endpoints consumed by the on-prem desktop agent. The agent talks to the Hikvision
|
||||
/// device on the gym LAN and to us over outbound HTTPS using its X-Api-Key.
|
||||
/// All endpoints except enrollment require the "AgentApiKey" policy; the agent's client
|
||||
/// is always resolved server-side from the authenticated key, never from the request body.
|
||||
/// </summary>
|
||||
[Route("api/agent")]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[EnableCors("AllowAll")]
|
||||
public class AgentController : ControllerBase
|
||||
{
|
||||
private readonly IAgentService _agentService;
|
||||
private readonly ITimeBankCardService _timeBankCardService;
|
||||
private readonly ILogger<AgentController> _logger;
|
||||
|
||||
public AgentController(IAgentService agentService, ITimeBankCardService timeBankCardService, ILogger<AgentController> logger)
|
||||
{
|
||||
_agentService = agentService;
|
||||
_timeBankCardService = timeBankCardService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Bootstrap: no api-key yet. Exchanges a pairing code for a new agent + api key.
|
||||
[HttpPost("enroll")]
|
||||
[AllowAnonymous]
|
||||
public Task<IActionResult> Enroll([FromBody] EnrollRequest request)
|
||||
=> Execute(() => _agentService.EnrollAsync(request));
|
||||
|
||||
[HttpPost("devices")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> RegisterDevice([FromBody] RegisterDeviceRequest request)
|
||||
=> Execute(() => _agentService.RegisterDeviceAsync(GetClientId(), GetAgentId(), request));
|
||||
|
||||
[HttpPost("device-inventory")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> ReconcileDeviceInventory([FromBody] DeviceInventoryRequest request)
|
||||
=> Execute(() => _agentService.ReconcileDeviceInventoryAsync(
|
||||
GetClientId(), GetAgentId(), request));
|
||||
|
||||
[HttpPost("attendance")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> Attendance([FromBody] AgentAttendanceRequest request)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Attendance endpoint hit: device {DeviceId}, {EventCount} events in body.",
|
||||
request?.DeviceId, request?.Events?.Count ?? 0);
|
||||
return Execute(() => _agentService.IngestAttendanceAsync(
|
||||
GetClientId(), GetAgentId(), request));
|
||||
}
|
||||
|
||||
[HttpGet("commands")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> GetCommands()
|
||||
=> Execute(() => _agentService.GetPendingCommandsAsync(GetClientId(), GetAgentId()));
|
||||
|
||||
[HttpPost("commands/{id}/result")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> SubmitCommandResult(Guid id, [FromBody] CommandResultRequest request)
|
||||
=> Execute(() => _agentService.SubmitCommandResultAsync(
|
||||
GetClientId(), GetAgentId(), id, request));
|
||||
|
||||
// Called by an RFID-reader-equipped agent station on a card swipe -- same
|
||||
// "agent resolves the club from its own API key" pattern as attendance/commands, so a
|
||||
// reader never needs staff credentials of its own. facilityId identifies which area the
|
||||
// reader is stationed at. First swipe on a card taps it in (starts the clock); the next
|
||||
// swipe taps it out and bills the elapsed time -- see TimeBankCardService.Swipe.
|
||||
[HttpPost("time-bank-cards/redeem")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public Task<IActionResult> RedeemTimeBankCard([FromBody] AgentSwipeTimeBankCardRequest request)
|
||||
=> Execute(() => _timeBankCardService.Swipe(new
|
||||
{
|
||||
rfidTagId = request.RfidTagId,
|
||||
facilityId = request.FacilityId
|
||||
}));
|
||||
|
||||
[HttpPost("heartbeat")]
|
||||
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
|
||||
public async Task<IActionResult> Heartbeat()
|
||||
{
|
||||
// Accept an empty body from older agents during a rolling deployment.
|
||||
var request = Request.HasJsonContentType()
|
||||
? await Request.ReadFromJsonAsync<AgentHeartbeatRequest>()
|
||||
: null;
|
||||
return await Execute(() => _agentService.HeartbeatAsync(
|
||||
GetClientId(), GetAgentId(), request ?? new AgentHeartbeatRequest()));
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private Guid GetClientId()
|
||||
{
|
||||
var value = User.FindFirst(AgentApiKeyDefaults.ClientIdClaim)?.Value;
|
||||
if (string.IsNullOrWhiteSpace(value) || !Guid.TryParse(value, out var clientId))
|
||||
throw new UnauthorizedException("Agent is not authenticated.");
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private Guid GetAgentId()
|
||||
{
|
||||
var value = User.FindFirst(AgentApiKeyDefaults.AgentIdClaim)?.Value;
|
||||
if (string.IsNullOrWhiteSpace(value) || !Guid.TryParse(value, out var agentId))
|
||||
throw new UnauthorizedException("Agent is not authenticated.");
|
||||
return agentId;
|
||||
}
|
||||
|
||||
// Mirrors the function-handler exception->ApiResponse mapping used by admin flows.
|
||||
private async Task<IActionResult> Execute(Func<Task<object>> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await action();
|
||||
return StatusCode(200, new ApiResponse<object>
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "success",
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedException ex)
|
||||
{
|
||||
return StatusCode(401, new ApiResponse<object> { StatusCode = 401, Message = ex.Message, Data = null! });
|
||||
}
|
||||
catch (NotFoundException ex)
|
||||
{
|
||||
return StatusCode(404, new ApiResponse<object> { StatusCode = 404, Message = ex.Message, Data = null! });
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
return StatusCode(400, new ApiResponse<object> { StatusCode = 400, Message = ex.Message, Data = null! });
|
||||
}
|
||||
catch (ForbiddenException ex)
|
||||
{
|
||||
return StatusCode(403, new ApiResponse<object> { StatusCode = 403, Message = ex.Message, Data = null! });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new ApiResponse<object> { StatusCode = 500, Message = $"An error occurred: {ex.Message}", Data = null! });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Fitness_core_BE.DTOs.Booking;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Booking spans three audiences with very different auth needs, so routes are absolute
|
||||
// per-action rather than grouped under one controller-level [Route] prefix: "api/app/..."
|
||||
// for logged-in members (same convention as CustomerController), "api/public/..." for a
|
||||
// non-member's anonymous guest booking, "api/client/..." for staff (check-in, cancel,
|
||||
// session roster, manual payment recording).
|
||||
[ApiController]
|
||||
public class BookingController : ControllerBase
|
||||
{
|
||||
private readonly IBookingService _bookingService;
|
||||
|
||||
public BookingController(IBookingService bookingService)
|
||||
{
|
||||
_bookingService = bookingService;
|
||||
}
|
||||
|
||||
// ---------- Member (logged-in) ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("api/app/bookings")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateMemberBooking([FromBody] CreateMemberBookingRequest request)
|
||||
=> Response(await _bookingService.CreateMemberBooking(new { facilitySessionId = request.FacilitySessionId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("api/app/bookings/mine")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyBookings()
|
||||
=> Response(await _bookingService.GetMyBookings(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("api/app/bookings/{id:guid}/cancel")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CancelMyBooking(Guid id)
|
||||
=> Response(await _bookingService.CancelBooking(new { bookingId = id }, HttpContext));
|
||||
|
||||
// ---------- Guest (no account at all) ----------
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("api/public/bookings/guest")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateGuestBooking([FromBody] CreateGuestBookingRequest request)
|
||||
=> Response(await _bookingService.CreateGuestBooking(new
|
||||
{
|
||||
facilitySessionId = request.FacilitySessionId,
|
||||
guestName = request.GuestName,
|
||||
guestPhone = request.GuestPhone
|
||||
}, HttpContext));
|
||||
|
||||
// The actual "copy the link and share it" page -- BookingDto.ShareUrl points here.
|
||||
// Plain HTML, not the JSON ApiResponse envelope, since this is meant to be opened
|
||||
// directly in a browser (or a chat app's link preview), not consumed by a client SDK.
|
||||
[AllowAnonymous]
|
||||
[HttpGet("api/public/bookings/{id:guid}/view")]
|
||||
public async Task<ContentResult> ViewBooking(Guid id)
|
||||
{
|
||||
var booking = await _bookingService.GetBookingView(new { bookingId = id }) as BookingDto;
|
||||
if (booking == null)
|
||||
return Content(BookingViewHtml.NotFound(), "text/html");
|
||||
|
||||
return Content(BookingViewHtml.Render(booking), "text/html");
|
||||
}
|
||||
|
||||
// ---------- Staff ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpGet("api/client/bookings/session/{facilitySessionId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetSessionBookings(Guid facilitySessionId)
|
||||
=> Response(await _bookingService.GetSessionBookings(new { facilitySessionId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("api/client/bookings/check-in")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CheckInBooking([FromBody] CheckInBookingRequest request)
|
||||
=> Response(await _bookingService.CheckInBooking(new { otpCode = request.OtpCode }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("api/client/bookings/{id:guid}/mark-paid")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> MarkGuestBookingPaid(Guid id, [FromBody] MarkGuestBookingPaidRequest request)
|
||||
=> Response(await _bookingService.MarkGuestBookingPaid(new { bookingId = id, amountCharged = request.AmountCharged }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("api/client/bookings/{id:guid}/cancel")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CancelBooking(Guid id)
|
||||
=> Response(await _bookingService.CancelBooking(new { bookingId = id }, HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// One-time setup only -- see BootstrapService for why this exists instead of the
|
||||
// multi-gym onboarding/activation flow. [AllowAnonymous] is safe here specifically because
|
||||
// the service itself refuses to run a second time once a Client row exists; there is no
|
||||
// window where this endpoint stays open as a standing unauthenticated write path.
|
||||
[ApiController]
|
||||
[Route("api/bootstrap")]
|
||||
public class BootstrapController : ControllerBase
|
||||
{
|
||||
private readonly IBootstrapService _bootstrapService;
|
||||
|
||||
public BootstrapController(IBootstrapService bootstrapService)
|
||||
{
|
||||
_bootstrapService = bootstrapService;
|
||||
}
|
||||
|
||||
public class BootstrapClubRequest
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string ClientName { get; set; } = string.Empty;
|
||||
public string? ClientPhone { get; set; }
|
||||
public string? ClientEmail { get; set; }
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("club")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> BootstrapClub([FromBody] BootstrapClubRequest request)
|
||||
=> Response(await _bootstrapService.BootstrapClub(new
|
||||
{
|
||||
userId = request.UserId,
|
||||
clientName = request.ClientName,
|
||||
clientPhone = request.ClientPhone,
|
||||
clientEmail = request.ClientEmail
|
||||
}));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
using Fitness_core_BE.DTOs.PlatformBilling;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// The gym owner's own HexaFitness platform subscription bill — distinct from
|
||||
// CustomerPaymentsController, which handles invoices billed to gym members.
|
||||
[ApiController]
|
||||
[Route("api/client")]
|
||||
public class ClientInvoiceController : ControllerBase
|
||||
{
|
||||
private readonly IClientInvoiceService _clientInvoiceService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IReceiptService _receiptService;
|
||||
private readonly IClientPaymentMethodService _clientPaymentMethodService;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
|
||||
public ClientInvoiceController(
|
||||
IClientInvoiceService clientInvoiceService,
|
||||
IPaymentService paymentService,
|
||||
IReceiptService receiptService,
|
||||
IClientPaymentMethodService clientPaymentMethodService,
|
||||
UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_clientInvoiceService = clientInvoiceService;
|
||||
_paymentService = paymentService;
|
||||
_receiptService = receiptService;
|
||||
_clientPaymentMethodService = clientPaymentMethodService;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetCustomerInvoices()
|
||||
{
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(HttpContext));
|
||||
return Response(await _clientInvoiceService.GetClientInvoicesByClientId(new { clientId }, HttpContext));
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices/{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetCustomerInvoice(Guid id)
|
||||
=> Response(await _clientInvoiceService.GetClientInvoiceById(new { clientInvoiceId = id }, HttpContext));
|
||||
|
||||
// Pay this gym's own HexaFitness platform invoice. Records a pending payment
|
||||
// attempt (an admin approves it, which flips the invoice to Paid and issues a receipt).
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpPost("customer-invoices/{id:guid}/pay")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> PayCustomerInvoice(Guid id, [FromBody] PayPlatformInvoiceRequest request)
|
||||
=> Response(await _paymentService.CreateClientPayment(new
|
||||
{
|
||||
clientInvoiceId = id,
|
||||
payAmount = request.PayAmount,
|
||||
cardId = request.CardId,
|
||||
payNote = request.PayNote
|
||||
}, HttpContext));
|
||||
|
||||
// Branded receipt PDF for a platform invoice (download), once paid.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices/{id:guid}/receipt.pdf")]
|
||||
public async Task<IActionResult> DownloadPlatformInvoiceReceipt(Guid id)
|
||||
{
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(HttpContext));
|
||||
var model = await _receiptService.BuildForClientInvoiceAsync(id, requireClientId: clientId);
|
||||
var pdf = _receiptService.GeneratePdf(model);
|
||||
return File(pdf, "application/pdf", $"{model.ReceiptNo}.pdf");
|
||||
}
|
||||
|
||||
// The gym's own saved card(s) for paying their HexaFitness platform subscription
|
||||
// (real Payment-Service-backed flow — distinct from the manual PayCustomerInvoice
|
||||
// above, which just records a pending payment for an admin to approve).
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpPost("customer-invoices/cards/onboard")]
|
||||
public async Task<ActionResult<ApiResponse<StartCardOnboardingResult>>> StartCardOnboarding(
|
||||
[FromBody] StartCardOnboardingRequest request)
|
||||
=> Response(await _clientPaymentMethodService.StartCardOnboardingAsync(HttpContext, request));
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices/cards/onboard/complete")]
|
||||
public async Task<ActionResult<ApiResponse<CardOnboardingStatusResult>>> CompleteCardOnboarding([FromQuery] string sessionId)
|
||||
=> Response(await _clientPaymentMethodService.CompleteCardOnboardingAsync(HttpContext, sessionId));
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices/cards")]
|
||||
public async Task<ActionResult<ApiResponse<List<ClientCardDto>>>> ListCards()
|
||||
=> Response(await _clientPaymentMethodService.ListCardsAsync(HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpDelete("customer-invoices/cards/{cardToken}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RemoveCard(string cardToken)
|
||||
{
|
||||
await _clientPaymentMethodService.RemoveCardAsync(HttpContext, cardToken);
|
||||
return Response<object>(new { removed = true });
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpPatch("customer-invoices/cards/{cardToken}/default")]
|
||||
public async Task<ActionResult<ApiResponse<ClientCardDto>>> SetDefaultCard(string cardToken)
|
||||
=> Response(await _clientPaymentMethodService.SetDefaultCardAsync(HttpContext, cardToken));
|
||||
|
||||
[CustomMiddleware]
|
||||
[RequiresPermission("billing")]
|
||||
[HttpGet("customer-invoices/transactions")]
|
||||
public async Task<ActionResult<ApiResponse<TransactionHistoryResult>>> GetTransactionHistory(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
=> Response(await _clientPaymentMethodService.GetTransactionHistoryAsync(HttpContext, page, pageSize));
|
||||
|
||||
public class PayPlatformInvoiceRequest
|
||||
{
|
||||
public decimal PayAmount { get; set; }
|
||||
public string? CardId { get; set; }
|
||||
public string? PayNote { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Fitness_core_BE.Services.FunctionHandler;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.Text.Json;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
using Fitness_core_BE.DTOs;
|
||||
using Fitness_core_BE.DTOs.Mobile;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
|
||||
[Route("api/app")]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[EnableCors("AllowAll")]
|
||||
public class CustomerController : ControllerBase
|
||||
{
|
||||
private readonly CustomerFunctionHandler _customerFuncHandler;
|
||||
private readonly MealFunctionHandler _mealFuncHandler;
|
||||
private readonly WorkoutFunctionHandler _workoutFuncHandler;
|
||||
private readonly ChatFunctionHandler _chatFuncHandler;
|
||||
private readonly TicketFuncHandler _ticketFuncHandler;
|
||||
private readonly DocumentFunctionHandler _documentFuncHandler;
|
||||
private readonly IDocumentService _docService;
|
||||
private readonly ICustomerService _customerService;
|
||||
private readonly IMealService _mealService;
|
||||
private readonly IWorkoutService _workoutService;
|
||||
private readonly IChatService _chatService;
|
||||
private readonly IFeedService _feedService;
|
||||
private readonly ITicketService _ticketService;
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public CustomerController(
|
||||
CustomerFunctionHandler customerFuncHandler,
|
||||
MealFunctionHandler mealFuncHandler,
|
||||
WorkoutFunctionHandler workoutFuncHandler,
|
||||
ChatFunctionHandler chatFuncHandler,
|
||||
TicketFuncHandler ticketFuncHandler,
|
||||
DocumentFunctionHandler documentFuncHandler,
|
||||
IDocumentService docService,
|
||||
ICustomerService customerService,
|
||||
IMealService mealService,
|
||||
IWorkoutService workoutService,
|
||||
IChatService chatService,
|
||||
IFeedService feedService,
|
||||
ITicketService ticketService,
|
||||
IAuthService authService)
|
||||
{
|
||||
_customerFuncHandler = customerFuncHandler;
|
||||
_mealFuncHandler = mealFuncHandler;
|
||||
_workoutFuncHandler = workoutFuncHandler;
|
||||
_chatFuncHandler = chatFuncHandler;
|
||||
_ticketFuncHandler = ticketFuncHandler;
|
||||
_documentFuncHandler = documentFuncHandler;
|
||||
_docService = docService;
|
||||
_customerService = customerService;
|
||||
_mealService = mealService;
|
||||
_workoutService = workoutService;
|
||||
_chatService = chatService;
|
||||
_feedService = feedService;
|
||||
_ticketService = ticketService;
|
||||
_authService = authService;
|
||||
}
|
||||
private ActionResult<ApiResponse<object>> FromAuth(Fitness_core_BE.DTOs.Auth.AuthResponse authResponse)
|
||||
=> Response(authResponse.Data, authResponse.StatusCode, authResponse.Message);
|
||||
|
||||
|
||||
private const string VerifyRequiredMessage =
|
||||
"Please verify your email or mobile number to use this feature.";
|
||||
|
||||
private async Task<ActionResult<ApiResponse<object>>?> RequireVerifiedAsync()
|
||||
{
|
||||
if (await _customerService.IsCustomerVerified(HttpContext))
|
||||
return null;
|
||||
return Response<object>(null!, 403, VerifyRequiredMessage);
|
||||
}
|
||||
|
||||
[HttpGet("status")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetStatus()
|
||||
{
|
||||
return Ok(new { status = "Customer API is running", timestamp = System.DateTime.UtcNow });
|
||||
}
|
||||
|
||||
// ---------- Auth / account lifecycle (REST) ----------
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("auth/otp")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendOtp([FromBody] SendOtpRequest request)
|
||||
=> FromAuth(await _authService.SendOtp(new
|
||||
{
|
||||
identifier = request.Identifier,
|
||||
numberOfDigits = request.NumberOfDigits,
|
||||
newUser = request.NewUser
|
||||
}, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("auth/otp/verify")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> VerifyOtpForLogin([FromBody] VerifyOtpLoginRequest request)
|
||||
=> FromAuth(await _authService.VerifyOtpForLogin(new
|
||||
{
|
||||
otpCode = request.OtpCode,
|
||||
referenceNumber = request.ReferenceNumber
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("auth/otp/verify-alt")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> VerifyOtpAlt([FromBody] VerifyOtpAltRequest request)
|
||||
=> FromAuth(await _authService.VerifyOTP(new
|
||||
{
|
||||
otpCode = request.OtpCode,
|
||||
referenceNumber = request.ReferenceNumber,
|
||||
identifireType = request.IdentifireType,
|
||||
identifier = request.Identifier
|
||||
}, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("auth/refresh-token")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RefreshToken([FromBody] RefreshTokenRequest request)
|
||||
=> FromAuth(await _authService.RefreshToken(new { refreshToken = request.RefreshToken }, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("auth/availability")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> IsAvailable([FromQuery] string identifier)
|
||||
=> FromAuth(await _authService.IsAvailable(new { Identifier = identifier, Recovery = "Recovery" }, HttpContext));
|
||||
|
||||
// Anonymous so logout still clears cookies even if the access token has expired.
|
||||
[AllowAnonymous]
|
||||
[HttpPost("auth/logout")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> Logout()
|
||||
=> FromAuth(await _authService.LogOut(new { }, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("account/register")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RegisterCustomer([FromBody] JsonElement request)
|
||||
=> Response(await _customerService.CreateCustomer(request, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("account/password/forgot")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ForgotPassword([FromBody] ForgotPasswordRequest request)
|
||||
=> FromAuth(await _authService.ForgotPassword(new
|
||||
{
|
||||
identifier = request.Identifier,
|
||||
useResetLink = request.UseResetLink,
|
||||
numberOfDigits = request.NumberOfDigits
|
||||
}, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("account/password/reset")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ResetPassword([FromBody] ResetPasswordRequest request)
|
||||
=> FromAuth(await _authService.ResetPassword(new
|
||||
{
|
||||
referenceNumber = request.ReferenceNumber,
|
||||
newPassword = request.NewPassword,
|
||||
confirmPassword = request.ConfirmPassword
|
||||
}, HttpContext));
|
||||
|
||||
// Verify a customer's own email/mobile: validates the OTP (sent via auth/otp with
|
||||
// newUser:true) and sets the Customer channel flag. Requires an authenticated customer.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("account/verify")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> VerifyChannel([FromBody] VerifyChannelRequest request)
|
||||
=> FromAuth(await _authService.VerifyOTPForCus(new
|
||||
{
|
||||
identifireType = request.IdentifireType,
|
||||
otpCode = request.OtpCode,
|
||||
referenceNumber = request.ReferenceNumber,
|
||||
identifier = request.Identifier
|
||||
}, HttpContext));
|
||||
|
||||
// ---------- Customer-scoped operations ----------
|
||||
|
||||
[HttpPost("customer")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteCustomer([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _customerFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("customer/exists")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CustomerExists([FromQuery] string identifier)
|
||||
=> Response(await _customerService.CustomerExists(new { identifier }, HttpContext));
|
||||
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/me")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyProfile()
|
||||
=> Response(await _customerService.GetCusBasicDetails(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/me/gym")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyGym()
|
||||
=> Response(await _customerService.GetMyGym(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpDelete("customer/me/gym")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UnfollowMyGym()
|
||||
=> Response(await _customerService.UnfollowMyGym(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/me/payments")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> MyPayment()
|
||||
=> Response(await _customerService.MyPayment(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("customer/me")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdateMyProfile([FromBody] UpdateCustomerProfileRequest request)
|
||||
=> Response(await _customerService.UpdateCustomer(new
|
||||
{
|
||||
fullname = request.Fullname,
|
||||
gender = request.Gender,
|
||||
bod = request.Bod,
|
||||
primaryPH = request.PrimaryPH,
|
||||
secondaryPH = request.SecondaryPH,
|
||||
email = request.Email,
|
||||
NIC = request.Nic,
|
||||
address = request.Address
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/me/health")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyHealth()
|
||||
=> Response(await _customerService.GetCusHealthDetails(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("customer/me/health")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdateMyHealth([FromBody] UpdateHealthRequest request)
|
||||
=> Response(await _customerService.UpdateCusHealthRec(new
|
||||
{
|
||||
bloodGroup = request.BloodGroup,
|
||||
disabilities = request.Disabilities
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/me/progress")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateMyProgress([FromBody] CreateProgressRequest request)
|
||||
=> Response(await _customerService.CreateCusProgressRec(new
|
||||
{
|
||||
Height = request.Height,
|
||||
currentWeight = request.CurrentWeight,
|
||||
targetWeight = request.TargetWeight,
|
||||
goal = request.Goal,
|
||||
age = request.Age,
|
||||
gender = request.Gender,
|
||||
activityLevel = request.ActivityLevel,
|
||||
timePeriod = request.TimePeriod
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("customer/me/progress")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdateMyProgress([FromBody] UpdateProgressRequest request)
|
||||
=> Response(await _customerService.UpdateCusProgressRec(new
|
||||
{
|
||||
Height = request.Height,
|
||||
Weight = request.Weight,
|
||||
ProgressDate = request.ProgressDate
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/me/progress")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyProgress()
|
||||
=> Response(await _customerService.GetCusProgressDetails(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/progress")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetCustomerProgress([FromQuery] string? gender, [FromQuery] string? age)
|
||||
=> Response(await _customerService.GetCustomerProgress(new { gender, age }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/progress/weekly/{customerTargetId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetWeeklyProgress(Guid customerTargetId)
|
||||
=> Response(await _customerService.GetWeeklyProgressForCustomer(new { customerTargetId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/meal-plans")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyFollowedMealPlans()
|
||||
=> Response(await _customerService.GetFollowedMealPlans(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/meal-plans/mine")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyOwnMealPlans()
|
||||
=> Response(await _customerService.GetMyMealPlans(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/workout-plans")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyFollowedWorkoutPlans()
|
||||
=> Response(await _customerService.GetFollowedWorkoutPlans(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/workout-plans/mine")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyOwnWorkoutPlans()
|
||||
=> Response(await _customerService.GetMyWorkoutPlans(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/meal-plans/{mealPlanId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyMealPlanById(Guid mealPlanId)
|
||||
=> Response(await _customerService.GetCustomerMealPlanById(new { mealPlanId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/workout-plans/{workoutPlanId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyWorkoutPlanById(Guid workoutPlanId)
|
||||
=> Response(await _customerService.GetCustomerWorkoutPlanById(new { workoutPlanId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/meal-plans/{mealPlanId:guid}/follow")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> FollowMealPlan(Guid mealPlanId)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _customerService.FollowMealPlan(new { mealPlanId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/workout-plans/{workoutPlanId:guid}/follow")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> FollowWorkoutPlan(Guid workoutPlanId)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _customerService.FollowWorkOutPlan(new { workOutPlan = workoutPlanId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/workout-plans/{workoutPlanId:guid}/exercises/{exerciseId:guid}/complete")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CompleteWorkoutExercise(Guid workoutPlanId, Guid exerciseId)
|
||||
=> Response(await _customerService.SetWorkoutExerciseCompletion(workoutPlanId, exerciseId, true, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpDelete("customer/workout-plans/{workoutPlanId:guid}/exercises/{exerciseId:guid}/complete")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UncompleteWorkoutExercise(Guid workoutPlanId, Guid exerciseId)
|
||||
=> Response(await _customerService.SetWorkoutExerciseCompletion(workoutPlanId, exerciseId, false, HttpContext));
|
||||
|
||||
// ---------- Saved cards / auto-deduct consent ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/subscription")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMySubscription()
|
||||
=> Response(await _customerService.GetMySubscription(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/cards")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyCards()
|
||||
=> Response(await _customerService.GetCardDetails(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/cards")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SaveCard([FromBody] SaveCardRequest request)
|
||||
=> Response(await _customerService.SaveCard(new
|
||||
{
|
||||
cardToken = request.CardToken,
|
||||
cardLast4 = request.CardLast4,
|
||||
cardType = request.CardType,
|
||||
nickName = request.NickName,
|
||||
consent = request.Consent
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPatch("customer/subscriptions/{subscriptionId:guid}/auto-charge")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SetAutoCharge(Guid subscriptionId, [FromBody] SetAutoChargeRequest request)
|
||||
=> Response(await _customerService.SetAutoCharge(subscriptionId, new
|
||||
{
|
||||
enabled = request.Enabled,
|
||||
cardId = request.CardId
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/gyms")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetGyms()
|
||||
=> Response(await _customerService.GetClientGyms(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/gyms/{clientBizId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetGymById(Guid clientBizId)
|
||||
=> Response(await _customerService.GetClientDataById(new { clientBizID = clientBizId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/gyms/{clientId:guid}/plans")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetGymPlans(Guid clientId)
|
||||
=> Response(await _customerService.GetGymPlans(new { clientId }, HttpContext));
|
||||
|
||||
// Gym-specific Terms & Conditions: view before joining, record acceptance, download PDF.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/gyms/{clientId:guid}/terms")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetGymTerms(Guid clientId)
|
||||
=> Response(await _customerService.GetGymTerms(new { clientId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("customer/gyms/{clientId:guid}/terms/accept")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> AcceptGymTerms(Guid clientId, [FromBody] AcceptGymTermsRequest request)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _customerService.AcceptGymTerms(new { clientId, version = request.Version }, HttpContext));
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("customer/gyms/{clientId:guid}/terms/pdf")]
|
||||
public async Task<IActionResult> DownloadGymTermsPdf(Guid clientId)
|
||||
{
|
||||
var pdf = await _customerService.GetGymTermsPdf(clientId);
|
||||
if (pdf == null)
|
||||
return NotFound("This gym has not published any terms yet.");
|
||||
return File(pdf, "application/pdf", "terms-and-conditions.pdf");
|
||||
}
|
||||
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("meal-plans/templates")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMealPlanTemplates()
|
||||
=> Response(await _mealService.GetMealPlanList(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("meal-categories")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMealCategories()
|
||||
=> Response(await _mealService.GetMealCategoryList(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("meal-plans/{mealPlanId:guid}/items")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMealPlanItems(Guid mealPlanId)
|
||||
=> Response(await _mealService.GetMealItemsByMealPlanId(new { mealPlanId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("meal-plans")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateMealPlan([FromBody] JsonElement request)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _mealService.CreateCustomerMealPlan(request, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("workout-plans/templates")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetWorkoutPlanTemplates()
|
||||
=> Response(await _workoutService.GetWorkoutPlanList(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("workout-categories")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetWorkoutCategories()
|
||||
=> Response(await _workoutService.GetWorkoutCategoryList(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("workout-plans")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateWorkoutPlan([FromBody] JsonElement request)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _workoutService.CreateCustomerWorkout(request, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("chat/meal/sessions")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateMealChatSession([FromBody] GetOrCreateChatRequest request)
|
||||
=> Response(await _chatService.GetOrCreateChatAsync(new { otherUserId = request.OtherUserId }));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("chat/workout/sessions")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateWorkoutChatSession([FromBody] GetOrCreateChatRequest request)
|
||||
=> Response(await _chatService.GetOrCreateChatAsync(new { otherUserId = request.OtherUserId }));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("chat/meal/messages")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendMealChatMessage([FromBody] SendChatMessageRequest request)
|
||||
=> Response(await _chatService.SendMessageAsync(new
|
||||
{
|
||||
chatId = request.ChatId,
|
||||
senderId = request.SenderId,
|
||||
message = request.Message,
|
||||
type = request.Type
|
||||
}));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("chat/workout/messages")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendWorkoutChatMessage([FromBody] SendChatMessageRequest request)
|
||||
=> Response(await _chatService.SendMessageAsync(new
|
||||
{
|
||||
chatId = request.ChatId,
|
||||
senderId = request.SenderId,
|
||||
message = request.Message,
|
||||
type = request.Type
|
||||
}));
|
||||
|
||||
[HttpPost("meal")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteMeal([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _mealFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("workout")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteWorkout([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _workoutFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("chat")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteChat([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _chatFuncHandler.Execute(request);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("initChat")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> InitChat([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _chatFuncHandler.Execute(request);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
// ---------- Feed / tickets / documents (REST) ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("feed")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetFeed([FromQuery] int skip = 0, [FromQuery] int take = 5)
|
||||
=> Response(await _feedService.GetFeedForCustomer(new { skip, take }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("feed/{postId:guid}/like")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> LikeFeedPost(Guid postId)
|
||||
=> Response(await _feedService.LikeAsync(new { postId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("feed")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateFeedPost([FromBody] JsonElement request)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _customerService.CreateFeed(request, HttpContext));
|
||||
|
||||
// User-scoped (mirrors the client portal, which also only shows the caller's own
|
||||
// tickets via GetTicketByUserId, never the unscoped GetTicketList).
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("tickets")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetTickets()
|
||||
=> Response(await _ticketService.GetTicketByUserId(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("tickets/{ticketId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetTicketById(Guid ticketId)
|
||||
=> Response(await _ticketService.GetTicketById(new { ticketId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("ticket-categories")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetTicketCategories()
|
||||
=> Response(await _ticketService.GetTicketCategoryList(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("tickets")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateTicket([FromBody] CreateTicketRequest request)
|
||||
=> await RequireVerifiedAsync()
|
||||
?? Response(await _ticketService.CreateTicket(new
|
||||
{
|
||||
mainReason = request.MainReason,
|
||||
ticketCategoryId = request.TicketCategoryId,
|
||||
ticketStatus = request.TicketStatus
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("tickets/{ticketId:guid}/messages")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendTicketMessage(Guid ticketId, [FromBody] SendTicketMessageRequest request)
|
||||
=> Response(await _ticketService.SendMessage(new { ticketId, message = request.Message }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("tickets/{ticketId:guid}/close")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CloseTicket(Guid ticketId)
|
||||
=> Response(await _ticketService.CloseTicket(new { ticketId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("tickets/{ticketId:guid}/feedback")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SubmitTicketFeedback(Guid ticketId, [FromBody] SubmitTicketFeedbackRequest request)
|
||||
=> Response(await _ticketService.SubmitTicketFeedback(new
|
||||
{
|
||||
ticketId,
|
||||
rating = request.Rating,
|
||||
feedback = request.Feedback
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("documents")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UploadDocument([FromBody] JsonElement request)
|
||||
=> Response(await _docService.UploadDocument(request, HttpContext));
|
||||
|
||||
|
||||
[HttpPost("ticket")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteTicket([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _ticketFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("document")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
public async Task<IActionResult> ExecuteDocument([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _documentFuncHandler.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
// ---------- Notification tab ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("customer/notifications")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyNotifications()
|
||||
=> Response(await _customerService.GetMyNotifications(new { }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPatch("customer/notifications/{notificationId:guid}/read")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> MarkNotificationRead(Guid notificationId)
|
||||
=> Response(await _customerService.MarkNotificationRead(notificationId, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("customer/push-token")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdatePushToken([FromBody] UpdatePushTokenRequest request)
|
||||
=> Response(await _customerService.UpdatePushToken(new { pushTokenId = request.PushTokenId }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("customer/profile-image")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdateProfileImage([FromBody] UpdateProfileImageRequest request)
|
||||
=> Response(await _customerService.UpdateProfileImage(new { documentId = request.DocumentId }, HttpContext));
|
||||
|
||||
// Serves an uploaded document by id (mobile builds <base>/docService/{id} for assets).
|
||||
[HttpGet("docService/{docId}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetDocument(string docId)
|
||||
{
|
||||
if (!int.TryParse(docId, out int documentId))
|
||||
return BadRequest("Invalid Document ID format.");
|
||||
|
||||
DocumentDownloadModel result = await _docService.GetFileBytesById(documentId);
|
||||
|
||||
if (result == null)
|
||||
return NotFound("Document not found");
|
||||
|
||||
try
|
||||
{
|
||||
byte[] fileBytes = result.FileData;
|
||||
string fileName = result.FileName;
|
||||
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
if (!provider.TryGetContentType(fileName, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
return File(fileBytes, contentType, fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest($"Error processing document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Invoices billed to gym members (CustomerInvoice) and their payments — distinct
|
||||
// from ClientInvoiceController, which handles the gym owner's own platform bill.
|
||||
[ApiController]
|
||||
[Route("api/client")]
|
||||
public class CustomerPaymentsController : ControllerBase
|
||||
{
|
||||
private readonly ICustomerInvoiceService _customerInvoiceService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IReceiptService _receiptService;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
|
||||
public CustomerPaymentsController(
|
||||
ICustomerInvoiceService customerInvoiceService,
|
||||
IPaymentService paymentService,
|
||||
IReceiptService receiptService,
|
||||
UserRoleHelper userRoleHelper)
|
||||
{
|
||||
_customerInvoiceService = customerInvoiceService;
|
||||
_paymentService = paymentService;
|
||||
_receiptService = receiptService;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
}
|
||||
|
||||
// scope=latest: one row per customer (their latest approved invoice) -- used by the
|
||||
// regular invoices tab now that a customer can have older Paid/RolledOver/Cancelled
|
||||
// invoices sitting alongside their current one. Omitted keeps the full, unfiltered
|
||||
// history for any other existing caller of this endpoint.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpGet("member-invoices")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMemberInvoices([FromQuery] string? scope = null)
|
||||
=> Response(await _customerInvoiceService.GetCustomerInvoicesByClientId(new { scope }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpGet("member-invoices/{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMemberInvoice(Guid id)
|
||||
=> Response(await _customerInvoiceService.GetCustomerInvoiceById(new { customerInvoiceId = id }, HttpContext));
|
||||
|
||||
// Full invoice history for one customer (previous cycles) -- used by the regular
|
||||
// invoices tab's "view previous invoices" action, since the tab itself now only shows
|
||||
// each customer's latest (see scope=latest above).
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpGet("member-invoices/customer/{customerId:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMemberInvoicesForCustomer(Guid customerId)
|
||||
=> Response(await _customerInvoiceService.GetCustomerInvoicesByCustomerId(new { customerId }, HttpContext));
|
||||
|
||||
// Branded "GYM OS" receipt PDF for a member invoice (download).
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpGet("member-invoices/{id:guid}/receipt.pdf")]
|
||||
public async Task<IActionResult> DownloadReceipt(Guid id)
|
||||
{
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(HttpContext));
|
||||
var model = await _receiptService.BuildForInvoiceAsync(id, requireClientId: clientId);
|
||||
var pdf = _receiptService.GeneratePdf(model);
|
||||
return File(pdf, "application/pdf", $"{model.ReceiptNo}.pdf");
|
||||
}
|
||||
|
||||
// Manually (re)send the receipt to the member via email + SMS.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpPost("member-invoices/{id:guid}/send-receipt")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendReceipt(Guid id)
|
||||
{
|
||||
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(HttpContext));
|
||||
var model = await _receiptService.BuildForInvoiceAsync(id, requireClientId: clientId);
|
||||
await _receiptService.SendReceiptAsync(model);
|
||||
return Response<object>(new { success = true, message = "Receipt sent" });
|
||||
}
|
||||
|
||||
// "Charge now" — attempt to auto-deduct from the member's consented saved card.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpPost("member-invoices/{id:guid}/auto-charge")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> AttemptAutoCharge(Guid id)
|
||||
=> Response(await _paymentService.AttemptAutoCharge(id, HttpContext));
|
||||
|
||||
// Email/SMS the member a link to pay this invoice online themselves.
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpPost("member-invoices/{id:guid}/payment-link")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SendPaymentLink(Guid id)
|
||||
=> Response(await _paymentService.SendPaymentLink(id, HttpContext));
|
||||
|
||||
// ---------- Public pay-by-link page (anonymous; the token itself is the authorization) ----------
|
||||
|
||||
[HttpGet("pay/{token}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetInvoiceByPaymentToken(string token)
|
||||
=> Response(await _paymentService.GetInvoiceByPaymentToken(token));
|
||||
|
||||
[HttpPost("pay/{token}/charge")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ChargeInvoiceByPaymentToken(string token, [FromBody] PayByLinkChargeRequest request)
|
||||
=> Response(await _paymentService.ChargeInvoiceByPaymentToken(token, new { cardToken = request.CardToken }));
|
||||
|
||||
// ---------- Subscription payments ----------
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpGet("subscriptions/{id:guid}/payments")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetSubscriptionPayments(Guid id)
|
||||
=> Response(await _paymentService.GetSubscriptionPayments(new { subscriptionId = id }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
[HttpPost("subscriptions/{id:guid}/payments")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RecordSubscriptionPayment(Guid id, [FromBody] RecordSubscriptionPaymentRequest request)
|
||||
{
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["subscriptionId"] = id,
|
||||
["amount"] = request.Amount,
|
||||
["paymentMethod"] = request.PaymentMethod
|
||||
};
|
||||
if (request.Note != null) payload["note"] = request.Note;
|
||||
if (request.BankRefNo != null) payload["bankRefNo"] = request.BankRefNo;
|
||||
return Response(await _paymentService.RecordSubscriptionPayment(payload, HttpContext));
|
||||
}
|
||||
|
||||
public class PayByLinkChargeRequest
|
||||
{
|
||||
public string CardToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RecordSubscriptionPaymentRequest
|
||||
{
|
||||
public decimal Amount { get; set; }
|
||||
public string? Note { get; set; }
|
||||
// "Cash" | "Bank" -- required so the owner picks one instead of it being implicitly cash.
|
||||
public string PaymentMethod { get; set; } = string.Empty;
|
||||
// Optional reference/slip number, relevant when PaymentMethod is "Bank".
|
||||
public string? BankRefNo { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Fitness_core_BE.DTOs.Facility;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Staff/owner-facing facility + facility-session management. Gated on the "bookings" nav
|
||||
// permission (NavItem seeded in CentralDbContext.OnModelCreating).
|
||||
[ApiController]
|
||||
[Route("api/client/facilities")]
|
||||
public class FacilityController : ControllerBase
|
||||
{
|
||||
private readonly IFacilityService _facilityService;
|
||||
|
||||
public FacilityController(IFacilityService facilityService)
|
||||
{
|
||||
_facilityService = facilityService;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpGet("")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetAllFacilities([FromQuery] bool includeInactive = false)
|
||||
=> Response(await _facilityService.GetAllFacilities(new { includeInactive }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateFacility([FromBody] CreateFacilityRequest request)
|
||||
=> Response(await _facilityService.CreateFacility(new
|
||||
{
|
||||
name = request.Name,
|
||||
facilityType = request.FacilityType,
|
||||
description = request.Description,
|
||||
defaultCapacity = request.DefaultCapacity
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdateFacility(Guid id, [FromBody] UpdateFacilityRequest request)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { ["facilityId"] = id };
|
||||
if (request.Name != null) payload["name"] = request.Name;
|
||||
if (request.FacilityType != null) payload["facilityType"] = request.FacilityType;
|
||||
if (request.Description != null) payload["description"] = request.Description;
|
||||
if (request.DefaultCapacity.HasValue) payload["defaultCapacity"] = request.DefaultCapacity.Value;
|
||||
if (request.IsActive.HasValue) payload["isActive"] = request.IsActive.Value;
|
||||
|
||||
return Response(await _facilityService.UpdateFacility(payload, HttpContext));
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPatch("{id:guid}/status")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SetFacilityStatus(Guid id, [FromBody] SetFacilityStatusRequest request)
|
||||
=> Response(await _facilityService.SetFacilityStatus(new { facilityId = id, isActive = request.IsActive }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("sessions")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreateFacilitySession([FromBody] CreateFacilitySessionRequest request)
|
||||
=> Response(await _facilityService.CreateFacilitySession(new
|
||||
{
|
||||
facilityId = request.FacilityId,
|
||||
startTime = request.StartTime.ToString("o"),
|
||||
endTime = request.EndTime.ToString("o"),
|
||||
capacity = request.Capacity
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpGet("sessions")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetSessions(
|
||||
[FromQuery] Guid? facilityId, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate)
|
||||
=> Response(await _facilityService.GetSessions(new
|
||||
{
|
||||
facilityId,
|
||||
fromDate = fromDate?.ToString("o"),
|
||||
toDate = toDate?.ToString("o")
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpDelete("sessions/{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CancelFacilitySession(Guid id)
|
||||
=> Response(await _facilityService.CancelFacilitySession(new { facilitySessionId = id }, HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Fitness_core_BE.DTOs.Rbac;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Owner-only role management. No [RequiresPermission] here on purpose — enforcement
|
||||
// lives inside IGymRoleService itself (every method requires resolving as the gym
|
||||
// owner), so no staff nav-section permission can ever unlock these actions.
|
||||
[ApiController]
|
||||
[Route("api/client/roles")]
|
||||
public class GymRoleController : ControllerBase
|
||||
{
|
||||
private readonly IGymRoleService _gymRoleService;
|
||||
|
||||
public GymRoleController(IGymRoleService gymRoleService)
|
||||
{
|
||||
_gymRoleService = gymRoleService;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("")]
|
||||
public async Task<ActionResult<ApiResponse<List<GymRoleDto>>>> GetRoles()
|
||||
=> Response(await _gymRoleService.GetRolesAsync(HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPost("")]
|
||||
public async Task<ActionResult<ApiResponse<GymRoleDto>>> CreateRole([FromBody] CreateGymRoleRequest request)
|
||||
=> Response(await _gymRoleService.CreateRoleAsync(request, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<GymRoleDto>>> UpdateRole(Guid id, [FromBody] UpdateGymRoleRequest request)
|
||||
=> Response(await _gymRoleService.UpdateRoleAsync(id, request, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> DeleteRole(Guid id)
|
||||
{
|
||||
await _gymRoleService.DeleteRoleAsync(id, HttpContext);
|
||||
return Response<object>(new { deleted = true });
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("{id:guid}/permissions")]
|
||||
public async Task<ActionResult<ApiResponse<GymRoleDto>>> SetRolePermissions(Guid id, [FromBody] SetRolePermissionsRequest request)
|
||||
=> Response(await _gymRoleService.SetRolePermissionsAsync(id, request, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpPut("~/api/client/staff/{staffId:guid}/role")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> AssignStaffRole(Guid staffId, [FromBody] AssignStaffRoleRequest request)
|
||||
{
|
||||
await _gymRoleService.AssignStaffRoleAsync(staffId, request, HttpContext);
|
||||
return Response<object>(new { assigned = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Fitness_core_BE.DTOs.Rbac;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Returns the permission-filtered sidebar nav for the current caller — the owner sees
|
||||
// everything, staff see only what their assigned role permits.
|
||||
[ApiController]
|
||||
[Route("api/client")]
|
||||
public class NavController : ControllerBase
|
||||
{
|
||||
private readonly INavigationService _navigationService;
|
||||
|
||||
public NavController(INavigationService navigationService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("nav")]
|
||||
public async Task<ActionResult<ApiResponse<NavResponseDto>>> GetNav()
|
||||
{
|
||||
return Response(await _navigationService.GetNavigationAsync(HttpContext));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Fitness_core_BE.DTOs.PaymentsEnablement;
|
||||
using Fitness_core_BE.DTOs.PaymentGateways;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Gym owner's payment-gateway registration/enablement: choosing a payout track
|
||||
// (own gateway vs. HexaFitness-held), submitting payout KYC, and viewing the
|
||||
// resulting wallet/payout schedule. The platform-admin equivalent (the global
|
||||
// gateway catalog) lives in AdminPaymentGatewayController.
|
||||
[ApiController]
|
||||
[Route("api/client")]
|
||||
public class PaymentsEnablementController : ControllerBase
|
||||
{
|
||||
private readonly IPaymentsEnablementService _payments;
|
||||
|
||||
public PaymentsEnablementController(IPaymentsEnablementService payments)
|
||||
{
|
||||
_payments = payments;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpGet("payments/tracks")]
|
||||
public async Task<ActionResult<ApiResponse<List<PaymentTrackOption>>>> GetPaymentTracks()
|
||||
=> Response(await _payments.GetTrackOptionsAsync());
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpPost("payments/track-selection")]
|
||||
public async Task<ActionResult<ApiResponse<PaymentTrackSelectionResult>>> SelectTrack([FromBody] SelectPaymentTrackRequest request)
|
||||
=> Response(await _payments.SelectTrackAsync(HttpContext, request));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpGet("payments/gateways")]
|
||||
public async Task<ActionResult<ApiResponse<List<GatewayDefinitionDto>>>> GetAvailableGateways()
|
||||
=> Response(await _payments.GetAvailableGatewaysAsync(HttpContext.RequestAborted));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpPost("payments/kyc")]
|
||||
public async Task<ActionResult<ApiResponse<ClientPayoutKycDto>>> SubmitKyc(
|
||||
[FromBody] SubmitGatewayOnboardingRequest request)
|
||||
=> Response(await _payments.SubmitKycAsync(
|
||||
HttpContext, request, HttpContext.RequestAborted));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpGet("payments/kyc/status")]
|
||||
public async Task<ActionResult<ApiResponse<GatewayOnboardingStatusDto>>> GetKycStatus()
|
||||
=> Response(await _payments.GetKycStatusAsync(HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpGet("payments/wallet")]
|
||||
public async Task<ActionResult<ApiResponse<WalletDto>>> GetWallet()
|
||||
=> Response(await _payments.GetWalletAsync(HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("payment-gateway")]
|
||||
[HttpGet("payments/payout-schedule")]
|
||||
public async Task<ActionResult<ApiResponse<PayoutScheduleDto>>> GetPayoutSchedule()
|
||||
=> Response(await _payments.GetPayoutScheduleAsync(HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Fitness_core_BE.DTOs.Place;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Staff/owner-facing management of physical access zones (Places) and which fingerprint
|
||||
// terminal (AccessDevice) sits in each. Gated on "memberships" since Places exist to
|
||||
// scope membership-plan access, not bookings.
|
||||
[ApiController]
|
||||
[Route("api/client/places")]
|
||||
public class PlaceController : ControllerBase
|
||||
{
|
||||
private readonly IPlaceService _placeService;
|
||||
|
||||
public PlaceController(IPlaceService placeService)
|
||||
{
|
||||
_placeService = placeService;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("memberships")]
|
||||
[HttpGet("")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetAllPlaces([FromQuery] bool includeInactive = false)
|
||||
=> Response(await _placeService.GetAllPlaces(new { includeInactive }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("memberships")]
|
||||
[HttpPost("")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> CreatePlace([FromBody] CreatePlaceRequest request)
|
||||
=> Response(await _placeService.CreatePlace(new
|
||||
{
|
||||
name = request.Name,
|
||||
description = request.Description
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("memberships")]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> UpdatePlace(Guid id, [FromBody] UpdatePlaceRequest request)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { ["placeId"] = id };
|
||||
if (request.Name != null) payload["name"] = request.Name;
|
||||
if (request.Description != null) payload["description"] = request.Description;
|
||||
if (request.IsActive.HasValue) payload["isActive"] = request.IsActive.Value;
|
||||
|
||||
return Response(await _placeService.UpdatePlace(payload, HttpContext));
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("memberships")]
|
||||
[HttpPatch("{id:guid}/status")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SetPlaceStatus(Guid id, [FromBody] SetPlaceStatusRequest request)
|
||||
=> Response(await _placeService.SetPlaceStatus(new { placeId = id, isActive = request.IsActive }, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("memberships")]
|
||||
[HttpPut("{id:guid}/device")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> AssignDevice(Guid id, [FromBody] AssignPlaceDeviceRequest request)
|
||||
=> Response(await _placeService.AssignDevice(new { placeId = id, deviceId = request.DeviceId }, HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
public record ChangeRenewalPlanRequest(Guid NewCustomerPlanId);
|
||||
|
||||
// Staff-facing approval queue for the unapproved (IsActive=false) invoices the daily
|
||||
// SubscriptionRenewalJob sweep creates. See ISubscriptionRenewalApprovalService.
|
||||
[ApiController]
|
||||
[Route("api/client/renewal-approvals")]
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("customer-payments")]
|
||||
public class SubscriptionRenewalApprovalController(ISubscriptionRenewalApprovalService approvalService) : ControllerBase
|
||||
{
|
||||
private readonly ISubscriptionRenewalApprovalService _approvalService = approvalService;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetUnapprovedInvoices()
|
||||
=> Response(await _approvalService.GetUnapprovedInvoicesAsync(HttpContext));
|
||||
|
||||
[HttpPost("{invoiceId:guid}/approve")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> Approve(Guid invoiceId)
|
||||
=> Response(await _approvalService.ApproveAsync(invoiceId, HttpContext));
|
||||
|
||||
[HttpPost("{invoiceId:guid}/change-plan")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> ChangePlan(Guid invoiceId, [FromBody] ChangeRenewalPlanRequest request)
|
||||
=> Response(await _approvalService.ChangePlanAsync(invoiceId, request.NewCustomerPlanId, HttpContext));
|
||||
|
||||
[HttpPost("{invoiceId:guid}/reject")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> Reject(Guid invoiceId)
|
||||
=> Response(await _approvalService.RejectAsync(invoiceId, HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Fitness_core_BE.DTOs.TimeBankCard;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.JwtMiddleware;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Same absolute-per-route reasoning as BookingController: staff issue/swipe under
|
||||
// "api/client/...", a member's own card list under "api/app/...". This is the
|
||||
// staff-facing manual swipe terminal; AgentController's own "time-bank-cards/redeem"
|
||||
// exposes the same tap-in/tap-out swipe to an RFID-reader-equipped agent station instead.
|
||||
[ApiController]
|
||||
public class TimeBankCardController : ControllerBase
|
||||
{
|
||||
private readonly ITimeBankCardService _timeBankCardService;
|
||||
|
||||
public TimeBankCardController(ITimeBankCardService timeBankCardService)
|
||||
{
|
||||
_timeBankCardService = timeBankCardService;
|
||||
}
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("api/client/time-bank-cards")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> IssueCard([FromBody] IssueTimeBankCardRequest request)
|
||||
=> Response(await _timeBankCardService.IssueCard(new
|
||||
{
|
||||
rfidTagId = request.RfidTagId,
|
||||
totalMinutesPurchased = request.TotalMinutesPurchased,
|
||||
amountPaid = request.AmountPaid,
|
||||
customerId = request.CustomerId,
|
||||
expiresAt = request.ExpiresAt?.ToString("o")
|
||||
}, HttpContext));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpPost("api/client/time-bank-cards/redeem")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> Swipe([FromBody] SwipeTimeBankCardRequest request)
|
||||
=> Response(await _timeBankCardService.Swipe(new
|
||||
{
|
||||
rfidTagId = request.RfidTagId,
|
||||
facilityId = request.FacilityId
|
||||
}));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[RequiresPermission("bookings")]
|
||||
[HttpGet("api/client/time-bank-cards/{rfidTagId}")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetBalance(string rfidTagId)
|
||||
=> Response(await _timeBankCardService.GetBalance(new { rfidTagId }));
|
||||
|
||||
[CustomMiddleware]
|
||||
[TenantResolution]
|
||||
[HttpGet("api/app/time-bank-cards/mine")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> GetMyCards()
|
||||
=> Response(await _timeBankCardService.GetMyCards(new { }, HttpContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Fitness_core_BE.DTOs.PlatformBilling;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces.Services;
|
||||
using Fitness_core_BE.Models.IOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Fitness_core_BE.Helpers.ApiResponseHelper;
|
||||
|
||||
namespace Fitness_core_BE.Controllers
|
||||
{
|
||||
// Trusted-machine-caller surface for the scheduled subscription-billing worker — not a
|
||||
// human/agent session, so it's gated by RequireWorkerKeyAttribute (X-Worker-Key) instead
|
||||
// of CustomMiddleware/JwtMiddleware.
|
||||
[ApiController]
|
||||
[Route("api/internal/billing")]
|
||||
[RequireWorkerKey]
|
||||
public class WorkerBillingController : ControllerBase
|
||||
{
|
||||
private readonly IPlatformBillingService _platformBilling;
|
||||
|
||||
public WorkerBillingController(IPlatformBillingService platformBilling)
|
||||
{
|
||||
_platformBilling = platformBilling;
|
||||
}
|
||||
|
||||
[HttpPost("run-subscription-charges")]
|
||||
public async Task<ActionResult<ApiResponse<SubscriptionChargeRunResult>>> RunSubscriptionCharges()
|
||||
{
|
||||
return Response(await _platformBilling.RunAndChargeSubscriptionBillingAsync(HttpContext.RequestAborted));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user