first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
+365
View File
@@ -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
});
}
}
}
+94
View File
@@ -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 });
}
}
}
+99
View File
@@ -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 });
}
}
}
+153
View File
@@ -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! });
}
}
}
}
+103
View File
@@ -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));
}
}
+43
View File
@@ -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
+135
View File
@@ -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; }
}
}
}
+715
View File
@@ -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}");
}
}
}
}
+154
View File
@@ -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; }
}
}
}
+100
View File
@@ -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));
}
}
+67
View File
@@ -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 });
}
}
}
+32
View File
@@ -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));
}
}
+71
View File
@@ -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));
}
}
+63
View File
@@ -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));
}
}
+31
View File
@@ -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));
}
}
}