Compare commits

...

47 Commits

Author SHA1 Message Date
khwezi f5efdde37c Merge pull request 'Refactored connection string name' (#41) from midrandshop into master
Reviewed-on: #41
2026-05-24 13:51:11 +02:00
Khwezi Mngoma 87da491ed6 Refactored connection string name
continuous-integration/drone/pr Build is passing
2026-05-24 13:50:41 +02:00
khwezi 1592d5dc8f Merge pull request 'midrandshop' (#40) from midrandshop into master
Reviewed-on: #40
2026-05-24 13:43:22 +02:00
Khwezi Mngoma 08a64d1578 Added test MidrandBooks projet
continuous-integration/drone/pr Build is passing
2026-05-24 13:42:59 +02:00
Khwezi Mngoma f67f5eaf53 Added MidrandBooks project 2026-05-24 13:38:47 +02:00
khwezi 50a8a59d92 Merge pull request 'Split Features to create space for more projects' (#39) from midrandshop into master
Reviewed-on: #39
2026-05-24 13:19:55 +02:00
Khwezi Mngoma 70c6e0bfbc Split Features to create space for more projects
continuous-integration/drone/pr Build is passing
2026-05-24 13:19:09 +02:00
khwezi b70d9559b0 Merge pull request 'midrandshop' (#38) from midrandshop into master
Reviewed-on: #38
2026-05-23 11:55:02 +02:00
Khwezi Mngoma 032b9e1818 Updated nuget packages
continuous-integration/drone/pr Build is passing
2026-05-23 11:54:32 +02:00
Khwezi Mngoma 424c1c6f8c Fixed email test 2026-05-23 11:52:47 +02:00
khwezi 81d5e8f07c Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master
Reviewed-on: #37
2026-05-23 11:49:27 +02:00
Khwezi Mngoma 7d5e9a18d8 Added MidrandShop feature and spl;it extensions and healthchecks
continuous-integration/drone/pr Build is failing
2026-05-23 11:48:47 +02:00
khwezi 20a53942b5 Merge pull request 'Added product metadata' (#36) from s3service into master
Reviewed-on: #36
2026-05-20 21:15:01 +02:00
Khwezi Mngoma 3656223b5f Added product metadata
continuous-integration/drone/pr Build is passing
Added dates to product model
Migrated database changes
2026-05-20 21:13:58 +02:00
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
Khwezi Mngoma ccf30ac36b Optimised UploadFileAsync()
continuous-integration/drone/pr Build is passing
Implemented and tested DeleteFileAsync()
2026-05-20 15:32:54 +02:00
khwezi 6ed023f2cf Merge pull request 'Refactored the S3 services to properly upload the file' (#34) from s3service into master
Reviewed-on: #34
2026-05-20 08:03:43 +02:00
Khwezi Mngoma d6fdf1b9c8 Refactored the S3 services to properly upload the file
continuous-integration/drone/pr Build is passing
2026-05-20 08:01:44 +02:00
khwezi 2c9f5a846c Merge pull request 'Updated how i use configs' (#33) from s3service into master
Reviewed-on: #33
2026-05-19 14:57:59 +02:00
Khwezi Mngoma 89a343a85f Updated how i use configs
continuous-integration/drone/pr Build is passing
2026-05-19 14:57:14 +02:00
khwezi 41f7c05be3 Merge pull request 'Refactored service to internalise the CDN' (#32) from s3service into master
Reviewed-on: #32
2026-05-19 11:34:51 +02:00
Khwezi Mngoma 52d204e286 Refactored service to internalise the CDN
continuous-integration/drone/pr Build is passing
2026-05-19 11:34:00 +02:00
khwezi 1a03355e84 Merge pull request 'Added S3 support' (#31) from s3service into master
Reviewed-on: #31
2026-05-19 10:24:05 +02:00
Khwezi Mngoma f245bc94e1 Added S3 support
continuous-integration/drone/pr Build is passing
2026-05-19 10:23:36 +02:00
khwezi 7743c3178e Merge pull request 'Simplified notification updating' (#30) from emailjobs into master
Reviewed-on: #30
2026-05-17 16:01:24 +02:00
Khwezi Mngoma da141311ff Simplified notification updating
continuous-integration/drone/pr Build is passing
2026-05-17 16:00:35 +02:00
khwezi ab3d8e6e9a Merge pull request 'Refactored GetNotificationsAsync() date handling' (#29) from emailjobs into master
Reviewed-on: #29
2026-05-17 13:14:01 +02:00
Khwezi Mngoma 97bde73777 Refactored GetNotificationsAsync() date handling
continuous-integration/drone/pr Build is passing
2026-05-17 13:12:58 +02:00
khwezi db4c348288 Merge pull request 'Fixed email sending logic' (#28) from emailjobs into master
Reviewed-on: #28
2026-05-16 00:29:01 +02:00
Khwezi Mngoma a65e926a53 Fixed email sending logic
continuous-integration/drone/pr Build is passing
2026-05-16 00:28:31 +02:00
khwezi 6683234642 Merge pull request 'Refactored batch drop logic' (#27) from emailjobs into master
Reviewed-on: #27
2026-05-16 00:05:51 +02:00
Khwezi Mngoma 1471d9e597 Refactored batch drop logic
continuous-integration/drone/pr Build is passing
2026-05-16 00:04:58 +02:00
khwezi 6ddbb9479a Merge pull request 'Added an empty constructor to ProcessEmailNotificationEvent' (#26) from emailjobs into master
Reviewed-on: #26
2026-05-15 23:53:09 +02:00
Khwezi Mngoma e978aa17f8 Added an empty constructor to ProcessEmailNotificationEvent
continuous-integration/drone/pr Build is passing
2026-05-15 23:52:38 +02:00
khwezi 6c7349a0f8 Merge pull request 'Added additional logging and traces' (#25) from emailjobs into master
Reviewed-on: #25
2026-05-15 23:21:52 +02:00
Khwezi Mngoma a31f75c5ef Added additional logging and traces
continuous-integration/drone/pr Build is passing
2026-05-15 23:21:31 +02:00
khwezi e97fd6cd3f Merge pull request 'Added debug logging' (#24) from emailjobs into master
Reviewed-on: #24
2026-05-15 23:09:21 +02:00
Khwezi Mngoma 7f4246ac63 Added debug logging
continuous-integration/drone/pr Build is passing
2026-05-15 23:08:15 +02:00
khwezi 184c7c252a Merge pull request 'Set misfireThreshold to 2min and eased Cluster node checkin limit' (#23) from emailjobs into master
Reviewed-on: #23
2026-05-15 22:29:08 +02:00
Khwezi Mngoma dfc62c8fe1 Set misfireThreshold to 2min and eased Cluster node checkin limit
continuous-integration/drone/pr Build is passing
2026-05-15 22:28:18 +02:00
khwezi bfe8c458d6 Merge pull request 'Optimised quartz' (#22) from emailjobs into master
Reviewed-on: #22
2026-05-15 09:52:06 +02:00
Khwezi Mngoma 0f91f102e5 Optimised quartz
continuous-integration/drone/pr Build is passing
2026-05-15 09:51:26 +02:00
khwezi e6e0475db1 Merge pull request 'emailjobs' (#21) from emailjobs into master
Reviewed-on: #21
2026-05-15 08:39:36 +02:00
Khwezi Mngoma be9c83c8a3 Removed Abstractions listing from Git Tag
continuous-integration/drone/pr Build is passing
2026-05-15 08:39:10 +02:00
Khwezi Mngoma 65687d231e Ensured UTC is used 2026-05-15 08:37:58 +02:00
khwezi 5090c60797 Merge pull request 'Fixed Lead->Customer Relationship' (#20) from emailjobs into master
Reviewed-on: #20
2026-05-15 07:55:56 +02:00
Khwezi Mngoma 4523ef6151 Fixed Lead->Customer Relationship
continuous-integration/drone/pr Build is passing
2026-05-15 07:54:35 +02:00
125 changed files with 3469 additions and 555 deletions
+5 -1
View File
@@ -19,6 +19,10 @@ steps:
commands: commands:
- 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
- dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- dotnet pack LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.MidrandBooks.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- name: gitea-tag-release - name: gitea-tag-release
image: alpine/git image: alpine/git
@@ -41,7 +45,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.Features\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* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false, \"draft\": false,
\"prerelease\": false \"prerelease\": false
}" }"
@@ -0,0 +1,38 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
public Fixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
//.AddMidrandShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>b205af96-ceef-44e1-851c-458c9fd1c437</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.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
<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,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": "*"
}
@@ -0,0 +1,30 @@
using LiteCharms.Features.MidrandBooks.HealthChecks;
using static LiteCharms.Features.Extensions.Postgres;
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>(SchedulerDbConfigName);
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());
return services;
}
}
@@ -0,0 +1,16 @@
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Postgres
{
public const string MidrandShopDbConfigName = "PostgresMidrandBooks";
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
return services;
}
}
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.MidrandBooks.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.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.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);
}
}
}
@@ -0,0 +1,166 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
<UserSecretsId>5be62f49-3ed0-4468-884e-1b04e048b45a</UserSecretsId>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Features.MidrandBooks</PackageId>
<Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>MidrandBooks feature 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>
<!-- 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 -->
<ItemGroup>
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</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" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<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="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
namespace LiteCharms.Features.MidrandBooks.Postgres;
public class MidrandShopDbContext(DbContextOptions<MidrandShopDbContext> options) : DbContext(options)
{
}
@@ -0,0 +1,21 @@
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.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,22 @@
{
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
},
"Port": 465,
"Host": "mail.litecharms.co.za",
"UseSsl": true
},
"Monitoring": {
"ApiKey": "",
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
"ServiceName": "MidrandBooks"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,38 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.TechShop.Extensions;
namespace LiteCharms.Features.TechShop.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
public Fixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
.AddTechShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddTechShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92</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.TechShop\LiteCharms.Features.TechShop.csproj" />
<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.TechShop.Notifications;
using LiteCharms.Features.TechShop.Notifications.Events;
using static LiteCharms.Features.Extensions.Email;
namespace LiteCharms.Features.TechShop.Tests;
public class NotificationsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
[Fact]
public async Task CreateNotificationCommand_ShouldSucceed()
{
Notifications.Models.CreateNotification request = new()
{
CorrelationId = Guid.CreateVersion7().ToString(),
CorrelationIdType = CorrelationIdTypes.None,
Direction = NotificationDirection.Outgoing,
Platform = NotificationPlatforms.Email,
Priority = 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.TechShop.Products;
namespace LiteCharms.Features.TechShop.Tests;
public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[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,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": "*"
}
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities; namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageConfirguration, Package>] [EntityTypeConfiguration<PackageConfirguration, Package>]
public class Package : Models.Package public class Package : Models.Package
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities; namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageConfirguration : IEntityTypeConfiguration<Package> public class PackageConfirguration : IEntityTypeConfiguration<Package>
{ {
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop.Products.Entities; using LiteCharms.Features.TechShop.Products.Entities;
namespace LiteCharms.Features.Shop.CartPackages.Entities; namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageItemConfiguration, PackageItem>] [EntityTypeConfiguration<PackageItemConfiguration, PackageItem>]
public class PackageItem : Models.PackageItem public class PackageItem : Models.PackageItem
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities; namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageItemConfiguration : IEntityTypeConfiguration<PackageItem> public class PackageItemConfiguration : IEntityTypeConfiguration<PackageItem>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Models; namespace LiteCharms.Features.TechShop.CartPackages.Models;
public class Package public class Package
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Models; namespace LiteCharms.Features.TechShop.CartPackages.Models;
public class PackageItem public class PackageItem
{ {
@@ -1,10 +1,10 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.CartPackages.Models; using LiteCharms.Features.TechShop.CartPackages.Models;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Extensions;
using static LiteCharms.Features.Extensions.Timezones; using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.CartPackages; namespace LiteCharms.Features.TechShop.CartPackages;
public class PackageService(IDbContextFactory<ShopDbContext> contextFactory) public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -51,7 +51,6 @@ public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
var newPackage = context.Packages.Add(new Entities.Package var newPackage = context.Packages.Add(new Entities.Package
{ {
UpdatedAt = null,
Name = name, Name = name,
Summary = summary, Summary = summary,
Description = description, Description = description,
@@ -206,7 +205,7 @@ public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
package.Summary = summary; package.Summary = summary;
package.Description = description; package.Description = description;
package.ImageUrl = ImageUrl; package.ImageUrl = ImageUrl;
package.UpdatedAt = SouthAfricanTimeZone.UtcNow(); package.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -1,9 +1,10 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Customers.Models; using LiteCharms.Features.TechShop.Customers.Models;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.Customers; namespace LiteCharms.Features.TechShop.Customers;
public class CustomerService(IDbContextFactory<ShopDbContext> contextFactory) public class CustomerService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -1,9 +1,9 @@
using LiteCharms.Features.Shop.Leads.Entities; using LiteCharms.Features.TechShop.Leads.Entities;
using LiteCharms.Features.Shop.Orders.Entities; using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.Shop.Quotes.Entities; using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.Shop.ShoppingCarts.Entities; using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.Shop.Customers.Entities; namespace LiteCharms.Features.TechShop.Customers.Entities;
[EntityTypeConfiguration<CustomerConfiguration, Customer>] [EntityTypeConfiguration<CustomerConfiguration, Customer>]
public class Customer : Models.Customer public class Customer : Models.Customer
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Entities; namespace LiteCharms.Features.TechShop.Customers.Entities;
public class CustomerConfiguration : IEntityTypeConfiguration<Customer> public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Models; namespace LiteCharms.Features.TechShop.Customers.Models;
public class Customer public class Customer
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Models; namespace LiteCharms.Features.TechShop.Customers.Models;
public record CreateCustomer public record CreateCustomer
{ {
@@ -1,16 +1,4 @@
namespace LiteCharms.Features.Shop; namespace LiteCharms.Features.TechShop;
public enum EmailStatuses : int
{
GeneralError = 0,
AuthenticationError = 1,
ProtocolError = 2,
Connected = 3,
Disconnected = 4,
TooManyConnections = 5,
ConnectionAborted = 6,
Success = 7
}
public enum CorrelationIdTypes : int public enum CorrelationIdTypes : int
{ {
@@ -27,13 +15,6 @@ public enum CorrelationIdTypes : int
LinkedIn = 10 LinkedIn = 10
} }
public enum Priorities : int
{
Low = 0,
Medium = 1,
High = 2,
}
public enum NotificationPlatforms : int public enum NotificationPlatforms : int
{ {
Email = 1, Email = 1,
@@ -0,0 +1,22 @@
using LiteCharms.Features.TechShop.HealthChecks;
using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
return services;
}
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(TechShopDbConfigName);
return services;
}
}
@@ -1,17 +1,18 @@
using LiteCharms.Features.Shop.CartPackages.Models; using LiteCharms.Features.TechShop.CartPackages.Models;
using LiteCharms.Features.Shop.Customers.Models; using LiteCharms.Features.TechShop.Customers.Models;
using LiteCharms.Features.Shop.Leads.Models; using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.Shop.Notifications.Models; using LiteCharms.Features.TechShop.Notifications.Models;
using LiteCharms.Features.Shop.Orders.Models; using LiteCharms.Features.TechShop.Orders.Models;
using LiteCharms.Features.Shop.Products.Models; using LiteCharms.Features.TechShop.Products.Entities;
using LiteCharms.Features.Shop.Quotes.Models; using LiteCharms.Features.TechShop.Products.Models;
using LiteCharms.Features.Shop.ShoppingCarts.Models; using LiteCharms.Features.TechShop.Quotes.Models;
using LiteCharms.Features.TechShop.ShoppingCarts.Models;
namespace LiteCharms.Features.Extensions; namespace LiteCharms.Features.TechShop.Extensions;
public static class EntityModeMappers public static class Mappers
{ {
public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) => public static ShoppingCartPackage ToModel(this ShoppingCarts.Entities.ShoppingCartPackage entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -20,7 +21,7 @@ public static class EntityModeMappers
ShoppingCartId = entity.ShoppingCartId ShoppingCartId = entity.ShoppingCartId
}; };
public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) => public static PackageItem ToModel(this CartPackages.Entities.PackageItem entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -30,7 +31,7 @@ public static class EntityModeMappers
ProductPriceId = entity.ProductPriceId ProductPriceId = entity.ProductPriceId
}; };
public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) => public static Package ToModel(this CartPackages.Entities.Package entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -43,7 +44,7 @@ public static class EntityModeMappers
Summary = entity.Summary Summary = entity.Summary
}; };
public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) => public static ShoppingCartItem ToModel(this ShoppingCarts.Entities.ShoppingCartItem entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -54,7 +55,7 @@ public static class EntityModeMappers
ShoppingCartId = entity.ShoppingCartId ShoppingCartId = entity.ShoppingCartId
}; };
public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) => public static ShoppingCart ToModel(this ShoppingCarts.Entities.ShoppingCart entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -64,7 +65,7 @@ public static class EntityModeMappers
OrderId = entity.OrderId OrderId = entity.OrderId
}; };
public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) => public static Quote ToModel(this Quotes.Entities.Quote entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -79,7 +80,7 @@ public static class EntityModeMappers
OrderId = entity.OrderId OrderId = entity.OrderId
}; };
public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) => public static Notification ToModel(this Notifications.Entities.Notification entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -103,7 +104,7 @@ public static class EntityModeMappers
Errors = entity.Errors Errors = entity.Errors
}; };
public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) => public static Customer ToModel(this Customers.Entities.Customer entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -128,7 +129,7 @@ public static class EntityModeMappers
Whatsapp = entity.Whatsapp Whatsapp = entity.Whatsapp
}; };
public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) => public static Lead ToModel(this Leads.Entities.Lead entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -149,7 +150,7 @@ public static class EntityModeMappers
Status = entity.Status Status = entity.Status
}; };
public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) => public static Order ToModel(this Orders.Entities.Order entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -163,7 +164,7 @@ public static class EntityModeMappers
InvoiceUrl = entity.InvoiceUrl InvoiceUrl = entity.InvoiceUrl
}; };
public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) => public static OrderRefund ToModel(this Orders.Entities.OrderRefund entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -173,19 +174,21 @@ public static class EntityModeMappers
Amount = entity.Amount Amount = entity.Amount
}; };
public static Product ToModel(this Features.Shop.Products.Entities.Product entity) => public static Products.Models.Product ToModel(this 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, Summary = entity.Summary,
ImageUrl = entity.ImageUrl, ImageUrl = entity.ImageUrl,
Thumbnails = entity.Thumbnails Thumbnails = entity.Thumbnails,
Metadata = entity.Metadata,
}; };
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) => public static Products.Models.ProductPrice ToModel(this Products.Entities.ProductPrice entity) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -0,0 +1,16 @@
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Postgres
{
public const string TechShopDbConfigName = "PostgresShop";
public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName)));
return services;
}
}
@@ -0,0 +1,27 @@
using LiteCharms.Features.TechShop.CartPackages;
using LiteCharms.Features.TechShop.Customers;
using LiteCharms.Features.TechShop.Leads;
using LiteCharms.Features.TechShop.Notifications;
using LiteCharms.Features.TechShop.Orders;
using LiteCharms.Features.TechShop.Products;
using LiteCharms.Features.TechShop.Quotes;
using LiteCharms.Features.TechShop.ShoppingCarts;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Shop
{
public static IServiceCollection AddTechShopServices(this IServiceCollection services)
{
services.AddSingleton<PackageService>()
.AddSingleton<LeadService>()
.AddSingleton<NotificationService>()
.AddSingleton<OrderService>()
.AddSingleton<ProductService>()
.AddSingleton<QuoteService>()
.AddSingleton<ShoppingCartService>()
.AddSingleton<CustomerService>();
return services;
}
}
@@ -1,8 +1,10 @@
namespace LiteCharms.Features.HealthChecks; using static LiteCharms.Features.TechShop.Extensions.Postgres;
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck namespace LiteCharms.Features.TechShop.HealthChecks;
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
{ {
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!; private readonly string connectionString = configuration.GetConnectionString(TechShopDbConfigName)!;
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($"{TechShopDbConfigName} is responsive.");
} }
catch (Exception ex) catch (Exception ex)
{ {
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex); return HealthCheckResult.Unhealthy($"{TechShopDbConfigName} is unreachable.", ex);
} }
} }
} }
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.TechShop.HealthChecks;
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(TechShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{TechShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{TechShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop.Customers.Models; using LiteCharms.Features.TechShop.Customers.Entities;
namespace LiteCharms.Features.Shop.Leads.Entities; namespace LiteCharms.Features.TechShop.Leads.Entities;
[EntityTypeConfiguration<LeadConfiguration, Lead>] [EntityTypeConfiguration<LeadConfiguration, Lead>]
public class Lead : Models.Lead public class Lead : Models.Lead
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Leads.Entities; namespace LiteCharms.Features.TechShop.Leads.Entities;
public class LeadConfiguration : IEntityTypeConfiguration<Lead> public class LeadConfiguration : IEntityTypeConfiguration<Lead>
{ {
@@ -24,7 +24,7 @@ public class LeadConfiguration : IEntityTypeConfiguration<Lead>
builder.Property(f => f.AttributionHash).IsRequired(true); builder.Property(f => f.AttributionHash).IsRequired(true);
builder.HasOne(f => f.Customer) builder.HasOne(f => f.Customer)
.WithMany() .WithMany(f => f.Leads)
.HasForeignKey(f => f.CustomerId) .HasForeignKey(f => f.CustomerId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
} }
@@ -1,10 +1,11 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Leads.Models; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Hash; using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.Shop.Leads; namespace LiteCharms.Features.TechShop.Leads;
public class LeadService(IDbContextFactory<ShopDbContext> contextFactory) public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -28,7 +29,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
FeedItemId = request.FeedItemId, FeedItemId = request.FeedItemId,
Status = LeadStatus.New, Status = LeadStatus.New,
TargetId = request.TargetId, TargetId = request.TargetId,
AttributionHash = GenerateSha256HashString.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
@@ -103,6 +104,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Lead with ID {leadId} not found.")); return Result.Fail(new Error($"Lead with ID {leadId} not found."));
lead.Status = status; lead.Status = status;
lead.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Leads.Models; namespace LiteCharms.Features.TechShop.Leads.Models;
public class Lead public class Lead
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Leads.Models; namespace LiteCharms.Features.TechShop.Leads.Models;
public record CreateLead public record CreateLead
{ {
@@ -0,0 +1,166 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
<UserSecretsId>fbd8f4a2-0420-44e2-baff-4678d9e7eee1</UserSecretsId>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Features.TechShop</PackageId>
<Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>TechShop feature 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>
<!-- 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 -->
<ItemGroup>
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</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" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<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="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Notifications.Entities; namespace LiteCharms.Features.TechShop.Notifications.Entities;
[EntityTypeConfiguration<NotificationConfiguration, Notification>] [EntityTypeConfiguration<NotificationConfiguration, Notification>]
public class Notification : Models.Notification; public class Notification : Models.Notification;
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Notifications.Entities; namespace LiteCharms.Features.TechShop.Notifications.Entities;
public class NotificationConfiguration : IEntityTypeConfiguration<Notification> public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
{ {
@@ -1,9 +1,8 @@
using LiteCharms.Features.Email; using LiteCharms.Features.Email;
using LiteCharms.Features.Shop.Notifications.Entities; using LiteCharms.Features.TechShop.Notifications.Models;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Notifications.Events.Handlers; namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers;
public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbContext> contextFactory, ILogger<ProcessEmailNotificationsEvent> logger, public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbContext> contextFactory, ILogger<ProcessEmailNotificationsEvent> logger,
EmailService emailService) : INotificationHandler<ProcessEmailNotificationsEvent> EmailService emailService) : INotificationHandler<ProcessEmailNotificationsEvent>
@@ -16,17 +15,20 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (emailService.Status != EmailStatuses.Connected)
await emailService.ConnectAsync(cancellationToken);
var notifications = await context.Notifications var notifications = await context.Notifications
.OrderByDescending(o => o.Priority) .OrderByDescending(o => o.CreatedAt)
.ThenBy(o => o.CreatedAt) .ThenBy(o => o.Priority)
.Where(n => n.CorrelationIdType == CorrelationIdTypes.Email) .Where(n => n.Platform == NotificationPlatforms.Email &&
.Where(n => n.Direction == NotificationDirection.Outgoing) n.Direction == NotificationDirection.Outgoing && n.Processed == false)
.Take(message.MaxRecords) .Take(message.MaxRecords)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (var notification in notifications) foreach (var notification in notifications)
{ {
if (dropBatch || cancellationToken.IsCancellationRequested) break; if (dropBatch) break;
var sendResult = await SendEmailAsync(notification,emailService, cancellationToken); var sendResult = await SendEmailAsync(notification,emailService, cancellationToken);
@@ -44,15 +46,19 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
} }
notification.Processed = true; notification.Processed = true;
notification.UpdatedAt = SouthAfricanTimeZone.UtcNow(); notification.UpdatedAt = DateTime.UtcNow;
} }
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, ex.Message); logger.LogError(ex, ex.Message);
} }
finally
{
await emailService.DisconnectAsync(cancellationToken);
}
} }
private async Task<Result> SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default) private async Task<Result> SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default)
@@ -1,14 +1,12 @@
using LiteCharms.Features.Shop; using static LiteCharms.Features.Extensions.Email;
using LiteCharms.Features.Shop.Notifications;
using static LiteCharms.Features.Email.Extensions.Constants;
namespace LiteCharms.Features.Email.Events.Handlers; namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers;
public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) : 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 await notificationService.CreateNotificationAsync(new Models.CreateNotification
{ {
CorrelationId = notification.CorrelationId, CorrelationId = notification.CorrelationId,
CorrelationIdType = CorrelationIdTypes.None, CorrelationIdType = CorrelationIdTypes.None,
@@ -1,6 +1,6 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.Abstractions;
namespace LiteCharms.Features.Shop.Notifications.Events; namespace LiteCharms.Features.TechShop.Notifications.Events;
public class ProcessEmailNotificationsEvent : EventBase, IEvent public class ProcessEmailNotificationsEvent : EventBase, IEvent
{ {
@@ -8,6 +8,8 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent
public int MaxRecords { get; set; } public int MaxRecords { get; set; }
public ProcessEmailNotificationsEvent() { MaxRecords = 1000; }
private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords;
public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords);
@@ -1,7 +1,6 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Shop;
namespace LiteCharms.Features.Email.Events; namespace LiteCharms.Features.TechShop.Notifications.Events;
public class SendShopEmailEnquiryEvent : EventBase, IEvent public class SendShopEmailEnquiryEvent : EventBase, IEvent
{ {
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Shop.Notifications.Models; using LiteCharms.Features.TechShop;
namespace LiteCharms.Features.TechShop.Notifications.Models;
public class Notification public class Notification
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Notifications.Models; namespace LiteCharms.Features.TechShop.Notifications.Models;
public record CreateNotification public record CreateNotification
{ {
@@ -1,9 +1,10 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Notifications.Models; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Notifications.Models;
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.Notifications; namespace LiteCharms.Features.TechShop.Notifications;
public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory) public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -63,8 +64,8 @@ public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory
{ {
try try
{ {
var fromDate = range.From.ToDateTime(TimeOnly.MinValue); var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue); var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -96,12 +97,9 @@ public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory
return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); return Result.Fail(new Error($"Notification with id {request.NotificationId} not found."));
notification.Processed = request.Processed; notification.Processed = request.Processed;
notification.UpdatedAt = DateTime.UtcNow;
if (request.HasError) notification.HasError = request.HasError;
{ notification.Errors = request.Errors;
notification.HasError = request.HasError;
notification.Errors = request.Errors;
}
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -1,8 +1,8 @@
using LiteCharms.Features.Shop.Customers.Entities; using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.Shop.Quotes.Entities; using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.Shop.ShoppingCarts.Entities; using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.Shop.Orders.Entities; namespace LiteCharms.Features.TechShop.Orders.Entities;
[EntityTypeConfiguration<OrderConfiguration, Order>] [EntityTypeConfiguration<OrderConfiguration, Order>]
public class Order : Models.Order public class Order : Models.Order
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Orders.Entities; namespace LiteCharms.Features.TechShop.Orders.Entities;
public class OrderConfiguration : IEntityTypeConfiguration<Order> public class OrderConfiguration : IEntityTypeConfiguration<Order>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Orders.Entities; namespace LiteCharms.Features.TechShop.Orders.Entities;
[EntityTypeConfiguration<OrderRefundConfiguration, OrderRefund>] [EntityTypeConfiguration<OrderRefundConfiguration, OrderRefund>]
public class OrderRefund : Models.OrderRefund public class OrderRefund : Models.OrderRefund
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Orders.Entities; namespace LiteCharms.Features.TechShop.Orders.Entities;
public class OrderRefundConfiguration : IEntityTypeConfiguration<OrderRefund> public class OrderRefundConfiguration : IEntityTypeConfiguration<OrderRefund>
{ {
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Shop.Orders.Models; using LiteCharms.Features.TechShop;
namespace LiteCharms.Features.TechShop.Orders.Models;
public class Order public class Order
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Orders.Models; namespace LiteCharms.Features.TechShop.Orders.Models;
public class OrderRefund public class OrderRefund
{ {
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Shop.Orders.Models; using LiteCharms.Features.TechShop;
namespace LiteCharms.Features.TechShop.Orders.Models;
public record CreateOrder public record CreateOrder
{ {
@@ -1,10 +1,10 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Orders.Models; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Orders.Models;
using static LiteCharms.Features.Extensions.Timezones; using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.Orders; namespace LiteCharms.Features.TechShop.Orders;
public class OrderService(IDbContextFactory<ShopDbContext> contextFactory) public class OrderService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -241,7 +241,7 @@ public class OrderService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Order {request.OrderId} not found")); return Result.Fail(new Error($"Order {request.OrderId} not found"));
order.Status = request.Status; order.Status = request.Status;
order.UpdatedAt = SouthAfricanTimeZone.UtcNow(); order.UpdatedAt = DateTime.UtcNow;
if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl; if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl;
@@ -1,6 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace LiteCharms.Features.Shop.Postgres.Migrations namespace LiteCharms.Features.TechShop.Postgres.Migrations
{ {
[DbContext(typeof(ShopDbContext))] [DbContext(typeof(ShopDbContext))]
[Migration("20260512065421_Init")] [Migration("20260512065421_Init")]
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace LiteCharms.Features.Shop.Postgres.Migrations namespace LiteCharms.Features.TechShop.Postgres.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Init : Migration public partial class Init : Migration
@@ -1,6 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace LiteCharms.Features.Shop.Postgres.Migrations namespace LiteCharms.Features.TechShop.Postgres.Migrations
{ {
[DbContext(typeof(ShopDbContext))] [DbContext(typeof(ShopDbContext))]
[Migration("20260514004002_UsedStringTableNames")] [Migration("20260514004002_UsedStringTableNames")]
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace LiteCharms.Features.Shop.Postgres.Migrations namespace LiteCharms.Features.TechShop.Postgres.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class UsedStringTableNames : Migration public partial class UsedStringTableNames : Migration
@@ -0,0 +1,776 @@
// <auto-generated />
using System;
using LiteCharms.Features.TechShop.Postgres;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
[DbContext(typeof(ShopDbContext))]
[Migration("20260515055221_FixedLeadCustomerRelationship")]
partial class FixedLeadCustomerRelationship
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Package", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ProductPriceId");
b.ToTable("PackageItem", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Leads", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Notification", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Orders", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderRefunds", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("ProductPrices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShoppingCartId")
.IsUnique();
b.ToTable("Quotes", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.ToTable("ShoppingCarts", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductPriceId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartItems", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartPackages", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany("PackageItems")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Leads")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Orders")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product")
.WithMany("ProductPrices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Quotes")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId");
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId");
b.Navigation("Customer");
b.Navigation("Order");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("ShoppingCarts")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("ShoppingCart")
.HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Customer");
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductPrice");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartPackages")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Package");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Navigation("PackageItems");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Navigation("Leads");
b.Navigation("Orders");
b.Navigation("Quotes");
b.Navigation("ShoppingCarts");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Navigation("Quote");
b.Navigation("Refunds");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Navigation("ProductPrices");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Navigation("Quote");
b.Navigation("ShoppingCartItems");
b.Navigation("ShoppingCartPackages");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,166 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class FixedLeadCustomerRelationship : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customer_CustomerId",
table: "Leads");
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customers_CustomerId1",
table: "Leads");
migrationBuilder.DropForeignKey(
name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropForeignKey(
name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropTable(
name: "Customer");
migrationBuilder.DropIndex(
name: "IX_ShoppingCartItems_ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropIndex(
name: "IX_ShoppingCartItems_ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropIndex(
name: "IX_Leads_CustomerId1",
table: "Leads");
migrationBuilder.DropColumn(
name: "ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropColumn(
name: "ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropColumn(
name: "CustomerId1",
table: "Leads");
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customers_CustomerId",
table: "Leads",
column: "CustomerId",
principalTable: "Customers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customers_CustomerId",
table: "Leads");
migrationBuilder.AddColumn<Guid>(
name: "ProductPriceId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ShoppingCartId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "CustomerId1",
table: "Leads",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "Customer",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Active = table.Column<bool>(type: "boolean", nullable: false),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
Company = table.Column<string>(type: "text", nullable: true),
Country = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Discord = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
LastName = table.Column<string>(type: "text", nullable: true),
LinkedIn = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "text", nullable: true),
Phone = table.Column<string>(type: "text", nullable: true),
PostalCode = table.Column<string>(type: "text", nullable: true),
Region = table.Column<string>(type: "text", nullable: true),
Slack = table.Column<string>(type: "text", nullable: true),
Tax = table.Column<string>(type: "text", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Website = table.Column<string>(type: "text", nullable: true),
Whatsapp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Customer", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ShoppingCartItems_ProductPriceId1",
table: "ShoppingCartItems",
column: "ProductPriceId1");
migrationBuilder.CreateIndex(
name: "IX_ShoppingCartItems_ShoppingCartId1",
table: "ShoppingCartItems",
column: "ShoppingCartId1");
migrationBuilder.CreateIndex(
name: "IX_Leads_CustomerId1",
table: "Leads",
column: "CustomerId1");
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customer_CustomerId",
table: "Leads",
column: "CustomerId",
principalTable: "Customer",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customers_CustomerId1",
table: "Leads",
column: "CustomerId1",
principalTable: "Customers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1",
table: "ShoppingCartItems",
column: "ProductPriceId1",
principalTable: "ProductPrices",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1",
table: "ShoppingCartItems",
column: "ShoppingCartId1",
principalTable: "ShoppingCarts",
principalColumn: "Id");
}
}
}
@@ -0,0 +1,788 @@
// <auto-generated />
using System;
using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
[DbContext(typeof(ShopDbContext))]
[Migration("20260520191059_AddedProductMetadata")]
partial class AddedProductMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Package", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ProductPriceId");
b.ToTable("PackageItem", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Leads", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Notification", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Orders", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderRefunds", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("ProductPrices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShoppingCartId")
.IsUnique();
b.ToTable("Quotes", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.ToTable("ShoppingCarts", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductPriceId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartItems", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartPackages", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany("PackageItems")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Leads")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Orders")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product")
.WithMany("ProductPrices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Quotes")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId");
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId");
b.Navigation("Customer");
b.Navigation("Order");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("ShoppingCarts")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("ShoppingCart")
.HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Customer");
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductPrice");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartPackages")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Package");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Navigation("PackageItems");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Navigation("Leads");
b.Navigation("Orders");
b.Navigation("Quotes");
b.Navigation("ShoppingCarts");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Navigation("Quote");
b.Navigation("Refunds");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Navigation("ProductPrices");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Navigation("Quote");
b.Navigation("ShoppingCartItems");
b.Navigation("ShoppingCartPackages");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,71 @@
using System;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedProductMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()");
migrationBuilder.AddColumn<ProductMetadata>(
name: "Metadata",
table: "Products",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Products");
migrationBuilder.DropColumn(
name: "Metadata",
table: "Products");
migrationBuilder.DropColumn(
name: "UpdatedAt",
table: "Products");
migrationBuilder.AlterColumn<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: false);
}
}
}
@@ -1,6 +1,7 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -8,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace LiteCharms.Features.Shop.Postgres.Migrations namespace LiteCharms.Features.TechShop.Postgres.Migrations
{ {
[DbContext(typeof(ShopDbContext))] [DbContext(typeof(ShopDbContext))]
partial class ShopDbContextModelSnapshot : ModelSnapshot partial class ShopDbContextModelSnapshot : ModelSnapshot
@@ -166,74 +167,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.ToTable("Customers", (string)null); b.ToTable("Customers", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<string>("LastName")
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -270,9 +203,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.Property<Guid?>("CustomerId") b.Property<Guid?>("CustomerId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid?>("CustomerId1")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId") b.Property<long?>("FeedItemId")
.HasColumnType("bigint"); .HasColumnType("bigint");
@@ -295,8 +225,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.HasIndex("CustomerId"); b.HasIndex("CustomerId");
b.HasIndex("CustomerId1");
b.ToTable("Leads", (string)null); b.ToTable("Leads", (string)null);
}); });
@@ -459,7 +387,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.Property<bool>("Active") b.Property<bool>("Active")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("boolean") .HasColumnType("boolean")
.HasDefaultValue(true); .HasDefaultValue(false);
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@@ -470,6 +403,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
.HasMaxLength(2048) .HasMaxLength(2048)
.HasColumnType("character varying(2048)"); .HasColumnType("character varying(2048)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -482,6 +418,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.PrimitiveCollection<string>("Thumbnails") b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb"); .HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Products", (string)null); b.ToTable("Products", (string)null);
@@ -615,9 +554,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.Property<Guid>("ProductPriceId") b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid?>("ProductPriceId1")
.HasColumnType("uuid");
b.Property<int>("Quantity") b.Property<int>("Quantity")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
@@ -626,9 +562,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.Property<Guid>("ShoppingCartId") b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid?>("ShoppingCartId1")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -636,12 +569,8 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
b.HasIndex("ProductPriceId"); b.HasIndex("ProductPriceId");
b.HasIndex("ProductPriceId1");
b.HasIndex("ShoppingCartId"); b.HasIndex("ShoppingCartId");
b.HasIndex("ShoppingCartId1");
b.ToTable("ShoppingCartItems", (string)null); b.ToTable("ShoppingCartItems", (string)null);
}); });
@@ -692,15 +621,11 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{ {
b.HasOne("LiteCharms.Features.Shop.Customers.Models.Customer", "Customer") b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany() .WithMany("Leads")
.HasForeignKey("CustomerId") .HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", null)
.WithMany("Leads")
.HasForeignKey("CustomerId1");
b.Navigation("Customer"); b.Navigation("Customer");
}); });
@@ -780,26 +705,18 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{ {
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", null) b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany() .WithMany()
.HasForeignKey("ProductPriceId") .HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany()
.HasForeignKey("ProductPriceId1");
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", null)
.WithMany("ShoppingCartItems") .WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId") .HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany()
.HasForeignKey("ShoppingCartId1");
b.Navigation("ProductPrice"); b.Navigation("ProductPrice");
b.Navigation("ShoppingCart"); b.Navigation("ShoppingCart");
@@ -0,0 +1,39 @@
using LiteCharms.Features.TechShop.CartPackages.Entities;
using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.TechShop.Leads.Entities;
using LiteCharms.Features.TechShop.Notifications.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.Products.Entities;
using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.TechShop.Postgres;
public class ShopDbContext(DbContextOptions<ShopDbContext> options) : DbContext(options)
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Lead> Leads { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderRefund> OrderRefunds { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductPrice> ProductPrices { get; set; }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Quote> Quotes { get; set; }
public DbSet<ShoppingCart> ShoppingCarts { get; set; }
public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }
public DbSet<Package> Packages { get; set; }
public DbSet<PackageItem> PackageItems { get; set; }
public DbSet<ShoppingCartPackage> ShoppingCartPackages { get; set; }
}
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Shop.Postgres; using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.TechShop.Postgres;
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext> public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{ {
@@ -12,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
.Build(); .Build();
var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>(); var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop")); optionsBuilder.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName));
return new ShopDbContext(optionsBuilder.Options); return new ShopDbContext(optionsBuilder.Options);
} }
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Entities; namespace LiteCharms.Features.TechShop.Products.Entities;
[EntityTypeConfiguration<ProductConfiguration, Product>] [EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product public class Product : Models.Product
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Entities; namespace LiteCharms.Features.TechShop.Products.Entities;
public class ProductConfiguration : IEntityTypeConfiguration<Product> public class ProductConfiguration : IEntityTypeConfiguration<Product>
{ {
@@ -8,10 +8,13 @@ public class ProductConfiguration : IEntityTypeConfiguration<Product>
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.Name).IsRequired(); builder.Property(f => f.Name).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); builder.Property(f => f.Description).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false); builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false);
builder.Property(f => f.Active).HasDefaultValue(true); builder.Property(f => f.Active).HasDefaultValue(false);
builder.Property(f => f.Metadata).HasColumnType("jsonb").IsRequired(false);
} }
} }
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Entities; namespace LiteCharms.Features.TechShop.Products.Entities;
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>] [EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice public class ProductPrice : Models.ProductPrice
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Entities; namespace LiteCharms.Features.TechShop.Products.Entities;
public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice> public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
{ {
@@ -7,7 +7,7 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
builder.ToTable("ProductPrices"); builder.ToTable("ProductPrices");
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.ProductId).IsRequired(); builder.Property(f => f.ProductId).IsRequired();
builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2);
@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
namespace LiteCharms.Features.TechShop.Products.Models;
public class CreateProductModel
{
[MaxLength(128)]
[Required(ErrorMessage = "Product name is required.")]
public string? Name { get; set; }
[MaxLength(512)]
[Required(ErrorMessage = "Summary is required.")]
public string? Summary { get; set; }
[MaxLength(2048)]
[Required(ErrorMessage = "Description is required.")]
public string? Description { get; set; }
[Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")]
public decimal Price { get; set; }
[MaxLength(128)]
[Required(ErrorMessage = "Author metadata is required.")]
public string? Author { get; set; }
[Required(ErrorMessage = "Publication Date is required.")]
public DateTime PublishDate { get; set; } = DateTime.Today;
[MaxLength(255)]
[Required(ErrorMessage = "Copyright Information field is required.")]
public string? CopyrightInfo { get; set; }
[MaxLength(128)]
[Required(ErrorMessage = "ISBN code is required.")]
[RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")]
public string? Isbn { get; set; }
[Required(ErrorMessage = "Primary image is required.")]
public string? ImageUrl { get; set; }
public List<string> Thumbnails { get; set; } = [];
}
@@ -1,9 +1,13 @@
namespace LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.TechShop.Products.Models;
public class Product public class Product
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Summary { get; set; } public string? Summary { get; set; }
@@ -15,4 +19,6 @@ public class Product
public string[]? Thumbnails { get; set; } public string[]? Thumbnails { get; set; }
public bool Active { get; set; } public bool Active { get; set; }
public ProductMetadata? Metadata { get; set; }
} }
@@ -0,0 +1,12 @@
namespace LiteCharms.Features.TechShop.Products.Models;
public class ProductMetadata
{
public string? Manufacturer { get; set; }
public string? ManufactureDate { get; set; }
public string? CopyrightInfo { get; set; }
public string? SerialNumber { get; set; }
}
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.TechShop.Products.Models;
public class ProductPrice public class ProductPrice
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.TechShop.Products.Models;
public record CreateProduct public record CreateProduct
{ {
@@ -11,4 +11,6 @@ public record CreateProduct
public required string ImageUrl { get; set; } public required string ImageUrl { get; set; }
public string[]? Thumbnails { get; set; } public string[]? Thumbnails { get; set; }
public ProductMetadata? Metadata { get; set; }
} }
@@ -1,9 +1,8 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.Shop.Products.Models; using LiteCharms.Features.TechShop.Products.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Products; namespace LiteCharms.Features.TechShop.Products;
public class ProductService(IDbContextFactory<ShopDbContext> contextFactory) public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -19,7 +18,7 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail($"Could not find product price with ID {productPriceId}"); return Result.Fail($"Could not find product price with ID {productPriceId}");
price.Active = active; price.Active = active;
price.UpdatedAt = SouthAfricanTimeZone.UtcNow(); price.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -68,10 +67,11 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
var newProduct = context.Products.Add(new Entities.Product var newProduct = context.Products.Add(new Entities.Product
{ {
Name = request.Name, Name = request.Name,
Summary = request.Summary, Summary = request.Summary,
Description = request.Description, Description = request.Description,
ImageUrl = request.ImageUrl, ImageUrl = request.ImageUrl,
Thumbnails = request.Thumbnails Thumbnails = request.Thumbnails,
Metadata = request.Metadata
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
@@ -90,12 +90,12 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
try try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice
{ {
Price = price, Price = price,
Discount = discount, Discount = discount,
ProductId = productId ProductId = productId
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
@@ -200,19 +200,19 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail($"Could not find product price with ID {productPriceId}"); return Result.Fail($"Could not find product price with ID {productPriceId}");
existingPrice.Active = false; existingPrice.Active = false;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); existingPrice.UpdatedAt = DateTime.UtcNow;
if (!(await context.SaveChangesAsync(cancellationToken) > 0)) if (!(await context.SaveChangesAsync(cancellationToken) > 0))
return Result.Fail<Guid>($"Failed to deactivate existing price of ID {productPriceId}, try again later"); return Result.Fail<Guid>($"Failed to deactivate existing price of ID {productPriceId}, try again later");
var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken);
if(result.IsFailed) if (result.IsFailed)
{ {
var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
existingPrice.Active = true; existingPrice.Active = true;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); existingPrice.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Fail<Guid>("Reverted to old price, creation of new price failed") ? Result.Fail<Guid>("Reverted to old price, creation of new price failed")
@@ -226,4 +226,52 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error(ex.Message).CausedBy(ex)); return Result.Fail(new Error(ex.Message).CausedBy(ex));
} }
} }
public async ValueTask<Result> SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
if (productPrice is null)
return Result.Fail($"Could not find product price with ID {productPriceId}");
productPrice.Active = active;
productPrice.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail($"Failed to change product price status by ID {productPriceId}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
if (product is null)
return Result.Fail($"Could not find product with ID {productId}");
product.Metadata = metadata;
product.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail($"Failed to update product metadata by ID {productId}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,8 +1,8 @@
using LiteCharms.Features.Shop.Customers.Entities; using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.Shop.Orders.Entities; using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.Shop.ShoppingCarts.Entities; using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.Shop.Quotes.Entities; namespace LiteCharms.Features.TechShop.Quotes.Entities;
[EntityTypeConfiguration<QuoteConfiguration, Quote>] [EntityTypeConfiguration<QuoteConfiguration, Quote>]
public class Quote : Models.Quote public class Quote : Models.Quote
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Quotes.Entities; namespace LiteCharms.Features.TechShop.Quotes.Entities;
public class QuoteConfiguration : IEntityTypeConfiguration<Quote> public class QuoteConfiguration : IEntityTypeConfiguration<Quote>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.TechShop.Quotes.Models;
public class Quote public class Quote
{ {
@@ -1,10 +1,10 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Quotes.Models; using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Timezones; using LiteCharms.Features.TechShop.Quotes.Models;
namespace LiteCharms.Features.Shop.Quotes; namespace LiteCharms.Features.TechShop.Quotes;
public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory) public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -26,6 +26,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}")); return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}"));
quote.OrderId = orderId; quote.OrderId = orderId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -52,6 +53,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found")); return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found"));
quote.ShoppingCartId = shoppingCartId; quote.ShoppingCartId = shoppingCartId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -140,7 +142,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error("Quote not found.")); return Result.Fail(new Error("Quote not found."));
quote.Status = status; quote.Status = status;
quote.UpdatedAt = SouthAfricanTimeZone.UtcNow(); quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -1,8 +1,8 @@
using LiteCharms.Features.Shop.Customers.Entities; using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.Shop.Orders.Entities; using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.Shop.Quotes.Entities; using LiteCharms.Features.TechShop.Quotes.Entities;
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartConfiguration, ShoppingCart>] [EntityTypeConfiguration<ShoppingCartConfiguration, ShoppingCart>]
public class ShoppingCart : Models.ShoppingCart public class ShoppingCart : Models.ShoppingCart
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
public class ShoppingCartConfiguration : IEntityTypeConfiguration<ShoppingCart> public class ShoppingCartConfiguration : IEntityTypeConfiguration<ShoppingCart>
{ {
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop.Products.Entities; using LiteCharms.Features.TechShop.Products.Entities;
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartItemConfiguration, ShoppingCartItem>] [EntityTypeConfiguration<ShoppingCartItemConfiguration, ShoppingCartItem>]
public class ShoppingCartItem : Models.ShoppingCartItem public class ShoppingCartItem : Models.ShoppingCartItem
@@ -1,6 +1,4 @@
using LiteCharms.Features.Shop.Products.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities;
public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCartItem> public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCartItem>
{ {
@@ -15,13 +13,13 @@ public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCa
builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.ShoppingCartId).IsRequired();
builder.Property(f => f.ProductPriceId).IsRequired(); builder.Property(f => f.ProductPriceId).IsRequired();
builder.HasOne<ShoppingCart>() builder.HasOne(f => f.ShoppingCart)
.WithMany(s => s.ShoppingCartItems) .WithMany(s => s.ShoppingCartItems)
.HasForeignKey(f => f.ShoppingCartId) .HasForeignKey(f => f.ShoppingCartId)
.IsRequired() .IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ProductPrice>() builder.HasOne(f => f.ProductPrice)
.WithMany() .WithMany()
.HasForeignKey(f => f.ProductPriceId) .HasForeignKey(f => f.ProductPriceId)
.IsRequired() .IsRequired()
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop.CartPackages.Entities; using LiteCharms.Features.TechShop.CartPackages.Entities;
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartPackageConfiguration, ShoppingCartPackage>] [EntityTypeConfiguration<ShoppingCartPackageConfiguration, ShoppingCartPackage>]
public class ShoppingCartPackage : Models.ShoppingCartPackage public class ShoppingCartPackage : Models.ShoppingCartPackage
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration<ShoppingCartPackage> public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration<ShoppingCartPackage>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.TechShop.ShoppingCarts.Models;
public class ShoppingCart public class ShoppingCart
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.TechShop.ShoppingCarts.Models;
public class ShoppingCartItem public class ShoppingCartItem
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.TechShop.ShoppingCarts.Models;
public class ShoppingCartPackage public class ShoppingCartPackage
{ {
@@ -1,9 +1,8 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.Shop.ShoppingCarts.Models; using LiteCharms.Features.TechShop.ShoppingCarts.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.ShoppingCarts; namespace LiteCharms.Features.TechShop.ShoppingCarts;
public class ShoppingCartService(IDbContextFactory<ShopDbContext> contextFactory) public class ShoppingCartService(IDbContextFactory<ShopDbContext> contextFactory)
{ {
@@ -284,7 +283,7 @@ public class ShoppingCartService(IDbContextFactory<ShopDbContext> contextFactory
return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}"); return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}");
item.Quantity = quantity; item.Quantity = quantity;
item.UpdatedAt = SouthAfricanTimeZone.UtcNow(); item.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Tests; namespace LiteCharms.Features.Tests;
public class CommonFixture : IDisposable public class Fixture : IDisposable
{ {
public IConfiguration Configuration { get; set; } public IConfiguration Configuration { get; set; }
@@ -10,22 +10,22 @@ public class CommonFixture : IDisposable
public IMediator Mediator { get; set; } public IMediator Mediator { get; set; }
public CommonFixture() public Fixture()
{ {
Configuration = new ConfigurationBuilder() Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json") .AddUserSecrets<Fixture>()
.AddUserSecrets<CommonFixture>() .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables() .AddEnvironmentVariables()
.Build(); .Build();
Services = new ServiceCollection() Services = new ServiceCollection()
.AddMediator() .AddMediator()
.AddLogging() .AddLogging()
.AddShopServices()
.AddEmailServiceBus() .AddEmailServiceBus()
.AddShopDatabase(Configuration) .AddGarageS3(Configuration)
.AddEmailServices(Configuration) .AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider(); .BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>(); Mediator = Services.GetRequiredService<IMediator>();
@@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.0"> <PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -27,10 +27,10 @@
<!-- Global Usings --> <!-- Global Usings -->
<ItemGroup> <ItemGroup>
<Using Include="Mediator"/> <Using Include="Mediator" />
<Using Include="Xunit.Abstractions"/> <Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection"/> <Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration"/> <Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -43,7 +43,7 @@
<ItemGroup> <ItemGroup>
<None Update="appsettings.json"> <None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
@@ -1,35 +0,0 @@
using LiteCharms.Features.Shop.Notifications;
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 = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]",
RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress,
Subject = "Test Message",
Message = "This is an automation test",
IsHtml = false,
IsInternal = true,
};
var createResult = await notificationService.CreateNotificationAsync(request);
Assert.True(createResult.IsSuccess);
foreach (var error in createResult.Errors) output.WriteLine(error.Message);
}
}
@@ -0,0 +1,54 @@
using LiteCharms.Features.S3.Abstractions;
namespace LiteCharms.Features.Tests;
public class S3ServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[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);
}
}
@@ -1,4 +1,16 @@
{ {
"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": { "Email": {
"Credentials": { "Credentials": {
"Username": "shop@litecharms.co.za" "Username": "shop@litecharms.co.za"

Some files were not shown because too many files have changed in this diff Show More