using System.Text.Json; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; using eprintServer.Data; using eprintServer.Entities; using eprintServer.Requests; using eprintServer.Interfaces; using eprintServer.Enums; using eprintServer.Interfaces; namespace eprintServer.Services; public class UserCustomizationService : IUserCustomizationService { private readonly DataContext _context; private readonly IFileStorageService _fileService; public UserCustomizationService(DataContext context, IFileStorageService fileService) { _context = context; _fileService = fileService; } /* public async Task CreateAndSaveStepsAsync(long userId, long productId, IEnumerable steps) { var product = await _context.CustomizableProducts.FindAsync(productId); if (product == null) throw new KeyNotFoundException($"Product with ID {productId} not found"); var customization = new UserCustomization { UserId = userId, ProductId = productId, Status = CustomizationStatusEnum.InProgress }; _context.UserCustomizations.Add(customization); await _context.SaveChangesAsync(); // Save all provided steps if (steps?.Any() == true) { foreach (var stepRequest in steps) { await SaveStepAsync(customization.Id, stepRequest); } } return await GetUserCustomizationByIdAsync(customization.Id); } */ public async Task CreateAndSaveStepsAsync(long userId, long productId, IEnumerable steps) { // 1. Validate Product existence var product = await _context.CustomizableProducts.FindAsync(productId); if (product == null) throw new KeyNotFoundException($"Product with ID {productId} not found"); // 2. Initialize the Parent Entity var customization = new UserCustomization { UserId = userId, ProductId = productId, Status = CustomizationStatusEnum.InProgress, // Ensure the collection is initialized Steps = new List() }; // 3. Map the Request Steps into the Entity graph if (steps != null) { foreach (var stepRequest in steps) { var newStep = new UserCustomizationStep { StepId = stepRequest.StepId, StepOrder = stepRequest.StepOrder, StepName = stepRequest.StepName ?? string.Empty, IsCompleted = true, // Assuming if they are sent, they are completed CompletedAt = DateTime.UtcNow, // Map Field Values FieldValues = stepRequest.FieldValues?.Select(fv => new UserCustomizationFieldValue { FormElementId = fv.FormElementId, FieldName = fv.FieldName ?? string.Empty, FieldLabel = fv.FieldLabel ?? string.Empty, InputType = fv.InputType, FieldValue = fv.FieldValue ?? string.Empty }).ToList() ?? new List(), // Map Mockup if it exists UserMockup = stepRequest.UserMockup != null ? new UserMockup { PrintWidth = stepRequest.UserMockup.PrintWidth, PrintHeight = stepRequest.UserMockup.PrintHeight, FileId = stepRequest.UserMockup.FileId, ThumbnailFileId = stepRequest.UserMockup.ThumbnailFileId, Timestamp = DateTime.UtcNow } : null }; customization.Steps.Add(newStep); } } // 4. Add the entire graph to the context // EF Core will automatically handle generating IDs and Foreign Keys for children _context.UserCustomizations.Add(customization); // 5. Atomic Save: This sends one transaction to the DB await _context.SaveChangesAsync(); // 6. Return the fully loaded object return await GetUserCustomizationByIdAsync(customization.Id); } public async Task SaveStepAsync(Guid customizationId, SaveCustomizationStepRequest request) { // 1. Fetch the customization with all related data var customization = await _context.UserCustomizations .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); if (customization.Status != CustomizationStatusEnum.InProgress) throw new InvalidOperationException("Cannot save steps for non-in-progress customizations"); // 2. Find or Create the Step var existingStep = customization.Steps.FirstOrDefault(s => s.StepId == request.StepId); if (existingStep == null) { existingStep = new UserCustomizationStep { // CRITICAL: Ensure the ID is empty so EF knows it's an INSERT Id = Guid.Empty, UserCustomizationId = customizationId, StepId = request.StepId, StepOrder = request.StepOrder, StepName = request.StepName ?? string.Empty, IsCompleted = true, CompletedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; customization.Steps.Add(existingStep); // Force EF to recognize this is a NEW record _context.Entry(existingStep).State = EntityState.Added; } else { existingStep.StepName = request.StepName ?? existingStep.StepName; existingStep.StepOrder = request.StepOrder; existingStep.IsCompleted = true; existingStep.CompletedAt = DateTime.UtcNow; existingStep.UpdatedAt = DateTime.UtcNow; _context.Entry(existingStep).State = EntityState.Modified; } // 3. Save Field Values if (request.FieldValues?.Any() == true) { foreach (var fieldValue in request.FieldValues) { var existingValue = existingStep.FieldValues .FirstOrDefault(fv => fv.FormElementId == fieldValue.FormElementId); if (existingValue == null) { var newValue = new UserCustomizationFieldValue { Id = Guid.Empty, // CRITICAL: Reset the GUID from the constructor FormElementId = fieldValue.FormElementId, FieldName = fieldValue.FieldName ?? string.Empty, FieldLabel = fieldValue.FieldLabel ?? string.Empty, InputType = fieldValue.InputType, FieldValue = fieldValue.FieldValue ?? string.Empty, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; existingStep.FieldValues.Add(newValue); _context.Entry(newValue).State = EntityState.Added; } else { existingValue.FieldValue = fieldValue.FieldValue ?? existingValue.FieldValue; existingValue.UpdatedAt = DateTime.UtcNow; _context.Entry(existingValue).State = EntityState.Modified; } } } // 4. Save Mockup if (request.UserMockup != null) { if (existingStep.UserMockup == null) { existingStep.UserMockup = new UserMockup { // CRITICAL: Reset the GUID UserCustomizationStepId = existingStep.Id }; _context.Entry(existingStep.UserMockup).State = EntityState.Added; } else { _context.Entry(existingStep.UserMockup).State = EntityState.Modified; } existingStep.UserMockup.PrintWidth = request.UserMockup.PrintWidth ?? existingStep.UserMockup.PrintWidth; existingStep.UserMockup.PrintHeight = request.UserMockup.PrintHeight ?? existingStep.UserMockup.PrintHeight; existingStep.UserMockup.FileId = request.UserMockup.FileId; existingStep.UserMockup.ThumbnailFileId = request.UserMockup.ThumbnailFileId; existingStep.UserMockup.Timestamp = DateTime.UtcNow; } customization.UpdatedAt = DateTime.UtcNow; _context.Entry(customization).State = EntityState.Modified; // 5. Final Save await _context.SaveChangesAsync(); return customization; } public async Task GetLastInProgressCustomizationByUserAsync(long userId, long productId) { var customization = await _context.UserCustomizations .Where(uc => uc.UserId == userId && uc.ProductId == productId && uc.Status == CustomizationStatusEnum.InProgress) .Include(uc => uc.Product) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .OrderByDescending(uc => uc.UpdatedAt) .FirstOrDefaultAsync(); return customization; } public async Task CompleteCustomizationAsync(Guid customizationId) { var customization = await _context.UserCustomizations .Include(uc => uc.Steps) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); if (customization.Status == CustomizationStatusEnum.Completed) throw new InvalidOperationException("Customization is already completed"); customization.Status = CustomizationStatusEnum.Completed; customization.CompletedAt = DateTime.UtcNow; customization.UpdatedAt = DateTime.UtcNow; foreach (var step in customization.Steps) { step.IsCompleted = true; step.CompletedAt = DateTime.UtcNow; step.UpdatedAt = DateTime.UtcNow; } await _context.SaveChangesAsync(); return customization; } public async Task GetUserCustomizationByIdAsync(Guid customizationId) { var customization = await _context.UserCustomizations .Include(uc => uc.Product) .ThenInclude(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); return customization; } /* public async Task GetUserCustomizationByIdAsync(Guid customizationId) { var customization = await _context.UserCustomizations .Include(uc => uc.Product) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); return customization; } */ public async Task> GetUserCustomizationsByUserIdAsync(long userId) { return await _context.UserCustomizations .Where(uc => uc.UserId == userId && uc.Status == CustomizationStatusEnum.Completed) .Include(uc => uc.Product) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .OrderByDescending(uc => uc.CompletedAt) .ToListAsync(); } public async Task> GetUserCustomizationsByProductAsync(long userId, long productId) { return await _context.UserCustomizations .Where(uc => uc.UserId == userId && uc.ProductId == productId && uc.Status == CustomizationStatusEnum.Completed) .Include(uc => uc.Product) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .OrderByDescending(uc => uc.CompletedAt) .ToListAsync(); } public async Task> GetAllCompletedCustomizationsAsync() { return await _context.UserCustomizations .Where(uc => uc.Status == CustomizationStatusEnum.Completed) .Include(uc => uc.Product) .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .OrderByDescending(uc => uc.CompletedAt) .ToListAsync(); } public async Task UpdateCustomizationAsync(Guid customizationId, UpdateCustomizationRequest request) { var customization = await _context.UserCustomizations .Include(uc => uc.Steps) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); if (!string.IsNullOrEmpty(request.StepName)) { var step = customization.Steps.FirstOrDefault(s => s.StepId == request.StepId); if (step != null) { step.StepName = request.StepName; step.UpdatedAt = DateTime.UtcNow; } } if (request.TotalPrice.HasValue) customization.TotalPrice = request.TotalPrice.Value; if (request.Status.HasValue) customization.Status = request.Status.Value; customization.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return customization; } public async Task DeleteCustomizationAsync(Guid customizationId) { var customization = await _context.UserCustomizations .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .FirstOrDefaultAsync(uc => uc.Id == customizationId); if (customization == null) throw new KeyNotFoundException($"Customization with ID {customizationId} not found"); _context.UserCustomizations.Remove(customization); await _context.SaveChangesAsync(); } } /* public class UserCustomizationService { private readonly DataContext _context; private readonly IConnectionMultiplexer _redis; private readonly IFileStorageService _fileStorageService; public UserCustomizationService(IFileStorageService fileStorageService, DataContext context, IConnectionMultiplexer redis) { _context = context; _redis = redis; _fileStorageService = fileStorageService; } #region Helper Methods /// /// Safely extracts a long value from JsonElement, handling both string and number types. /// private long SafeGetInt64(JsonElement element) { return element.ValueKind == JsonValueKind.String ? long.Parse(element.GetString() ?? "0") : element.GetInt64(); } /// /// Safely extracts a string value from JsonElement. /// private string SafeGetString(JsonElement element) { return element.ValueKind == JsonValueKind.String ? element.GetString() ?? string.Empty : element.ToString(); } /// /// Safely extracts a DateTime from JsonElement, returning UtcNow if null/invalid. /// private DateTime SafeGetDateTime(JsonElement element) { if (element.ValueKind == JsonValueKind.Null) return DateTime.UtcNow; try { return element.ValueKind == JsonValueKind.String ? DateTime.Parse(element.GetString() ?? string.Empty) : element.GetDateTime(); } catch { return DateTime.UtcNow; } } /// /// Safely extracts a nullable DateTime from JsonElement. /// private DateTime? SafeGetDateTimeNullable(JsonElement element) { if (element.ValueKind == JsonValueKind.Null) return null; try { return element.ValueKind == JsonValueKind.String ? DateTime.Parse(element.GetString() ?? string.Empty) : element.GetDateTime(); } catch { return null; } } #endregion public async Task StartCustomizationAsync(int userId, StartCustomizationRequest request) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Steps.OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .FirstOrDefaultAsync(p => p.Id == request.ProductId && !p.Archive); if (product == null) throw new Exception("Product not found"); var progressId = Guid.NewGuid(); var redisKey = $"customization:{userId}:{request.ProductId}:{progressId}"; var redisData = new { userId = userId, productId = request.ProductId, progressId = progressId.ToString(), productName = product.Names.FirstOrDefault()?.Value ?? "Product", startedAt = DateTime.UtcNow, currentStepOrder = 1, totalSteps = product.Steps.Count, steps = product.Steps.ToDictionary( s => s.StepOrder.ToString(), s => new { stepId = s.Id, stepOrder = s.StepOrder, stepName = s.StepName.FirstOrDefault()?.Value ?? "Step", stepType = s.StepType.ToString(), isCompleted = false, completedAt = (DateTime?)null, fields = new Dictionary(), mockup = (object)null }) }; var db = _redis.GetDatabase(); await db.StringSetAsync(redisKey, JsonSerializer.Serialize(redisData), TimeSpan.FromDays(7)); return new { progressId = progressId, product = new { id = product.Id, name = product.Names.FirstOrDefault()?.Value, basePrice = product.BasePrice, isCanvasEnabled = product.IsCanvasEnabled, steps = product.Steps.Select(s => new { id = s.Id, order = s.StepOrder, name = s.StepName.FirstOrDefault()?.Value, type = s.StepType.ToString(), formElements = s.FormElements.Select(fe => new { id = fe.Id, label = fe.Label.FirstOrDefault()?.Value, fieldName = fe.FieldName, inputType = fe.InputType.ToString(), isRequired = fe.IsRequired, order = fe.ElementOrder, defaultValue = fe.DefaultValue, placeholder = fe.Placeholder, options = fe.Options.Select(o => new { id = o.Id, label = o.OptionLabel.FirstOrDefault()?.Value, value = o.OptionValue }) }) }) }, currentStep = 1 }; } public async Task SaveStepDataAsync(int userId, SaveStepDataRequest request) { var db = _redis.GetDatabase(); var pattern = $"customization:{userId}:*:{request.ProgressId}"; var server = _redis.GetServer(_redis.GetEndPoints().First()); var keys = server.Keys(pattern: pattern).ToList(); if (!keys.Any()) throw new Exception("Progress not found in Redis"); var redisKey = keys.First().ToString(); var existingData = await db.StringGetAsync(redisKey); if (existingData.IsNullOrEmpty) throw new Exception("Progress data not found"); var data = JsonSerializer.Deserialize>(existingData.ToString()); var stepsData = JsonSerializer.Deserialize>(data["steps"].ToString()); var stepKey = request.StepOrder.ToString(); if (!stepsData.ContainsKey(stepKey)) throw new Exception("Step not found"); var stepData = JsonSerializer.Deserialize>(stepsData[stepKey].ToString()); stepData["isCompleted"] = true; stepData["completedAt"] = DateTime.UtcNow; stepData["fields"] = request.Fields; if (request.UserMockup != null) { stepData["mockup"] = new { printWidth = request.UserMockup.PrintWidth, printHeight = request.UserMockup.PrintHeight, fileId = request.UserMockup.FileId, thumbnailFileId = request.UserMockup.ThumbnailFileId, timestamp = DateTime.UtcNow }; } stepsData[stepKey] = JsonSerializer.SerializeToElement(stepData); data["steps"] = JsonSerializer.SerializeToElement(stepsData); data["currentStepOrder"] = JsonSerializer.SerializeToElement(request.StepOrder + 1); await db.StringSetAsync(redisKey, JsonSerializer.Serialize(data), TimeSpan.FromDays(7)); return new { message = "Step data saved successfully", currentStep = request.StepOrder + 1 }; } public async Task CompleteCustomizationAsync(int userId, CompleteCustomizationRequest request) { try { var db = _redis.GetDatabase(); var pattern = $"customization:{userId}:*:{request.ProgressId}"; var server = _redis.GetServer(_redis.GetEndPoints().First()); var keys = server.Keys(pattern: pattern).ToList(); if (!keys.Any()) throw new Exception("Progress not found in Redis"); var redisKey = keys.First().ToString(); var existingData = await db.StringGetAsync(redisKey); if (existingData.IsNullOrEmpty) throw new Exception("Progress data not found in Redis"); var redisData = JsonSerializer.Deserialize>(existingData.ToString()); if (redisData == null) throw new Exception("Failed to deserialize Redis data"); var productId = SafeGetInt64(redisData["productId"]); var progressId = Guid.Parse(redisData["progressId"].GetString() ?? throw new Exception("Invalid progressId")); var stepsData = JsonSerializer.Deserialize>(redisData["steps"].ToString()); if (stepsData == null) throw new Exception("Failed to deserialize steps data"); // Check if already completed var existingCustomization = await _context.UserCustomizations .FirstOrDefaultAsync(uc => uc.ProgressId == progressId && uc.UserId == userId); if (existingCustomization != null) throw new Exception("This customization has already been completed"); // Verify product exists var product = await _context.CustomizableProducts.FindAsync(productId); if (product == null) throw new Exception($"Product with id {productId} not found"); // Create main customization record var userCustomization = new UserCustomization { UserId = userId, ProductId = productId, ProgressId = progressId, Status = CustomizationStatusEnum.Completed, StartedAt = redisData["startedAt"].GetDateTime(), CompletedAt = DateTime.UtcNow, TotalPrice = product.BasePrice }; _context.UserCustomizations.Add(userCustomization); await _context.SaveChangesAsync(); // Process each step foreach (var stepEntry in stepsData) { var stepData = stepEntry.Value; var stepOrder = int.Parse(stepEntry.Key); var stepId = stepData.TryGetProperty("stepId", out var stepIdProp) ? SafeGetInt64(stepIdProp) : 0; DateTime? completedAt = null; if (stepData.TryGetProperty("completedAt", out var completedAtProp) && completedAtProp.ValueKind != JsonValueKind.Null) { completedAt = SafeGetDateTimeNullable(completedAtProp); } // Create step var userStep = new UserCustomizationStep { UserCustomizationId = userCustomization.Id, StepId = stepId, StepOrder = stepOrder, StepName = stepData.GetProperty("stepName").GetString() ?? "Step", IsCompleted = stepData.GetProperty("isCompleted").GetBoolean(), CompletedAt = completedAt }; _context.UserCustomizationSteps.Add(userStep); await _context.SaveChangesAsync(); // Process field values for this step if (stepData.TryGetProperty("fields", out var fieldsElement) && fieldsElement.ValueKind != JsonValueKind.Null) { try { var fields = JsonSerializer.Deserialize>(fieldsElement.ToString()); if (fields != null && fields.Count > 0) { foreach (var field in fields) { if (string.IsNullOrEmpty(field.Key)) continue; var fieldValue = new UserCustomizationFieldValue { UserCustomizationStepId = userStep.Id, FormElementId = 0, FieldName = field.Key, FieldLabel = field.Key, InputType = InputTypeEnum.Text, FieldValue = field.Value ?? string.Empty }; _context.UserCustomizationFieldValues.Add(fieldValue); } await _context.SaveChangesAsync(); } } catch (Exception fieldEx) { throw new Exception($"Error processing fields for step {stepOrder}: {fieldEx.Message}"); } } // Process mockup for this step if (stepData.TryGetProperty("mockup", out var mockupElement) && mockupElement.ValueKind != JsonValueKind.Null) { try { var mockupData = JsonSerializer.Deserialize>(mockupElement.ToString()); if (mockupData != null) { var userMockup = new UserMockup { UserCustomizationStepId = userStep.Id, PrintWidth = GetOptionalDouble(mockupData, "printWidth"), PrintHeight = GetOptionalDouble(mockupData, "printHeight"), FileId = GetOptionalInt64(mockupData, "fileId"), ThumbnailFileId = GetOptionalInt64(mockupData, "thumbnailFileId"), Timestamp = GetOptionalDateTime(mockupData, "timestamp") ?? DateTime.UtcNow }; _context.UserMockups.Add(userMockup); await _context.SaveChangesAsync(); } } catch (Exception mockupEx) { throw new Exception($"Error processing mockup for step {stepOrder}: {mockupEx.Message}"); } } } // Delete from Redis after successful completion await db.KeyDeleteAsync(redisKey); return new { message = "Customization completed successfully", customizationId = userCustomization.Id, progressId = progressId }; } catch (DbUpdateException dbEx) { var errorMessage = dbEx.InnerException?.Message ?? dbEx.Message; throw new Exception($"Database error: {errorMessage}"); } catch (Exception ex) { throw; } } // Helper methods for optional fields private double? GetOptionalDouble(Dictionary data, string key) { if (!data.ContainsKey(key) || data[key].ValueKind == JsonValueKind.Null) return null; try { return data[key].GetDouble(); } catch { return null; } } private long? GetOptionalInt64(Dictionary data, string key) { if (!data.ContainsKey(key) || data[key].ValueKind == JsonValueKind.Null) return null; try { return SafeGetInt64(data[key]); } catch { return null; } } private DateTime? GetOptionalDateTime(Dictionary data, string key) { if (!data.ContainsKey(key) || data[key].ValueKind == JsonValueKind.Null) return null; try { return SafeGetDateTime(data[key]); } catch { return null; } } public async Task GetUserProgressAsync(int userId, Guid progressId) { var db = _redis.GetDatabase(); var pattern = $"customization:{userId}:*:{progressId}"; var server = _redis.GetServer(_redis.GetEndPoints().First()); var keys = server.Keys(pattern: pattern).ToList(); if (!keys.Any()) { var dbCustomization = await _context.UserCustomizations .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .FirstOrDefaultAsync(uc => uc.UserId == userId && uc.ProgressId == progressId); if (dbCustomization != null) { return new { source = "database", data = dbCustomization }; } throw new Exception("Progress not found"); } var redisKey = keys.First().ToString(); var data = await db.StringGetAsync(redisKey); return new { source = "redis", data = JsonSerializer.Deserialize(data.ToString()) }; } public async Task> GetUserCustomizationsAsync(int userId) { var db = _redis.GetDatabase(); var pattern = $"customization:{userId}:*"; var server = _redis.GetServer(_redis.GetEndPoints().First()); var redisKeys = server.Keys(pattern: pattern).ToList(); var inProgressList = new List(); foreach (var key in redisKeys) { var data = await db.StringGetAsync(key); if (!data.IsNullOrEmpty) { inProgressList.Add(JsonSerializer.Deserialize(data.ToString())); } } var completedList = await _context.UserCustomizations .Include(uc => uc.Product) .ThenInclude(p => p.Names) .Where(uc => uc.UserId == userId) .OrderByDescending(uc => uc.CompletedAt) .Select(uc => new { id = uc.Id, progressId = uc.ProgressId, productName = uc.Product.Names.FirstOrDefault() != null ? uc.Product.Names.FirstOrDefault().Value : "Unknown", status = uc.Status.ToString(), startedAt = uc.StartedAt, completedAt = uc.CompletedAt, totalPrice = uc.TotalPrice }) .ToListAsync(); return new List { new { inProgress = inProgressList, completed = completedList } }; } public async Task GetLatestProductProgressAsync(long userId, long productId) { var db = _redis.GetDatabase(); var pattern = $"customization:{userId}:{productId}:*"; var server = _redis.GetServer(_redis.GetEndPoints().First()); var keys = server.Keys(pattern: pattern).ToList(); if (!keys.Any()) { var lastDbEntry = await _context.UserCustomizations .Include(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .Where(uc => uc.UserId == userId && uc.ProductId == productId) .OrderByDescending(uc => uc.CompletedAt) .FirstOrDefaultAsync(); if (lastDbEntry != null) { return new { source = "database", data = lastDbEntry }; } throw new Exception("No progress found for this product."); } var allSessions = new List(); foreach (var key in keys) { var data = await db.StringGetAsync(key); if (!data.IsNullOrEmpty) { allSessions.Add(JsonSerializer.Deserialize(data.ToString())); } } var latestSession = allSessions .OrderByDescending(s => s.GetProperty("startedAt").GetDateTime()) .First(); var stepsObj = JsonSerializer.Deserialize>(latestSession.GetProperty("steps").ToString()); var filledSteps = stepsObj .Select(kvp => JsonSerializer.Deserialize>(kvp.Value.ToString())) .Where(s => s.ContainsKey("isCompleted") && s["isCompleted"].ValueKind == JsonValueKind.True) .OrderBy(s => s["stepOrder"].GetInt32()) .ToList(); return new { source = "redis", progressId = latestSession.GetProperty("progressId").GetString(), productId = productId, currentStepOrder = latestSession.GetProperty("currentStepOrder").GetInt32(), filledSteps = filledSteps }; } } */