Skip to content

Commit dcf428c

Browse files
feat(config): externalize configuration to YAML files (BD-845)
Migrate all hardcoded configuration, secrets, and environment-specific values to external YAML files with ASP.NET Core IConfiguration integration. - Add custom YAML configuration provider (YamlConfigurationSource/Provider) - Add YamlConfigLoader for flat key-value YAML parsing with __ → : mapping - Add ConfigurationValidator for startup validation of required settings - Add config models: DatabaseSettings, BusinessHoursSettings, HealthCheckSettings, SwaggerSettings - Remove hardcoded encryption key from PasswordEncryptor (now from config) - Remove hardcoded holidays from BusinessHoursCalculator (now from config) - Clean appsettings.json of all secrets and environment-specific values - Delete appsettings.Development.json (contained encrypted passwords) - Add OrderMonitor_ENV.yml master config template - Add OrderMonitor_ENV.development.yml for local development - Add OrderMonitor_ENV.secrets.yml.template for credentials reference - Add docker-compose.yml for local dev with SQL Server - Defer DB connection factory resolution for test compatibility - Update integration test factory with config and no-op validator - Add 64 new tests (176 unit + 38 integration = 214 total, all passing) Closes BD-845, BD-849, BD-850, BD-851, BD-852, BD-853, BD-854, BD-855, BD-856, BD-857, BD-858 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 46cfd10 commit dcf428c

30 files changed

+1544
-166
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ coverage*.xml
7878
!.env.example
7979
appsettings.*.json
8080
!appsettings.json
81-
!appsettings.Development.json
81+
82+
# YAML config - production secrets must never be committed
83+
OrderMonitor_ENV.production.yml
84+
OrderMonitor_ENV.staging.yml
8285

8386
# Secrets
8487
*.pem

OrderMonitor_ENV.development.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# OrderMonitor API - Development Environment Configuration
2+
# Non-sensitive values for local development.
3+
# Sensitive values (passwords, keys) should be set as OS environment variables.
4+
5+
# ===== Database Configuration =====
6+
Database__Provider: "sqlserver"
7+
Database__ConnectionString: "Server=localhost,1433;Database=PrinterPix_BO_Live;User Id=sa;Password={ENCRYPTED};TrustServerCertificate=True;ApplicationIntent=ReadOnly;"
8+
Database__EncryptedPassword: "" # Set via env var: Database__EncryptedPassword
9+
Database__EncryptionKey: "" # Set via env var: Database__EncryptionKey
10+
Database__MaxPoolSize: "50"
11+
Database__CommandTimeout: "30"
12+
13+
# ===== SMTP Configuration =====
14+
SmtpSettings__Host: "pod51017.outlook.com"
15+
SmtpSettings__Port: "587"
16+
SmtpSettings__Username: "backoffice@printerpix.com"
17+
SmtpSettings__Password: "" # Set via env var: SmtpSettings__Password
18+
SmtpSettings__FromEmail: "backoffice@printerpix.com"
19+
SmtpSettings__UseSsl: "true"
20+
21+
# ===== Alert Configuration =====
22+
Alerts__Enabled: "true"
23+
Alerts__Recipients: "ranganathan.e@syncoms.com"
24+
Alerts__SubjectPrefix: "[Order Monitor DEV]"
25+
26+
# ===== Scanner Configuration =====
27+
Scanner__Enabled: "true"
28+
Scanner__IntervalMinutes: "15"
29+
Scanner__BatchSize: "100"
30+
31+
# ===== Business Hours =====
32+
BusinessHours__Timezone: "Europe/London"
33+
BusinessHours__StartHour: "0"
34+
BusinessHours__EndHour: "0"
35+
BusinessHours__Holidays: "2026-01-01,2026-04-03,2026-04-06,2026-05-04,2026-05-25,2026-08-31,2026-12-25,2026-12-28"
36+
37+
# ===== Swagger =====
38+
Swagger__Enabled: "true"
39+
40+
# ===== Logging =====
41+
Logging__LogLevel__Default: "Debug"
42+
Logging__LogLevel__Microsoft.AspNetCore: "Information"
43+
Logging__LogLevel__OrderMonitor: "Debug"
44+
45+
# ===== Application =====
46+
ASPNETCORE_ENVIRONMENT: "Development"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# OrderMonitor API - Secrets Configuration Template
2+
# =================================================
3+
# IMPORTANT: Never commit this file with real values!
4+
# Copy to OrderMonitor_ENV.secrets.yml and fill with actual Base64-encoded values.
5+
# This file is for reference only.
6+
#
7+
# How to Base64 encode:
8+
# Linux/Mac: echo -n "your-value" | base64
9+
# PowerShell: [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("your-value"))
10+
11+
# ===== Database Credentials =====
12+
# Connection string with password placeholder
13+
Database__ConnectionString: ""
14+
# AES-encrypted database password (Base64)
15+
Database__EncryptedPassword: ""
16+
# AES-256 encryption key (exactly 32 characters)
17+
Database__EncryptionKey: ""
18+
19+
# ===== SMTP Credentials =====
20+
# Encrypted SMTP password (Base64 AES-encrypted)
21+
SmtpSettings__Password: ""
22+
23+
# ===== Alert Recipients =====
24+
# Comma-separated email addresses
25+
Alerts__Recipients: ""

OrderMonitor_ENV.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# OrderMonitor API - Environment Configuration
2+
# This file contains ALL configuration values for the application.
3+
# Override precedence: K8s Secrets > ConfigMap > OS env vars > this file
4+
#
5+
# Key naming: Use double-underscore (__) as hierarchy separator.
6+
# Example: Database__ConnectionString maps to IConfiguration["Database:ConnectionString"]
7+
8+
# ===== Database Configuration =====
9+
Database__Provider: "sqlserver" # sqlserver | mysql | postgresql
10+
Database__ConnectionString: "" # Full connection string (without password if using EncryptedPassword)
11+
Database__EncryptedPassword: "" # Base64-encoded AES-encrypted password
12+
Database__EncryptionKey: "" # AES-256 key for decrypting EncryptedPassword (32 chars)
13+
Database__MaxPoolSize: "100"
14+
Database__CommandTimeout: "30"
15+
16+
# ===== SMTP Configuration =====
17+
SmtpSettings__Host: ""
18+
SmtpSettings__Port: "587"
19+
SmtpSettings__Username: ""
20+
SmtpSettings__Password: "" # Encrypted SMTP password
21+
SmtpSettings__FromEmail: ""
22+
SmtpSettings__UseSsl: "true"
23+
24+
# ===== Alert Configuration =====
25+
Alerts__Enabled: "true"
26+
Alerts__Recipients: "" # Comma-separated email addresses
27+
Alerts__SubjectPrefix: "[Order Monitor]"
28+
29+
# ===== Scanner Configuration =====
30+
Scanner__Enabled: "true"
31+
Scanner__IntervalMinutes: "15"
32+
Scanner__BatchSize: "1000"
33+
34+
# ===== Status Thresholds =====
35+
StatusThresholds__PrepStatuses__MinStatusId: "3001"
36+
StatusThresholds__PrepStatuses__MaxStatusId: "3910"
37+
StatusThresholds__PrepStatuses__ThresholdHours: "6"
38+
StatusThresholds__FacilityStatuses__MinStatusId: "4001"
39+
StatusThresholds__FacilityStatuses__MaxStatusId: "5830"
40+
StatusThresholds__FacilityStatuses__ThresholdHours: "48"
41+
42+
# ===== Business Hours =====
43+
BusinessHours__Timezone: "Europe/London"
44+
BusinessHours__StartHour: "0" # 0 = full 24-hour days
45+
BusinessHours__EndHour: "0"
46+
BusinessHours__Holidays: "" # Comma-separated yyyy-MM-dd dates
47+
48+
# ===== Health Check =====
49+
HealthCheck__Path: "/health"
50+
HealthCheck__IncludeDatabase: "true"
51+
52+
# ===== Swagger =====
53+
Swagger__Enabled: "true"
54+
Swagger__Title: "OrderMonitor API"
55+
Swagger__Version: "v1"
56+
57+
# ===== Logging =====
58+
Logging__LogLevel__Default: "Information"
59+
Logging__LogLevel__Microsoft.AspNetCore: "Warning"
60+
Logging__LogLevel__OrderMonitor: "Debug"
61+
62+
# ===== Application =====
63+
AllowedHosts: "*"
64+
ASPNETCORE_ENVIRONMENT: "Production"
65+
ASPNETCORE_URLS: "http://+:8080"

docker-compose.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
version: '3.8'
2+
3+
services:
4+
ordermonitor-api:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
ports:
9+
- "8080:8080"
10+
environment:
11+
- ASPNETCORE_ENVIRONMENT=Development
12+
volumes:
13+
- ./OrderMonitor_ENV.yml:/app/OrderMonitor_ENV.yml:ro
14+
- ./OrderMonitor_ENV.development.yml:/app/OrderMonitor_ENV.development.yml:ro
15+
depends_on:
16+
sqlserver:
17+
condition: service_healthy
18+
networks:
19+
- ordermonitor
20+
21+
sqlserver:
22+
image: mcr.microsoft.com/mssql/server:2022-latest
23+
environment:
24+
- ACCEPT_EULA=Y
25+
- MSSQL_SA_PASSWORD=YourStr0ngP@ssword!
26+
ports:
27+
- "1433:1433"
28+
volumes:
29+
- sqlserver-data:/var/opt/mssql
30+
healthcheck:
31+
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "YourStr0ngP@ssword!" -Q "SELECT 1" -C || exit 1
32+
interval: 10s
33+
timeout: 5s
34+
retries: 10
35+
start_period: 30s
36+
networks:
37+
- ordermonitor
38+
39+
volumes:
40+
sqlserver-data:
41+
42+
networks:
43+
ordermonitor:
44+
driver: bridge

src/OrderMonitor.Api/Program.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using OrderMonitor.Core.Interfaces;
12
using OrderMonitor.Infrastructure;
3+
using OrderMonitor.Infrastructure.Configuration;
24
using Serilog;
35

46
// Configure Serilog early for startup logging
@@ -12,6 +14,14 @@
1214

1315
var builder = WebApplication.CreateBuilder(args);
1416

17+
// Add YAML configuration sources (before builder.Build)
18+
// Override precedence: appsettings.json → YML file → environment variables
19+
var environment = builder.Environment.EnvironmentName;
20+
builder.Configuration
21+
.AddYamlFile("OrderMonitor_ENV.yml", optional: true)
22+
.AddYamlFile($"OrderMonitor_ENV.{environment.ToLowerInvariant()}.yml", optional: true)
23+
.AddEnvironmentVariables();
24+
1525
// Configure Serilog from appsettings
1626
builder.Host.UseSerilog((context, services, configuration) => configuration
1727
.ReadFrom.Configuration(context.Configuration)
@@ -24,14 +34,18 @@
2434
builder.Services.AddEndpointsApiExplorer();
2535
builder.Services.AddSwaggerGen();
2636

27-
// Add infrastructure services (database, repositories)
37+
// Add infrastructure services (database, repositories, config validation)
2838
builder.Services.AddInfrastructure(builder.Configuration);
2939

3040
// Add health checks
3141
builder.Services.AddHealthChecks();
3242

3343
var app = builder.Build();
3444

45+
// Validate configuration at startup
46+
var validator = app.Services.GetService<IConfigurationValidator>();
47+
validator?.Validate();
48+
3549
// Configure the HTTP request pipeline
3650
if (app.Environment.IsDevelopment())
3751
{

src/OrderMonitor.Api/appsettings.Development.json

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/OrderMonitor.Api/appsettings.json

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
{
2-
"ConnectionStrings": {
3-
"BackofficeDb": "Server=localhost;Database=Backoffice;Trusted_Connection=True;TrustServerCertificate=True;ApplicationIntent=ReadOnly;"
4-
},
5-
62
"StatusThresholds": {
73
"PrepStatuses": {
84
"MinStatusId": 3001,
@@ -16,33 +12,10 @@
1612
}
1713
},
1814

19-
"Scanner": {
20-
"Enabled": true,
21-
"IntervalMinutes": 15,
22-
"BatchSize": 1000
23-
},
24-
25-
"Alerts": {
26-
"Enabled": true,
27-
"Recipients": [
28-
"ranganathan.e@syncoms.com"
29-
],
30-
"SubjectPrefix": "[Order Monitor]"
31-
},
32-
33-
"SmtpSettings": {
34-
"Host": "pod51017.outlook.com",
35-
"Port": 587,
36-
"Username": "backoffice@printerpix.com",
37-
"FromEmail": "backoffice@printerpix.com",
38-
"UseSsl": true
39-
},
40-
4115
"Logging": {
4216
"LogLevel": {
4317
"Default": "Information",
44-
"Microsoft.AspNetCore": "Warning",
45-
"OrderMonitor": "Debug"
18+
"Microsoft.AspNetCore": "Warning"
4619
}
4720
},
4821

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace OrderMonitor.Core.Configuration;
2+
3+
/// <summary>
4+
/// Business hours and holiday configuration settings.
5+
/// </summary>
6+
public class BusinessHoursSettings
7+
{
8+
public const string SectionName = "BusinessHours";
9+
10+
/// <summary>
11+
/// IANA timezone identifier (e.g., "Europe/London").
12+
/// </summary>
13+
public string Timezone { get; set; } = "Europe/London";
14+
15+
/// <summary>
16+
/// Business day start hour (24h format).
17+
/// </summary>
18+
public int StartHour { get; set; } = 0;
19+
20+
/// <summary>
21+
/// Business day end hour (24h format). 0 means full 24-hour days.
22+
/// </summary>
23+
public int EndHour { get; set; } = 0;
24+
25+
/// <summary>
26+
/// Comma-separated list of holiday dates in yyyy-MM-dd format.
27+
/// </summary>
28+
public string Holidays { get; set; } = string.Empty;
29+
30+
/// <summary>
31+
/// Parses the Holidays string into a list of DateTime values.
32+
/// </summary>
33+
public IEnumerable<DateTime> GetHolidayDates()
34+
{
35+
if (string.IsNullOrWhiteSpace(Holidays))
36+
return Enumerable.Empty<DateTime>();
37+
38+
return Holidays
39+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
40+
.Select(s => DateTime.TryParse(s, out var date) ? date : (DateTime?)null)
41+
.Where(d => d.HasValue)
42+
.Select(d => d!.Value.Date);
43+
}
44+
}

0 commit comments

Comments
 (0)