diff --git a/.drone.yml b/.drone.yml
index 0c10662..ededa97 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -16,31 +16,10 @@ steps:
NEXUS_KEY: { from_secret: nexus_api_key }
NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/
VERSION: 1.${DRONE_BUILD_NUMBER}.0
- commands:
- # Abstractions
- - dotnet pack LiteCharms.Abstractions/LiteCharms.Abstractions.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- - dotnet nuget push dist/LiteCharms.Abstractions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
-
- # Models
- - dotnet pack LiteCharms.Models/LiteCharms.Models.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- - dotnet nuget push dist/LiteCharms.Models.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
-
- # Infrastructure
- - dotnet pack LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- - dotnet nuget push dist/LiteCharms.Infrastructure.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
-
- # Features
+ commands:
- dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- # Extensions
- - dotnet pack LiteCharms.Extensions/LiteCharms.Extensions.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- - dotnet nuget push dist/LiteCharms.Extensions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
-
- # Entities
- - dotnet pack LiteCharms.Entities/LiteCharms.Entities.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- - dotnet nuget push dist/LiteCharms.Entities.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
-
- name: gitea-tag-release
image: alpine/git
environment:
@@ -61,7 +40,7 @@ steps:
\"tag_name\": \"$VERSION\",
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
\"name\": \"Library Suite $VERSION\",
- \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Models\n* LiteCharms.Infrastructure\n* LiteCharms.Features\n* LiteCharms.Extensions\n* LiteCharms.Entities\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
+ \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false,
\"prerelease\": false
}"
diff --git a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj
deleted file mode 100644
index 4074ad4..0000000
--- a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- True
- ..\LiteCharms.snk
-
-
-
-
- LiteCharms.Abstractions
- 1.0.20
- Khwezi Mngoma
- Lite Charms (PTY) Ltd
- Shared abstractions for Lite Charms applications.
- https://gitea.khongisa.co.za/litecharms/components
- https://gitea.khongisa.co.za/litecharms/components.git
- git
- LICENSE
- utility;dotnet
- icon.png
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LiteCharms.Entities/Configuration/OrderConfiguration.cs b/LiteCharms.Entities/Configuration/OrderConfiguration.cs
deleted file mode 100644
index 138e136..0000000
--- a/LiteCharms.Entities/Configuration/OrderConfiguration.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class OrderConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(Order));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd();
- builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate();
- builder.Property(f => f.CustomerId).IsRequired();
- builder.Property(f => f.QuoteId).IsRequired(false);
- builder.Property(f => f.RefundId).IsRequired(false);
- builder.Property(f => f.ShoppingCartId).IsRequired();
- builder.Property(f => f.Status).HasConversion().IsRequired();
- builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false);
- builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false);
- builder.Property(f => f.Terms).HasColumnType("jsonb").IsRequired(false);
- builder.Property(f => f.DepositRequired);
-
- builder.HasOne(f => f.Quote)
- .WithOne(f => f.Order)
- .HasForeignKey(f => f.QuoteId)
- .OnDelete(DeleteBehavior.Restrict);
-
- builder.HasOne(f => f.Customer)
- .WithMany(f => f.Orders)
- .HasForeignKey(f => f.CustomerId)
- .OnDelete(DeleteBehavior.Restrict);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/PackageConfirguration.cs b/LiteCharms.Entities/Configuration/PackageConfirguration.cs
deleted file mode 100644
index 71aa4fc..0000000
--- a/LiteCharms.Entities/Configuration/PackageConfirguration.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class PackageConfirguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(Package));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
- builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate();
- builder.Property(f => f.Name).IsRequired();
- builder.Property(f => f.Description).IsRequired();
- builder.Property(f => f.Active);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/ProductConfiguration.cs b/LiteCharms.Entities/Configuration/ProductConfiguration.cs
deleted file mode 100644
index 3b5ca6d..0000000
--- a/LiteCharms.Entities/Configuration/ProductConfiguration.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class ProductConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(Product));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.Name).IsRequired();
- builder.Property(f => f.Description).IsRequired();
- builder.Property(f => f.Active).HasDefaultValue(true);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs
deleted file mode 100644
index d48a768..0000000
--- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class QuoteConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(Quote));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
- 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().HasConversion();
- builder.Property(f => f.ShoppingCartId).IsRequired();
- builder.Property(f => f.Reason).IsRequired(false);
-
- builder.HasOne(f => f.Customer)
- .WithMany()
- .HasForeignKey(f => f.CustomerId)
- .OnDelete(DeleteBehavior.Cascade);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs
deleted file mode 100644
index 109fd99..0000000
--- a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class ShoppingCartConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(ShoppingCart));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
- builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate();
- builder.Property(f => f.CustomerId).IsRequired(false);
- builder.Property(f => f.OrderId).IsRequired(false);
- builder.Property(f => f.QuoteId).IsRequired(false);
-
- builder.HasOne(f => f.Customer)
- .WithMany(c => c.ShoppingCarts)
- .HasForeignKey(f => f.CustomerId)
- .OnDelete(DeleteBehavior.NoAction);
-
- builder.HasOne(f => f.Order)
- .WithOne(o => o.ShoppingCart)
- .HasForeignKey(o => o.ShoppingCartId)
- .OnDelete(DeleteBehavior.NoAction);
-
- builder.HasOne(f => f.Quote)
- .WithOne(o => o.ShoppingCart)
- .HasForeignKey(o => o.ShoppingCartId)
- .OnDelete(DeleteBehavior.NoAction);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs
deleted file mode 100644
index 948f988..0000000
--- a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class ShoppingCartItemConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(ShoppingCartItem));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
- builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate();
- builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1);
- builder.Property(f => f.ProductPriceId).IsRequired();
-
- builder.HasOne(f => f.ProductPrice)
- .WithMany()
- .HasForeignKey(f => f.ProductPriceId)
- .OnDelete(DeleteBehavior.NoAction);
-
- builder.HasOne(f => f.ShoppingCart)
- .WithMany(f => f.ShoppingCartItems)
- .HasForeignKey(f => f.ShoppingCartId)
- .OnDelete(DeleteBehavior.NoAction);
- }
-}
diff --git a/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs
deleted file mode 100644
index 7d2f296..0000000
--- a/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace LiteCharms.Entities.Configuration;
-
-public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable(nameof(ShoppingCartPackage));
-
- builder.HasKey(f => f.Id);
- builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
- builder.Property(f => f.ShoppingCartId).IsRequired();
- builder.Property(f => f.PackageId).IsRequired();
-
- builder.HasOne(f => f.Package)
- .WithMany()
- .HasForeignKey(f => f.PackageId)
- .OnDelete(DeleteBehavior.NoAction);
-
- builder.HasOne(f => f.ShoppingCart)
- .WithMany()
- .HasForeignKey(f => f.ShoppingCartId)
- .OnDelete(DeleteBehavior.NoAction);
- }
-}
diff --git a/LiteCharms.Entities/LiteCharms.Entities.csproj b/LiteCharms.Entities/LiteCharms.Entities.csproj
deleted file mode 100644
index b32a09d..0000000
--- a/LiteCharms.Entities/LiteCharms.Entities.csproj
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- True
- ..\LiteCharms.snk
-
-
-
-
- LiteCharms.Entities
- 1.0.20
- Khwezi Mngoma
- Lite Charms (PTY) Ltd
- Shared entities for Lite Charms applications.
- https://gitea.khongisa.co.za/litecharms/components
- https://gitea.khongisa.co.za/litecharms/components.git
- git
- LICENSE
- utility;dotnet
- icon.png
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LiteCharms.Entities/Order.cs b/LiteCharms.Entities/Order.cs
deleted file mode 100644
index 5f7d5dc..0000000
--- a/LiteCharms.Entities/Order.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using LiteCharms.Entities.Configuration;
-
-namespace LiteCharms.Entities;
-
-[EntityTypeConfiguration]
-public class Order : Models.Order
-{
- public virtual OrderRefund? Refund { get; set; }
-
- public virtual Customer? Customer { get; set; }
-
- public virtual Quote? Quote { get; set; }
-
- public virtual ShoppingCart? ShoppingCart { get; set; }
-}
diff --git a/LiteCharms.Entities/PackageItem.cs b/LiteCharms.Entities/PackageItem.cs
deleted file mode 100644
index 986fc75..0000000
--- a/LiteCharms.Entities/PackageItem.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using LiteCharms.Entities.Configuration;
-
-namespace LiteCharms.Entities;
-
-[EntityTypeConfiguration]
-public class PackageItem : Models.PackageItem
-{
- public virtual Package? Package { get; set; }
-}
diff --git a/LiteCharms.Entities/ShoppingCartItem.cs b/LiteCharms.Entities/ShoppingCartItem.cs
deleted file mode 100644
index d5e5634..0000000
--- a/LiteCharms.Entities/ShoppingCartItem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace LiteCharms.Entities;
-
-public class ShoppingCartItem : Models.ShoppingCartItem
-{
- public virtual ShoppingCart? ShoppingCart { get; set; }
-
- public virtual ProductPrice? ProductPrice { get; set; }
-}
diff --git a/LiteCharms.Extensions/Email.cs b/LiteCharms.Extensions/Email.cs
deleted file mode 100644
index e081f4e..0000000
--- a/LiteCharms.Extensions/Email.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using LiteCharms.Models.Configuraton.Email;
-
-namespace LiteCharms.Extensions;
-
-public static class Email
-{
- public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
- {
- services.Configure(configuration.GetSection("Email"));
-
- return services;
- }
-}
diff --git a/LiteCharms.Extensions/LiteCharms.Extensions.csproj b/LiteCharms.Extensions/LiteCharms.Extensions.csproj
deleted file mode 100644
index 3f531b7..0000000
--- a/LiteCharms.Extensions/LiteCharms.Extensions.csproj
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- True
- ..\LiteCharms.snk
- true
-
-
-
-
- $(NoWarn);MA0004
-
- $(NoWarn);AD0001
- true
- $(NoWarn);IL2080;IL2065;IL2075;IL2087;IL2057;IL2060;IL2070;IL2067;IL2072;IL2026;IL2104
- $(NoWarn);IL2110;IL2111
-
-
-
-
- LiteCharms.Extensions
- 1.0.20
- Khwezi Mngoma
- Lite Charms (PTY) Ltd
- Extension components for Lite Charms applications.
- https://gitea.khongisa.co.za/litecharms/components
- https://gitea.khongisa.co.za/litecharms/components.git
- git
- LICENSE
- utility;dotnet
- icon.png
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/CommonFixture.cs
new file mode 100644
index 0000000..a73bed2
--- /dev/null
+++ b/LiteCharms.Features.Tests/CommonFixture.cs
@@ -0,0 +1,35 @@
+using LiteCharms.Features.Extensions;
+
+namespace LiteCharms.Features.Tests;
+
+public class CommonFixture : IDisposable
+{
+ public IConfiguration Configuration { get; set; }
+
+ public IServiceProvider Services { get; set; }
+
+ public IMediator Mediator { get; set; }
+
+ public CommonFixture()
+ {
+ Configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json")
+ .AddUserSecrets()
+ .AddEnvironmentVariables()
+ .Build();
+
+ Services = new ServiceCollection()
+ .AddMediator()
+ .AddLogging()
+ .AddShopServices()
+ .AddEmailServiceBus()
+ .AddShopDatabase(Configuration)
+ .AddEmailServices(Configuration)
+ .BuildServiceProvider();
+
+ Mediator = Services.GetRequiredService();
+ }
+
+ public void Dispose() { }
+}
diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj
new file mode 100644
index 0000000..7070851
--- /dev/null
+++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj
@@ -0,0 +1,50 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ 62fa604a-1340-4edb-9ddd-3305fcf46fca
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
\ No newline at end of file
diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs
new file mode 100644
index 0000000..ede7fd8
--- /dev/null
+++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs
@@ -0,0 +1,35 @@
+using LiteCharms.Features.Shop.Notifications;
+
+namespace LiteCharms.Features.Tests;
+
+public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture
+{
+ private readonly NotificationService notificationService = fixture.Services.GetRequiredService();
+
+ [Fact]
+ public async Task CreateNotificationCommand_ShouldSucceed()
+ {
+ Shop.Notifications.Models.CreateNotification request = new()
+ {
+ CorrelationId = Guid.CreateVersion7().ToString(),
+ CorrelationIdType = Shop.CorrelationIdTypes.None,
+ Direction = Shop.NotificationDirection.Outgoing,
+ Platform = Shop.NotificationPlatforms.Email,
+ Priority = Shop.Priorities.Medium,
+ Sender = "xUnit Test",
+ SenderAddress = "khwezi@mngoma.africa",
+ Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]",
+ RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress,
+ Subject = "Test Message",
+ Message = "This is an automation test",
+ IsHtml = false,
+ IsInternal = true,
+ };
+
+ var createResult = await notificationService.CreateNotificationAsync(request);
+
+ Assert.True(createResult.IsSuccess);
+
+ foreach (var error in createResult.Errors) output.WriteLine(error.Message);
+ }
+}
diff --git a/LiteCharms.Features.Tests/appsettings.json b/LiteCharms.Features.Tests/appsettings.json
new file mode 100644
index 0000000..aec5c2e
--- /dev/null
+++ b/LiteCharms.Features.Tests/appsettings.json
@@ -0,0 +1,22 @@
+{
+ "Email": {
+ "Credentials": {
+ "Username": "shop@litecharms.co.za"
+ },
+ "Port": 465,
+ "Host": "mail.litecharms.co.za",
+ "UseSsl": true
+ },
+ "Monitoring": {
+ "ApiKey": "",
+ "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
+ "ServiceName": "LiteCharms.LeadGenerator"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/LiteCharms.Abstractions/EventBase.cs b/LiteCharms.Features/Abstractions/EventBase.cs
similarity index 65%
rename from LiteCharms.Abstractions/EventBase.cs
rename to LiteCharms.Features/Abstractions/EventBase.cs
index 6c11736..32def99 100644
--- a/LiteCharms.Abstractions/EventBase.cs
+++ b/LiteCharms.Features/Abstractions/EventBase.cs
@@ -1,6 +1,7 @@
-using static LiteCharms.Abstractions.Timezones;
+using LiteCharms.Features.Extensions;
+using static LiteCharms.Features.Extensions.Timezones;
-namespace LiteCharms.Abstractions;
+namespace LiteCharms.Features.Abstractions;
public abstract class EventBase
{
diff --git a/LiteCharms.Abstractions/IEvent.cs b/LiteCharms.Features/Abstractions/IEvent.cs
similarity index 79%
rename from LiteCharms.Abstractions/IEvent.cs
rename to LiteCharms.Features/Abstractions/IEvent.cs
index 08bc091..366ad86 100644
--- a/LiteCharms.Abstractions/IEvent.cs
+++ b/LiteCharms.Features/Abstractions/IEvent.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Abstractions;
+namespace LiteCharms.Features.Abstractions;
public interface IEvent : INotification
{
diff --git a/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs b/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs
deleted file mode 100644
index be87a47..0000000
--- a/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index 4a86846..0000000
--- a/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-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
deleted file mode 100644
index 5957dce..0000000
--- a/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index c9aa3e0..0000000
--- a/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index ffdd24a..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-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
deleted file mode 100644
index 4945a73..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 5ec6745..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 8f4e8e7..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-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
deleted file mode 100644
index 7889c52..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-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
deleted file mode 100644
index 5ef4a91..0000000
--- a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-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
deleted file mode 100644
index 938ca44..0000000
--- a/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 7be4651..0000000
--- a/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-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
deleted file mode 100644
index e0830af..0000000
--- a/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644
index 1384783..0000000
--- a/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644
index 351a141..0000000
--- a/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-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
deleted file mode 100644
index 487b203..0000000
--- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 7a01fb5..0000000
--- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index b495bb0..0000000
--- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-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/Customers/Commands/CreateCustomerCommand.cs b/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs
deleted file mode 100644
index f0b3560..0000000
--- a/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-namespace LiteCharms.Features.Customers.Commands;
-
-public class CreateCustomerCommand : IRequest>
-{
- public string? Company { get; set; }
-
- public string Name { get; set; }
-
- public string LastName { get; set; }
-
- public string? Tax { get; set; }
-
- public string Email { get; set; }
-
- public string? Discord { get; set; }
-
- public string? Slack { get; set; }
-
- public string? LinkedIn { get; set; }
-
- public string? Whatsapp { get; set; }
-
- public string? Website { get; set; }
-
- public string? Phone { get; set; }
-
- public string? Address { get; set; }
-
- public string? City { get; set; }
-
- public string? Region { get; set; }
-
- public string? Country { get; set; }
-
- public string? PostalCode { get; set; }
-
- private CreateCustomerCommand(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode)
- {
- Name = name;
- LastName = lastName;
- Company = company;
- Tax = tax;
- Email = email;
- Discord = discord;
- Slack = slack;
- LinkedIn = linkedIn;
- Whatsapp = whatsapp;
- Website = website;
- Phone = phone;
- Address = address;
- City = city;
- Region = region;
- Country = country;
- PostalCode = postalCode;
- }
-
- public static CreateCustomerCommand Create(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode)
- {
- if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email))
- throw new ArgumentException("At the following fields must be provided: Name, LastName, Email");
-
- return new(name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode);
- }
-}
diff --git a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs
deleted file mode 100644
index 82f836b..0000000
--- a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Customers.Commands.Handlers;
-
-public class CreateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var customerEmail = request.Email.ToLower().Trim();
-
- if (await context.Customers.AnyAsync(c => c.Email == customerEmail, cancellationToken))
- return Result.Fail(new Error($"A customer with the email {customerEmail} already exists"));
-
- var newCustomer = context.Customers.Add(new Entities.Customer
- {
- Company = request.Company,
- Name = request.Name,
- LastName = request.LastName,
- Tax = request.Tax,
- Email = customerEmail,
- Discord = request.Discord,
- Slack = request.Slack,
- LinkedIn = request.LinkedIn,
- Whatsapp = request.Whatsapp,
- Website = request.Website,
- Phone = request.Phone,
- Address = request.Address,
- City = request.City,
- Region = request.Region,
- Country = request.Country,
- PostalCode = request.PostalCode,
- Active = true,
- });
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok(newCustomer.Entity.Id)
- : Result.Fail(new Error($"Failed to create customer {customerEmail}"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs
deleted file mode 100644
index 72b9109..0000000
--- a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Customers.Commands.Handlers;
-
-public class UpdateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(UpdateCustomerCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken);
-
- if (customer is null)
- return Result.Fail(new Error($"Customer with ID {request.CustomerId} not found."));
-
- customer.Name = request.Name;
- customer.LastName = request.LastName;
- customer.Email = request.Email;
- customer.Company = request.Company;
- customer.Address = request.Address;
- customer.City = request.City;
- customer.Region = request.Region;
- customer.Country = request.Country;
- customer.PostalCode = request.PostalCode;
- customer.Phone = request.Phone;
- customer.Tax = request.Tax;
- customer.City = request.City;
- customer.Discord = request.Discord;
- customer.Slack = request.Slack;
- customer.LinkedIn = request.LinkedIn;
- customer.Whatsapp = request.Whatsapp;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error($"Failed to update the customer {request.CustomerId}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
\ No newline at end of file
diff --git a/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs b/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs
deleted file mode 100644
index ac4f0cd..0000000
--- a/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-namespace LiteCharms.Features.Customers.Commands;
-
-public class UpdateCustomerCommand : IRequest
-{
- public Guid CustomerId { get; set; }
-
- public string? Company { get; set; }
-
- public string? Name { get; set; }
-
- public string? LastName { get; set; }
-
- public string? Tax { get; set; }
-
- public string? Email { get; set; }
-
- public string? Discord { get; set; }
-
- public string? Slack { get; set; }
-
- public string? LinkedIn { get; set; }
-
- public string? Whatsapp { get; set; }
-
- public string? Website { get; set; }
-
- public string? Phone { get; set; }
-
- public string? Address { get; set; }
-
- public string? City { get; set; }
-
- public string? Region { get; set; }
-
- public string? Country { get; set; }
-
- public string? PostalCode { get; set; }
-
- private UpdateCustomerCommand(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode)
- {
- CustomerId = customerId;
- Name = name;
- LastName = lastName;
- Company = company;
- Tax = tax;
- Email = email;
- Discord = discord;
- Slack = slack;
- LinkedIn = linkedIn;
- Whatsapp = whatsapp;
- Website = website;
- Phone = phone;
- Address = address;
- City = city;
- Region = region;
- Country = country;
- PostalCode = postalCode;
- }
-
- public static UpdateCustomerCommand Create(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode)
- {
- if (customerId == Guid.Empty)
- throw new ArgumentException("Customer ID is required.", nameof(customerId));
-
- if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email))
- throw new ArgumentException("At the following fields must be provided: Name, LastName, Email");
-
- return new(customerId, name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode);
- }
-}
diff --git a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs b/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs
deleted file mode 100644
index 5ea7394..0000000
--- a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Customers.Queries;
-
-public class GetCustomerQuery : IRequest>
-{
- public Guid CustomerId { get; set; }
-
- private GetCustomerQuery(Guid customerId) => CustomerId = customerId;
-
- public static GetCustomerQuery Create(Guid customerId)
- {
- if(customerId == Guid.Empty)
- throw new ArgumentException("Customer ID is required.", nameof(customerId));
-
- return new(customerId);
- }
-}
diff --git a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs b/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs
deleted file mode 100644
index 3f271b3..0000000
--- a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Customers.Queries;
-
-public class GetCustomersQuery : IRequest>
-{
- public DateOnly From { get; set; }
-
- public DateOnly To { get; set; }
-
- public int MaxRecords { get; set; }
-
- private GetCustomersQuery(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- From = from;
- To = to;
- MaxRecords = maxRecords;
- }
-
- public static GetCustomersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- 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);
- }
-}
diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs
deleted file mode 100644
index 0f921db..0000000
--- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Customers.Queries.Handlers;
-
-public class GetCustomerQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetCustomerQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken);
-
- return customer is not null
- ? Result.Ok(customer.ToModel())
- : Result.Fail($"Customer not found with id {request.CustomerId}");
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs
deleted file mode 100644
index 960ef3a..0000000
--- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Customers.Queries.Handlers;
-
-public class GetCustomersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetCustomersQuery 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 customers = await context.Customers.AsNoTracking()
- .OrderByDescending(o => o.CreatedAt)
- .Where(c => c.CreatedAt >= fromDate && c.CreatedAt <= toDate)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return customers?.Length > 0
- ? Result.Ok(customers.Select(c => c.ToModel()).ToArray())
- : Result.Fail(new Error("No customers found in the specified date range."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs
deleted file mode 100644
index 30f86dc..0000000
--- a/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using LiteCharms.Features.Email.Commands;
-using LiteCharms.Models.Configuraton.Email;
-
-namespace LiteCharms.Features.Email.Commands.Handlers;
-
-public class SendEmailCommandHandler(IOptions smtpOptions) : IRequestHandler
-{
- public async ValueTask Handle(SendEmailCommand request, CancellationToken cancellationToken)
- {
- try
- {
- var settings = smtpOptions.Value;
-
- if(settings == null)
- return Result.Fail(new Error("SMTP settings are not configured."));
-
- if(settings.Credentials == null)
- return Result.Fail(new Error("SMTP credentials are not configured."));
-
- if(string.IsNullOrWhiteSpace(settings?.Credentials.Username) || string.IsNullOrWhiteSpace(settings.Credentials.Password))
- return Result.Fail(new Error("SMTP credentials are incomplete."));
-
- if(string.IsNullOrWhiteSpace(settings.Host) || settings.Port == 0)
- return Result.Fail(new Error("SMTP host and port must be configured."));
-
- var message = new MimeMessage();
- message.From.Add(new MailboxAddress(request.SenderName, request.From!));
- message.To.Add(new MailboxAddress(request.RecipientName, request.To!));
- message.Subject = request.Subject!;
-
- var bodyBuilder = new BodyBuilder();
-
- if(request.Attachment?.Length > 0 && !string.IsNullOrEmpty(request.AttachmentFileName))
- bodyBuilder.Attachments.Add(request.AttachmentFileName!, request.Attachment!, cancellationToken);
-
- if (!request.IsHtml) bodyBuilder.TextBody = request.Message;
- if (request.IsHtml) bodyBuilder.HtmlBody = request.Message;
-
- message.Body = bodyBuilder.ToMessageBody();
-
- using var client = new SmtpClient();
-
- await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken);
- await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken);
-
- var response = await client.SendAsync(message, cancellationToken);
-
- bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase);
-
- await client.DisconnectAsync(true, cancellationToken);
-
- return emailSent
- ? Result.Ok()
- : Result.Fail(new Error("Failed to send email. SMTP response: " + response));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Email/Commands/SendEmailCommand.cs b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs
deleted file mode 100644
index 972e496..0000000
--- a/LiteCharms.Features/Email/Commands/SendEmailCommand.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-namespace LiteCharms.Features.Email.Commands;
-
-public class SendEmailCommand : IRequest
-{
- public string? From { get; set; }
-
- public string? SenderName { get; set; }
-
- public string? To { get; set; }
-
- public string? RecipientName { get; set; }
-
- public string? Subject { get; set; }
-
- public string? Message { get; set; }
-
- public bool IsHtml { get; set; }
-
- public Stream? Attachment { get; set; }
-
- public string? AttachmentFileName { get; set; }
-
- private SendEmailCommand(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null)
- {
- From = from;
- To = to;
- Subject = subject;
- Message = message;
- IsHtml = isHtml;
- Attachment = attachment;
- AttachmentFileName = attachmentFileName;
- SenderName = senderName;
- RecipientName = recipientName;
- }
-
- public static SendEmailCommand Create(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null)
- {
- if (string.IsNullOrWhiteSpace(from))
- throw new ArgumentException("From address is required.");
-
- if (string.IsNullOrWhiteSpace(senderName))
- throw new ArgumentException("Sender name is required.");
-
- if (!string.IsNullOrWhiteSpace(senderName) && senderName?.Length > 255)
- throw new ArgumentException("Sender name cannot exceed 255 characters.");
-
- if (string.IsNullOrWhiteSpace(to))
- throw new ArgumentException("To address is required.");
-
- if (string.IsNullOrWhiteSpace(recipientName))
- throw new ArgumentException("Recipient name is required.");
-
- if (!string.IsNullOrWhiteSpace(recipientName) && recipientName?.Length > 255)
- throw new ArgumentException("Recipient name cannot exceed 255 characters.");
-
- if (string.IsNullOrWhiteSpace(subject))
- throw new ArgumentException("Subject is required.");
-
- if (!string.IsNullOrWhiteSpace(subject) && subject?.Length > 2048)
- throw new ArgumentException("Subject cannot exceed 2048 characters.");
-
- if (string.IsNullOrWhiteSpace(message))
- throw new ArgumentException("Message is required.");
-
- if (message.Length > 10485760)
- throw new ArgumentException("Message cannot exceed 10 MB.");
-
- if (attachment != null && string.IsNullOrWhiteSpace(attachmentFileName))
- throw new ArgumentException("Attachment file name must be provided when an attachment is included.");
-
- if (attachment is not null && attachment.Length > 10485760)
- throw new ArgumentException("Attachment cannot exceed 10 MB.");
-
- if (!string.IsNullOrWhiteSpace(attachmentFileName) && attachmentFileName.Length > 255)
- throw new ArgumentException("Attachment file name cannot exceed 255 characters.");
-
- return new(from, senderName!, to, recipientName!, subject!, message, isHtml, attachment, attachmentFileName);
- }
-}
diff --git a/LiteCharms.Models/Configuraton/Email/Account.cs b/LiteCharms.Features/Email/Configuration/Account.cs
similarity index 67%
rename from LiteCharms.Models/Configuraton/Email/Account.cs
rename to LiteCharms.Features/Email/Configuration/Account.cs
index 96424ce..e0c65e7 100644
--- a/LiteCharms.Models/Configuraton/Email/Account.cs
+++ b/LiteCharms.Features/Email/Configuration/Account.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Models.Configuraton.Email;
+namespace LiteCharms.Features.Email.Configuration;
public class Account
{
diff --git a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs
similarity index 77%
rename from LiteCharms.Models/Configuraton/Email/SmtpSettings.cs
rename to LiteCharms.Features/Email/Configuration/SmtpSettings.cs
index c44fbbe..78798f2 100644
--- a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs
+++ b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Models.Configuraton.Email;
+namespace LiteCharms.Features.Email.Configuration;
public class SmtpSettings
{
diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs
new file mode 100644
index 0000000..3e6c103
--- /dev/null
+++ b/LiteCharms.Features/Email/EmailService.cs
@@ -0,0 +1,192 @@
+using LiteCharms.Features.Email.Configuration;
+using LiteCharms.Features.Email.Extensions;
+using LiteCharms.Features.Email.Models;
+using LiteCharms.Features.Shop;
+
+namespace LiteCharms.Features.Email;
+
+public class EmailService(IOptions options) : IDisposable
+{
+ private readonly SmtpSettings settings = options.Value;
+ private readonly SmtpClient client = new();
+ private readonly int sendMaxCount = 10;
+ private int sendCount = 0;
+
+ public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected;
+
+ public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default)
+ {
+ using var activity = EmailTelemetry.Source.StartActivity("Email Send");
+ activity?.SetTag("email.recipient", message.Recipient?.Address);
+
+ try
+ {
+ if (Status != EmailStatuses.Connected)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, "Disconnected");
+
+ return Result.Fail("Smtp service is disconnected.");
+ }
+
+ var email = new MimeMessage();
+ email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!));
+ email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!));
+ email.Subject = message.Subject!;
+
+ var bodyBuilder = new BodyBuilder();
+
+ foreach (var attachment in message.Body?.Attachments!)
+ bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
+
+ if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
+ if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
+
+ email.Body = bodyBuilder.ToMessageBody();
+
+ var response = await client.SendAsync(email, cancellationToken);
+
+ bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase);
+
+ message.Dispose();
+
+ Interlocked.Increment(ref sendCount);
+
+ if (sendCount % sendMaxCount == 0)
+ {
+ using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause");
+
+ sendCount = 0;
+
+ await Task.Delay(1000, cancellationToken);
+ }
+
+ if (emailSent)
+ {
+ EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } });
+
+ return Result.Ok(Response.Create(EmailStatuses.Success));
+ }
+
+ await DisconnectAsync(cancellationToken);
+
+ if (response.Contains("421"))
+ {
+ Status = EmailStatuses.TooManyConnections;
+
+ return Result.Fail(response);
+ }
+
+ if (response.Contains("451"))
+ {
+ Status = EmailStatuses.ConnectionAborted;
+
+ return Result.Fail(response);
+ }
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
+
+ Status = EmailStatuses.Disconnected;
+
+ return Result.Fail("General error, disconnected");
+ }
+ catch (Exception ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.AddException(ex);
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
+
+ return Result.Fail(new Error(ex.Message).CausedBy(ex));
+ }
+ }
+
+ public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ using var activity = EmailTelemetry.Source.StartActivity("Email Connect");
+ activity?.SetTag("email.smtp.connect", settings.Host);
+
+ try
+ {
+ if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status));
+
+ await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken);
+ await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken);
+
+ Status = EmailStatuses.Connected;
+
+ activity?.SetStatus(ActivityStatusCode.Ok, "Connected");
+
+ return Result.Ok(Response.Create(Status));
+ }
+ catch (MailKit.ProtocolException ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.AddException(ex);
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
+
+ Status = EmailStatuses.ProtocolError;
+
+ return Result.Fail(new Error(ex.Message).CausedBy(ex));
+ }
+ catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.AddException(ex);
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
+
+ Status = EmailStatuses.AuthenticationError;
+
+ return Result.Fail(new Error(ex.Message).CausedBy(ex));
+ }
+ catch (Exception ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.AddException(ex);
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
+
+ Status = EmailStatuses.GeneralError;
+
+ return Result.Fail(new Error(ex.Message).CausedBy(ex));
+ }
+ }
+
+ public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default)
+ {
+ using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect");
+ activity?.SetTag("email.smtp.disconnect", settings.Host);
+
+ try
+ {
+ if (Status is EmailStatuses.Disconnected) return Result.Ok();
+
+ await client.DisconnectAsync(true, cancellationToken);
+
+ activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected");
+
+ Status = EmailStatuses.Disconnected;
+
+ return Result.Ok();
+ }
+ catch (Exception ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.AddException(ex);
+
+ EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
+
+ Status = EmailStatuses.GeneralError;
+
+ return Result.Fail(new Error(ex.Message).CausedBy(ex));
+ }
+ }
+
+ public void Dispose()
+ {
+ client.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs
index 9c19500..f4173a6 100644
--- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs
+++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs
@@ -1,18 +1,27 @@
-using LiteCharms.Features.Notifications.Commands;
-using static LiteCharms.Abstractions.Constants;
+using LiteCharms.Features.Shop;
+using LiteCharms.Features.Shop.Notifications;
+using static LiteCharms.Features.Email.Extensions.Constants;
namespace LiteCharms.Features.Email.Events.Handlers;
-public class SendShopEmailEnquiryEventHandler(ISender mediator) :
+public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) :
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);
- }
+ public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) =>
+ await notificationService.CreateNotificationAsync(new Shop.Notifications.Models.CreateNotification
+ {
+ CorrelationId = notification.CorrelationId,
+ CorrelationIdType = CorrelationIdTypes.None,
+ Direction = NotificationDirection.Outgoing,
+ IsHtml = false,
+ IsInternal = true,
+ Message = notification.Message,
+ Platform = NotificationPlatforms.Email,
+ Priority = notification.Priority,
+ Subject = notification.Subject!,
+ Sender = notification.SenderName!,
+ SenderAddress = notification.SenderAddress!,
+ Recipient = ShopEmailFromName,
+ RecipientAddress = ShopEmailFromAddress
+ }, cancellationToken);
}
diff --git a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs
index 07e3830..3377b44 100644
--- a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs
+++ b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs
@@ -1,5 +1,5 @@
-using LiteCharms.Abstractions;
-using LiteCharms.Models;
+using LiteCharms.Features.Abstractions;
+using LiteCharms.Features.Shop;
namespace LiteCharms.Features.Email.Events;
diff --git a/LiteCharms.Features/Email/Extensions/Constants.cs b/LiteCharms.Features/Email/Extensions/Constants.cs
new file mode 100644
index 0000000..1d02879
--- /dev/null
+++ b/LiteCharms.Features/Email/Extensions/Constants.cs
@@ -0,0 +1,8 @@
+namespace LiteCharms.Features.Email.Extensions;
+
+public static class Constants
+{
+ public const string ShopSchedulerName = "shop";
+ public const string ShopEmailFromName = "Khongisa Shop";
+ public const string ShopEmailFromAddress = "shop@litecharms.co.za";
+}
diff --git a/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs
new file mode 100644
index 0000000..48a9a4a
--- /dev/null
+++ b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs
@@ -0,0 +1,9 @@
+namespace LiteCharms.Features.Email.Extensions;
+
+public static class EmailTelemetry
+{
+ public static readonly ActivitySource Source = new("LiteCharms.EmailService");
+ public static readonly Meter Meter = new("LiteCharms.EmailService");
+ public static readonly Counter EmailsSent = Meter.CreateCounter("emails_sent_total", "count", "Total successful emails sent");
+ public static readonly Counter EmailsFailed = Meter.CreateCounter("emails_failed_total", "count", "Total failed email attempts");
+}
diff --git a/LiteCharms.Features/Email/Models/Attachment.cs b/LiteCharms.Features/Email/Models/Attachment.cs
new file mode 100644
index 0000000..6558058
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/Attachment.cs
@@ -0,0 +1,8 @@
+namespace LiteCharms.Features.Email.Models;
+
+public class Attachment
+{
+ public string? Name { get; set; }
+
+ public Stream? FileStream { get; set; }
+}
diff --git a/LiteCharms.Features/Email/Models/Body.cs b/LiteCharms.Features/Email/Models/Body.cs
new file mode 100644
index 0000000..99156d8
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/Body.cs
@@ -0,0 +1,26 @@
+namespace LiteCharms.Features.Email.Models;
+
+public class Body : IDisposable
+{
+ public string? Message { get; set; }
+
+ public ReadOnlyCollection? Attachments { get; set; }
+
+ public BodyProperties Properties { get; set; } = new();
+
+ public void Dispose()
+ {
+ if (Attachments is null) return;
+
+ foreach (var attachment in Attachments!)
+ {
+ if (attachment is not null)
+ {
+ attachment.FileStream!.Close();
+ attachment.FileStream!.Dispose();
+ }
+ }
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/LiteCharms.Features/Email/Models/BodyProperties.cs b/LiteCharms.Features/Email/Models/BodyProperties.cs
new file mode 100644
index 0000000..7bdfa01
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/BodyProperties.cs
@@ -0,0 +1,8 @@
+namespace LiteCharms.Features.Email.Models;
+
+public class BodyProperties
+{
+ public bool IsHtml { get; set; }
+
+ public bool HasAttachments { get; set; }
+}
diff --git a/LiteCharms.Models/EmailEnquiry.cs b/LiteCharms.Features/Email/Models/EmailEnquiry.cs
similarity index 86%
rename from LiteCharms.Models/EmailEnquiry.cs
rename to LiteCharms.Features/Email/Models/EmailEnquiry.cs
index a858328..97c2fe3 100644
--- a/LiteCharms.Models/EmailEnquiry.cs
+++ b/LiteCharms.Features/Email/Models/EmailEnquiry.cs
@@ -1,4 +1,6 @@
-namespace LiteCharms.Models;
+using System.ComponentModel.DataAnnotations;
+
+namespace LiteCharms.Features.Email.Models;
public sealed class EmailEnquiry
{
diff --git a/LiteCharms.Features/Email/Models/Message.cs b/LiteCharms.Features/Email/Models/Message.cs
new file mode 100644
index 0000000..7432545
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/Message.cs
@@ -0,0 +1,19 @@
+namespace LiteCharms.Features.Email.Models;
+
+public class Message : IDisposable
+{
+ public Party? Sender { get; set; }
+
+ public Party? Recipient { get; set; }
+
+ public string? Subject { get; set; }
+
+ public Body? Body { get; set; }
+
+ public void Dispose()
+ {
+ Body?.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/LiteCharms.Features/Email/Models/Party.cs b/LiteCharms.Features/Email/Models/Party.cs
new file mode 100644
index 0000000..65c2e85
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/Party.cs
@@ -0,0 +1,8 @@
+namespace LiteCharms.Features.Email.Models;
+
+public class Party
+{
+ public string? Name { get; set; }
+
+ public string? Address { get; set; }
+}
diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs
new file mode 100644
index 0000000..4474d9e
--- /dev/null
+++ b/LiteCharms.Features/Email/Models/Response.cs
@@ -0,0 +1,22 @@
+using LiteCharms.Features.Shop;
+
+namespace LiteCharms.Features.Email.Models;
+
+public class Response
+{
+ public int Code { get; set; }
+
+ public string? Error { get; set; }
+
+ public EmailStatuses Status { get; set; }
+
+ private Response(EmailStatuses status, int code = 0, string? error = null)
+ {
+ Status = status;
+ Code = code;
+ Error = error;
+ }
+
+ public static Response Create(EmailStatuses status, int code = 0, string? error = null) =>
+ new(status, code, error);
+}
diff --git a/LiteCharms.Features/Extensions/Email.cs b/LiteCharms.Features/Extensions/Email.cs
new file mode 100644
index 0000000..ae3756d
--- /dev/null
+++ b/LiteCharms.Features/Extensions/Email.cs
@@ -0,0 +1,20 @@
+using LiteCharms.Features.Email;
+using LiteCharms.Features.Email.Configuration;
+
+namespace LiteCharms.Features.Extensions;
+
+public static class Email
+{
+ public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.Configure(configuration.GetSection("Email"));
+
+ services.AddSingleton();
+
+ services.AddOpenTelemetry()
+ .WithTracing(tracing => tracing.AddSource("LiteCharms.EmailService"))
+ .WithMetrics(metrics => metrics.AddMeter("LiteCharms.EmailService"));
+
+ return services;
+ }
+}
diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs
similarity index 65%
rename from LiteCharms.Extensions/EntityModeMappers.cs
rename to LiteCharms.Features/Extensions/EntityModeMappers.cs
index 9a1d97a..d73a028 100644
--- a/LiteCharms.Extensions/EntityModeMappers.cs
+++ b/LiteCharms.Features/Extensions/EntityModeMappers.cs
@@ -1,29 +1,36 @@
-using LiteCharms.Models;
+using LiteCharms.Features.Shop.CartPackages.Models;
+using LiteCharms.Features.Shop.Customers.Models;
+using LiteCharms.Features.Shop.Leads.Models;
+using LiteCharms.Features.Shop.Notifications.Models;
+using LiteCharms.Features.Shop.Orders.Models;
+using LiteCharms.Features.Shop.Products.Models;
+using LiteCharms.Features.Shop.Quotes.Models;
+using LiteCharms.Features.Shop.ShoppingCarts.Models;
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class EntityModeMappers
{
- public static ShoppingCartPackage ToModel(this Entities.ShoppingCartPackage entity) =>
+ public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) =>
new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
PackageId = entity.PackageId,
- ShoppingCartId = entity.ShoppingCartId
+ ShoppingCartId = entity.ShoppingCartId
};
- public static PackageItem ToModel(this Entities.PackageItem entity) =>
+ public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) =>
new()
{
Id = entity.Id,
Active = entity.Active,
CreatedAt = entity.CreatedAt,
PackageId = entity.PackageId,
- ProductPriceId = entity.ProductPriceId
+ ProductPriceId = entity.ProductPriceId
};
- public static Package ToModel(this Entities.Package entity) =>
+ public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) =>
new()
{
Id = entity.Id,
@@ -31,10 +38,12 @@ public static class EntityModeMappers
Active = entity.Active,
Description = entity.Description,
Name = entity.Name,
- UpdatedAt = entity.UpdatedAt
+ UpdatedAt = entity.UpdatedAt,
+ ImageUrl = entity.ImageUrl,
+ Summary = entity.Summary
};
- public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) =>
+ public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) =>
new()
{
Id = entity.Id,
@@ -42,21 +51,20 @@ public static class EntityModeMappers
UpdatedAt = entity.UpdatedAt,
ProductPriceId = entity.ProductPriceId,
Quantity = entity.Quantity,
- ShoppingCartId = entity.ShoppingCartId
+ ShoppingCartId = entity.ShoppingCartId
};
- public static ShoppingCart ToModel(this Entities.ShoppingCart entity) =>
+ public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) =>
new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
CustomerId = entity.CustomerId,
- OrderId = entity.OrderId,
- QuoteId = entity.QuoteId
+ OrderId = entity.OrderId
};
- public static Quote ToModel(this Entities.Quote entity) =>
+ public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) =>
new()
{
Id = entity.Id,
@@ -66,10 +74,12 @@ public static class EntityModeMappers
ExpiredAt = entity.ExpiredAt,
Reason = entity.Reason,
ShoppingCartId = entity.ShoppingCartId,
- Status = entity.Status
+ Status = entity.Status,
+ InvoiceUrl = entity.InvoiceUrl,
+ OrderId = entity.OrderId
};
- public static Notification ToModel(this Entities.Notification entity) =>
+ public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) =>
new()
{
Id = entity.Id,
@@ -79,9 +89,9 @@ public static class EntityModeMappers
CorrelationId = entity.CorrelationId,
CorrelationIdType = entity.CorrelationIdType,
IsInternal = entity.IsInternal,
- Sender = entity.Sender,
+ SenderAddress = entity.SenderAddress,
Platform = entity.Platform,
- Recipient = entity.Recipient,
+ RecipientName = entity.RecipientName,
Subject = entity.Subject,
Processed = entity.Processed,
SenderName = entity.SenderName,
@@ -93,7 +103,7 @@ public static class EntityModeMappers
Errors = entity.Errors
};
- public static Customer ToModel(this Entities.Customer entity) =>
+ public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) =>
new()
{
Id = entity.Id,
@@ -118,7 +128,7 @@ public static class EntityModeMappers
Whatsapp = entity.Whatsapp
};
- public static Lead ToModel(this Entities.Lead entity) =>
+ public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) =>
new()
{
Id = entity.Id,
@@ -136,10 +146,10 @@ public static class EntityModeMappers
ClickId = entity.ClickId,
TargetId = entity.TargetId,
WebClickId = entity.WebClickId,
- Status = entity.Status
+ Status = entity.Status
};
- public static Order ToModel(this Entities.Order entity) =>
+ public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) =>
new()
{
Id = entity.Id,
@@ -147,35 +157,35 @@ public static class EntityModeMappers
UpdatedAt = entity.UpdatedAt,
CustomerId = entity.CustomerId,
Notes = entity.Notes,
- RefundId = entity.RefundId,
- QuoteId = entity.QuoteId,
Status = entity.Status,
- ShoppingCartId = entity.ShoppingCartId,
- DepositRequired = entity.DepositRequired,
Requirements = entity.Requirements,
- Terms = entity.Terms
+ Terms = entity.Terms,
+ InvoiceUrl = entity.InvoiceUrl
};
- public static OrderRefund ToModel(this Entities.OrderRefund entity) =>
+ public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) =>
new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
OrderId = entity.OrderId,
Reason = entity.Reason,
- Amount = entity.Amount
+ Amount = entity.Amount
};
- public static Product ToModel(this Entities.Product entity) =>
+ public static Product ToModel(this Features.Shop.Products.Entities.Product entity) =>
new()
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
- Active = entity.Active
+ Active = entity.Active,
+ Summary = entity.Summary,
+ ImageUrl = entity.ImageUrl,
+ Thumbnails = entity.Thumbnails
};
- public static ProductPrice ToModel(this Entities.ProductPrice entity) =>
+ public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
new()
{
Id = entity.Id,
@@ -184,6 +194,6 @@ public static class EntityModeMappers
Active = entity.Active,
CreatedAt = entity.CreatedAt,
Discount = entity.Discount,
- UpdatedAt = entity.UpdatedAt
+ UpdatedAt = entity.UpdatedAt
};
}
diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs
new file mode 100644
index 0000000..ab76ecc
--- /dev/null
+++ b/LiteCharms.Features/Extensions/Hash.cs
@@ -0,0 +1,7 @@
+namespace LiteCharms.Features.Extensions;
+
+public static class Hash
+{
+ public static Func GenerateSha256HashString = (input) =>
+ Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
+}
diff --git a/LiteCharms.Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs
similarity index 92%
rename from LiteCharms.Extensions/HealthChecks.cs
rename to LiteCharms.Features/Extensions/HealthChecks.cs
index 01bb1ae..ebabd8f 100644
--- a/LiteCharms.Extensions/HealthChecks.cs
+++ b/LiteCharms.Features/Extensions/HealthChecks.cs
@@ -1,6 +1,6 @@
-using LiteCharms.Infrastructure.HealthChecks;
+using LiteCharms.Features.HealthChecks;
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class HealthChecks
{
diff --git a/LiteCharms.Extensions/Monitoring.cs b/LiteCharms.Features/Extensions/Monitoring.cs
similarity index 97%
rename from LiteCharms.Extensions/Monitoring.cs
rename to LiteCharms.Features/Extensions/Monitoring.cs
index a5b52da..8e5db29 100644
--- a/LiteCharms.Extensions/Monitoring.cs
+++ b/LiteCharms.Features/Extensions/Monitoring.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class Monitoring
{
diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs
similarity index 79%
rename from LiteCharms.Extensions/Postgres.cs
rename to LiteCharms.Features/Extensions/Postgres.cs
index 50c420c..0e70287 100644
--- a/LiteCharms.Extensions/Postgres.cs
+++ b/LiteCharms.Features/Extensions/Postgres.cs
@@ -1,6 +1,6 @@
-using LiteCharms.Infrastructure.Database;
+using LiteCharms.Features.Shop.Postgres;
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class Postgres
{
diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs
similarity index 96%
rename from LiteCharms.Extensions/Quartz.cs
rename to LiteCharms.Features/Extensions/Quartz.cs
index a92d462..e22b1ca 100644
--- a/LiteCharms.Extensions/Quartz.cs
+++ b/LiteCharms.Features/Extensions/Quartz.cs
@@ -1,7 +1,7 @@
-using LiteCharms.Abstractions;
-using LiteCharms.Infrastructure.Quartz;
+using LiteCharms.Features.Quartz;
+using LiteCharms.Features.Quartz.Abstractions;
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class Quartz
{
diff --git a/LiteCharms.Extensions/ServiceBus.cs b/LiteCharms.Features/Extensions/ServiceBus.cs
similarity index 80%
rename from LiteCharms.Extensions/ServiceBus.cs
rename to LiteCharms.Features/Extensions/ServiceBus.cs
index ab8d7c0..3a57832 100644
--- a/LiteCharms.Extensions/ServiceBus.cs
+++ b/LiteCharms.Features/Extensions/ServiceBus.cs
@@ -1,9 +1,9 @@
-using LiteCharms.Abstractions;
-using LiteCharms.Infrastructure.ServiceBus;
-using LiteCharms.Infrastructure.ServiceBus.Exchanges;
-using LiteCharms.Infrastructure.ServiceBus.Queues;
+using LiteCharms.Features.ServiceBus;
+using LiteCharms.Features.ServiceBus.Abstractions;
+using LiteCharms.Features.ServiceBus.Exchanges;
+using LiteCharms.Features.ServiceBus.Queues;
-namespace LiteCharms.Extensions;
+namespace LiteCharms.Features.Extensions;
public static class ServiceBus
{
diff --git a/LiteCharms.Features/Extensions/Shop.cs b/LiteCharms.Features/Extensions/Shop.cs
new file mode 100644
index 0000000..78502de
--- /dev/null
+++ b/LiteCharms.Features/Extensions/Shop.cs
@@ -0,0 +1,27 @@
+using LiteCharms.Features.Shop.CartPackages;
+using LiteCharms.Features.Shop.Customers;
+using LiteCharms.Features.Shop.Leads;
+using LiteCharms.Features.Shop.Notifications;
+using LiteCharms.Features.Shop.Orders;
+using LiteCharms.Features.Shop.Products;
+using LiteCharms.Features.Shop.Quotes;
+using LiteCharms.Features.Shop.ShoppingCarts;
+
+namespace LiteCharms.Features.Extensions;
+
+public static class Shop
+{
+ public static IServiceCollection AddShopServices(this IServiceCollection services)
+ {
+ services.AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton();
+
+ return services;
+ }
+}
diff --git a/LiteCharms.Abstractions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs
similarity index 87%
rename from LiteCharms.Abstractions/Timezones.cs
rename to LiteCharms.Features/Extensions/Timezones.cs
index 5457130..3976240 100644
--- a/LiteCharms.Abstractions/Timezones.cs
+++ b/LiteCharms.Features/Extensions/Timezones.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Abstractions;
+namespace LiteCharms.Features.Extensions;
public static class Timezones
{
@@ -23,5 +23,5 @@ public static class Timezones
return DateTimeOffset.Parse(localised!);
}
- public static DateTimeOffset UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone);
+ public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime;
}
diff --git a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs
similarity index 94%
rename from LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs
rename to LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs
index 9cb5f03..377da08 100644
--- a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs
+++ b/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Infrastructure.HealthChecks;
+namespace LiteCharms.Features.HealthChecks;
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
{
diff --git a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs
similarity index 93%
rename from LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs
rename to LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs
index d1a8bd0..59eb397 100644
--- a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs
+++ b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Infrastructure.HealthChecks;
+namespace LiteCharms.Features.HealthChecks;
public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
diff --git a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs b/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs
deleted file mode 100644
index 5b120df..0000000
--- a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-namespace LiteCharms.Features.Leads.Commands;
-
-public class CreateLeadCommand : IRequest>
-{
- public Guid? CustomerId { get; set; }
-
- public string? Source { get; set; }
-
- public string? ClickId { get; set; }
-
- public string? WebClickId { get; set; }
-
- public string? AppClickId { get; set; }
-
- public long? CampaignId { get; set; }
-
- public long? AdGroupId { get; set; }
-
- public long? AdName { get; set; }
-
- public long? TargetId { get; set; }
-
- public long? FeedItemId { get; set; }
-
- public string? ClickLocation { get; set; }
-
- public string? AttribusionHash { get; set; }
-
- private CreateLeadCommand(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash)
- {
- CustomerId = customerId;
- Source = source;
- ClickId = clickId;
- WebClickId = webClickId;
- AppClickId = appClickId;
- CampaignId = campaignId;
- AdGroupId = adGroupId;
- AdName = adName;
- TargetId = targetId;
- FeedItemId = feedItemId;
- ClickLocation = clickLocation;
- AttribusionHash = attribusionHash;
- }
-
- public static CreateLeadCommand Create(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash)
- {
- if(string.IsNullOrWhiteSpace(source))
- throw new ArgumentNullException("Lead source is required to create a lead.", nameof(source));
-
- if (string.IsNullOrWhiteSpace(clickId) || string.IsNullOrWhiteSpace(appClickId) || string.IsNullOrWhiteSpace(webClickId))
- throw new ArgumentException("ClickId, App ClickId and Web ClickId are required to create a lead.");
-
- return new(customerId, source, clickId, webClickId, appClickId, campaignId, adGroupId, adName, targetId, feedItemId, clickLocation, attribusionHash);
- }
-}
diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs
deleted file mode 100644
index 256bc22..0000000
--- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using LiteCharms.Features.Utilities.Hash.Commands;
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Leads.Commands.Handlers;
-
-public class CreateLeadCommandHandler(IDbContextFactory contextFactory, ISender mediator) : IRequestHandler>
-{
- public async ValueTask> Handle(CreateLeadCommand request, CancellationToken cancellationToken)
- {
- try
- {
- var hashCommand = ComputeHashCommand.Create($"{request.ClickId}{request.AppClickId}{request.WebClickId}");
- var hashResult = await mediator.Send(hashCommand, cancellationToken);
-
- if(hashResult.IsFailed)
- return Result.Fail(new Error($"Failed to compute hash for lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")
- .CausedBy(hashResult.Errors));
-
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var newLead = context.Leads.Add(new Entities.Lead
- {
- WebClickId = request.WebClickId,
- AppClickId = request.AppClickId,
- Source = request.Source,
- ClickId = request.ClickId,
- AdGroupId = request.AdGroupId,
- AdName = request.AdName,
- CampaignId = request.CampaignId,
- ClickLocation = request.ClickLocation,
- CustomerId = request.CustomerId,
- FeedItemId = request.FeedItemId,
- Status = Models.LeadStatus.New,
- TargetId = request.TargetId,
- AttributionHash = hashResult.Value
- });
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok(newLead.Entity.Id)
- : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs
deleted file mode 100644
index afaa15c..0000000
--- a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Leads.Commands.Handlers;
-
-public class UpdateLeadCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(UpdateLeadCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == request.LeadId, cancellationToken);
-
- if (lead is null)
- return Result.Fail(new Error($"Lead with ID {request.LeadId} not found."));
-
- lead.Status = request.Status;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error($"Failed to update the lead {request.LeadId}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs b/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs
deleted file mode 100644
index a201b70..0000000
--- a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Leads.Commands;
-
-public class UpdateLeadCommand : IRequest
-{
- public Guid LeadId { get; set; }
-
- public LeadStatus Status { get; set; }
-
- private UpdateLeadCommand(Guid leadId, LeadStatus status)
- {
- LeadId = leadId;
- Status = status;
- }
-
- public static UpdateLeadCommand Create(Guid leadId, LeadStatus status)
- {
- if (leadId == Guid.Empty)
- throw new ArgumentException("Lead ID cannot be empty.", nameof(leadId));
-
- if (!Enum.IsDefined(typeof(LeadStatus), status))
- throw new ArgumentException("Invalid lead status.", nameof(status));
-
- return new(leadId, status);
- }
-}
diff --git a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs b/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs
deleted file mode 100644
index ddede3c..0000000
--- a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Leads.Queries;
-
-public class GetCustomerLeadsQuery : IRequest>
-{
- public Guid CustomerId { get; }
-
- public DateOnly From { get; set; }
-
- public DateOnly To { get; set; }
-
- private GetCustomerLeadsQuery(Guid customerId, DateOnly from, DateOnly to)
- {
- CustomerId = customerId;
- From = from;
- To = to;
- }
-
- public static GetCustomerLeadsQuery Create(Guid customerId, DateOnly from, DateOnly to)
- {
- if(customerId == Guid.Empty)
- throw new ArgumentException("Customer ID cannot be empty.", nameof(customerId));
-
- if(from > to)
- throw new ArgumentException("The 'From' date cannot be later than the 'To' date.");
-
- return new(customerId, from, to);
- }
-}
diff --git a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs b/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs
deleted file mode 100644
index 3278bf7..0000000
--- a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Leads.Queries;
-
-public class GetLeadsQuery : IRequest>
-{
- public DateOnly From { get; set; }
-
- public DateOnly To { get; set; }
-
- public int MaxRecords { get; set; }
-
- private GetLeadsQuery(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- From = from;
- To = to;
- MaxRecords = maxRecords;
- }
-
- public static GetLeadsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- 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);
- }
-}
diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs
deleted file mode 100644
index ed90212..0000000
--- a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Leads.Queries.Handlers;
-
-public class GetCustomerLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetCustomerLeadsQuery 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 leads = await context.Leads.AsNoTracking()
- .OrderByDescending(o => o.CreatedAt)
- .Where(lead => lead.CustomerId == request.CustomerId)
- .Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate)
- .ToArrayAsync(cancellationToken);
-
- return leads?.Length > 0
- ? Result.Ok(leads.Select(l => l.ToModel()).ToArray())
- : Result.Fail(new Error($"No customer {request.CustomerId} leads found for the specified date range {request.From} to {request.To}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs
deleted file mode 100644
index 0ebaafb..0000000
--- a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Leads.Queries.Handlers;
-
-public class GetLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetLeadsQuery 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 leads = await context.Leads.AsNoTracking()
- .OrderByDescending(o => o.CreatedAt)
- .Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return leads?.Length > 0
- ? Result.Ok(leads.Select(l => l.ToModel()).ToArray())
- : Result.Fail(new Error($"No leads found for the specified date range {request.From} to {request.To}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj
index f541547..a9aedad 100644
--- a/LiteCharms.Features/LiteCharms.Features.csproj
+++ b/LiteCharms.Features/LiteCharms.Features.csproj
@@ -21,6 +21,7 @@
LICENSE
utility;dotnet
icon.png
+ 8a78916e-c86b-4f4b-9f4e-d8e7769b5d23
@@ -28,6 +29,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
@@ -50,16 +131,26 @@
+
+
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+ PreserveNewest
+
+
+
+
diff --git a/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs
new file mode 100644
index 0000000..2bf0410
--- /dev/null
+++ b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs
@@ -0,0 +1,29 @@
+namespace LiteCharms.Features.Mediator;
+
+public sealed class LoggingPipelineBehavior(ILogger> logger) :
+ IPipelineBehavior
+ where TRequest : IRequest
+ where TResponse : ResultBase, new()
+{
+ public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ TResponse? response = await next(message, cancellationToken);
+
+ if (response is null)
+ logger.LogCritical("{Request} {TypeName} was returned as null", typeof(TRequest).Name, typeof(TRequest).Name);
+
+ if(response?.IsFailed == true || response?.Errors?.Any() == true)
+ {
+ foreach (var error in response.Errors)
+ {
+ if (!string.IsNullOrWhiteSpace(error.Message))
+ logger.LogWarning("{Request} {Error}", typeof(TRequest).Name, error.Message);
+
+ if (error?.Reasons?.Count > 0)
+ error.Reasons.ForEach(r => logger.LogError("{Request} {Reason}", typeof(TRequest).Name, r.ToString()));
+ }
+ }
+
+ return response;
+ }
+}
diff --git a/LiteCharms.Features/Mediator/MediatorTelemetry.cs b/LiteCharms.Features/Mediator/MediatorTelemetry.cs
new file mode 100644
index 0000000..00493c1
--- /dev/null
+++ b/LiteCharms.Features/Mediator/MediatorTelemetry.cs
@@ -0,0 +1,12 @@
+namespace LiteCharms.Features.Mediator;
+
+public static class MediatorTelemetry
+{
+ public const string ServiceName = "LiteCharms.Mediator";
+
+ public static readonly ActivitySource Source = new(ServiceName);
+ public static readonly Meter Meter = new(ServiceName);
+
+ public static readonly Counter RequestCounter = Meter.CreateCounter("mediator_requests_total");
+ public static readonly Histogram RequestDuration = Meter.CreateHistogram("mediator_request_duration_ms");
+}
diff --git a/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs
new file mode 100644
index 0000000..0a9efbd
--- /dev/null
+++ b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs
@@ -0,0 +1,66 @@
+namespace LiteCharms.Features.Mediator;
+
+public sealed class TelemetryPipelineBehavior :
+ IPipelineBehavior
+ where TRequest : IRequest
+ where TResponse : ResultBase, new()
+{
+ public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ using var activity = MediatorTelemetry.Source.StartActivity(requestName);
+
+ activity?.SetTag("mediator.request_type", typeof(TRequest).FullName);
+
+ var stopWatch = Stopwatch.StartNew();
+ var status = "Success";
+
+ try
+ {
+ TResponse? response = await next(message, cancellationToken);
+
+ if (response is null)
+ {
+ status = "NullResponse";
+ activity?.SetStatus(ActivityStatusCode.Error, "Response was null");
+
+ return response;
+ }
+
+ if (response.IsFailed)
+ {
+ status = "Failed";
+ activity?.SetStatus(ActivityStatusCode.Error, "Request failed");
+
+ var firstError = response.Errors.FirstOrDefault()?.Message ?? "Unknown Error";
+ activity?.SetTag("error.message", firstError);
+
+ foreach (var error in response.Errors)
+ activity?.AddEvent(new ActivityEvent("Result Error", tags: new() { { "message", error.Message } }));
+ }
+ else
+ activity?.SetStatus(ActivityStatusCode.Ok);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ status = "Exception";
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+
+ activity?.AddException(ex);
+
+ throw;
+ }
+ finally
+ {
+ stopWatch.Stop();
+
+ var tags = new TagList { { "request", requestName }, { "status", status } };
+
+ MediatorTelemetry.RequestCounter.Add(1, tags);
+ MediatorTelemetry.RequestDuration.Record(stopWatch.Elapsed.TotalMilliseconds, tags);
+ }
+ }
+}
\ No newline at end of file
diff --git a/LiteCharms.Features/Models/DateRange.cs b/LiteCharms.Features/Models/DateRange.cs
new file mode 100644
index 0000000..c82cba3
--- /dev/null
+++ b/LiteCharms.Features/Models/DateRange.cs
@@ -0,0 +1,10 @@
+namespace LiteCharms.Features.Models;
+
+public class DateRange
+{
+ public DateOnly From { get; set; }
+
+ public DateOnly To { get; set; }
+
+ public int MaxRecords { get; set; }
+}
diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs
deleted file mode 100644
index 2b3e933..0000000
--- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Notifications.Commands;
-
-public class CreateNotificationCommand : IRequest>
-{
- public NotificationDirection Direction { get; set; }
-
- public string? Sender { get; set; }
-
- public string? SenderAddress { get; set; }
-
- public string? Subject { get; set; }
-
- public string? Message { get; set; }
-
- public NotificationPlatforms Platform { get; set; }
-
- public Priorities Priority { get; set; }
-
- public string? Recipient { get; set; }
-
- public string? RecipientAddress { get; set; }
-
- public string? CorrelationId { 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, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false)
- {
- Direction = direction;
- Sender = sender;
- SenderAddress = senderAddress;
- Subject = subject;
- Message = message;
- Platform = platform;
- Priority = priority;
- Recipient = recipient;
- RecipientAddress = recipientAddress;
- CorrelationId = correlationId;
- CorrelationIdType = correlationIdType;
- IsInternal = isInternal;
- 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, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false)
- {
- if (string.IsNullOrWhiteSpace(sender))
- throw new ArgumentException("Sender name is required.", nameof(sender));
-
- if (string.IsNullOrWhiteSpace(subject))
- throw new ArgumentException("Subject is required.", nameof(subject));
-
- if (string.IsNullOrWhiteSpace(message))
- throw new ArgumentException("Message is required.", nameof(message));
-
- 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 is required.", nameof(correlationId));
-
- 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
deleted file mode 100644
index e7a3fb4..0000000
--- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Notifications.Commands.Handlers;
-
-public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var newNotification = context.Notifications.Add(new Entities.Notification
- {
- Direction = request.Direction,
- SenderName = request.Sender,
- Sender = request.SenderAddress,
- Recipient = request.Recipient,
- RecipientAddress = request.RecipientAddress,
- Subject = request.Subject,
- Message = request.Message,
- Platform = request.Platform,
- 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)
- : Result.Fail(new Error("Failed to create notification"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs
deleted file mode 100644
index 5ce4e81..0000000
--- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Notifications.Commands.Handlers;
-
-public class UpdateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(UpdateNotificationCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken);
-
- if(notification is null)
- return Result.Fail(new Error($"Notification with id {request.NotificationId} not found."));
-
- 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}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs
deleted file mode 100644
index 950442d..0000000
--- a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace LiteCharms.Features.Notifications.Commands;
-
-public class UpdateNotificationCommand : IRequest
-{
- public Guid NotificationId { get; set; }
-
- public bool Processed { get; set; }
-
- 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, bool hasError = false, string[]? errors = null)
- {
- if(notificationId == Guid.Empty)
- throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId));
-
- return new(notificationId, processed);
- }
-}
diff --git a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs
deleted file mode 100644
index 61b24d5..0000000
--- a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-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/Queries/GetNotificationQuery.cs b/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs
deleted file mode 100644
index f41aea5..0000000
--- a/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Notifications.Queries;
-
-public class GetNotificationQuery : IRequest>
-{
- public Guid NotificationId { get; set; }
-
- private GetNotificationQuery(Guid notificationId) => NotificationId = notificationId;
-
- public static GetNotificationQuery Create(Guid notificationId)
- {
- if (notificationId == Guid.Empty)
- throw new ArgumentException("Notification ID is required.", nameof(notificationId));
-
- return new(notificationId);
- }
-}
diff --git a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs b/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs
deleted file mode 100644
index 6eef589..0000000
--- a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Notifications.Queries;
-
-public class GetNotificationsQuery : IRequest>
-{
- public DateOnly From { get; set; }
-
- public DateOnly To { get; set; }
-
- public int MaxRecords { get; set; }
-
- private GetNotificationsQuery(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- From = from;
- To = to;
- MaxRecords = maxRecords;
- }
-
- public static GetNotificationsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- 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.", nameof(maxRecords));
-
- return new(from, to, maxRecords);
- }
-}
diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs
deleted file mode 100644
index 5eac5d8..0000000
--- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Notifications.Queries.Handlers;
-
-public class GetNotificationQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetNotificationQuery request, CancellationToken cancellationToken)
- {
- try
- {
- await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken);
-
- return notification is not null
- ? Result.Ok(notification.ToModel())
- : Result.Fail(new Error($"Notification with id {request.NotificationId} not found"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs
deleted file mode 100644
index 70e448f..0000000
--- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Notifications.Queries.Handlers;
-
-public class GetNotificationsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetNotificationsQuery 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 notifications = await context.Notifications.AsNoTracking()
- .Where(n => n.CreatedAt >= fromDate && n.CreatedAt <= toDate)
- .OrderByDescending(n => n.CreatedAt)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return notifications?.Length > 0
- ? Result.Ok(notifications.Select(n => n.ToModel()).ToArray())
- : Result.Fail(new Error($"No notifications found for the specified date range {request.From} to {request.To}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs
deleted file mode 100644
index 8baf7e1..0000000
--- a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace LiteCharms.Features.Orders.Commands;
-
-public class CreateOrderCommand : IRequest>
-{
- public Guid CustomerId { get; set; }
-
- public Guid ShoppingCartId { get; set; }
-
- public Guid? QuoteId { get; set; }
-
- 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, 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));
-
- if (shoppingCartId == Guid.Empty)
- throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId));
-
- 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
deleted file mode 100644
index b97f83e..0000000
--- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Commands.Handlers;
-
-public class CreateOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- if(!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken))
- return Result.Fail(new Error($"Customer {request.CustomerId} does not exist."));
-
- if(!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken))
- return Result.Fail(new Error($"Shopping cart {request.ShoppingCartId} does not exist."));
-
- if(request.QuoteId.HasValue && !await context.Quotes.AnyAsync(q => q.Id == request.QuoteId.Value, cancellationToken))
- return Result.Fail(new Error($"Quote {request.QuoteId.Value} does not exist."));
-
- var newOrder = context.Orders.Add(new Entities.Order
- {
- CreatedAt = DateTime.UtcNow,
- Status = OrderStatus.Pending,
- CustomerId = request.CustomerId,
- QuoteId = request.QuoteId,
- ShoppingCartId = request.ShoppingCartId,
- DepositRequired = request.DepositRequired,
- Requirements = request.Requirements,
- Notes = request.Notes,
- Terms = request.Terms
- });
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok(newOrder.Entity.Id)
- : Result.Fail(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs
deleted file mode 100644
index 0cfe348..0000000
--- a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Orders.Commands.Handlers;
-
-public class UpdateOrderStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
-
- if (order is null)
- return Result.Fail(new Error($"Order {request.OrderId} not found"));
-
- order.Status = request.Status;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error($"Failed to update order {request.OrderId}"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs b/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs
deleted file mode 100644
index 3e6d1c6..0000000
--- a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Commands;
-
-public class UpdateOrderStatusCommand : IRequest
-{
- public Guid OrderId { get; set; }
-
- public OrderStatus Status { get; set; }
-
- public string? Note { get; set; }
-
- private UpdateOrderStatusCommand(Guid orderId, OrderStatus status, string? note)
- {
- OrderId = orderId;
- Status = status;
- Note = note;
- }
-
- public static UpdateOrderStatusCommand Create(Guid orderId, OrderStatus status, string? note)
- {
- if (orderId == Guid.Empty)
- throw new ArgumentException("OrderId is required.", nameof(orderId));
-
- if (!Enum.IsDefined(typeof(OrderStatus), status))
- throw new ArgumentException("Invalid order status.", nameof(status));
-
- return new(orderId, status, note);
- }
-}
diff --git a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs b/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs
deleted file mode 100644
index 7d11f91..0000000
--- a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries;
-
-public class GetCustomerOrdersQuery : IRequest>
-{
- public Guid CustomerId { get; }
-
- private GetCustomerOrdersQuery(Guid customerId) => CustomerId = customerId;
-
- public static GetCustomerOrdersQuery Create(Guid customerId)
- {
- if (customerId == Guid.Empty)
- throw new ArgumentException("CustomerId is required.", nameof(customerId));
-
- return new(customerId);
- }
-}
diff --git a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs b/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs
deleted file mode 100644
index 887e03b..0000000
--- a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries;
-
-public class GetOrderRefundQuery : IRequest>
-{
- public Guid OrderId { get; set; }
-
- public Guid OrderRefundId { get; set; }
-
- private GetOrderRefundQuery(Guid orderId, Guid orderRefundId)
- {
- OrderId = orderId;
- OrderRefundId = orderRefundId;
- }
-
- public static GetOrderRefundQuery Create(Guid orderId, Guid orderRefundId)
- {
- if (orderId == Guid.Empty)
- throw new ArgumentException("OrderId is required.", nameof(orderId));
-
- if (orderRefundId == Guid.Empty)
- throw new ArgumentException("OrderRefundId is required.", nameof(orderRefundId));
-
- return new(orderId, orderRefundId);
- }
-}
diff --git a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs b/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs
deleted file mode 100644
index c0c2c3b..0000000
--- a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries;
-
-public class GetOrdersQuery : IRequest>
-{
- public DateOnly From { get; set; }
-
- public DateOnly To { get; set; }
-
- public int MaxRecords { get; set; }
-
- private GetOrdersQuery(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- From = from;
- To = to;
- MaxRecords = maxRecords;
- }
-
- public static GetOrdersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000)
- {
- 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);
- }
-}
\ No newline at end of file
diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs
deleted file mode 100644
index eb9e79f..0000000
--- a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries.Handlers;
-
-public class GetCustomerOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- if(!await context.Customers.AsNoTracking().AnyAsync(c => c.Id == request.CustomerId, cancellationToken))
- return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist."));
-
- var orders = await context.Orders.AsNoTracking()
- .OrderByDescending(o => o.CreatedAt)
- .Where(o => o.CustomerId == request.CustomerId)
- .ToArrayAsync(cancellationToken);
-
- return orders?.Length > 0
- ? Result.Ok(orders.Select(o => o.ToModel()).ToArray())
- : Result.Fail(new Error($"No orders found for customer with Id {request.CustomerId}."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs
deleted file mode 100644
index 47dd481..0000000
--- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries.Handlers;
-
-public class GetOrderRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var refund = await context.OrderRefunds.AsNoTracking()
- .FirstOrDefaultAsync(r => r.OrderId == request.OrderId && r.Id == request.OrderRefundId, cancellationToken);
-
- return refund is not null
- ? Result.Ok(refund.ToModel())
- : Result.Fail(new Error($"Refund {request.OrderRefundId} not found for the given OrderId: {request.OrderId}"));
- }
- 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
deleted file mode 100644
index f244869..0000000
--- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Orders.Queries.Handlers;
-
-public class GetOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetOrdersQuery 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 orders = await context.Orders
- .AsNoTracking()
- .OrderByDescending(o => o.CreatedAt)
- .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return orders?.Length > 0
- ? Result.Ok(orders.Select(o => o.ToModel()).ToArray())
- : Result.Fail(new Error($"No orders 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/Products/Queries/GetProductPriceQuery.cs b/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs
deleted file mode 100644
index 347d759..0000000
--- a/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries;
-
-public class GetProductPriceQuery : IRequest>
-{
- public Guid ProductId { get; set; }
-
- private GetProductPriceQuery(Guid productId) => ProductId = productId;
-
- public static GetProductPriceQuery Create(Guid productId)
- {
- if (productId == Guid.Empty)
- throw new ArgumentException("ProductId is required.", nameof(productId));
-
- return new(productId);
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs b/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs
deleted file mode 100644
index 6e9cf66..0000000
--- a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries;
-
-public class GetProductPricesQuery : IRequest>
-{
- public int MaxRecords { get; set; }
-
- private GetProductPricesQuery(int maxRecords = 1000) => MaxRecords = maxRecords;
-
- public static GetProductPricesQuery Create(int maxRecords = 1000)
- {
- if (maxRecords <= 0)
- throw new ArgumentOutOfRangeException(nameof(maxRecords), "MaxRecords must be greater than zero.");
-
- return new(maxRecords);
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/GetProductQuery.cs b/LiteCharms.Features/Products/Queries/GetProductQuery.cs
deleted file mode 100644
index 2f5b2a8..0000000
--- a/LiteCharms.Features/Products/Queries/GetProductQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries;
-
-public class GetProductQuery : IRequest>
-{
- public Guid ProductId { get; set; }
-
- private GetProductQuery(Guid productId) => ProductId = productId;
-
- public static GetProductQuery Create(Guid productId)
- {
- if(productId == Guid.Empty)
- throw new ArgumentException("Product ID is required.", nameof(productId));
-
- return new(productId);
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs b/LiteCharms.Features/Products/Queries/GetProductsQuery.cs
deleted file mode 100644
index 1c7082d..0000000
--- a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries;
-
-public class GetProductsQuery : IRequest>
-{
- public int MaxRecords { get; set; }
-
- private GetProductsQuery(int maxRecords = 1000) => MaxRecords = maxRecords;
-
- public static GetProductsQuery Create(int maxRecords = 1000)
- {
- if (maxRecords <= 0)
- throw new ArgumentException("MaxRecords must be a positive integer.");
-
- return new(maxRecords);
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs
deleted file mode 100644
index f73bced..0000000
--- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries.Handlers;
-
-public class GetProductPriceQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetProductPriceQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- if(!await context.Products.AnyAsync(p => p.Id == request.ProductId, cancellationToken))
- return Result.Fail(new Error($"Product {request.ProductId} not found."));
-
- var productPrice = await context.ProductPrices.AsNoTracking()
- .Where(pp => pp.ProductId == request.ProductId && pp.Active)
- .OrderByDescending(pp => pp.CreatedAt)
- .FirstOrDefaultAsync(cancellationToken);
-
- return productPrice is not null
- ? Result.Ok(productPrice.ToModel())
- : Result.Fail(new Error($"Product price {request.ProductId} not found."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs
deleted file mode 100644
index c9eac0a..0000000
--- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries.Handlers;
-
-public class GetProductPricesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetProductPricesQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var products = await context.ProductPrices.AsNoTracking()
- .OrderByDescending(o => o.Id)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return Result.Ok(products.Select(p => p.ToModel()).ToArray());
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs
deleted file mode 100644
index 6cf09c6..0000000
--- a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries.Handlers;
-
-public class GetProductQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetProductQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var product = await context.Products.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);
-
- return product is not null
- ? Result.Ok(product.ToModel())
- : Result.Fail(new Error($"Product with ID {request.ProductId} not found."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs
deleted file mode 100644
index 3fc0366..0000000
--- a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using LiteCharms.Extensions;
-using LiteCharms.Infrastructure.Database;
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Products.Queries.Handlers;
-
-public class GetProductsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler>
-{
- public async ValueTask> Handle(GetProductsQuery request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var products = await context.Products.AsNoTracking()
- .OrderByDescending(o => o.Id)
- .Take(request.MaxRecords)
- .ToArrayAsync(cancellationToken);
-
- return Result.Ok(products.Select(p => p.ToModel()).ToArray());
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Abstractions/IJobOrchestrator.cs b/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs
similarity index 79%
rename from LiteCharms.Abstractions/IJobOrchestrator.cs
rename to LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs
index d98c155..8ce2c33 100644
--- a/LiteCharms.Abstractions/IJobOrchestrator.cs
+++ b/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs
@@ -1,4 +1,6 @@
-namespace LiteCharms.Abstractions;
+using LiteCharms.Features.Abstractions;
+
+namespace LiteCharms.Features.Quartz.Abstractions;
public interface IJobOrchestrator
{
diff --git a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs
similarity index 93%
rename from LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs
rename to LiteCharms.Features/Quartz/JobOrchestrator.cs
index 6874541..4f578b5 100644
--- a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs
+++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs
@@ -1,7 +1,8 @@
-using LiteCharms.Abstractions;
-using static LiteCharms.Abstractions.Timezones;
+using LiteCharms.Features.Abstractions;
+using LiteCharms.Features.Quartz.Abstractions;
+using static LiteCharms.Features.Extensions.Timezones;
-namespace LiteCharms.Infrastructure.Quartz;
+namespace LiteCharms.Features.Quartz;
public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator
{
diff --git a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs
similarity index 87%
rename from LiteCharms.Infrastructure/Quartz/MediatorJob.cs
rename to LiteCharms.Features/Quartz/MediatorJob.cs
index 8cf9c75..b3ea928 100644
--- a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs
+++ b/LiteCharms.Features/Quartz/MediatorJob.cs
@@ -1,6 +1,6 @@
-using LiteCharms.Abstractions;
+using LiteCharms.Features.Abstractions;
-namespace LiteCharms.Infrastructure.Quartz;
+namespace LiteCharms.Features.Quartz;
[DisallowConcurrentExecution]
public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent
diff --git a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs
similarity index 93%
rename from LiteCharms.Infrastructure/Quartz/RetryJobListener.cs
rename to LiteCharms.Features/Quartz/RetryJobListener.cs
index afe84e7..968b8bb 100644
--- a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs
+++ b/LiteCharms.Features/Quartz/RetryJobListener.cs
@@ -1,4 +1,4 @@
-namespace LiteCharms.Infrastructure.Quartz;
+namespace LiteCharms.Features.Quartz;
public class RetryJobListener : IJobListener
{
diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs b/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs
deleted file mode 100644
index 498aa4a..0000000
--- a/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace LiteCharms.Features.Quotes.Commands;
-
-public class AssignQuoteToOrderCommand : IRequest
-{
- public Guid OrderId { get; set; }
-
- public Guid QuoteId { get; set; }
-
- private AssignQuoteToOrderCommand(Guid orderId, Guid quoteId)
- {
- OrderId = orderId;
- QuoteId = quoteId;
- }
-
- public static AssignQuoteToOrderCommand Create(Guid orderId, Guid quoteId)
- {
- if(orderId == Guid.Empty)
- throw new ArgumentException("Order ID is required.", nameof(orderId));
-
- if(quoteId == Guid.Empty)
- throw new ArgumentException("Quote ID is required.", nameof(quoteId));
-
- return new AssignQuoteToOrderCommand(orderId, quoteId);
- }
-}
diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs b/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs
deleted file mode 100644
index 61c0902..0000000
--- a/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace LiteCharms.Features.Quotes.Commands;
-
-public class AssignQuoteToShoppingCartCommand : IRequest
-{
- public Guid QuoteId { get; set; }
-
- public Guid ShoppingCartId { get; set; }
-
- private AssignQuoteToShoppingCartCommand(Guid quoteId, Guid shoppingCartId)
- {
- QuoteId = quoteId;
- ShoppingCartId = shoppingCartId;
- }
-
- public static AssignQuoteToShoppingCartCommand Create(Guid quoteId, Guid shoppingCartId)
- {
- if(quoteId == Guid.Empty)
- throw new ArgumentException("QuoteId cannot be empty.", nameof(quoteId));
-
- if (shoppingCartId == Guid.Empty)
- throw new ArgumentException("ShoppingCartId cannot be empty.", nameof(shoppingCartId));
-
- return new(quoteId, shoppingCartId);
- }
-}
diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs
deleted file mode 100644
index 3145cd4..0000000
--- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Quotes.Commands.Handlers;
-
-public class AssignQuoteToOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
-
- if (order is null)
- return Result.Fail(new Error($"Order with id {request.OrderId} not found"));
-
- if(!await context.Quotes.AnyAsync(q => q.Id == request.OrderId, cancellationToken))
- return Result.Fail(new Error($"Quote with id {request.QuoteId} not found"));
-
- if(order.QuoteId == request.QuoteId)
- return Result.Fail(new Error($"Quote with id {request.QuoteId} is already assigned to order with id {request.OrderId}"));
-
- order.QuoteId = request.QuoteId;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error($"Failed to assign quote with id {request.QuoteId} to order with id {request.OrderId}"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs
deleted file mode 100644
index 443c907..0000000
--- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Quotes.Commands.Handlers;
-
-public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var shoppingCart = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.ShoppingCartId, cancellationToken);
-
- if (shoppingCart is null)
- return Result.Fail(new Error($"ShoppingCart with id {request.ShoppingCartId} not found"));
-
- if(!await context.Quotes.AnyAsync(q => q.Id == request.QuoteId, cancellationToken))
- return Result.Fail(new Error($"Quote with id {request.QuoteId} not found"));
-
- shoppingCart.QuoteId = request.QuoteId;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error("Failed to assign quote to shopping cart"));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs
deleted file mode 100644
index a279370..0000000
--- a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using LiteCharms.Infrastructure.Database;
-
-namespace LiteCharms.Features.Quotes.Commands.Handlers;
-
-public class UpdateQuoteStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler
-{
- public async ValueTask Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken)
- {
- try
- {
- using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
-
- var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == request.QuoteId, cancellationToken);
-
- if (quote is null)
- return Result.Fail(new Error("Quote not found."));
-
- quote.Status = request.Status;
-
- return await context.SaveChangesAsync(cancellationToken) > 0
- ? Result.Ok()
- : Result.Fail(new Error("Failed to update quote status."));
- }
- catch (Exception ex)
- {
- return Result.Fail(new Error(ex.Message).CausedBy(ex));
- }
- }
-}
diff --git a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs b/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs
deleted file mode 100644
index 901bc46..0000000
--- a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using LiteCharms.Models;
-
-namespace LiteCharms.Features.Quotes.Commands;
-
-public class UpdateQuoteStatusCommand : IRequest