Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master

Reviewed-on: #37
This commit was merged in pull request #37.
This commit is contained in:
2026-05-23 11:49:27 +02:00
15 changed files with 185 additions and 49 deletions
@@ -0,0 +1,19 @@
using LiteCharms.Features.Shop.Products;
namespace LiteCharms.Features.Tests;
public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
{
[Fact]
public async Task GetProductsAsync_ReturnsProducts()
{
var productService = fixture.Services.GetRequiredService<ProductService>();
var result = await productService.GetProductsAsync();
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine($"Retrieved {result.Value.Length} products.");
}
}
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop;
using LiteCharms.Features.Shop.Notifications;
using static LiteCharms.Features.Email.Extensions.Constants;
using static LiteCharms.Features.Extensions.Email;
namespace LiteCharms.Features.Email.Events.Handlers;
@@ -1,8 +0,0 @@
namespace LiteCharms.Features.Email.Extensions;
public static class Constants
{
public const string ShopSchedulerName = "shop";
public const string ShopEmailFromName = "Khongisa Shop";
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
}
+4 -1
View File
@@ -4,7 +4,10 @@ using LiteCharms.Features.Email.Configuration;
namespace LiteCharms.Features.Extensions;
public static class Email
{
{
public const string ShopEmailFromName = "Khongisa Shop";
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<SmtpSettings>(configuration.GetSection("Email"));
+19 -4
View File
@@ -1,19 +1,34 @@
using LiteCharms.Features.HealthChecks;
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services)
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<QuartzHealthCheck>("Quartz");
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
return services;
}
public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services)
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresHealthCheck>("PostgreSQL");
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>("MidrandShopQuartz");
return services;
}
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(ShopDbConfigName);
return services;
}
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandShopDbConfigName);
return services;
}
+15 -2
View File
@@ -1,13 +1,26 @@
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.MidrandShop.Postgres;
using LiteCharms.Features.Shop.Postgres;
namespace LiteCharms.Features.Extensions;
public static class Postgres
{
public const string MidrandShopDbConfigName = "PostgresMidrandShop";
public const string ShopDbConfigName = "PostgresShop";
public const string SchedulerDbConfigName = "PostgresScheduler";
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("PostgresShop")));
options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)));
return services;
}
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
return services;
}
+5 -3
View File
@@ -1,15 +1,17 @@
using LiteCharms.Features.Quartz;
using LiteCharms.Features.Quartz.Abstractions;
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.Extensions;
public static class Quartz
{
private const string databaseConfigName = "PostgresScheduler";
public const string ShopSchedulerName = "shop";
public const string MidrandShopSchedulerName = "midrandshop";
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(databaseConfigName);
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
services.ConfigureCommon();
@@ -44,7 +46,7 @@ public static class Quartz
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(databaseConfigName);
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
services.ConfigureCommon();
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.HealthChecks;
public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.HealthChecks;
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
{
private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = "SELECT 1";
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex);
}
}
}
@@ -1,8 +1,10 @@
namespace LiteCharms.Features.HealthChecks;
using static LiteCharms.Features.Extensions.Postgres;
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
namespace LiteCharms.Features.HealthChecks;
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
{
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!;
private readonly string connectionString = configuration.GetConnectionString(ShopDbConfigName)!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
@@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex);
}
}
}
@@ -1,23 +0,0 @@
namespace LiteCharms.Features.HealthChecks;
public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy("Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy("Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store");
}
}
}
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.HealthChecks;
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -0,0 +1,6 @@
namespace LiteCharms.Features.MidrandShop.Postgres;
public class MidrandShopDbContext(DbContextOptions<MidrandShopDbContext> options) : DbContext(options)
{
}
@@ -0,0 +1,21 @@
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.MidrandShop.Postgres;
public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory<MidrandShopDbContext>
{
public MidrandShopDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets(typeof(MidrandShopDbContext).Assembly)
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
var optionsBuilder = new DbContextOptionsBuilder<MidrandShopDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName));
return new MidrandShopDbContext(optionsBuilder.Options);
}
}
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Shop.Postgres;
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.Shop.Postgres;
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{
@@ -12,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
.Build();
var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop"));
optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName));
return new ShopDbContext(optionsBuilder.Options);
}