From 394429677e70bd5973d29c759b138c0b373987b4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 14:18:56 +0200 Subject: [PATCH 1/4] Added package management --- LiteCharms.Entities/ShoppingCart.cs | 2 + LiteCharms.Extensions/EntityModeMappers.cs | 46 +++++++++++++-- .../Commands/AddPackageItemsCommand.cs | 25 ++++++++ .../Commands/CreatePackageCommand.cs | 23 ++++++++ .../Commands/DeletePackageCommand.cs | 25 ++++++++ .../Commands/DeletePackageItemsCommand.cs | 16 ++++++ .../Handlers/AddPackageItemCommandHandler.cs | 38 +++++++++++++ .../Handlers/CreatePackageCommandHandler.cs | 32 +++++++++++ .../DeletePackageItemCommandHandler.cs | 32 +++++++++++ .../DeletePackageItemsCommandHandler.cs | 29 ++++++++++ .../Handlers/UpdatePackageCommandHandler.cs | 33 +++++++++++ .../UpdatePackageStatusCommandHandler.cs | 29 ++++++++++ .../Commands/UpdatePackageCommand.cs | 28 +++++++++ .../Commands/UpdatePackageStatusCommand.cs | 22 +++++++ .../Queries/GetPackageItemsQuery.cs | 18 ++++++ .../CartPackages/Queries/GetPackageQuery.cs | 18 ++++++ .../CartPackages/Queries/GetPackagesQuery.cs | 33 +++++++++++ .../Handlers/GetPackageItemsQueryHandler.cs | 32 +++++++++++ .../Handlers/GetPackageQueryHandler.cs | 27 +++++++++ .../Handlers/GetPackagesQueryHandler.cs | 35 ++++++++++++ .../Commands/CreateNotificationCommand.cs | 57 ++++++++++++------- .../CreateNotificationCommandHandler.cs | 25 ++++---- .../Queries/Handlers/GetOrdersQueryHandler.cs | 1 + .../AddPackageToShoppingCartCommand.cs | 25 ++++++++ .../AddPackageToShoppingCartCommandHandler.cs | 39 +++++++++++++ ...vePackageFromShoppingCartCommandHandler.cs | 35 ++++++++++++ .../RemovePackageFromShoppingCartCommand.cs | 25 ++++++++ .../Queries/GetShoppingCartPackagesQuery.cs | 18 ++++++ .../GetShoppingCartPackagesQueryHandler.cs | 32 +++++++++++ 29 files changed, 765 insertions(+), 35 deletions(-) create mode 100644 LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs diff --git a/LiteCharms.Entities/ShoppingCart.cs b/LiteCharms.Entities/ShoppingCart.cs index 3c974dc..ead1175 100644 --- a/LiteCharms.Entities/ShoppingCart.cs +++ b/LiteCharms.Entities/ShoppingCart.cs @@ -12,4 +12,6 @@ public class ShoppingCart : Models.ShoppingCart public virtual Quote? Quote { get; set; } public virtual ICollection? ShoppingCartItems { get; set; } + + public virtual ICollection? Packages { get; set; } } diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index b3b8e64..37ad38e 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -4,6 +4,36 @@ namespace LiteCharms.Extensions; public static class EntityModeMappers { + public static ShoppingCartPackage ToModel(this Entities.ShoppingCartPackage entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ShoppingCartId = entity.ShoppingCartId + }; + + public static PackageItem ToModel(this Entities.PackageItem entity) => + new() + { + Id = entity.Id, + Active = entity.Active, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ProductPriceId = entity.ProductPriceId + }; + + public static Package ToModel(this Entities.Package entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Active = entity.Active, + Description = entity.Description, + Name = entity.Name, + UpdatedAt = entity.UpdatedAt + }; + public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) => new() { @@ -36,7 +66,7 @@ public static class EntityModeMappers ExpiredAt = entity.ExpiredAt, Reason = entity.Reason, ShoppingCartId = entity.ShoppingCartId, - Status = entity.Status + Status = entity.Status }; public static Notification ToModel(this Entities.Notification entity) => @@ -53,7 +83,12 @@ public static class EntityModeMappers Platform = entity.Platform, Recipient = entity.Recipient, Subject = entity.Subject, - Processed = entity.Processed + Processed = entity.Processed, + SenderName = entity.SenderName, + RecipientAddress = entity.RecipientAddress, + Priority = entity.Priority, + UpdatedAt = entity?.UpdatedAt, + IsHtml = entity!.IsHtml }; public static Customer ToModel(this Entities.Customer entity) => @@ -78,7 +113,7 @@ public static class EntityModeMappers Slack = entity.Slack, Tax = entity.Tax, Website = entity.Website, - Whatsapp = entity.Whatsapp + Whatsapp = entity.Whatsapp }; public static Lead ToModel(this Entities.Lead entity) => @@ -113,7 +148,10 @@ public static class EntityModeMappers RefundId = entity.RefundId, QuoteId = entity.QuoteId, Status = entity.Status, - ShoppingCartId = entity.ShoppingCartId + ShoppingCartId = entity.ShoppingCartId, + DepositRequired = entity.DepositRequired, + Requirements = entity.Requirements, + Terms = entity.Terms }; public static OrderRefund ToModel(this Entities.OrderRefund entity) => diff --git a/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs b/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs new file mode 100644 index 0000000..be87a47 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class AddPackageItemCommand : IRequest> +{ + public Guid PackageId { get; set; } + + public Guid ProductPriceId { get; set; } + + private AddPackageItemCommand(Guid packageId, Guid productPriceId) + { + PackageId = packageId; + ProductPriceId = productPriceId; + } + + public static AddPackageItemCommand Create(Guid packageId, Guid productPriceId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package id is required", nameof(packageId)); + + if (productPriceId == Guid.Empty) + throw new ArgumentException("Product price id is required", nameof(productPriceId)); + + return new(packageId, productPriceId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs new file mode 100644 index 0000000..4a86846 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class CreatePackageCommand : IRequest> +{ + public string? Name { get; set; } + + public string? Description { get; set; } + + private CreatePackageCommand(string? name, string? description) + { + Name = name; + Description = description; + } + + public static CreatePackageCommand Create(string? name, string? description) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + ArgumentException.ThrowIfNullOrWhiteSpace(description, nameof(description)); + + return new(name, description); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs new file mode 100644 index 0000000..5957dce --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class DeletePackageItemCommand : IRequest +{ + public Guid PackageId { get; set; } + + public Guid PackageItemId { get; set; } + + private DeletePackageItemCommand(Guid packageId, Guid packageItemId) + { + PackageId = packageId; + PackageItemId = packageItemId; + } + + public static DeletePackageItemCommand Create(Guid packageId, Guid packageItemId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package id is required", nameof(packageId)); + + if (packageItemId == Guid.Empty) + throw new ArgumentException("Product price id is required", nameof(packageItemId)); + + return new(packageId, packageItemId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs b/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs new file mode 100644 index 0000000..c9aa3e0 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class DeletePackageItemsCommand : IRequest +{ + public Guid PackageId { get; set; } + + private DeletePackageItemsCommand(Guid packageId) => PackageId = packageId; + + public static DeletePackageItemsCommand Create(Guid packageId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs new file mode 100644 index 0000000..ffdd24a --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs @@ -0,0 +1,38 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class AddPackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(AddPackageItemCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + if (!await context.ProductPrices.AnyAsync(p => p.Id == request.ProductPriceId && p.Active == true, cancellationToken)) + return Result.Fail($"Could not find an active product price by ID {request.ProductPriceId}"); + + if (await context.PackageItems.AnyAsync(p => p.ProductPriceId == request.ProductPriceId && p.PackageId == request.PackageId, cancellationToken)) + return Result.Fail($"Product price {request.ProductPriceId} is already added to this package {request.PackageId}"); + + var newPackageItem = context.PackageItems.Add(new Entities.PackageItem + { + PackageId = request.PackageId, + ProductPriceId = request.ProductPriceId, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackageItem.Entity.Id) + : Result.Fail($"Failed to add new package item by ID {request.ProductPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs new file mode 100644 index 0000000..4945a73 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class CreatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(CreatePackageCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {request.Name}"); + + var newPackage = context.Packages.Add(new Entities.Package + { + Name = request.Name, + Description = request.Description, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackage.Entity.Id) + : Result.Fail($"Failed to create a new package by the name: {request.Name}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs new file mode 100644 index 0000000..5ec6745 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class DeletePackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(DeletePackageItemCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + var item = await context.PackageItems.FirstOrDefaultAsync(p => p.Id == request.PackageItemId && p.PackageId == request.PackageId, cancellationToken); + + if(item is null) + return Result.Fail($"Product item {request.PackageItemId} is already added to this package {request.PackageId}"); + + context.PackageItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package item by id {request.PackageItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs new file mode 100644 index 0000000..8f4e8e7 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs @@ -0,0 +1,29 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class DeletePackageItemsCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(DeletePackageItemsCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + var items = await context.PackageItems.Where(i => i.PackageId == request.PackageId).ToArrayAsync(cancellationToken); + + context.PackageItems.RemoveRange(items); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package {request.PackageId} items"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs new file mode 100644 index 0000000..7889c52 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs @@ -0,0 +1,33 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class UpdatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(UpdatePackageCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {request.Name}"); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {request.PackageId}"); + + package.Name = request.Name; + package.Description = request.Description; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs new file mode 100644 index 0000000..5ef4a91 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs @@ -0,0 +1,29 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class UpdatePackageStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(UpdatePackageStatusCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {request.PackageId}"); + + package.Active = request.Active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs new file mode 100644 index 0000000..938ca44 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class UpdatePackageCommand : IRequest +{ + public Guid PackageId { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + private UpdatePackageCommand(Guid packageId, string? name, string? description) + { + PackageId = packageId; + Name = name; + Description = description; + } + + public static UpdatePackageCommand Create(Guid packageId, string? name, string? description) + { + if (packageId == Guid.Empty) + throw new ArgumentException($"Package ID is required", nameof(packageId)); + + ArgumentNullException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(description, nameof(description)); + + return new(packageId, name, description); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs b/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs new file mode 100644 index 0000000..7be4651 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class UpdatePackageStatusCommand : IRequest +{ + public Guid PackageId { get; set; } + + public bool Active { get; set; } + + private UpdatePackageStatusCommand(Guid packageId, bool active) + { + PackageId = packageId; + Active = active; + } + + public static UpdatePackageStatusCommand Create(Guid packageId, bool active) + { + if(packageId == Guid.Empty) + throw new ArgumentException($"Package id is required", nameof(packageId)); + + return new(packageId, active); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs new file mode 100644 index 0000000..e0830af --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackageItemsQuery : IRequest> +{ + public Guid PackageId { get; set; } + + private GetPackageItemsQuery(Guid packageId) => PackageId = packageId; + + public static GetPackageItemsQuery Create(Guid packageId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs new file mode 100644 index 0000000..1384783 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackageQuery : IRequest> +{ + public Guid PackageId { get; set; } + + private GetPackageQuery(Guid packageId) => PackageId = packageId; + + public static GetPackageQuery Create(Guid packageId) + { + if(packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs new file mode 100644 index 0000000..351a141 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs @@ -0,0 +1,33 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackagesQuery : IRequest> +{ + public DateOnly From { get; set; } + + public DateOnly To { get; set; } + + public int MaxRecords { get; set; } + + public bool Active { get; set; } + + private GetPackagesQuery(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) + { + From = from; + To = to; + MaxRecords = maxRecords; + Active = active; + } + + public static GetPackagesQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) + { + if (from > to) + throw new ArgumentException("From date cannot be greater than To date."); + + if (maxRecords <= 0) + throw new ArgumentException("MaxRecords must be a positive integer."); + + return new(from, to, maxRecords, active); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs new file mode 100644 index 0000000..487b203 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackageItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackageItemsQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Package could not be found with ID {request.PackageId}"); + + var items = await context.PackageItems.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.PackageId == request.PackageId) + .ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Could not find package items by package ID {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs new file mode 100644 index 0000000..7a01fb5 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs @@ -0,0 +1,27 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackageQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackageQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + return package is not null + ? Result.Ok(package.ToModel()) + : Result.Fail($"Failed to find package by ID {request.PackageId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs new file mode 100644 index 0000000..b495bb0 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs @@ -0,0 +1,35 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackagesQuery request, CancellationToken cancellationToken) + { + try + { + var fromDate = request.From.ToDateTime(TimeOnly.MinValue); + var toDate = request.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var packages = await context.Packages + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Where(p => p.Active == request.Active) + .Take(request.MaxRecords) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No packages found for the specified date range {request.From} - {request.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs index be81438..a9cf063 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs @@ -6,15 +6,21 @@ public class CreateNotificationCommand : IRequest> { public NotificationDirection Direction { get; set; } - public string? Author { get; set; } + public string? Sender { get; set; } - public string? Title { get; set; } + public string? SenderAddress { get; set; } - public string? Description { get; set; } + public string? Subject { get; set; } + + public string? Message { get; set; } public NotificationPlatforms Platform { get; set; } - public string? PlatformAddress { get; set; } + public Priorities Priority { get; set; } + + public string? Recipient { get; set; } + + public string? RecipientAddress { get; set; } public string? CorrelationId { get; set; } @@ -22,39 +28,48 @@ public class CreateNotificationCommand : IRequest> public bool IsInternal { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + public bool IsHtml { get; set; } + + private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) { Direction = direction; - Author = author; - Title = title; - Description = description; + Sender = sender; + SenderAddress = senderAddress; + Subject = subject; + Message = message; Platform = platform; - PlatformAddress = platformAddress; + Priority = priority; + Recipient = recipient; + RecipientAddress = recipientAddress; CorrelationId = correlationId; CorrelationIdType = correlationIdType; IsInternal = isInternal; + IsHtml = isHtml; } - public static CreateNotificationCommand Create(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) { - if (string.IsNullOrWhiteSpace(author)) - throw new ArgumentException("Author cannot be null or whitespace.", nameof(author)); + if (string.IsNullOrWhiteSpace(sender)) + throw new ArgumentException("Sender name is required.", nameof(sender)); - if (string.IsNullOrWhiteSpace(title)) - throw new ArgumentException("Title cannot be null or whitespace.", nameof(title)); + if (string.IsNullOrWhiteSpace(subject)) + throw new ArgumentException("Subject is required.", nameof(subject)); - if (string.IsNullOrWhiteSpace(description)) - throw new ArgumentException("Description cannot be null or whitespace.", nameof(description)); + if (string.IsNullOrWhiteSpace(message)) + throw new ArgumentException("Message is required.", nameof(message)); - if (string.IsNullOrWhiteSpace(platformAddress)) - throw new ArgumentException("PlatformAddress cannot be null or whitespace.", nameof(platformAddress)); + if (string.IsNullOrWhiteSpace(recipient)) + throw new ArgumentException("Recipient name is required.", nameof(recipient)); + + if (string.IsNullOrWhiteSpace(recipientAddress)) + throw new ArgumentException("Recipient address is required.", nameof(recipientAddress)); if (string.IsNullOrWhiteSpace(correlationId)) - throw new ArgumentException("CorrelationId cannot be null or whitespace.", nameof(correlationId)); + throw new ArgumentException("CorrelationId is required.", nameof(correlationId)); if (string.IsNullOrWhiteSpace(correlationIdType)) - throw new ArgumentException("CorrelationIdType cannot be null or whitespace.", nameof(correlationIdType)); + throw new ArgumentException("CorrelationIdType is required.", nameof(correlationIdType)); - return new(direction, author, title, description, platform, platformAddress, correlationId, correlationIdType, isInternal); + return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml); } } diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs index d33fad6..e7a3fb4 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs @@ -6,29 +6,34 @@ public class CreateNotificationCommandHandler(IDbContextFactory c { public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) { - try - { + try + { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var newNotification = context.Notifications.Add(new Entities.Notification { Direction = request.Direction, - Sender = request.Author, - Subject = request.Title, - Message = request.Description, + SenderName = request.Sender, + Sender = request.SenderAddress, + Recipient = request.Recipient, + RecipientAddress = request.RecipientAddress, + Subject = request.Subject, + Message = request.Message, Platform = request.Platform, - Recipient = request.PlatformAddress, + Priority = request.Priority, CorrelationId = request.CorrelationId, CorrelationIdType = request.CorrelationIdType, IsInternal = request.IsInternal, + IsHtml = request.IsHtml, + Processed = false }); - return newNotification is not null - ? Result.Ok(newNotification.Entity.Id) + return newNotification is not null + ? Result.Ok(newNotification.Entity.Id) : Result.Fail(new Error("Failed to create notification")); } - catch (Exception ex) - { + catch (Exception ex) + { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs index 8276bb4..f244869 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs @@ -16,6 +16,7 @@ public class GetOrdersQueryHandler(IDbContextFactory contextFacto using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var orders = await context.Orders + .AsNoTracking() .OrderByDescending(o => o.CreatedAt) .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) .Take(request.MaxRecords) diff --git a/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs new file mode 100644 index 0000000..81e8bac --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.ShoppingCarts.Commands; + +public class AddPackageToShoppingCartCommand : IRequest +{ + public Guid ShoppingCartId { get; set; } + + public Guid PackageId { get; set; } + + private AddPackageToShoppingCartCommand(Guid shoppingCartId, Guid packageId) + { + ShoppingCartId = shoppingCartId; + PackageId = packageId; + } + + public static AddPackageToShoppingCartCommand Create(Guid shoppingCartId, Guid packageId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); + + if (packageId == Guid.Empty) + throw new ArgumentException($"Package ID is required", nameof(packageId)); + + return new(shoppingCartId, packageId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs new file mode 100644 index 0000000..67e7949 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs @@ -0,0 +1,39 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; + +public class AddPackageToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(AddPackageToShoppingCartCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Package cold not be found by ID {request.PackageId}"); + + var shoppingCart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); + + if (shoppingCart is null) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(cp => cp.ShoppingCartId == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Package {request.PackageId} is already in the cart"); + + var newShoppingCartPackage = context.ShoppingCartPackages.Add(new Entities.ShoppingCartPackage + { + ShoppingCartId = request.ShoppingCartId, + PackageId = request.PackageId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not add package of id {request.PackageId} to shopping cart {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs new file mode 100644 index 0000000..6294199 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs @@ -0,0 +1,35 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; + +public class RemovePackageFromShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(RemovePackageFromShoppingCartCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(p => p.Id == request.ShoppingCartPackageId, cancellationToken)) + return Result.Fail($"Shopping cart package {request.ShoppingCartPackageId} is not in the shopping cart {request.ShoppingCartId}"); + + var shoppingCartPackage = await context.ShoppingCartPackages.FirstOrDefaultAsync(cp => cp.Id == request.ShoppingCartPackageId, cancellationToken); + + if (shoppingCartPackage is null) + return Result.Ok(); + + context.ShoppingCartPackages.Remove(shoppingCartPackage!); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could remove package of id {request.ShoppingCartPackageId} from shopping cart {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs new file mode 100644 index 0000000..6aa8f25 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.ShoppingCarts.Commands; + +public class RemovePackageFromShoppingCartCommand : IRequest +{ + public Guid ShoppingCartId { get; set; } + + public Guid ShoppingCartPackageId { get; set; } + + private RemovePackageFromShoppingCartCommand(Guid shoppingCartId, Guid shoppingCartPackageId) + { + ShoppingCartId = shoppingCartId; + ShoppingCartPackageId = shoppingCartPackageId; + } + + public static RemovePackageFromShoppingCartCommand Create(Guid shoppingCartId, Guid shoppingCartPackageId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); + + if (shoppingCartPackageId == Guid.Empty) + throw new ArgumentException($"Shopping cart Package ID is required", nameof(shoppingCartPackageId)); + + return new(shoppingCartId, shoppingCartPackageId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs new file mode 100644 index 0000000..2f64359 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.ShoppingCarts.Queries; + +public class GetShoppingCartPackagesQuery : IRequest> +{ + public Guid ShoppingCartId { get; set; } + + private GetShoppingCartPackagesQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; + + public static GetShoppingCartPackagesQuery Create(Guid shoppingCartId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException("Shopping cart id is required", nameof(shoppingCartId)); + + return new(shoppingCartId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs new file mode 100644 index 0000000..d6727ee --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; + +public class GetShoppingCartPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetShoppingCartPackagesQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + var packages = await context.ShoppingCartPackages.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(cp => cp.ShoppingCartId == request.ShoppingCartId) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(p => p.ToModel()).ToArray()) + : Result.Fail($"Could not find packaged in shopping cart by ID {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} From e8e9a85c57f2de17c47a6299d296773464ec3590 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 15:27:26 +0200 Subject: [PATCH 2/4] Migrated database changes after refactoring the Notification model --- .../NotificationConfiguration.cs | 14 +- .../Configuration/QuoteConfiguration.cs | 2 +- LiteCharms.Extensions/EntityModeMappers.cs | 4 +- .../Handlers/SendEmailCommandHandler.cs | 5 +- .../Commands/SendEmailCommand.cs | 2 +- .../Handlers/CreateLeadCommandHandler.cs | 2 +- .../LiteCharms.Features.csproj | 4 + .../UpdateNotificationCommandHandler.cs | 6 + .../Commands/UpdateNotificationCommand.cs | 10 +- .../Orders/Commands/CreateOrderCommand.cs | 18 +- .../Handlers/CreateOrderCommandHandler.cs | 10 +- .../{ => Hash}/Commands/ComputeHashCommand.cs | 2 +- .../Handlers/ComputeHashCommandHandler.cs | 4 +- .../20260510090446_Init.Designer.cs | 631 ------------------ .../20260510091540_AddedPackages.cs | 114 ---- ...ner.cs => 20260510132008_Init.Designer.cs} | 31 +- ...0090446_Init.cs => 20260510132008_Init.cs} | 112 +++- .../Migrations/ShopDbContextModelSnapshot.cs | 27 +- LiteCharms.Models/Enums.cs | 15 + LiteCharms.Models/Notification.cs | 8 +- 20 files changed, 244 insertions(+), 777 deletions(-) rename LiteCharms.Features/{Utilities => Email}/Commands/Handlers/SendEmailCommandHandler.cs (94%) rename LiteCharms.Features/{Utilities => Email}/Commands/SendEmailCommand.cs (98%) rename LiteCharms.Features/Utilities/{ => Hash}/Commands/ComputeHashCommand.cs (86%) rename LiteCharms.Features/Utilities/{ => Hash}/Commands/Handlers/ComputeHashCommandHandler.cs (80%) delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs rename LiteCharms.Infrastructure/Database/Migrations/{20260510091540_AddedPackages.Designer.cs => 20260510132008_Init.Designer.cs} (96%) rename LiteCharms.Infrastructure/Database/Migrations/{20260510090446_Init.cs => 20260510132008_Init.cs} (78%) diff --git a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs index bd103f7..45319a4 100644 --- a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs +++ b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Entities.Configuration; +using LiteCharms.Models; + +namespace LiteCharms.Entities.Configuration; public class NotificationConfiguration : IEntityTypeConfiguration { @@ -9,18 +11,20 @@ public class NotificationConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.Direction).IsRequired(); - builder.Property(f => f.Platform).IsRequired(); - builder.Property(f => f.Priority).IsRequired(); + builder.Property(f => f.Direction).IsRequired().HasConversion(); + builder.Property(f => f.Platform).IsRequired().HasConversion(); + builder.Property(f => f.Priority).IsRequired().HasConversion(); + builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion(); builder.Property(f => f.Sender).IsRequired(); builder.Property(f => f.Subject).IsRequired(); builder.Property(f => f.Message).IsRequired(); builder.Property(f => f.Recipient).IsRequired(); builder.Property(f => f.RecipientAddress).IsRequired(); builder.Property(f => f.CorrelationId).IsRequired(); - builder.Property(f => f.CorrelationIdType).IsRequired(); builder.Property(f => f.IsHtml).HasDefaultValue(false); builder.Property(f => f.IsInternal).HasDefaultValue(true); builder.Property(f => f.Processed).HasDefaultValue(false); + builder.Property(f => f.HasError).HasDefaultValue(false); + builder.Property(f => f.Errors).HasColumnType("jsonb").IsRequired(false); } } \ No newline at end of file diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs index de5240a..d48a768 100644 --- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs +++ b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs @@ -11,7 +11,7 @@ public class QuoteConfiguration : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.ExpiredAt).IsRequired(false); builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.Status).IsRequired().HasConversion(); builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.Reason).IsRequired(false); diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index 37ad38e..9a1d97a 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -88,7 +88,9 @@ public static class EntityModeMappers RecipientAddress = entity.RecipientAddress, Priority = entity.Priority, UpdatedAt = entity?.UpdatedAt, - IsHtml = entity!.IsHtml + IsHtml = entity!.IsHtml, + HasError = entity.HasError, + Errors = entity.Errors }; public static Customer ToModel(this Entities.Customer entity) => diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs similarity index 94% rename from LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs rename to LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs index e3fcdf7..30f86dc 100644 --- a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs +++ b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs @@ -1,6 +1,7 @@ -using LiteCharms.Models.Configuraton.Email; +using LiteCharms.Features.Email.Commands; +using LiteCharms.Models.Configuraton.Email; -namespace LiteCharms.Features.Utilities.Commands.Handlers; +namespace LiteCharms.Features.Email.Commands.Handlers; public class SendEmailCommandHandler(IOptions smtpOptions) : IRequestHandler { diff --git a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs similarity index 98% rename from LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs rename to LiteCharms.Features/Email/Commands/SendEmailCommand.cs index 4393edd..972e496 100644 --- a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs +++ b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Utilities.Commands; +namespace LiteCharms.Features.Email.Commands; public class SendEmailCommand : IRequest { diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs index bc8295b..256bc22 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.Utilities.Commands; +using LiteCharms.Features.Utilities.Hash.Commands; using LiteCharms.Infrastructure.Database; namespace LiteCharms.Features.Leads.Commands.Handlers; diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 9a2a0ac..6f9f146 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -61,4 +61,8 @@ + + + + diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs index 066638a..5ce4e81 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs @@ -17,6 +17,12 @@ public class UpdateNotificationCommandHandler(IDbContextFactory c notification.Processed = request.Processed; + if (request.HasError) + { + notification.HasError = request.HasError; + notification.Errors = request.Errors; + } + return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); diff --git a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs index d5961f2..950442d 100644 --- a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs @@ -6,13 +6,19 @@ public class UpdateNotificationCommand : IRequest public bool Processed { get; set; } - private UpdateNotificationCommand(Guid notificationId, bool processed) + public bool HasError { get; set; } + + public string[]? Errors { get; set; } + + private UpdateNotificationCommand(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) { NotificationId = notificationId; Processed = processed; + HasError = hasError; + Errors = errors; } - public static UpdateNotificationCommand Create(Guid notificationId, bool processed) + public static UpdateNotificationCommand Create(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) { if(notificationId == Guid.Empty) throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId)); diff --git a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs index e18258e..8baf7e1 100644 --- a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs +++ b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs @@ -8,14 +8,26 @@ public class CreateOrderCommand : IRequest> public Guid? QuoteId { get; set; } - private CreateOrderCommand(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) + public string[]? Requirements { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public bool DepositRequired { get; set; } + + private CreateOrderCommand(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) { CustomerId = customerId; ShoppingCartId = shoppingCartId; + DepositRequired = depositRequired; QuoteId = quoteId; + Requirements = requirements; + Notes = notes; + Terms = terms; } - public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) + public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) { if (customerId == Guid.Empty) throw new ArgumentException("CustomerId is required.", nameof(customerId)); @@ -23,6 +35,6 @@ public class CreateOrderCommand : IRequest> if (shoppingCartId == Guid.Empty) throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId)); - return new(customerId, shoppingCartId, quoteId); + return new(customerId, shoppingCartId, depositRequired, quoteId, requirements, notes, terms); } } diff --git a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs index f06e53b..b97f83e 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs @@ -1,4 +1,5 @@ using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; namespace LiteCharms.Features.Orders.Commands.Handlers; @@ -21,10 +22,15 @@ public class CreateOrderCommandHandler(IDbContextFactory contextF var newOrder = context.Orders.Add(new Entities.Order { + CreatedAt = DateTime.UtcNow, + Status = OrderStatus.Pending, CustomerId = request.CustomerId, - ShoppingCartId = request.ShoppingCartId, QuoteId = request.QuoteId, - CreatedAt = DateTime.UtcNow + ShoppingCartId = request.ShoppingCartId, + DepositRequired = request.DepositRequired, + Requirements = request.Requirements, + Notes = request.Notes, + Terms = request.Terms }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs b/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs similarity index 86% rename from LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs rename to LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs index ef235fc..252a9e7 100644 --- a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs +++ b/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Utilities.Commands; +namespace LiteCharms.Features.Utilities.Hash.Commands; public class ComputeHashCommand : IRequest> { diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs b/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs similarity index 80% rename from LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs rename to LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs index d4ef5b4..013ab03 100644 --- a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs +++ b/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Utilities.Commands.Handlers; +using LiteCharms.Features.Utilities.Hash.Commands; + +namespace LiteCharms.Features.Utilities.Hash.Commands.Handlers; public class ComputeHashCommandHandler : IRequestHandler> { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs deleted file mode 100644 index 6ae5382..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs +++ /dev/null @@ -1,631 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260510090446_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Notification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("IsHtml") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("IsInternal") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Message") - .IsRequired() - .HasColumnType("text"); - - b.Property("Platform") - .HasColumnType("integer"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("Processed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("Recipient") - .IsRequired() - .HasColumnType("text"); - - b.Property("RecipientAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sender") - .IsRequired() - .HasColumnType("text"); - - b.Property("SenderName") - .HasColumnType("text"); - - b.Property("Subject") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Notification", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("DepositRequired") - .HasColumnType("boolean"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Requirements") - .HasColumnType("jsonb"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.PrimitiveCollection("Terms") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("CustomerId1") - .HasColumnType("uuid"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Reason") - .HasColumnType("text"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("CustomerId1"); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - - b.ToTable("Quote", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("ShoppingCart", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductPriceId"); - - b.HasIndex("ShoppingCartId"); - - b.ToTable("ShoppingCartItems"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("ShoppingCarts") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => - { - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithMany("ShoppingCartItems") - .HasForeignKey("ShoppingCartId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ProductPrice"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - - b.Navigation("Quotes"); - - b.Navigation("ShoppingCarts"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.Navigation("Order"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCartItems"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs deleted file mode 100644 index 351ac6b..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedPackages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Package", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Package", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "PackageItem", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PackageId1 = table.Column(type: "uuid", nullable: true), - PackageId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PackageItem", x => x.Id); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId", - column: x => x.PackageId, - principalTable: "Package", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId1", - column: x => x.PackageId1, - principalTable: "Package", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ShoppingCartPackage", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - PackageId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCartPackage_Package_PackageId", - column: x => x.PackageId, - principalTable: "Package", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_PackageItem_PackageId", - table: "PackageItem", - column: "PackageId"); - - migrationBuilder.CreateIndex( - name: "IX_PackageItem_PackageId1", - table: "PackageItem", - column: "PackageId1"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartPackage_PackageId", - table: "ShoppingCartPackage", - column: "PackageId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartPackage_ShoppingCartId", - table: "ShoppingCartPackage", - column: "ShoppingCartId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PackageItem"); - - migrationBuilder.DropTable( - name: "ShoppingCartPackage"); - - migrationBuilder.DropTable( - name: "Package"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs similarity index 96% rename from LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs index f83fabb..4624007 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { [DbContext(typeof(ShopDbContext))] - [Migration("20260510091540_AddedPackages")] - partial class AddedPackages + [Migration("20260510132008_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -171,9 +171,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -182,6 +181,14 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsHtml") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -339,12 +346,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("ShoppingCartId"); + b.ToTable("Package", (string)null); }); @@ -614,6 +626,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.HasOne("LiteCharms.Entities.ShoppingCart", null) + .WithMany("Packages") + .HasForeignKey("ShoppingCartId"); + }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") @@ -746,6 +765,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.Navigation("Order"); + b.Navigation("Packages"); + b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs similarity index 78% rename from LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs index 3bc9554..773f0d6 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs @@ -51,6 +51,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations Direction = table.Column(type: "integer", nullable: false), Platform = table.Column(type: "integer", nullable: false), Priority = table.Column(type: "integer", nullable: false), + CorrelationIdType = table.Column(type: "integer", nullable: false), Sender = table.Column(type: "text", nullable: false), SenderName = table.Column(type: "text", nullable: true), Subject = table.Column(type: "text", nullable: false), @@ -58,10 +59,11 @@ namespace LiteCharms.Infrastructure.Database.Migrations Recipient = table.Column(type: "text", nullable: false), RecipientAddress = table.Column(type: "text", nullable: false), CorrelationId = table.Column(type: "text", nullable: false), - CorrelationIdType = table.Column(type: "text", nullable: false), IsHtml = table.Column(type: "boolean", nullable: false, defaultValue: false), IsInternal = table.Column(type: "boolean", nullable: false, defaultValue: true), - Processed = table.Column(type: "boolean", nullable: false, defaultValue: false) + Processed = table.Column(type: "boolean", nullable: false, defaultValue: false), + HasError = table.Column(type: "boolean", nullable: false, defaultValue: false), + Errors = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -157,6 +159,28 @@ namespace LiteCharms.Infrastructure.Database.Migrations onDelete: ReferentialAction.Restrict); }); + migrationBuilder.CreateTable( + name: "Package", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Package", x => x.Id); + table.ForeignKey( + name: "FK_Package_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Quote", columns: table => new @@ -220,6 +244,56 @@ namespace LiteCharms.Infrastructure.Database.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "PackageItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId1 = table.Column(type: "uuid", nullable: true), + PackageId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PackageItem", x => x.Id); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId1", + column: x => x.PackageId1, + principalTable: "Package", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Order", columns: table => new @@ -308,6 +382,21 @@ namespace LiteCharms.Infrastructure.Database.Migrations column: "OrderId", unique: true); + migrationBuilder.CreateIndex( + name: "IX_Package_ShoppingCartId", + table: "Package", + column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId", + table: "PackageItem", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId1", + table: "PackageItem", + column: "PackageId1"); + migrationBuilder.CreateIndex( name: "IX_ProductPrice_ProductId", table: "ProductPrice", @@ -343,6 +432,16 @@ namespace LiteCharms.Infrastructure.Database.Migrations name: "IX_ShoppingCartItems_ShoppingCartId", table: "ShoppingCartItems", column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); } /// @@ -357,15 +456,24 @@ namespace LiteCharms.Infrastructure.Database.Migrations migrationBuilder.DropTable( name: "OrderRefund"); + migrationBuilder.DropTable( + name: "PackageItem"); + migrationBuilder.DropTable( name: "ShoppingCartItems"); + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + migrationBuilder.DropTable( name: "Order"); migrationBuilder.DropTable( name: "ProductPrice"); + migrationBuilder.DropTable( + name: "Package"); + migrationBuilder.DropTable( name: "Quote"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs index 8a32836..6708099 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs @@ -168,9 +168,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -179,6 +178,14 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsHtml") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -336,12 +343,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("ShoppingCartId"); + b.ToTable("Package", (string)null); }); @@ -611,6 +623,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.HasOne("LiteCharms.Entities.ShoppingCart", null) + .WithMany("Packages") + .HasForeignKey("ShoppingCartId"); + }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") @@ -743,6 +762,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.Navigation("Order"); + b.Navigation("Packages"); + b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); diff --git a/LiteCharms.Models/Enums.cs b/LiteCharms.Models/Enums.cs index 385a5b5..b672e25 100644 --- a/LiteCharms.Models/Enums.cs +++ b/LiteCharms.Models/Enums.cs @@ -1,5 +1,20 @@ namespace LiteCharms.Models; +public enum CorrelationIdTypes : int +{ + None = 0, + Email = 1, + Discord = 2, + Slack = 3, + Whatsapp = 4, + Customer = 5, + Order = 6, + Refund = 7, + Lead = 8, + Quote = 9, + LinkedIn = 10 +} + public enum Priorities : int { Low = 0, diff --git a/LiteCharms.Models/Notification.cs b/LiteCharms.Models/Notification.cs index 90ac390..6108cbb 100644 --- a/LiteCharms.Models/Notification.cs +++ b/LiteCharms.Models/Notification.cs @@ -14,6 +14,8 @@ public class Notification public Priorities Priority { get; set; } + public CorrelationIdTypes CorrelationIdType { get; set; } + public string? Sender { get; set; } public string? SenderName { get; set; } @@ -28,11 +30,13 @@ public class Notification public string? CorrelationId { get; set; } - public string? CorrelationIdType { get; set; } - public bool IsHtml { get; set; } public bool IsInternal { get; set; } public bool Processed { get; set; } + + public bool HasError { get; set; } + + public string[]? Errors { get; set; } } From 73ba41beaf17301bbfcb8d8b4a918390120e5ac6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 16:07:53 +0200 Subject: [PATCH 3/4] Added outgoing email notification processing event --- LiteCharms.Abstractions/Constants.cs | 2 + LiteCharms.Abstractions/EventBase.cs | 12 ++++ .../LiteCharms.Features.csproj | 5 +- .../ProcessEmailNotificationsEventHandler.cs | 70 +++++++++++++++++++ .../Events/ProcessEmailNotificationsEvent.cs | 16 +++++ .../Handlers/GetNotificationQueryHandler.cs | 2 +- 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 LiteCharms.Abstractions/EventBase.cs create mode 100644 LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs create mode 100644 LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs index 31be72a..ae08f94 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Abstractions/Constants.cs @@ -5,6 +5,8 @@ public static class Constants public const int QueueBounds = 100000; public const string ShopSchedulerName = "shop"; + public const string ShopEmailFromName = "Khongisa Shop"; + public const string ShopEmailFromAddress = "shop@litecharms.co.za"; public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); diff --git a/LiteCharms.Abstractions/EventBase.cs b/LiteCharms.Abstractions/EventBase.cs new file mode 100644 index 0000000..6c11736 --- /dev/null +++ b/LiteCharms.Abstractions/EventBase.cs @@ -0,0 +1,12 @@ +using static LiteCharms.Abstractions.Timezones; + +namespace LiteCharms.Abstractions; + +public abstract class EventBase +{ + public Guid Id { get; set; } = Guid.CreateVersion7(); + + public DateTimeOffset EnqueueAt { get; set; } = SouthAfricanTimeZone.UtcNow(); + + public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 6f9f146..f541547 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -54,6 +54,7 @@ + @@ -61,8 +62,4 @@ - - - - diff --git a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs new file mode 100644 index 0000000..61b24d5 --- /dev/null +++ b/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -0,0 +1,70 @@ +using LiteCharms.Features.Email.Commands; +using LiteCharms.Infrastructure.Database; +using static LiteCharms.Abstractions.Constants; + +namespace LiteCharms.Features.Notifications.Events.Handlers; + +public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, ISender mediator) : + INotificationHandler +{ + public async ValueTask Handle(ProcessEmailNotificationsEvent message, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notifications = await context.Notifications + .OrderByDescending(o => o.Priority) + .ThenBy(o => o.CreatedAt) + .Where(n => n.CorrelationIdType == Models.CorrelationIdTypes.Email) + .Where(n => n.Direction == Models.NotificationDirection.Outgoing) + .Take(message.MaxRecords) + .ToListAsync(cancellationToken); + + foreach (var notification in notifications) + { + var sendResult = await SendEmailAsync(notification, cancellationToken); + + if(sendResult.IsFailed) + { + var errors = new List(1000); + + errors.AddRange(sendResult.Errors.Select(e => e.Message)); + + if (sendResult.Reasons?.Count > 0) + errors.AddRange(sendResult.Reasons.Select(e => e.Message)); + + notification.HasError = true; + notification.Errors = [.. errors]; + } + + notification.Processed = true; + } + + await context.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + + private async Task SendEmailAsync(Entities.Notification notification, CancellationToken cancellationToken = default) + { + try + { + var request = SendEmailCommand.Create(notification.Sender!, notification.SenderName!, ShopEmailFromAddress, + ShopEmailFromName, notification.Subject!, notification.Message!); + + var result = await mediator.Send(request, cancellationToken); + + return result.IsFailed + ? Result.Fail(result.Errors) + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs new file mode 100644 index 0000000..3735ecd --- /dev/null +++ b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -0,0 +1,16 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Features.Notifications.Events; + +public class ProcessEmailNotificationsEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(ProcessEmailNotificationsEvent); + + public int MaxRecords { get; set; } + + public ProcessEmailNotificationsEvent() => MaxRecords = 1000; + + private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; + + public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); +} diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs index 106dd88..5eac5d8 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs @@ -12,7 +12,7 @@ public class GetNotificationQueryHandler(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var notification = await context.Notifications.FindAsync(new object[] { request.NotificationId }, cancellationToken); + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); return notification is not null ? Result.Ok(notification.ToModel()) From cecd9f90e9812a2bd84b2bb8026dbc9cce79741f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 16:50:36 +0200 Subject: [PATCH 4/4] Implemented service bus handling of emails and notification processing --- .../SendShopEmailEnquiryEventHandler.cs | 18 +++++++++ .../Email/Events/SendShopEmailEnquiryEvent.cs | 40 +++++++++++++++++++ .../Commands/CreateNotificationCommand.cs | 9 ++--- .../Events/ProcessEmailNotificationsEvent.cs | 2 - .../LiteCharms.Infrastructure.csproj | 3 +- .../ServiceBus/Exchanges/EmailExchange.cs | 29 +++++++++++++- 6 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs create mode 100644 LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs new file mode 100644 index 0000000..9c19500 --- /dev/null +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Notifications.Commands; +using static LiteCharms.Abstractions.Constants; + +namespace LiteCharms.Features.Email.Events.Handlers; + +public class SendShopEmailEnquiryEventHandler(ISender mediator) : + INotificationHandler +{ + public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) + { + var command = CreateNotificationCommand.Create(Models.NotificationDirection.Outgoing, notification.SenderName!, + notification.SenderAddress!, notification.Subject!, notification.Message!, Models.NotificationPlatforms.Email, + notification.Priority, ShopEmailFromName, ShopEmailFromAddress, Guid.CreateVersion7().ToString(), + Models.CorrelationIdTypes.None, isInternal: true, isHtml: false); + + await mediator.Send(command, cancellationToken); + } +} diff --git a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs new file mode 100644 index 0000000..07e3830 --- /dev/null +++ b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs @@ -0,0 +1,40 @@ +using LiteCharms.Abstractions; +using LiteCharms.Models; + +namespace LiteCharms.Features.Email.Events; + +public class SendShopEmailEnquiryEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(SendShopEmailEnquiryEvent); + + public string? SenderName { get; set; } + + public string? SenderAddress { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public Priorities Priority { get; set; } + + public SendShopEmailEnquiryEvent() { } + + private SendShopEmailEnquiryEvent(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + SenderName = senderName; + SenderAddress = senderAddress; + Subject = subject; + Message = message; + Priority = priority; + } + + public static SendShopEmailEnquiryEvent Create(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderName, nameof(senderName)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderAddress, nameof(senderAddress)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(subject, nameof(subject)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(message, nameof(message)); + + return new(senderName, senderAddress, subject, message, priority); + } +} diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs index a9cf063..2b3e933 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs @@ -24,13 +24,13 @@ public class CreateNotificationCommand : IRequest> public string? CorrelationId { get; set; } - public string? CorrelationIdType { get; set; } + public CorrelationIdTypes CorrelationIdType { get; set; } public bool IsInternal { get; set; } public bool IsHtml { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) + private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { Direction = direction; Sender = sender; @@ -47,7 +47,7 @@ public class CreateNotificationCommand : IRequest> IsHtml = isHtml; } - public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) + public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { if (string.IsNullOrWhiteSpace(sender)) throw new ArgumentException("Sender name is required.", nameof(sender)); @@ -67,9 +67,6 @@ public class CreateNotificationCommand : IRequest> if (string.IsNullOrWhiteSpace(correlationId)) throw new ArgumentException("CorrelationId is required.", nameof(correlationId)); - if (string.IsNullOrWhiteSpace(correlationIdType)) - throw new ArgumentException("CorrelationIdType is required.", nameof(correlationIdType)); - return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml); } } diff --git a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs index 3735ecd..9ac754f 100644 --- a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -8,8 +8,6 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent public int MaxRecords { get; set; } - public ProcessEmailNotificationsEvent() => MaxRecords = 1000; - private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj index 765140f..b387b65 100644 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj @@ -89,10 +89,11 @@ - + + diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs index 52928fd..145ce46 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs +++ b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs @@ -2,11 +2,36 @@ namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; -public class EmailExchange(EmailQueue messages) : BackgroundService +public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if(messages.Incoming.CanCount) + while (!stoppingToken.IsCancellationRequested) + { + while (messages.Incoming.TryRead(out var message)) + { + try + { + switch (message.Name) + { + case "SendShopEmailEnquiryEvent": + await mediator.Publish(message, stoppingToken); + break; + case "ProcessEmailNotificationsEvent": + await mediator.Publish(message, stoppingToken); + break; + default: + logger.LogWarning("Unsupported email event {Event}", message.Name); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + await Task.Delay(1000, stoppingToken); + } } }