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> 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>?> RequireVerifiedAsync() { if (await _customerService.IsCustomerVerified(HttpContext)) return null; return Response(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>> 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>> 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>> 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>> RefreshToken([FromBody] RefreshTokenRequest request) => FromAuth(await _authService.RefreshToken(new { refreshToken = request.RefreshToken }, HttpContext)); [AllowAnonymous] [HttpGet("auth/availability")] public async Task>> 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>> Logout() => FromAuth(await _authService.LogOut(new { }, HttpContext)); [AllowAnonymous] [HttpPost("account/register")] public async Task>> RegisterCustomer([FromBody] JsonElement request) => Response(await _customerService.CreateCustomer(request, HttpContext)); [AllowAnonymous] [HttpPost("account/password/forgot")] public async Task>> 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>> 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>> 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 ExecuteCustomer([FromBody] ApiRequest request) { var response = await _customerFuncHandler.Execute(request, HttpContext); return StatusCode(response.StatusCode, response); } [AllowAnonymous] [HttpGet("customer/exists")] public async Task>> CustomerExists([FromQuery] string identifier) => Response(await _customerService.CustomerExists(new { identifier }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/me")] public async Task>> GetMyProfile() => Response(await _customerService.GetCusBasicDetails(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/me/gym")] public async Task>> GetMyGym() => Response(await _customerService.GetMyGym(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpDelete("customer/me/gym")] public async Task>> UnfollowMyGym() => Response(await _customerService.UnfollowMyGym(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/me/payments")] public async Task>> MyPayment() => Response(await _customerService.MyPayment(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPut("customer/me")] public async Task>> 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>> GetMyHealth() => Response(await _customerService.GetCusHealthDetails(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPut("customer/me/health")] public async Task>> 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>> 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>> 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>> GetMyProgress() => Response(await _customerService.GetCusProgressDetails(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/progress")] public async Task>> 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>> GetWeeklyProgress(Guid customerTargetId) => Response(await _customerService.GetWeeklyProgressForCustomer(new { customerTargetId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/meal-plans")] public async Task>> GetMyFollowedMealPlans() => Response(await _customerService.GetFollowedMealPlans(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/meal-plans/mine")] public async Task>> GetMyOwnMealPlans() => Response(await _customerService.GetMyMealPlans(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/workout-plans")] public async Task>> GetMyFollowedWorkoutPlans() => Response(await _customerService.GetFollowedWorkoutPlans(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/workout-plans/mine")] public async Task>> GetMyOwnWorkoutPlans() => Response(await _customerService.GetMyWorkoutPlans(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/meal-plans/{mealPlanId:guid}")] public async Task>> GetMyMealPlanById(Guid mealPlanId) => Response(await _customerService.GetCustomerMealPlanById(new { mealPlanId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/workout-plans/{workoutPlanId:guid}")] public async Task>> GetMyWorkoutPlanById(Guid workoutPlanId) => Response(await _customerService.GetCustomerWorkoutPlanById(new { workoutPlanId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("customer/meal-plans/{mealPlanId:guid}/follow")] public async Task>> FollowMealPlan(Guid mealPlanId) => await RequireVerifiedAsync() ?? Response(await _customerService.FollowMealPlan(new { mealPlanId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("customer/workout-plans/{workoutPlanId:guid}/follow")] public async Task>> 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>> 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>> 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>> GetMySubscription() => Response(await _customerService.GetMySubscription(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/cards")] public async Task>> GetMyCards() => Response(await _customerService.GetCardDetails(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("customer/cards")] public async Task>> 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>> 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>> GetGyms() => Response(await _customerService.GetClientGyms(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/gyms/{clientBizId:guid}")] public async Task>> GetGymById(Guid clientBizId) => Response(await _customerService.GetClientDataById(new { clientBizID = clientBizId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("customer/gyms/{clientId:guid}/plans")] public async Task>> 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>> GetGymTerms(Guid clientId) => Response(await _customerService.GetGymTerms(new { clientId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("customer/gyms/{clientId:guid}/terms/accept")] public async Task>> 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 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>> GetMealPlanTemplates() => Response(await _mealService.GetMealPlanList(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("meal-categories")] public async Task>> GetMealCategories() => Response(await _mealService.GetMealCategoryList(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("meal-plans/{mealPlanId:guid}/items")] public async Task>> GetMealPlanItems(Guid mealPlanId) => Response(await _mealService.GetMealItemsByMealPlanId(new { mealPlanId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("meal-plans")] public async Task>> CreateMealPlan([FromBody] JsonElement request) => await RequireVerifiedAsync() ?? Response(await _mealService.CreateCustomerMealPlan(request, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("workout-plans/templates")] public async Task>> GetWorkoutPlanTemplates() => Response(await _workoutService.GetWorkoutPlanList(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("workout-categories")] public async Task>> GetWorkoutCategories() => Response(await _workoutService.GetWorkoutCategoryList(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("workout-plans")] public async Task>> CreateWorkoutPlan([FromBody] JsonElement request) => await RequireVerifiedAsync() ?? Response(await _workoutService.CreateCustomerWorkout(request, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("chat/meal/sessions")] public async Task>> CreateMealChatSession([FromBody] GetOrCreateChatRequest request) => Response(await _chatService.GetOrCreateChatAsync(new { otherUserId = request.OtherUserId })); [CustomMiddleware] [TenantResolution] [HttpPost("chat/workout/sessions")] public async Task>> CreateWorkoutChatSession([FromBody] GetOrCreateChatRequest request) => Response(await _chatService.GetOrCreateChatAsync(new { otherUserId = request.OtherUserId })); [CustomMiddleware] [TenantResolution] [HttpPost("chat/meal/messages")] public async Task>> 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>> 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 ExecuteMeal([FromBody] ApiRequest request) { var response = await _mealFuncHandler.Execute(request, HttpContext); return StatusCode(response.StatusCode, response); } [HttpPost("workout")] [CustomMiddleware] [TenantResolution] public async Task ExecuteWorkout([FromBody] ApiRequest request) { var response = await _workoutFuncHandler.Execute(request, HttpContext); return StatusCode(response.StatusCode, response); } [HttpPost("chat")] [CustomMiddleware] [TenantResolution] public async Task ExecuteChat([FromBody] ApiRequest request) { var response = await _chatFuncHandler.Execute(request); return StatusCode(response.StatusCode, response); } [HttpPost("initChat")] [CustomMiddleware] [TenantResolution] public async Task 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>> 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>> LikeFeedPost(Guid postId) => Response(await _feedService.LikeAsync(new { postId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("feed")] public async Task>> 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>> GetTickets() => Response(await _ticketService.GetTicketByUserId(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("tickets/{ticketId:guid}")] public async Task>> GetTicketById(Guid ticketId) => Response(await _ticketService.GetTicketById(new { ticketId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpGet("ticket-categories")] public async Task>> GetTicketCategories() => Response(await _ticketService.GetTicketCategoryList(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("tickets")] public async Task>> 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>> 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>> CloseTicket(Guid ticketId) => Response(await _ticketService.CloseTicket(new { ticketId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPost("tickets/{ticketId:guid}/feedback")] public async Task>> 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>> UploadDocument([FromBody] JsonElement request) => Response(await _docService.UploadDocument(request, HttpContext)); [HttpPost("ticket")] [CustomMiddleware] [TenantResolution] public async Task ExecuteTicket([FromBody] ApiRequest request) { var response = await _ticketFuncHandler.Execute(request, HttpContext); return StatusCode(response.StatusCode, response); } [HttpPost("document")] [CustomMiddleware] [TenantResolution] public async Task 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>> GetMyNotifications() => Response(await _customerService.GetMyNotifications(new { }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPatch("customer/notifications/{notificationId:guid}/read")] public async Task>> MarkNotificationRead(Guid notificationId) => Response(await _customerService.MarkNotificationRead(notificationId, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPut("customer/push-token")] public async Task>> UpdatePushToken([FromBody] UpdatePushTokenRequest request) => Response(await _customerService.UpdatePushToken(new { pushTokenId = request.PushTokenId }, HttpContext)); [CustomMiddleware] [TenantResolution] [HttpPut("customer/profile-image")] public async Task>> UpdateProfileImage([FromBody] UpdateProfileImageRequest request) => Response(await _customerService.UpdateProfileImage(new { documentId = request.DocumentId }, HttpContext)); // Serves an uploaded document by id (mobile builds /docService/{id} for assets). [HttpGet("docService/{docId}")] [AllowAnonymous] public async Task 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}"); } } } }