Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b70d9559b0 | |||
| 032b9e1818 | |||
| 424c1c6f8c | |||
| 81d5e8f07c | |||
| 7d5e9a18d8 | |||
| 20a53942b5 | |||
| 3656223b5f | |||
| 9edb2aa4aa | |||
| ccf30ac36b | |||
| 6ed023f2cf | |||
| d6fdf1b9c8 | |||
| 2c9f5a846c | |||
| 89a343a85f | |||
| 41f7c05be3 | |||
| 52d204e286 | |||
| 1a03355e84 | |||
| f245bc94e1 | |||
| 7743c3178e | |||
| da141311ff | |||
| ab3d8e6e9a | |||
| 97bde73777 | |||
| db4c348288 | |||
| a65e926a53 | |||
| 6683234642 | |||
| 1471d9e597 | |||
| 6ddbb9479a | |||
| e978aa17f8 | |||
| 6c7349a0f8 | |||
| a31f75c5ef | |||
| e97fd6cd3f | |||
| 7f4246ac63 | |||
| 184c7c252a | |||
| dfc62c8fe1 | |||
| bfe8c458d6 | |||
| 0f91f102e5 | |||
| e6e0475db1 | |||
| be9c83c8a3 | |||
| 65687d231e | |||
| 5090c60797 | |||
| 4523ef6151 | |||
| 9432252e15 | |||
| 36b3656886 | |||
| 47111a1a3a | |||
| f606b8fd3c | |||
| 2610275bef | |||
| 134d8429c0 | |||
| 42001998d6 | |||
| a42c51d7b2 | |||
| 6eb3d50375 | |||
| 26075cd9a7 | |||
| 4deb732804 | |||
| 20d9387d0b | |||
| 9f6d0ccaa0 | |||
| 1acbc4d213 | |||
| 8c99668fac | |||
| ad44f46204 | |||
| 49d999c1e3 | |||
| 9ed4777a18 | |||
| 0cf44f68cc | |||
| 41ed5a4288 | |||
| bbcba5e06c | |||
| 502cc326dd | |||
| 4675d4c5fc | |||
| f80bb2fff9 | |||
| 6767906b0d | |||
| a344af4498 |
+2
-22
@@ -17,30 +17,9 @@ steps:
|
|||||||
NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/
|
NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/
|
||||||
VERSION: 1.${DRONE_BUILD_NUMBER}.0
|
VERSION: 1.${DRONE_BUILD_NUMBER}.0
|
||||||
commands:
|
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
|
|
||||||
- dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/
|
- 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
|
- 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
|
- name: gitea-tag-release
|
||||||
image: alpine/git
|
image: alpine/git
|
||||||
environment:
|
environment:
|
||||||
@@ -49,6 +28,7 @@ steps:
|
|||||||
GITEA_PASS: { from_secret: git_password }
|
GITEA_PASS: { from_secret: git_password }
|
||||||
VERSION: 1.${DRONE_BUILD_NUMBER}.0
|
VERSION: 1.${DRONE_BUILD_NUMBER}.0
|
||||||
commands:
|
commands:
|
||||||
|
- echo "169.255.58.144 gitea.khongisa.co.za" >> /etc/hosts
|
||||||
- apk add --no-cache curl
|
- apk add --no-cache curl
|
||||||
- git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git
|
- git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git
|
||||||
- git tag $VERSION
|
- git tag $VERSION
|
||||||
@@ -61,7 +41,7 @@ steps:
|
|||||||
\"tag_name\": \"$VERSION\",
|
\"tag_name\": \"$VERSION\",
|
||||||
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
|
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
|
||||||
\"name\": \"Library Suite $VERSION\",
|
\"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.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
|
||||||
\"draft\": false,
|
\"draft\": false,
|
||||||
\"prerelease\": false
|
\"prerelease\": false
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<SignAssembly>True</SignAssembly>
|
|
||||||
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Nuget Package Details -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>LiteCharms.Abstractions</PackageId>
|
|
||||||
<Version>1.0.20</Version>
|
|
||||||
<Authors>Khwezi Mngoma</Authors>
|
|
||||||
<Company>Lite Charms (PTY) Ltd</Company>
|
|
||||||
<Description>Shared abstractions for Lite Charms applications.</Description>
|
|
||||||
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
|
|
||||||
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
|
||||||
<PackageTags>utility;dotnet</PackageTags>
|
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
|
|
||||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="FluentResults" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
|
|
||||||
|
|
||||||
<Using Include="Mediator" />
|
|
||||||
<Using Include="FluentResults" />
|
|
||||||
<Using Include="System.Threading.Channels" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class OrderConfiguration : IEntityTypeConfiguration<Order>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<Order> 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<int>().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<Order>(f => f.QuoteId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
builder.HasOne(f => f.Customer)
|
|
||||||
.WithMany(f => f.Orders)
|
|
||||||
.HasForeignKey(f => f.CustomerId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class PackageConfirguration : IEntityTypeConfiguration<Package>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<Package> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class ProductConfiguration : IEntityTypeConfiguration<Product>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<Product> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class QuoteConfiguration : IEntityTypeConfiguration<Quote>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<Quote> 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<int>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class ShoppingCartConfiguration : IEntityTypeConfiguration<ShoppingCart>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<ShoppingCart> 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<Order>(o => o.ShoppingCartId)
|
|
||||||
.OnDelete(DeleteBehavior.NoAction);
|
|
||||||
|
|
||||||
builder.HasOne(f => f.Quote)
|
|
||||||
.WithOne(o => o.ShoppingCart)
|
|
||||||
.HasForeignKey<Quote>(o => o.ShoppingCartId)
|
|
||||||
.OnDelete(DeleteBehavior.NoAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCartItem>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<ShoppingCartItem> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
namespace LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration<ShoppingCartPackage>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<ShoppingCartPackage> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<SignAssembly>True</SignAssembly>
|
|
||||||
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Nuget Package Details -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>LiteCharms.Entities</PackageId>
|
|
||||||
<Version>1.0.20</Version>
|
|
||||||
<Authors>Khwezi Mngoma</Authors>
|
|
||||||
<Company>Lite Charms (PTY) Ltd</Company>
|
|
||||||
<Description>Shared entities for Lite Charms applications.</Description>
|
|
||||||
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
|
|
||||||
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
|
||||||
<PackageTags>utility;dotnet</PackageTags>
|
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\LICENSE" Pack="true" PackagePath="\"/>
|
|
||||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Database -->
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
|
||||||
|
|
||||||
<!-- Global Usings -->
|
|
||||||
<Using Include="Microsoft.EntityFrameworkCore" />
|
|
||||||
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\LiteCharms.Models\LiteCharms.Models.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
namespace LiteCharms.Entities;
|
|
||||||
|
|
||||||
[EntityTypeConfiguration<OrderConfiguration, Order>]
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using LiteCharms.Entities.Configuration;
|
|
||||||
|
|
||||||
namespace LiteCharms.Entities;
|
|
||||||
|
|
||||||
[EntityTypeConfiguration<PackageItemConfiguration, PackageItem>]
|
|
||||||
public class PackageItem : Models.PackageItem
|
|
||||||
{
|
|
||||||
public virtual Package? Package { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LiteCharms.Entities;
|
|
||||||
|
|
||||||
public class ShoppingCartItem : Models.ShoppingCartItem
|
|
||||||
{
|
|
||||||
public virtual ShoppingCart? ShoppingCart { get; set; }
|
|
||||||
|
|
||||||
public virtual ProductPrice? ProductPrice { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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<SmtpSettings>(configuration.GetSection("Email"));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.HealthChecks;
|
|
||||||
|
|
||||||
namespace LiteCharms.Extensions;
|
|
||||||
|
|
||||||
public static class HealthChecks
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddHealthChecks().AddCheck<QuartzHealthCheck>("Quartz");
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddHealthChecks().AddCheck<PostgresHealthCheck>("PostgreSQL");
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddHealthChecks()
|
|
||||||
.AddCheck("Self", () => HealthCheckResult.Healthy());
|
|
||||||
|
|
||||||
//services.AddHealthChecksUI(setup =>
|
|
||||||
//{
|
|
||||||
// setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health");
|
|
||||||
// setup.SetEvaluationTimeInSeconds(15);
|
|
||||||
//}).AddInMemoryStorage(databaseName: "healthuidb");
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<SignAssembly>True</SignAssembly>
|
|
||||||
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
|
|
||||||
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Warnings And Exclusions -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);MA0004</NoWarn>
|
|
||||||
<!-- https://github.com/dotnet/aspnetcore/issues/50836 -->
|
|
||||||
<NoWarn>$(NoWarn);AD0001</NoWarn>
|
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
|
||||||
<NoWarn>$(NoWarn);IL2080;IL2065;IL2075;IL2087;IL2057;IL2060;IL2070;IL2067;IL2072;IL2026;IL2104</NoWarn>
|
|
||||||
<NoWarn>$(NoWarn);IL2110;IL2111</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Nuget Package Details -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>LiteCharms.Extensions</PackageId>
|
|
||||||
<Version>1.0.20</Version>
|
|
||||||
<Authors>Khwezi Mngoma</Authors>
|
|
||||||
<Company>Lite Charms (PTY) Ltd</Company>
|
|
||||||
<Description>Extension components for Lite Charms applications.</Description>
|
|
||||||
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
|
|
||||||
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
|
||||||
<PackageTags>utility;dotnet</PackageTags>
|
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
|
|
||||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Health Checks -->
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
|
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
|
|
||||||
|
|
||||||
<!-- Global Usings -->
|
|
||||||
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
|
||||||
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Open Telemetry -->
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
|
||||||
|
|
||||||
<!-- Global Usings -->
|
|
||||||
<Using Include="OpenTelemetry.Resources" />
|
|
||||||
<Using Include="OpenTelemetry.Exporter" />
|
|
||||||
<Using Include="OpenTelemetry.Logs" />
|
|
||||||
<Using Include="OpenTelemetry.Metrics" />
|
|
||||||
<Using Include="OpenTelemetry.Trace" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Database -->
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
|
|
||||||
<!-- Global Usings -->
|
|
||||||
<Using Include="Npgsql" />
|
|
||||||
<Using Include="Microsoft.EntityFrameworkCore" />
|
|
||||||
<Using Include="Microsoft.EntityFrameworkCore.Design" />
|
|
||||||
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Quartz" />
|
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
|
||||||
<Using Include="Microsoft.Extensions.Configuration" />
|
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
|
||||||
<Using Include="Microsoft.Extensions.Logging" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Project References -->
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\LiteCharms.Entities\LiteCharms.Entities.csproj" />
|
|
||||||
<ProjectReference Include="..\LiteCharms.Infrastructure\LiteCharms.Infrastructure.csproj" />
|
|
||||||
<ProjectReference Include="..\LiteCharms.Models\LiteCharms.Models.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Extensions;
|
|
||||||
|
|
||||||
public static class Postgres
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddPooledDbContextFactory<ShopDbContext>(options =>
|
|
||||||
options.UseNpgsql(configuration.GetConnectionString("PostgresShop")));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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())
|
||||||
|
.AddUserSecrets<CommonFixture>()
|
||||||
|
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Services = new ServiceCollection()
|
||||||
|
.AddMediator()
|
||||||
|
.AddLogging()
|
||||||
|
.AddShopServices()
|
||||||
|
.AddEmailServiceBus()
|
||||||
|
.AddGarageS3(Configuration)
|
||||||
|
.AddShopDatabase(Configuration)
|
||||||
|
.AddEmailServices(Configuration)
|
||||||
|
.AddSingleton(Configuration)
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
Mediator = Services.GetRequiredService<IMediator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<UserSecretsId>62fa604a-1340-4edb-9ddd-3305fcf46fca</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Mediator" />
|
||||||
|
<Using Include="Xunit.Abstractions" />
|
||||||
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
<Using Include="Microsoft.Extensions.Configuration" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using LiteCharms.Features.Models;
|
||||||
|
using LiteCharms.Features.Shop.Notifications;
|
||||||
|
using LiteCharms.Features.Shop.Notifications.Events;
|
||||||
|
using static LiteCharms.Features.Extensions.Email;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
|
||||||
|
{
|
||||||
|
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
|
||||||
|
|
||||||
|
[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 = $"{ShopEmailFromName} [Test]",
|
||||||
|
RecipientAddress = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNotifications_ShouldReturn_AllNotifications()
|
||||||
|
{
|
||||||
|
DateRange range = new()
|
||||||
|
{
|
||||||
|
From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)),
|
||||||
|
To = DateOnly.FromDateTime(DateTime.UtcNow),
|
||||||
|
MaxRecords = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var getResult = await notificationService.GetNotificationsAsync(range);
|
||||||
|
|
||||||
|
Assert.True(getResult.IsSuccess);
|
||||||
|
|
||||||
|
foreach (var error in getResult.Errors) output.WriteLine(error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessEmailNotificationsEvent_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var notification = ProcessEmailNotificationsEvent.Create();
|
||||||
|
|
||||||
|
await fixture.Mediator.Publish(notification);
|
||||||
|
|
||||||
|
Assert.True(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using LiteCharms.Features.Shop.Products;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetProductsAsync_ReturnsProducts()
|
||||||
|
{
|
||||||
|
var productService = fixture.Services.GetRequiredService<ProductService>();
|
||||||
|
|
||||||
|
var result = await productService.GetProductsAsync();
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
|
||||||
|
output.WriteLine($"Retrieved {result.Value.Length} products.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task BookshopS3Service_MustReturnUrl()
|
||||||
|
{
|
||||||
|
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
|
||||||
|
|
||||||
|
var fileName = "appsettings.json";
|
||||||
|
|
||||||
|
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
|
||||||
|
var stream = File.OpenRead(path);
|
||||||
|
|
||||||
|
var result = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
Assert.NotEmpty(result.Value);
|
||||||
|
|
||||||
|
output.WriteLine(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BookshopS3Service_MustDeleteFile()
|
||||||
|
{
|
||||||
|
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
|
||||||
|
|
||||||
|
var fileName = "appsettings.json";
|
||||||
|
|
||||||
|
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
|
||||||
|
var stream = File.OpenRead(path);
|
||||||
|
|
||||||
|
var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
|
||||||
|
|
||||||
|
Assert.True(uploadResult.IsSuccess);
|
||||||
|
Assert.NotNull(uploadResult.Value);
|
||||||
|
Assert.NotEmpty(uploadResult.Value);
|
||||||
|
|
||||||
|
var fileKey = uploadResult.Value.Split('/').Last();
|
||||||
|
|
||||||
|
var deleteResult = await service!.DeleteFileAsync(fileKey);
|
||||||
|
|
||||||
|
Assert.True(deleteResult.IsSuccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"BookshopS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "bookshop",
|
||||||
|
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
|
"BookshopQuotesS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "bookshop.quotes",
|
||||||
|
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
|
"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": "*"
|
||||||
|
}
|
||||||
+3
-2
@@ -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
|
public abstract class EventBase
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace LiteCharms.Abstractions;
|
namespace LiteCharms.Features.Abstractions;
|
||||||
|
|
||||||
public interface IEvent : INotification
|
public interface IEvent : INotification
|
||||||
{
|
{
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class AddPackageItemCommand : IRequest<Result<Guid>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class CreatePackageCommand : IRequest<Result<Guid>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class DeletePackageItemCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class DeletePackageItemsCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class AddPackageItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AddPackageItemCommand, Result<Guid>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Guid>> 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<Guid>($"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<Guid>($"Failed to add new package item by ID {request.ProductPriceId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class CreatePackageCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreatePackageCommand, Result<Guid>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Guid>> 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<Guid>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class DeletePackageItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<DeletePackageItemCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class DeletePackageItemsCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<DeletePackageItemsCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class UpdatePackageCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdatePackageCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
|
|
||||||
|
|
||||||
public class UpdatePackageStatusCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdatePackageStatusCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class UpdatePackageCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
namespace LiteCharms.Features.CartPackages.Commands;
|
|
||||||
|
|
||||||
public class UpdatePackageStatusCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries;
|
|
||||||
|
|
||||||
public class GetPackageItemsQuery : IRequest<Result<PackageItem[]>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries;
|
|
||||||
|
|
||||||
public class GetPackageQuery : IRequest<Result<Package>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries;
|
|
||||||
|
|
||||||
public class GetPackagesQuery : IRequest<Result<Package[]>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetPackageItemsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackageItemsQuery, Result<PackageItem[]>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<PackageItem[]>> 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<PackageItem[]>($"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<PackageItem[]>($"Could not find package items by package ID {request.PackageId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<PackageItem[]>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetPackageQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackageQuery, Result<Package>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Package>> 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<Package>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetPackagesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackagesQuery, Result<Package[]>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Package[]>> 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<Package[]>(new Error($"No packages found for the specified date range {request.From} - {request.To}."));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Package[]>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Customers.Commands;
|
|
||||||
|
|
||||||
public class CreateCustomerCommand : IRequest<Result<Guid>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Commands.Handlers;
|
|
||||||
|
|
||||||
public class CreateCustomerCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateCustomerCommand, Result<Guid>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Guid>> 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<Guid>(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<Guid>(new Error($"Failed to create customer {customerEmail}"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Commands.Handlers;
|
|
||||||
|
|
||||||
public class UpdateCustomerCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateCustomerCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Customers.Commands;
|
|
||||||
|
|
||||||
public class UpdateCustomerCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Queries;
|
|
||||||
|
|
||||||
public class GetCustomerQuery : IRequest<Result<Customer>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Queries;
|
|
||||||
|
|
||||||
public class GetCustomersQuery : IRequest<Result<Customer[]>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetCustomerQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerQuery, Result<Customer>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Customer>> 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>($"Customer not found with id {request.CustomerId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Customer>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Customers.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetCustomersQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomersQuery, Result<Customer[]>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Customer[]>> 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<Customer[]>(new Error("No customers found in the specified date range."));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Customer[]>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using LiteCharms.Features.Email.Commands;
|
|
||||||
using LiteCharms.Models.Configuraton.Email;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Email.Commands.Handlers;
|
|
||||||
|
|
||||||
public class SendEmailCommandHandler(IOptions<SmtpSettings> smtpOptions) : IRequestHandler<SendEmailCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Email.Commands;
|
|
||||||
|
|
||||||
public class SendEmailCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace LiteCharms.Models.Configuraton.Email;
|
namespace LiteCharms.Features.Email.Configuration;
|
||||||
|
|
||||||
public class Account
|
public class Account
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace LiteCharms.Models.Configuraton.Email;
|
namespace LiteCharms.Features.Email.Configuration;
|
||||||
|
|
||||||
public class SmtpSettings
|
public class SmtpSettings
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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<SmtpSettings> 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<Result<Response>> 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<Response>("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();
|
||||||
|
|
||||||
|
if (message.Body!.Properties.HasAttachments)
|
||||||
|
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>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Contains("451"))
|
||||||
|
{
|
||||||
|
Status = EmailStatuses.ConnectionAborted;
|
||||||
|
|
||||||
|
return Result.Fail<Response>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
|
||||||
|
|
||||||
|
Status = EmailStatuses.Disconnected;
|
||||||
|
|
||||||
|
return Result.Fail<Response>("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<Result<Response>> 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<Response>(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<Response>(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<Response>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
using LiteCharms.Features.Notifications.Commands;
|
using LiteCharms.Features.Shop;
|
||||||
using static LiteCharms.Abstractions.Constants;
|
using LiteCharms.Features.Shop.Notifications;
|
||||||
|
using static LiteCharms.Features.Extensions.Email;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Email.Events.Handlers;
|
namespace LiteCharms.Features.Email.Events.Handlers;
|
||||||
|
|
||||||
public class SendShopEmailEnquiryEventHandler(ISender mediator) :
|
public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) :
|
||||||
INotificationHandler<SendShopEmailEnquiryEvent>
|
INotificationHandler<SendShopEmailEnquiryEvent>
|
||||||
{
|
{
|
||||||
public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken)
|
public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) =>
|
||||||
{
|
await notificationService.CreateNotificationAsync(new Shop.Notifications.Models.CreateNotification
|
||||||
var command = CreateNotificationCommand.Create(Models.NotificationDirection.Outgoing, notification.SenderName!,
|
{
|
||||||
notification.SenderAddress!, notification.Subject!, notification.Message!, Models.NotificationPlatforms.Email,
|
CorrelationId = notification.CorrelationId,
|
||||||
notification.Priority, ShopEmailFromName, ShopEmailFromAddress, Guid.CreateVersion7().ToString(),
|
CorrelationIdType = CorrelationIdTypes.None,
|
||||||
Models.CorrelationIdTypes.None, isInternal: true, isHtml: false);
|
Direction = NotificationDirection.Outgoing,
|
||||||
|
IsHtml = false,
|
||||||
await mediator.Send(command, cancellationToken);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using LiteCharms.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Models;
|
using LiteCharms.Features.Shop;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Email.Events;
|
namespace LiteCharms.Features.Email.Events;
|
||||||
|
|
||||||
|
|||||||
@@ -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<long> EmailsSent = Meter.CreateCounter<long>("emails_sent_total", "count", "Total successful emails sent");
|
||||||
|
public static readonly Counter<long> EmailsFailed = Meter.CreateCounter<long>("emails_failed_total", "count", "Total failed email attempts");
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
|
public class Attachment
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public Stream? FileStream { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
|
public class Body : IDisposable
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
public ReadOnlyCollection<Attachment>? 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
|
public class BodyProperties
|
||||||
|
{
|
||||||
|
public bool IsHtml { get; set; }
|
||||||
|
|
||||||
|
public bool HasAttachments { get; set; }
|
||||||
|
}
|
||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
namespace LiteCharms.Models;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
public sealed class EmailEnquiry
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
|
public sealed class EmailEnquiryModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[MinLength(2)]
|
[MinLength(2)]
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
|
public class Party
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public string? Address { get; set; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using LiteCharms.Features.Email;
|
||||||
|
using LiteCharms.Features.Email.Configuration;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class Email
|
||||||
|
{
|
||||||
|
public const string ShopEmailFromName = "Khongisa Shop";
|
||||||
|
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
|
||||||
|
|
||||||
|
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.Configure<SmtpSettings>(configuration.GetSection("Email"));
|
||||||
|
|
||||||
|
services.AddSingleton<EmailService>();
|
||||||
|
|
||||||
|
services.AddOpenTelemetry()
|
||||||
|
.WithTracing(tracing => tracing.AddSource("LiteCharms.EmailService"))
|
||||||
|
.WithMetrics(metrics => metrics.AddMeter("LiteCharms.EmailService"));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-27
@@ -1,10 +1,17 @@
|
|||||||
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 class EntityModeMappers
|
||||||
{
|
{
|
||||||
public static ShoppingCartPackage ToModel(this Entities.ShoppingCartPackage entity) =>
|
public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -13,7 +20,7 @@ public static class EntityModeMappers
|
|||||||
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -23,7 +30,7 @@ public static class EntityModeMappers
|
|||||||
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -31,10 +38,12 @@ public static class EntityModeMappers
|
|||||||
Active = entity.Active,
|
Active = entity.Active,
|
||||||
Description = entity.Description,
|
Description = entity.Description,
|
||||||
Name = entity.Name,
|
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -45,18 +54,17 @@ public static class EntityModeMappers
|
|||||||
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
CreatedAt = entity.CreatedAt,
|
CreatedAt = entity.CreatedAt,
|
||||||
UpdatedAt = entity.UpdatedAt,
|
UpdatedAt = entity.UpdatedAt,
|
||||||
CustomerId = entity.CustomerId,
|
CustomerId = entity.CustomerId,
|
||||||
OrderId = entity.OrderId,
|
OrderId = entity.OrderId
|
||||||
QuoteId = entity.QuoteId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Quote ToModel(this Entities.Quote entity) =>
|
public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -66,10 +74,12 @@ public static class EntityModeMappers
|
|||||||
ExpiredAt = entity.ExpiredAt,
|
ExpiredAt = entity.ExpiredAt,
|
||||||
Reason = entity.Reason,
|
Reason = entity.Reason,
|
||||||
ShoppingCartId = entity.ShoppingCartId,
|
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -79,9 +89,9 @@ public static class EntityModeMappers
|
|||||||
CorrelationId = entity.CorrelationId,
|
CorrelationId = entity.CorrelationId,
|
||||||
CorrelationIdType = entity.CorrelationIdType,
|
CorrelationIdType = entity.CorrelationIdType,
|
||||||
IsInternal = entity.IsInternal,
|
IsInternal = entity.IsInternal,
|
||||||
Sender = entity.Sender,
|
SenderAddress = entity.SenderAddress,
|
||||||
Platform = entity.Platform,
|
Platform = entity.Platform,
|
||||||
Recipient = entity.Recipient,
|
RecipientName = entity.RecipientName,
|
||||||
Subject = entity.Subject,
|
Subject = entity.Subject,
|
||||||
Processed = entity.Processed,
|
Processed = entity.Processed,
|
||||||
SenderName = entity.SenderName,
|
SenderName = entity.SenderName,
|
||||||
@@ -93,7 +103,7 @@ public static class EntityModeMappers
|
|||||||
Errors = entity.Errors
|
Errors = entity.Errors
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Customer ToModel(this Entities.Customer entity) =>
|
public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -118,7 +128,7 @@ public static class EntityModeMappers
|
|||||||
Whatsapp = entity.Whatsapp
|
Whatsapp = entity.Whatsapp
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Lead ToModel(this Entities.Lead entity) =>
|
public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -139,7 +149,7 @@ public static class EntityModeMappers
|
|||||||
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -147,16 +157,13 @@ public static class EntityModeMappers
|
|||||||
UpdatedAt = entity.UpdatedAt,
|
UpdatedAt = entity.UpdatedAt,
|
||||||
CustomerId = entity.CustomerId,
|
CustomerId = entity.CustomerId,
|
||||||
Notes = entity.Notes,
|
Notes = entity.Notes,
|
||||||
RefundId = entity.RefundId,
|
|
||||||
QuoteId = entity.QuoteId,
|
|
||||||
Status = entity.Status,
|
Status = entity.Status,
|
||||||
ShoppingCartId = entity.ShoppingCartId,
|
|
||||||
DepositRequired = entity.DepositRequired,
|
|
||||||
Requirements = entity.Requirements,
|
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -166,16 +173,21 @@ public static class EntityModeMappers
|
|||||||
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()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
Name = entity.Name,
|
Name = entity.Name,
|
||||||
Description = entity.Description,
|
Description = entity.Description,
|
||||||
Active = entity.Active
|
Active = entity.Active,
|
||||||
|
Summary = entity.Summary,
|
||||||
|
ImageUrl = entity.ImageUrl,
|
||||||
|
Thumbnails = entity.Thumbnails,
|
||||||
|
Metadata = entity.Metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static ProductPrice ToModel(this Entities.ProductPrice entity) =>
|
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class Hash
|
||||||
|
{
|
||||||
|
public static Func<string?, string?> StringToSha256Hash = (input) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
||||||
|
|
||||||
|
public static Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(stream));
|
||||||
|
|
||||||
|
public static Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using LiteCharms.Features.HealthChecks;
|
||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class HealthChecks
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>("MidrandShopQuartz");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(ShopDbConfigName);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandShopDbConfigName);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks()
|
||||||
|
.AddCheck("Self", () => HealthCheckResult.Healthy());
|
||||||
|
|
||||||
|
//services.AddHealthChecksUI(setup =>
|
||||||
|
//{
|
||||||
|
// setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health");
|
||||||
|
// setup.SetEvaluationTimeInSeconds(15);
|
||||||
|
//}).AddInMemoryStorage(databaseName: "healthuidb");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace LiteCharms.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Monitoring
|
public static class Monitoring
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class Postgres
|
||||||
|
{
|
||||||
|
public const string MidrandShopDbConfigName = "PostgresMidrandShop";
|
||||||
|
public const string ShopDbConfigName = "PostgresShop";
|
||||||
|
public const string SchedulerDbConfigName = "PostgresScheduler";
|
||||||
|
|
||||||
|
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddPooledDbContextFactory<ShopDbContext>(options =>
|
||||||
|
options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
|
||||||
|
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
using LiteCharms.Abstractions;
|
using LiteCharms.Features.Quartz;
|
||||||
using LiteCharms.Infrastructure.Quartz;
|
using LiteCharms.Features.Quartz.Abstractions;
|
||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
namespace LiteCharms.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Quartz
|
public static class Quartz
|
||||||
{
|
{
|
||||||
private const string databaseConfigName = "PostgresScheduler";
|
public const string ShopSchedulerName = "shop";
|
||||||
|
public const string MidrandShopSchedulerName = "midrandshop";
|
||||||
|
|
||||||
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString(databaseConfigName);
|
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
|
||||||
|
|
||||||
services.ConfigureCommon();
|
services.ConfigureCommon();
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ public static class Quartz
|
|||||||
storage.UseClustering(cluster =>
|
storage.UseClustering(cluster =>
|
||||||
{
|
{
|
||||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
|
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -44,10 +46,12 @@ public static class Quartz
|
|||||||
|
|
||||||
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString(databaseConfigName);
|
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
|
||||||
|
|
||||||
services.ConfigureCommon();
|
services.ConfigureCommon();
|
||||||
|
|
||||||
|
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
services.AddQuartz(config =>
|
services.AddQuartz(config =>
|
||||||
{
|
{
|
||||||
config.SchedulerName = schedulerName;
|
config.SchedulerName = schedulerName;
|
||||||
@@ -60,6 +64,8 @@ public static class Quartz
|
|||||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
||||||
config.UseTimeZoneConverter();
|
config.UseTimeZoneConverter();
|
||||||
|
|
||||||
|
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString());
|
||||||
|
|
||||||
config.UsePersistentStore(storage =>
|
config.UsePersistentStore(storage =>
|
||||||
{
|
{
|
||||||
storage.PerformSchemaValidation = false;
|
storage.PerformSchemaValidation = false;
|
||||||
@@ -72,7 +78,7 @@ public static class Quartz
|
|||||||
storage.UseClustering(cluster =>
|
storage.UseClustering(cluster =>
|
||||||
{
|
{
|
||||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
|
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -86,13 +92,13 @@ public static class Quartz
|
|||||||
{
|
{
|
||||||
options.Scheduling.IgnoreDuplicates = true;
|
options.Scheduling.IgnoreDuplicates = true;
|
||||||
options.Scheduling.OverWriteExistingData = true;
|
options.Scheduling.OverWriteExistingData = true;
|
||||||
|
|
||||||
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
|
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
|
||||||
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
|
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddTransient<RetryJobListener>();
|
services.AddTransient<RetryJobListener>();
|
||||||
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
|
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
|
||||||
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using LiteCharms.Features.S3;
|
||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class S3
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopBucketName, (provider, client) =>
|
||||||
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value,
|
||||||
|
configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value),
|
||||||
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddKeyedScoped<IS3Service, BookshopS3Service>(BookshopBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopInvoicesBucketName, (provider, client) =>
|
||||||
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:AccessKey").Value,
|
||||||
|
configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:SecretKey").Value),
|
||||||
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddKeyedScoped<IS3Service, BookshopInvoicesS3Service>(BookshopInvoicesBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopQuotesBucketName, (provider, client) =>
|
||||||
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:AccessKey").Value,
|
||||||
|
configuration.GetSection($"{BookshopQuotesS3SettingsSection}:SecretKey").Value),
|
||||||
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddKeyedScoped<IS3Service, BookshopQuotesS3Service>(BookshopQuotesBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using LiteCharms.Abstractions;
|
using LiteCharms.Features.ServiceBus;
|
||||||
using LiteCharms.Infrastructure.ServiceBus;
|
using LiteCharms.Features.ServiceBus.Abstractions;
|
||||||
using LiteCharms.Infrastructure.ServiceBus.Exchanges;
|
using LiteCharms.Features.ServiceBus.Exchanges;
|
||||||
using LiteCharms.Infrastructure.ServiceBus.Queues;
|
using LiteCharms.Features.ServiceBus.Queues;
|
||||||
|
|
||||||
namespace LiteCharms.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class ServiceBus
|
public static class ServiceBus
|
||||||
{
|
{
|
||||||
@@ -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<PackageService>()
|
||||||
|
.AddSingleton<LeadService>()
|
||||||
|
.AddSingleton<NotificationService>()
|
||||||
|
.AddSingleton<OrderService>()
|
||||||
|
.AddSingleton<ProductService>()
|
||||||
|
.AddSingleton<QuoteService>()
|
||||||
|
.AddSingleton<ShoppingCartService>()
|
||||||
|
.AddSingleton<CustomerService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace LiteCharms.Abstractions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Timezones
|
public static class Timezones
|
||||||
{
|
{
|
||||||
@@ -23,5 +23,5 @@ public static class Timezones
|
|||||||
return DateTimeOffset.Parse(localised!);
|
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;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Quartz;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||||
|
{
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken);
|
||||||
|
|
||||||
|
if(scheduler == null)
|
||||||
|
return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found.");
|
||||||
|
|
||||||
|
if (!scheduler.IsStarted)
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running");
|
||||||
|
|
||||||
|
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready");
|
||||||
|
}
|
||||||
|
catch (SchedulerException)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!;
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||||
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT 1";
|
||||||
|
|
||||||
|
await command.ExecuteScalarAsync(cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -1,8 +1,10 @@
|
|||||||
namespace LiteCharms.Infrastructure.HealthChecks;
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||||
{
|
{
|
||||||
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!;
|
private readonly string connectionString = configuration.GetConnectionString(ShopDbConfigName)!;
|
||||||
|
|
||||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
|
|||||||
|
|
||||||
await command.ExecuteScalarAsync(cancellationToken);
|
await command.ExecuteScalarAsync(cancellationToken);
|
||||||
|
|
||||||
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
|
return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
|
return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Quartz;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||||
|
{
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken);
|
||||||
|
|
||||||
|
if(scheduler == null)
|
||||||
|
return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found.");
|
||||||
|
|
||||||
|
if (!scheduler.IsStarted)
|
||||||
|
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running");
|
||||||
|
|
||||||
|
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready");
|
||||||
|
}
|
||||||
|
catch (SchedulerException)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Leads.Commands;
|
|
||||||
|
|
||||||
public class CreateLeadCommand : IRequest<Result<Guid>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using LiteCharms.Features.Utilities.Hash.Commands;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Commands.Handlers;
|
|
||||||
|
|
||||||
public class CreateLeadCommandHandler(IDbContextFactory<ShopDbContext> contextFactory, ISender mediator) : IRequestHandler<CreateLeadCommand, Result<Guid>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Guid>> 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<Guid>(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<Guid>(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Commands.Handlers;
|
|
||||||
|
|
||||||
public class UpdateLeadCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateLeadCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Commands;
|
|
||||||
|
|
||||||
public class UpdateLeadCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Queries;
|
|
||||||
|
|
||||||
public class GetCustomerLeadsQuery : IRequest<Result<Lead[]>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Queries;
|
|
||||||
|
|
||||||
public class GetLeadsQuery : IRequest<Result<Lead[]>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetCustomerLeadsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerLeadsQuery, Result<Lead[]>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Lead[]>> 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<Lead[]>(new Error(ex.Message).CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using LiteCharms.Extensions;
|
|
||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Leads.Queries.Handlers;
|
|
||||||
|
|
||||||
public class GetLeadsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetLeadsQuery, Result<Lead[]>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Lead[]>> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
<PackageTags>utility;dotnet</PackageTags>
|
<PackageTags>utility;dotnet</PackageTags>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
|
<UserSecretsId>8a78916e-c86b-4f4b-9f4e-d8e7769b5d23</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,6 +29,86 @@
|
|||||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Quartz Scheduler-->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||||
|
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||||
|
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
||||||
|
<PackageReference Include="Quartz.Plugins.TimeZoneConverter" Version="3.18.1" />
|
||||||
|
<PackageReference Include="Quartz.Serialization.SystemTextJson" Version="3.18.1" />
|
||||||
|
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
|
||||||
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<Using Include="Quartz" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Configuration -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<Using Include="Microsoft.Extensions.Configuration" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Health Checks -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.8" />
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Open Telemetry -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<Using Include="OpenTelemetry.Resources" />
|
||||||
|
<Using Include="OpenTelemetry.Exporter" />
|
||||||
|
<Using Include="OpenTelemetry.Logs" />
|
||||||
|
<Using Include="OpenTelemetry.Metrics" />
|
||||||
|
<Using Include="OpenTelemetry.Trace" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
|
|
||||||
|
<!-- Global Usings -->
|
||||||
|
<Using Include="Npgsql" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore.Design" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MailKit" Version="4.16.0" />
|
<PackageReference Include="MailKit" Version="4.16.0" />
|
||||||
@@ -48,18 +129,39 @@
|
|||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Amazon S3 SDK -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
|
||||||
|
|
||||||
|
<!-- global Usings -->
|
||||||
|
<Using Include="Amazon.S3" />
|
||||||
|
<Using Include="Amazon.S3.Model" />
|
||||||
|
<Using Include="Amazon.Runtime" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
|
<Using Include="Microsoft.Extensions.Hosting" />
|
||||||
<Using Include="System.Text" />
|
<Using Include="System.Text" />
|
||||||
|
<Using Include="System.Text.Json" />
|
||||||
|
<Using Include="System.Threading.Channels" />
|
||||||
|
<Using Include="System.Collections.ObjectModel" />
|
||||||
|
<Using Include="System.Diagnostics" />
|
||||||
|
<Using Include="System.Diagnostics.Metrics" />
|
||||||
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<Using Include="System.Security.Cryptography" />
|
<Using Include="System.Security.Cryptography" />
|
||||||
<Using Include="Microsoft.EntityFrameworkCore" />
|
|
||||||
<Using Include="Microsoft.Extensions.Options" />
|
<Using Include="Microsoft.Extensions.Options" />
|
||||||
<Using Include="Microsoft.Extensions.Logging" />
|
<Using Include="Microsoft.Extensions.Logging" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LiteCharms.Extensions\LiteCharms.Extensions.csproj" />
|
<None Update="appsettings.json">
|
||||||
<ProjectReference Include="..\LiteCharms.Infrastructure\LiteCharms.Infrastructure.csproj" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Shop\Postgres\Migrations\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace LiteCharms.Features.Mediator;
|
||||||
|
|
||||||
|
public sealed class LoggingPipelineBehavior<TRequest, TResponse>(ILogger<LoggingPipelineBehavior<TRequest, TResponse>> logger) :
|
||||||
|
IPipelineBehavior<TRequest, TResponse?>
|
||||||
|
where TRequest : IRequest<TResponse>
|
||||||
|
where TResponse : ResultBase, new()
|
||||||
|
{
|
||||||
|
public async ValueTask<TResponse?> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<long> RequestCounter = Meter.CreateCounter<long>("mediator_requests_total");
|
||||||
|
public static readonly Histogram<double> RequestDuration = Meter.CreateHistogram<double>("mediator_request_duration_ms");
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
namespace LiteCharms.Features.Mediator;
|
||||||
|
|
||||||
|
public sealed class TelemetryPipelineBehavior<TRequest, TResponse> :
|
||||||
|
IPipelineBehavior<TRequest, TResponse?>
|
||||||
|
where TRequest : IRequest<TResponse>
|
||||||
|
where TResponse : ResultBase, new()
|
||||||
|
{
|
||||||
|
public async ValueTask<TResponse?> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse?> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
|
||||||
|
public class MidrandShopDbContext(DbContextOptions<MidrandShopDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
|
||||||
|
public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory<MidrandShopDbContext>
|
||||||
|
{
|
||||||
|
public MidrandShopDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddUserSecrets(typeof(MidrandShopDbContext).Assembly)
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<MidrandShopDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName));
|
||||||
|
|
||||||
|
return new MidrandShopDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using LiteCharms.Models;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Notifications.Commands;
|
|
||||||
|
|
||||||
public class CreateNotificationCommand : IRequest<Result<Guid>>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Notifications.Commands.Handlers;
|
|
||||||
|
|
||||||
public class CreateNotificationCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateNotificationCommand, Result<Guid>>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result<Guid>> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
using LiteCharms.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Notifications.Commands.Handlers;
|
|
||||||
|
|
||||||
public class UpdateNotificationCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateNotificationCommand, Result>
|
|
||||||
{
|
|
||||||
public async ValueTask<Result> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Notifications.Commands;
|
|
||||||
|
|
||||||
public class UpdateNotificationCommand : IRequest<Result>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user