using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; using Org.BouncyCastle.Asn1.Ocsp; namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var inventory = await context.Inventories .AsNoTracking() .Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId) .OrderByDescending(o => o.Id) .FirstOrDefaultAsync(cancellationToken); return inventory is not null ? Result.Ok(inventory.ToModel()) : Result.Fail("Product sold out"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var oldInventory = await context.Inventories .AsNoTracking() .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) .OrderByDescending(o => o.Id) .FirstOrDefaultAsync(cancellationToken); var newAllocation = 0; var newReservation = 0; if (oldInventory is not null) { newAllocation = oldInventory.TotalAllocated; newReservation = oldInventory.TotalReserved + request.Reservation; } else { newAllocation = 0; newReservation = request.Reservation; } if (newAllocation - newReservation < 0) return Result.Fail("Allocation failure: The requested book quantity exceeds current physical inventory availability."); var inventory = context.Inventories.Add(new Entities.ProductInventory { CreatedAt = DateTime.UtcNow, ProductId = request.ProductId, ProductPriceId = request.ProductPriceId, Status = InventoryStatuses.Reserved, TotalAllocated = newAllocation, TotalReserved = newReservation, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(inventory.Entity.Id) : Result.Fail("Failed to create inventory entry"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var oldInventory = await context.Inventories .AsNoTracking() .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) .OrderByDescending(o => o.Id) .FirstOrDefaultAsync(cancellationToken); var newAllocation = 0; var newReservation = 0; if (oldInventory is not null) { newAllocation = oldInventory.TotalAllocated + request.Allocation; newReservation = oldInventory.TotalReserved; } else { newAllocation = request.Allocation; newReservation = 0; } var inventory = context.Inventories.Add(new Entities.ProductInventory { CreatedAt = DateTime.UtcNow, ProductId = request.ProductId, ProductPriceId = request.ProductPriceId, Status = InventoryStatuses.Adjustment, TotalAllocated = newAllocation, TotalReserved = newReservation, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(inventory.Entity.Id) : Result.Fail("Failed to create inventory entry"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Products.AnyAsync(p => p.Id == productId && p.Enabled, cancellationToken)) return Result.Fail("Product does not exist"); if (!await context.Categories.AnyAsync(c => c.Id == categoryId && c.Enabled, cancellationToken)) return Result.Fail("Category does not exist"); if (await context.ProductCategories.AnyAsync(c => c.ProductId == productId && c.CategoryId == categoryId, cancellationToken)) return Result.Fail("Category already assigned to product"); context.ProductCategories.Add(new Entities.ProductCategory { ProductId = productId, CategoryId = categoryId, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Could not add category to product"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var categories = await context.ProductCategories.AsNoTracking() .Where(p => p.ProductId == productId) .OrderByDescending(o => o.Id) .Select(p => p.Category) .ToArrayAsync(cancellationToken); return categories?.Length > 0 ? Result.Ok(categories.Select(c => c!.ToModel()).ToArray()) : Result.Fail("Failed to get product categories"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask DeleteProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsDeleted = await context.ProductCategories .Where(p => p.ProductId == productId && p.CategoryId == categoryId) .ExecuteDeleteAsync(cancellationToken); return rowsDeleted > 0 ? Result.Ok() : Result.Fail("No product categories were deleted"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask DeleteAllProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsDeleted = await context.ProductCategories .Where(p => p.ProductId == productId) .ExecuteDeleteAsync(cancellationToken); return rowsDeleted > 0 ? Result.Ok() : Result.Fail("No product categories were deleted"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsUpdated = await context.Prices .Where(p => p.Id == productPriceId) .ExecuteUpdateAsync(setters => setters .SetProperty(p => p.Enabled, isEnabled) .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail(new Error($"Product price with ID {productPriceId} not found")); } 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 rowsUpdated = await context.Products .Where(p => p.Id == productId) .ExecuteUpdateAsync(setters => setters .SetProperty(p => p.Enabled, isEnabled) .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail(new Error($"Product with ID {productId} not found")); } 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.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, 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)); } } }