using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); if (productPrice is null) return Result.Fail(new Error($"Product price with ID {productPriceId} not found")); productPrice.UpdatedAt = DateTime.UtcNow; productPrice.Enabled = isEnabled; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); if (product is null) return Result.Fail(new Error($"Product with ID {productId} not found")); product.UpdatedAt = DateTime.UtcNow; product.Enabled = isEnabled; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail(new Error($"Failed to change status of product with ID {productId}")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var query = context.Products.AsQueryable(); var cultureInfo = CultureInfo.InvariantCulture; if (!string.IsNullOrWhiteSpace(filter.Name)) query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%")); if (!string.IsNullOrWhiteSpace(filter.Title)) query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); if (!string.IsNullOrWhiteSpace(filter.Category)) query = query.Where(p => p.Categories.Contains(filter.Category)); if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%")); if (filter.MinPrice > 0) query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken); return products?.Count > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) : Result.Fail("No products found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) return Result.Fail($"Product with ID {productId} does not exist."); var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); if (existingPrices.Count > 0) foreach (var existingPrice in existingPrices) { existingPrice.Enabled = false; existingPrice.UpdatedAt = DateTime.UtcNow; context.Prices.Update(existingPrice); } var price = context.Prices.Add(new Entities.ProductPrice { ProductId = productId, Amount = request.Amount, Discount = request.Discount, Enabled = true, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(price.Entity.Id) : Result.Fail("Failed to create product price."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) return Result.Fail("A product with the same name already exists."); if (request.Metadata is not null) if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) return Result.Fail("A product with the same metadata already exists."); var product = context.Products.Add(new Entities.Product { UpdatedAt = DateTime.UtcNow, Type = request.Type, Name = request.Name, Summary = request.Summary, Description = request.Description, ImageUrl = request.ImageUrl, ThumbnailUrls = request.ThumbnailUrls, Metadata = request.Metadata, Categories = request.Categories, Enabled = true }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(product.Entity.Id) : Result.Fail("Failed to create product."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var product = await context.Prices .AsNoTracking() .OrderByDescending(p => p.CreatedAt) .ThenBy(p => p.UpdatedAt) .FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken); return product is not null ? Result.Ok(product.ToModel()) : Result.Fail(new Error($"No price found for product with ID {productId}")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); return prices?.Count > 0 ? prices.Select(p => p.ToModel()).ToArray() : Result.Fail(new Error($"No prices found for product with ID {productId}")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductAsync(long productId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); return product is null ? Result.Fail(new Error($"Product with ID {productId} not found.")) : Result.Ok(product.ToModel()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default) { try { var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var products = await context.Products .AsNoTracking() .Include(p => p.Prices) .OrderByDescending(p => p.CreatedAt) .ThenByDescending(p => p.UpdatedAt) .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) .Skip(offset) .Take(range.MaxRecords) .AsSplitQuery() .ToArrayAsync(cancellationToken); return products?.Length > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) : Result.Fail(new Error("Failed to retrieve products.")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }