using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; namespace LiteCharms.Features.TechShop.Products; public class ProductService(IDbContextFactory contextFactory) { public async ValueTask ChangeProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var price = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); if (price is null) return Result.Fail($"Could not find product price with ID {productPriceId}"); price.Active = active; price.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail($"Failed to change product price by ID {productPriceId}"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask ChangeProductStatusAsync(Guid productId, bool active, CancellationToken cancellationToken = default) { try { 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($"Could not find product with ID {productId}"); product.Active = active; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail($"Failed to change product status by ID {productId}"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) return Result.Fail($"A product by the same name '{request.Name}' already exists"); var newProduct = context.Products.Add(new Entities.Product { Name = request.Name, Summary = request.Summary, Description = request.Description, ImageUrl = request.ImageUrl, Thumbnails = request.Thumbnails, Metadata = request.Metadata }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(newProduct.Entity.Id) : Result.Fail($"Failed to create new product '{request.Name}'"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateProductPriceAsync(Guid productId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice { Price = price, Discount = discount, ProductId = productId }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(newProductPrice.Entity.Id) : Result.Fail($"Failed to create new product price for product id {productId}"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductAsync(Guid productId, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); return product is not null ? Result.Ok(product.ToModel()) : 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> GetProductPriceAsync(Guid productPriceId, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Products.AnyAsync(p => p.Id == productPriceId, cancellationToken)) return Result.Fail(new Error($"Product {productPriceId} not found.")); var productPrice = await context.ProductPrices.AsNoTracking() .OrderByDescending(pp => pp.CreatedAt) .FirstOrDefaultAsync(pp => pp.Id == productPriceId, cancellationToken); return productPrice is not null ? Result.Ok(productPrice.ToModel()) : Result.Fail(new Error($"Product price {productPriceId} not found.")); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductPricesAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var products = await context.ProductPrices.AsNoTracking() .OrderByDescending(o => o.Id) .Take(maxRecords) .ToArrayAsync(cancellationToken); return Result.Ok(products.Select(p => p.ToModel()).ToArray()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetProductsAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var products = await context.Products.AsNoTracking() .OrderByDescending(o => o.Id) .Take(maxRecords) .ToArrayAsync(cancellationToken); return Result.Ok(products.Select(p => p.ToModel()).ToArray()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> ReplaceProductPriceAsync(Guid productPriceId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var existingPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); if (existingPrice is null) return Result.Fail($"Could not find product price with ID {productPriceId}"); existingPrice.Active = false; existingPrice.UpdatedAt = DateTime.UtcNow; if (!(await context.SaveChangesAsync(cancellationToken) > 0)) return Result.Fail($"Failed to deactivate existing price of ID {productPriceId}, try again later"); var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); if (result.IsFailed) { var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); existingPrice.Active = true; existingPrice.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Fail("Reverted to old price, creation of new price failed") : Result.Fail($"Failed to reactivate price of ID {productPriceId} after new price creation failed"); } return Result.Ok(result.Value); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); if (productPrice is null) return Result.Fail($"Could not find product price with ID {productPriceId}"); productPrice.Active = active; productPrice.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail($"Failed to change product price status by ID {productPriceId}"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default) { try { 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($"Could not find product with ID {productId}"); product.Metadata = metadata; product.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail($"Failed to update product metadata by ID {productId}"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }