using Fitness_core_BE.Models; using Fitness_core_BE.Helpers; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Exceptions; namespace Fitness_core_BE.Services.Meal { public class MealService : IMealService { private readonly IMealRepository _repository; private readonly UserRoleHelper _userRoleHelper; private readonly IUnitOfWork _uow; private readonly IActivityLogService _activityLogService; private readonly IPushNotificationService _pushNotificationService; public MealService(IMealRepository repository, UserRoleHelper userRoleHelper, IUnitOfWork uow, IActivityLogService activityLogService, IPushNotificationService pushNotificationService) { _repository = repository; _userRoleHelper = userRoleHelper; _uow = uow; _activityLogService = activityLogService; _pushNotificationService = pushNotificationService; } // Meal Category operations public async Task GetMealCategoryList(object payload, HttpContext httpContext) { try { var categories = await _repository.GetMealCategoryListAsync(); return categories; } catch { throw; } } public async Task CreateMealCategory(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var mealCategory = new MealCategory { Name = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, CreateDate = DateTime.UtcNow }; await _repository.CreateMealCategoryAsync(mealCategory); await _uow.CommitAsync(); try { await _activityLogService.LogActivity(new { description = $"Meal category created with ID {mealCategory.MealCategoryId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return mealCategory; } catch { await _uow.RollbackAsync(); throw; } } // Meal Plan operations public async Task GetMealPlanList(object payload, HttpContext httpContext) { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); string userType = await _userRoleHelper.GetUserType(userId); Guid clientId; if (userType == "Customer") { Guid cusId = await _userRoleHelper.GetCusId(userId); try { clientId = await _userRoleHelper.GetClientIdByCusId(cusId); } catch (NotFoundException) { // Not every customer has joined a gym yet — they should still see the // platform-wide admin suggestions (FKClientId == null in the repo query). clientId = Guid.Empty; } } else { clientId = await _userRoleHelper.GetClientId(userId); } try { var mealPlans = await _repository.GetMealPlanListAsync(userType, clientId); return mealPlans; } catch { throw; } } public async Task GetMealPlanById(object payload, HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!); var mealPlan = await _repository.GetMealPlanByIdAsync(mealPlanId); if (mealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found"); // Parse MealItems JSON strings to objects in nested meal items if (mealPlan.MealItems != null) { foreach (var item in mealPlan.MealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } } return mealPlan; } catch { throw new Exception("An error occurred while fetching the meal plan details."); } } public async Task GetMealPlanByCustomerId(object payload, HttpContext httpContext) { try { Guid customerId; var data = DeserializePayload.Deserialize(payload); if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { customerId = Guid.Parse(data["customerId"].GetString()!); } else { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); customerId = await _userRoleHelper.GetCusId(userId); } var mealPlans = await _repository.GetMealPlansByCustomerIdAsync(customerId); foreach (var mealPlan in mealPlans) { if (mealPlan.MealItems != null) { foreach (var item in mealPlan.MealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } } } return mealPlans; } catch { throw new Exception("An error occurred while fetching meal plans for the customer."); } } public async Task CreateMealPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid userId = DataExtractor.GetUserIdFromContext(httpContext); Guid clientId = await _userRoleHelper.GetClientId(userId); var newMealCategoryId = Guid.Parse(data["mealCategoryId"].GetString()!); if (!await _repository.MealCategoryExistsAsync(newMealCategoryId)) throw new NotFoundException("Meal category not found."); // Create meal plan var mealPlan = new MealPlan { Name = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(), FKMealCategoryId = newMealCategoryId, FKClientId = clientId, CreateDate = DateTime.UtcNow }; await _repository.CreateMealPlanAsync(mealPlan); // Handle customer assignments if provided if (data.ContainsKey("customerIds") && data["customerIds"].ValueKind == System.Text.Json.JsonValueKind.Array) { var customerIdsArray = data["customerIds"].EnumerateArray(); foreach (var customerIdElement in customerIdsArray) { if (Guid.TryParse(customerIdElement.GetString(), out var customerId)) { var customerMeal = new CustomerMeal { FKCustomerId = customerId, FKMealPlanId = mealPlan.MealPlanId, AssignedDate = DateTime.UtcNow, IsActive = true }; await _repository.AssignMealToCustomerAsync(customerMeal); } } } // Backward compatibility: single customerId field else if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { var customerId = Guid.Parse(data["customerId"].GetString()!); var customerMeal = new CustomerMeal { FKCustomerId = customerId, FKMealPlanId = mealPlan.MealPlanId, AssignedDate = DateTime.UtcNow, IsActive = true }; await _repository.AssignMealToCustomerAsync(customerMeal); } // Handle meal items if provided if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { var mealItemsArray = data["mealItems"].EnumerateArray(); foreach (var mealItemData in mealItemsArray) { // Only create meal if name is provided if (mealItemData.TryGetProperty("name", out var nameElement) && !string.IsNullOrEmpty(nameElement.GetString())) { // Parse nested mealItems JSON string? mealItemsJson = null; if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement)) { if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array) { mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement); } else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String) { mealItemsJson = mealItemsElement.GetString(); } } var meal = new Models.Meal { Name = nameElement.GetString()!, Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null, Calories = TryParseDecimal(mealItemData, "calories"), Protein = TryParseDecimal(mealItemData, "protein"), Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"), Fats = TryParseDecimal(mealItemData, "fats"), ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null, MealItems = mealItemsJson, PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null, FKMealPlanId = mealPlan.MealPlanId, CreateDate = DateTime.UtcNow }; await _repository.CreateMealAsync(meal); } } } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Meal plan created with ID {mealPlan.MealPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } // Fetch and return the created meal plan with parsed JSON var createdMealPlan = await _repository.GetMealPlanByIdAsync(mealPlan.MealPlanId); if (createdMealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlan.MealPlanId} not found after creation"); // Parse MealItems JSON strings to objects in nested meal items if (createdMealPlan.MealItems != null) { foreach (var item in createdMealPlan.MealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } } return createdMealPlan; } catch { await _uow.RollbackAsync(); throw; } } // Plans created via the AI chat flow aren't given a category by the customer — the // mobile app used to guess one by fetching the category list and taking the first // entry, which was both an extra round-trip and an arbitrary pick. Categories are a // global taxonomy (MealCategory has no FKClientId), so a single well-known "AI // Generated" category is get-or-created here and reused for every gym's AI plans. private const string AiGeneratedCategoryName = "AI Generated"; private async Task GetOrCreateAiMealCategoryIdAsync() { var categories = await _repository.GetMealCategoryListAsync(); var existing = categories.FirstOrDefault(c => string.Equals(c.Name, AiGeneratedCategoryName, StringComparison.OrdinalIgnoreCase)); if (existing != null) return existing.MealCategoryId; var created = await _repository.CreateMealCategoryAsync(new MealCategory { MealCategoryId = Guid.NewGuid(), Name = AiGeneratedCategoryName, Description = "Plans generated by the AI meal planner", IsActive = true, CreateDate = DateTime.UtcNow }); return created.MealCategoryId; } public async Task CreateCustomerMealPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid customerId; if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { customerId = Guid.Parse(data["customerId"].GetString()!); } else { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); customerId = await _userRoleHelper.GetCusId(userId); } // The plan belongs to the customer's own gym, not a fixed placeholder client. Guid clientId = await _userRoleHelper.GetClientIdByCusId(customerId); Guid mealCategoryId = data.ContainsKey("mealCategoryId") && !string.IsNullOrEmpty(data["mealCategoryId"].GetString()) ? Guid.Parse(data["mealCategoryId"].GetString()!) : await GetOrCreateAiMealCategoryIdAsync(); var mealPlan = new MealPlan { Name = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(), FKMealCategoryId = mealCategoryId, FKClientId = clientId, CreateDate = DateTime.UtcNow }; await _repository.CreateMealPlanAsync(mealPlan); if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { var mealItemsArray = data["mealItems"].EnumerateArray(); foreach (var mealItemData in mealItemsArray) { if (mealItemData.TryGetProperty("name", out var nameElement) && !string.IsNullOrEmpty(nameElement.GetString())) { string? mealItemsJson = null; if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement)) { if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array) { mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement); } else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String) { mealItemsJson = mealItemsElement.GetString(); } } var meal = new Models.Meal { Name = nameElement.GetString()!, Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null, Calories = TryParseDecimal(mealItemData, "calories"), Protein = TryParseDecimal(mealItemData, "protein"), Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"), Fats = TryParseDecimal(mealItemData, "fats"), ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null, MealItems = mealItemsJson, PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null, FKMealPlanId = mealPlan.MealPlanId, CreateDate = DateTime.UtcNow }; await _repository.CreateMealAsync(meal); } } } var customerMeal = new CustomerMeal { FKCustomerId = customerId, FKMealPlanId = mealPlan.MealPlanId, AssignedDate = DateTime.UtcNow, IsActive = true, IsFollowing = true }; await _repository.AssignMealToCustomerAsync(customerMeal); await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Customer meal plan created with ID {mealPlan.MealPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } var createdMealPlan = await _repository.GetMealPlanByIdAsync(mealPlan.MealPlanId); if (createdMealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlan.MealPlanId} not found after creation"); if (createdMealPlan.MealItems != null) { foreach (var item in createdMealPlan.MealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } } return createdMealPlan; } catch { await _uow.RollbackAsync(); throw; } } // Meal Item operations public async Task GetMealItemsByMealPlanId(object payload, HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!); var mealItems = await _repository.GetMealsByMealPlanIdAsync(mealPlanId); // Parse MealItems JSON strings to objects foreach (var item in mealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } return mealItems; } catch { throw; } } public async Task CreateMealItem(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); // Parse mealItems array and serialize to JSON string string? mealItemsJson = null; if (data.ContainsKey("mealItems")) { var mealItemsElement = data["mealItems"]; if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array) { mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement); } else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String) { mealItemsJson = mealItemsElement.GetString(); } } var mealItem = new Models.Meal { Name = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, Calories = data.ContainsKey("calories") && !string.IsNullOrEmpty(data["calories"].GetString()) ? decimal.Parse(data["calories"].GetString()!) : (decimal?)null, Protein = data.ContainsKey("protein") && !string.IsNullOrEmpty(data["protein"].GetString()) ? decimal.Parse(data["protein"].GetString()!) : (decimal?)null, Carbohydrates = data.ContainsKey("carbohydrates") && !string.IsNullOrEmpty(data["carbohydrates"].GetString()) ? decimal.Parse(data["carbohydrates"].GetString()!) : (decimal?)null, Fats = data.ContainsKey("fats") && !string.IsNullOrEmpty(data["fats"].GetString()) ? decimal.Parse(data["fats"].GetString()!) : (decimal?)null, ServingSize = data.ContainsKey("servingSize") ? data["servingSize"].GetString() : null, MealItems = mealItemsJson, PreparationNotes = data.ContainsKey("preparationNotes") ? data["preparationNotes"].GetString() : null, FKMealPlanId = Guid.Parse(data["mealPlanId"].GetString()!), CreateDate = DateTime.UtcNow }; await _repository.CreateMealAsync(mealItem); await _uow.CommitAsync(); // Log activity - non-blocking, failures won't crash the operation try { await _activityLogService.LogActivity(new { description = $"Meal item created with ID {mealItem.MealId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } // Parse mealItems JSON string to object before returning object? parsedMealItems = mealItem.MealItems; if (!string.IsNullOrEmpty(mealItem.MealItems)) { try { parsedMealItems = System.Text.Json.JsonSerializer.Deserialize(mealItem.MealItems); } catch { // Keep as string if parsing fails } } return new { mealItem.MealId, mealItem.Name, mealItem.Description, mealItem.Calories, mealItem.Protein, mealItem.Carbohydrates, mealItem.Fats, mealItem.ServingSize, MealItems = parsedMealItems, mealItem.PreparationNotes, FKMealPlanId = mealItem.FKMealPlanId, mealItem.IsActive, mealItem.CreateDate }; } catch { await _uow.RollbackAsync(); throw; } } // Update Meal Plan public async Task UpdateMealPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!); var existingMealPlan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId); if (existingMealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found"); // Update meal plan properties if (data.ContainsKey("name") && !string.IsNullOrEmpty(data["name"].GetString())) existingMealPlan.Name = data["name"].GetString()!; if (data.ContainsKey("description")) existingMealPlan.Description = data["description"].GetString(); if (data.ContainsKey("isTemplate")) existingMealPlan.IsTemplate = data["isTemplate"].GetBoolean(); if (data.ContainsKey("mealCategoryId")) existingMealPlan.FKMealCategoryId = Guid.Parse(data["mealCategoryId"].GetString()!); await _repository.UpdateMealPlanAsync(existingMealPlan); // Handle meal items update if provided if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { // Get existing meal items for this plan var existingMeals = await _repository.GetMealsByMealPlanIdAsync(mealPlanId); // Delete all existing meals (clean slate approach) foreach (var existingMeal in existingMeals) { await _repository.DeleteMealAsync(existingMeal.MealId); } // Create new meals from payload var mealItemsArray = data["mealItems"].EnumerateArray(); foreach (var mealItemData in mealItemsArray) { // Only create meal if name is provided if (mealItemData.TryGetProperty("name", out var nameElement) && !string.IsNullOrEmpty(nameElement.GetString())) { string? mealItemsJson = null; if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement)) { if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array) { mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement); } else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String) { mealItemsJson = mealItemsElement.GetString(); } } var meal = new Models.Meal { Name = nameElement.GetString()!, Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null, Calories = TryParseDecimal(mealItemData, "calories"), Protein = TryParseDecimal(mealItemData, "protein"), Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"), Fats = TryParseDecimal(mealItemData, "fats"), ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null, MealItems = mealItemsJson, PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null, FKMealPlanId = mealPlanId, CreateDate = DateTime.UtcNow }; await _repository.CreateMealAsync(meal); } } } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Meal plan updated with ID {mealPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } var updatedMealPlan = await _repository.GetMealPlanByIdAsync(mealPlanId); if (updatedMealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found after update"); // Parse MealItems JSON strings to objects in nested meal items if (updatedMealPlan.MealItems != null) { foreach (var item in updatedMealPlan.MealItems) { var itemAsString = item.MealItems as string; if (!string.IsNullOrEmpty(itemAsString)) { try { item.MealItems = System.Text.Json.JsonSerializer.Deserialize(itemAsString); } catch { // Keep as string if parsing fails } } } } return updatedMealPlan; } catch { await _uow.RollbackAsync(); throw; } } // Delete Meal Plan public async Task DeleteMealPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!); var existingMealPlan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId); if (existingMealPlan == null) throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found"); // Delete all associated meal items first (foreign key constraint) var existingMeals = await _repository.GetMealsByMealPlanIdAsync(mealPlanId); foreach (var meal in existingMeals) { await _repository.DeleteMealAsync(meal.MealId); } // Delete the meal plan await _repository.DeleteMealPlanAsync(mealPlanId); await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Meal plan deleted with ID {mealPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { Success = true, Message = $"Meal plan with ID {mealPlanId} has been deleted successfully" }; } catch { await _uow.RollbackAsync(); throw; } } // Helper method private decimal? TryParseDecimal(System.Text.Json.JsonElement element, string propertyName) { if (element.TryGetProperty(propertyName, out var prop)) { if (prop.ValueKind == System.Text.Json.JsonValueKind.Number) { return prop.GetDecimal(); } else if (prop.ValueKind == System.Text.Json.JsonValueKind.String) { var strValue = prop.GetString(); if (!string.IsNullOrEmpty(strValue) && decimal.TryParse(strValue, out var result)) { return result; } } } return null; } // Helper for JsonElement private decimal? TryParseDecimal(Dictionary data, string key) { if (!data.ContainsKey(key)) return null; var element = data[key]; if (element.ValueKind == System.Text.Json.JsonValueKind.Number) return element.GetDecimal(); if (element.ValueKind == System.Text.Json.JsonValueKind.String) { var strValue = element.GetString(); if (!string.IsNullOrEmpty(strValue) && decimal.TryParse(strValue, out var result)) return result; } return null; } // Customer Meal Assignment operations public async Task AssignMealToCustomer(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!); Guid customerId = Guid.Parse(data["customerId"].GetString()!); var customerMeal = new CustomerMeal { FKCustomerId = customerId, FKMealPlanId = mealPlanId, AssignedDate = DateTime.UtcNow, Notes = data.ContainsKey("notes") ? data["notes"].GetString() : null, IsActive = true }; await _repository.AssignMealToCustomerAsync(customerMeal); await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Meal plan {mealPlanId} assigned to customer {customerId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } try { var plan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId); var planName = plan?.Name ?? "a meal plan"; await _pushNotificationService.NotifyCustomerAsync( customerId, "New meal plan assigned", $"Your coach assigned you \"{planName}\". Tap to view.", "MealPlanAssigned", "MealPlanSuggestionDetail", new { mealPlanId } ); } catch (Exception notifyEx) { Console.WriteLine($"Push notification failed (non-critical): {notifyEx.Message}"); } return new { customerMeal.CustomerMealId, customerMeal.FKCustomerId, customerMeal.FKMealPlanId, customerMeal.AssignedDate, customerMeal.Notes, customerMeal.IsActive }; } catch { await _uow.RollbackAsync(); throw; } } public async Task UnassignMealFromCustomer(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid customerMealId = Guid.Parse(data["customerMealId"].GetString()!); var result = await _repository.UnassignMealFromCustomerAsync(customerMealId); if (!result) { throw new KeyNotFoundException($"Customer meal assignment with ID {customerMealId} not found"); } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Meal assignment {customerMealId} removed" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { success = true, message = "Meal unassigned from customer successfully" }; } catch { await _uow.RollbackAsync(); throw; } } } }