using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Orders; public sealed class OrderService(IDbContextFactory contextFactory) : IService { public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) return Result.Fail("Customer not found."); var order = context.Orders.Add(new Entities.Order { CustomerId = customerId, Status = OrderStatus.Pending, Total = request.TotalPrice }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(order.Entity.Id) : Result.Fail("Failed to create order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> AddItemToOrderAsync(long orderId, CreateOrderItem request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return Result.Fail("Order not found."); if(!await context.Books.AnyAsync(ab => ab.Id == request.AuthorBookId, cancellationToken)) return Result.Fail("Author book not found."); if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) return Result.Fail("Product price not found."); var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == request.ProductPriceId && i.OrderId == orderId, cancellationToken); if(existingItem is not null) { existingItem.Quantity += request.Quantity; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(existingItem.Id) : Result.Fail("Update existing order item."); } var orderItem = context.OrderItems.Add(new Entities.OrderItem { OrderId = orderId, AuthorBookId = request.AuthorBookId, ProductPriceId = request.ProductPriceId, Quantity = request.Quantity }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(orderItem.Entity.Id) : Result.Fail("Failed to add item to order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask AddItemsToOrderAsync(long orderId, CreateOrderItem[] items, CancellationToken cancellationToken = default) { try { if(items.Length == 0) return Result.Fail("No items to add."); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return Result.Fail("Order not found."); foreach (var item in items) { if (!await context.Books.AnyAsync(ab => ab.Id == item.AuthorBookId, cancellationToken)) return Result.Fail($"Author book with ID {item.AuthorBookId} not found."); if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken); if (existingItem is not null) existingItem.Quantity += item.Quantity; else context.OrderItems.Add(new Entities.OrderItem { OrderId = orderId, AuthorBookId = item.AuthorBookId, ProductPriceId = item.ProductPriceId, Quantity = item.Quantity }); await context.SaveChangesAsync(cancellationToken); } return Result.Ok(); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask RemoveItemFromOrderAsync(long orderId, long orderItemId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsDeleted = await context.OrderItems .Where(oi => oi.Id == orderItemId && oi.OrderId == orderId) .ExecuteDeleteAsync(cancellationToken); return rowsDeleted > 0 ? Result.Ok() : Result.Fail("Order item not found or failed to remove."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId) .ExecuteDeleteAsync(cancellationToken); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to clear order items."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) => await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken); public async ValueTask> GetOrderAsync(long orderId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var order = await context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); return order is not null ? Result.Ok(order.ToModel()) : Result.Fail("Order not found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetOrdersByCustomerAsync(long customerId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if(!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) return Result.Fail("Customer not found."); var orders = await context.Orders .AsNoTracking() .Where(o => o.CustomerId == customerId) .ToListAsync(cancellationToken); return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetOrdersAsync(DateRange range, int index, 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 orders = await context.Orders .AsNoTracking() .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) .Skip(index).Take(range.MaxRecords) .ToListAsync(cancellationToken); return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateOrderStatusAsync(long orderId, OrderStatus newStatus, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsUpdated = await context.Orders .Where(o => o.Id == orderId) .ExecuteUpdateAsync(setters => setters .SetProperty(o => o.Status, newStatus) .SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail("Order not found or status update failed."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return Result.Fail("Order not found."); if(!await context.Addresses.AnyAsync(a => a.Id == request.AddressId, cancellationToken)) return Result.Fail("Address not found."); if(!await context.ShippingProviders.AnyAsync(sp => sp.Id == request.ShippingProviderId && sp.Enabled, cancellationToken)) return Result.Fail("Shipping provider not found or disabled."); if(await context.Shippings.AnyAsync(s => s.OrderId == orderId, cancellationToken)) return Result.Fail("Shipping already exists for this order."); var shipping = context.Shippings.Add(new Entities.Shipping { OrderId = orderId, AddressId = request.AddressId, ShippingProviderId = request.ShippingProviderId, Status = ShippingStatuses.Pending, TrackingNumber = request.TrackingNumber }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to add shipping to order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateShippingStatusAsync(long orderId, ShippingStatuses newStatus, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsUpdated = await context.Shippings .Where(s => s.OrderId == orderId) .ExecuteUpdateAsync(setters => setters .SetProperty(s => s.Status, newStatus) .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail("Shipping not found for this order or status update failed."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async Task> GetShippingByOrderIdAsync(long orderId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var shipping = await context.Shippings .AsNoTracking() .FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); return shipping is not null ? Result.Ok(shipping.ToModel()) : Result.Fail("Shipping not found for this order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async Task RemoveShippingFromOrderAsync(long orderId, long shippingId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsDeleted = await context.Shippings .Where(s => s.Id == shippingId && s.OrderId == orderId) .ExecuteDeleteAsync(cancellationToken); return rowsDeleted > 0 ? Result.Ok() : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateShippingTrackingNumberAsync(long orderId, long shippingId, string trackingNumber, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsUpdated = await context.Shippings .Where(s => s.Id == shippingId && s.OrderId == orderId) .ExecuteUpdateAsync(setters => setters .SetProperty(s => s.TrackingNumber, trackingNumber) .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if(await context.ShippingProviders.AnyAsync(sp => sp.Type == request.Type, cancellationToken)) return Result.Fail("Shipping provider with the same type already exists."); var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider { Name = request.Name, Type = request.Type, Price = request.Price, TrackingUrl = request.TrackingUrl }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create shipping provider."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetShippingProvidersAsync(bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var providers = await context.ShippingProviders.AsNoTracking().Where(sp => sp.Enabled == isEnabled) .ToListAsync(cancellationToken); return Result.Ok(providers.Select(sp => sp.ToModel()).ToArray()); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetShippingProviderAsync(long providerId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var provider = await context.ShippingProviders.AsNoTracking() .FirstOrDefaultAsync(sp => sp.Id == providerId, cancellationToken); return provider is not null ? Result.Ok(provider.ToModel()) : Result.Fail("Shipping provider not found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateShippingProviderAsync(UpdateShippingProvider request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rowsUpdated = await context.ShippingProviders .Where(sp => sp.Id == request.ProviderId) .ExecuteUpdateAsync(setters => setters .SetProperty(sp => sp.Name, request.Name) .SetProperty(sp => sp.Price, request.Price) .SetProperty(sp => sp.TrackingUrl, request.TrackingUrl) .SetProperty(sp => sp.Enabled, request.Enabled) .SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken); return rowsUpdated > 0 ? Result.Ok() : Result.Fail("Shipping provider not found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }