diff --git a/LiteCharms.Features.Tests/ProductsFeatureTests.cs b/LiteCharms.Features.Tests/ProductsFeatureTests.cs new file mode 100644 index 0000000..1aee1cf --- /dev/null +++ b/LiteCharms.Features.Tests/ProductsFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Shop.Products; + +namespace LiteCharms.Features.Tests; + +public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public async Task GetProductsAsync_ReturnsProducts() + { + var productService = fixture.Services.GetRequiredService(); + + var result = await productService.GetProductsAsync(); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine($"Retrieved {result.Value.Length} products."); + } +} diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs index f4173a6..1b7fda9 100644 --- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -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; diff --git a/LiteCharms.Features/Email/Extensions/Constants.cs b/LiteCharms.Features/Email/Extensions/Constants.cs deleted file mode 100644 index 1d02879..0000000 --- a/LiteCharms.Features/Email/Extensions/Constants.cs +++ /dev/null @@ -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"; -} diff --git a/LiteCharms.Features/Extensions/Email.cs b/LiteCharms.Features/Extensions/Email.cs index ae3756d..d52f04a 100644 --- a/LiteCharms.Features/Extensions/Email.cs +++ b/LiteCharms.Features/Extensions/Email.cs @@ -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(configuration.GetSection("Email")); diff --git a/LiteCharms.Features/Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs index ebabd8f..f27502a 100644 --- a/LiteCharms.Features/Extensions/HealthChecks.cs +++ b/LiteCharms.Features/Extensions/HealthChecks.cs @@ -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("Quartz"); + services.AddHealthChecks().AddCheck("ShopQuartz"); return services; } - public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services) + public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck("PostgreSQL"); + services.AddHealthChecks().AddCheck("MidrandShopQuartz"); + + return services; + } + + public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(ShopDbConfigName); + + return services; + } + + public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(MidrandShopDbConfigName); return services; } diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs index 0e70287..982f5f9 100644 --- a/LiteCharms.Features/Extensions/Postgres.cs +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -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(options => - options.UseNpgsql(configuration.GetConnectionString("PostgresShop"))); + options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName))); + + return services; + } + + public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); return services; } diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index fe6103a..7db12a8 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -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(); diff --git a/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs new file mode 100644 index 0000000..9588d24 --- /dev/null +++ b/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.HealthChecks; + +public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task 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"); + } + } +} diff --git a/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs new file mode 100644 index 0000000..f88b22e --- /dev/null +++ b/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs similarity index 62% rename from LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs rename to LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs index 377da08..4a65640 100644 --- a/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs +++ b/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs @@ -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 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); } } } \ No newline at end of file diff --git a/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs deleted file mode 100644 index 59eb397..0000000 --- a/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Features.HealthChecks; - -public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck -{ - public async Task 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"); - } - } -} diff --git a/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs new file mode 100644 index 0000000..0ccae12 --- /dev/null +++ b/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.HealthChecks; + +public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task 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"); + } + } +} diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs new file mode 100644 index 0000000..efa8047 --- /dev/null +++ b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.MidrandShop.Postgres; + +public class MidrandShopDbContext(DbContextOptions options) : DbContext(options) +{ + +} diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs new file mode 100644 index 0000000..deb8ff4 --- /dev/null +++ b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs @@ -0,0 +1,21 @@ +using static LiteCharms.Features.Extensions.Postgres; + +namespace LiteCharms.Features.MidrandShop.Postgres; + +public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory +{ + 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(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)); + + return new MidrandShopDbContext(optionsBuilder.Options); + } +} diff --git a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs index b8437d9..b8e61dc 100644 --- a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs +++ b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.Extensions.Postgres; + +namespace LiteCharms.Features.Shop.Postgres; public class ShopDbContextFactory : IDesignTimeDbContextFactory { @@ -12,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory .Build(); var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop")); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)); return new ShopDbContext(optionsBuilder.Options); }