Split Features to create space for more projects
continuous-integration/drone/pr Build is passing

This commit is contained in:
Khwezi Mngoma
2026-05-24 13:19:09 +02:00
parent 032b9e1818
commit 70c6e0bfbc
95 changed files with 621 additions and 314 deletions
@@ -0,0 +1,15 @@
using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.TechShop.Quotes.Entities;
[EntityTypeConfiguration<QuoteConfiguration, Quote>]
public class Quote : Models.Quote
{
public virtual Customer? Customer { get; set; }
public virtual Order? Order { get; set; }
public virtual ShoppingCart? ShoppingCart { get; set; }
}
@@ -0,0 +1,33 @@
namespace LiteCharms.Features.TechShop.Quotes.Entities;
public class QuoteConfiguration : IEntityTypeConfiguration<Quote>
{
public void Configure(EntityTypeBuilder<Quote> builder)
{
builder.ToTable("Quotes");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null);
builder.Property(f => f.ExpiredAt).IsRequired(false).HasDefaultValueSql(null);
builder.Property(f => f.CustomerId).IsRequired();
builder.Property(f => f.OrderId);
builder.Property(f => f.ShoppingCartId);
builder.Property(f => f.Status).IsRequired().HasConversion<int>();
builder.Property(f => f.InvoiceUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.Reason).IsRequired(false);
builder.HasOne(q => q.Customer)
.WithMany(c => c.Quotes)
.HasForeignKey(q => q.CustomerId)
.IsRequired();
builder.HasOne(q => q.Order)
.WithOne(o => o.Quote)
.HasForeignKey<Quote>(q => q.OrderId);
builder.HasOne(q => q.ShoppingCart)
.WithOne(o => o.Quote)
.HasForeignKey<Quote>(q => q.ShoppingCartId);
}
}
@@ -0,0 +1,24 @@
namespace LiteCharms.Features.TechShop.Quotes.Models;
public class Quote
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? ExpiredAt { get; set; }
public Guid CustomerId { get; set; }
public Guid? OrderId { get; set; }
public Guid? ShoppingCartId { get; set; }
public QuoteStatus Status { get; set; }
public string? InvoiceUrl { get; set; }
public string? Reason { get; set; }
}
@@ -0,0 +1,156 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.TechShop.Quotes.Models;
namespace LiteCharms.Features.TechShop.Quotes;
public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
{
public async ValueTask<Result> AssignQuoteToOrderAsync(Guid quoteId, Guid orderId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken);
if (quote is null)
return Result.Fail(new Error($"Quote with id {orderId} not found"));
if (!await context.Orders.AnyAsync(q => q.Id == orderId, cancellationToken))
return Result.Fail(new Error($"Order with id {quoteId} not found"));
if (quote.OrderId == orderId)
return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}"));
quote.OrderId = orderId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to assign quote with id {quoteId} to order with id {orderId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> AssignQuoteToShoppingCartAsync(Guid quoteId, Guid shoppingCartId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken);
if (quote is null)
return Result.Fail(new Error($"Quote with id {quoteId} not found"));
if (!await context.ShoppingCarts.AnyAsync(q => q.Id == shoppingCartId, cancellationToken))
return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found"));
quote.ShoppingCartId = shoppingCartId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to assign quote to shopping cart"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Quote[]>> GetCustomerQuotesAsync(Guid customerId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<Quote[]>(new Error($"Customer with Id {customerId} does not exist."));
var quotes = await context.Quotes.AsNoTracking()
.Where(q => q.CustomerId == customerId).ToArrayAsync(cancellationToken);
return quotes?.Length > 0
? Result.Ok(quotes.Select(q => q.ToModel()).ToArray())
: Result.Fail<Quote[]>(new Error($"No quotes found for customer with Id {customerId}."));
}
catch (Exception ex)
{
return Result.Fail<Quote[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Quote>> GetQuoteAsync(Guid quoteId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var quote = await context.Quotes.AsNoTracking().FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken);
return quote is not null
? Result.Ok(quote.ToModel())
: Result.Fail<Quote>(new Error($"Quote with ID {quoteId} not found."));
}
catch (Exception ex)
{
return Result.Fail<Quote>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Quote[]>> GetQuotesAsync(DateRange range, CancellationToken cancellationToken = default)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var quotes = await context.Quotes.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate)
.Take(range.MaxRecords)
.ToArrayAsync(cancellationToken);
return quotes?.Length > 0
? Result.Ok(quotes.Select(o => o.ToModel()).ToArray())
: Result.Fail<Quote[]>(new Error($"No quotes found for the specified date range {range.From} - {range.To}."));
}
catch (Exception ex)
{
return Result.Fail<Quote[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateQuoteStatusAsync(Guid quoteId, QuoteStatus status, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken);
if (quote is null)
return Result.Fail(new Error("Quote not found."));
quote.Status = status;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update quote status."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}