Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Example environment variables for Docker deployment
# Copy this file to .env and adjust values as needed

# Predictor Settings
PREDICTOR__MAXCALCULATIONPERIODMONTHS=36
PREDICTOR__DEFAULTINITIALBUDGET=48750
PREDICTOR__ENABLEEXAMPLEDATA=true
PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=120

# ASP.NET Core Settings
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080

# Logging
LOGGING__LOGLEVEL__DEFAULT=Information
LOGGING__LOGLEVEL__MICROSOFT_ASPNETCORE=Warning
84 changes: 84 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Configuration Guide

## Application Settings

The Predictor application is fully configurable through the `PredictorSettings` section in `appsettings.json`. All settings can be overridden using environment variables or other ASP.NET Core configuration sources.

### Available Settings

| Setting | Default | Description |
| ----------------------------- | ------- | -------------------------------------------------------------- |
| `MaxCalculationPeriodMonths` | 36 | Maximum number of months to calculate predictions for |
| `DefaultInitialBudget` | 48750 | Default initial budget used in example data |
| `EnableExampleData` | true | Enable/disable the `/example-data` endpoint |
| `MaxAllowedCalculationPeriod` | 120 | Maximum allowed calculation period to prevent abuse (10 years) |

### Configuration Examples

#### appsettings.json

```json
{
"PredictorSettings": {
"MaxCalculationPeriodMonths": 24,
"DefaultInitialBudget": 25000,
"EnableExampleData": true,
"MaxAllowedCalculationPeriod": 60
}
}
```

#### Environment Variables

```bash
# Docker/Container deployment
PREDICTOR__MAXCALCULATIONPERIODMONTHS=24
PREDICTOR__DEFAULTINITIALBUDGET=25000
PREDICTOR__ENABLEEXAMPLEDATA=true
PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=60

# Or use the ASPNETCORE prefix
ASPNETCORE_PREDICTORSETTINGS__MAXCALCULATIONPERIODMONTHS=24
```

#### Docker Compose

```yaml
version: "3.8"
services:
predictor:
image: predictor:latest
environment:
- PREDICTOR__MAXCALCULATIONPERIODMONTHS=24
- PREDICTOR__DEFAULTINITIALBUDGET=25000
- PREDICTOR__ENABLEEXAMPLEDATA=true
- PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=60
ports:
- "8080:8080"
```

### Environment-Specific Configuration

The application uses standard ASP.NET Core configuration layering:

1. **appsettings.json** - Base configuration
2. **appsettings.{Environment}.json** - Environment-specific overrides
3. **Environment variables** - Runtime overrides
4. **Command line arguments** - Highest priority

#### Development vs Production

- **Development**: Uses `appsettings.Development.json` with shorter calculation periods for faster testing
- **Production**: Uses `appsettings.json` with full calculation periods

### Configuration Validation

The application validates configuration at startup and uses sensible defaults if values are missing. The calculation period is automatically capped at `MaxAllowedCalculationPeriod` to prevent resource abuse.

### Why This Matters

✅ **Deployment Flexibility**: Change limits without code changes
✅ **Environment-Specific**: Different settings for dev/staging/production
✅ **Docker Ready**: Full support for container deployment
✅ **Twelve-Factor**: Follows twelve-factor app configuration principles
✅ **Security**: Sensitive settings can be injected via environment variables
7 changes: 7 additions & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
# Project private files
DownloadResults/

# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# User-specific files
*.rsuser
*.suo
Expand Down
28 changes: 17 additions & 11 deletions src/Predictor.Web/ExampleData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ namespace Predictor.Web;

public static class ExampleData
{
public static CalculateInput CalculateInputExample { get; } = new(
InitialBudget: 48_750,
StartCalculationMonth: MonthDate.Now,
Incomes: [
// Infinite recurring income (no EndDate)
new("Primary Salary", 5_400, MonthDate.Now, new RecurringConfig(1)),
new("Spouse Salary", 4_100, MonthDate.Now, new RecurringConfig(1)),
new("Rental Property A", 1_500, MonthDate.Now, new RecurringConfig(1)),
new("Rental Property B", 1_100, MonthDate.Now.AddMonths(3), new RecurringConfig(1)),
new("Investment Dividends", 320, MonthDate.Now.AddMonths(1), new RecurringConfig(3)),
new("Side Business", 850, MonthDate.Now.AddMonths(2), new RecurringConfig(1)),
[Obsolete("Use GetCalculateInputExample(PredictorSettings settings) instead")]
public static CalculateInput CalculateInputExample => GetCalculateInputExample(new PredictorSettings());

public static CalculateInput GetCalculateInputExample(PredictorSettings settings)
{
return new CalculateInput(
InitialBudget: settings.DefaultInitialBudget,
StartCalculationMonth: MonthDate.Now,
Incomes: [
// Infinite recurring income (no EndDate)
new("Primary Salary", 5_400, MonthDate.Now, new RecurringConfig(1)),
new("Spouse Salary", 4_100, MonthDate.Now, new RecurringConfig(1)),
new("Rental Property A", 1_500, MonthDate.Now, new RecurringConfig(1)),
new("Rental Property B", 1_100, MonthDate.Now.AddMonths(3), new RecurringConfig(1)),
new("Investment Dividends", 320, MonthDate.Now.AddMonths(1), new RecurringConfig(3)),
new("Side Business", 850, MonthDate.Now.AddMonths(2), new RecurringConfig(1)),

// Finite recurring income (with EndDate)
new("Contract Work", 2_200, MonthDate.Now, new RecurringConfig(1, MonthDate.Now.AddMonths(18))),
Expand Down Expand Up @@ -123,4 +128,5 @@ public static class ExampleData
new("Luxury Purchase", 18_000, MonthDate.Now.AddMonths(42)),
new("Investment Property Down Payment", 25_000, MonthDate.Now.AddMonths(45)),
new("Retirement Catch-up", 30_000, MonthDate.Now.AddMonths(48))]);
}
}
26 changes: 26 additions & 0 deletions src/Predictor.Web/Models/PredictorSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Predictor.Web.Models;

public class PredictorSettings
{
public const string SectionName = "PredictorSettings";

/// <summary>
/// Maximum number of months to calculate predictions for
/// </summary>
public int MaxCalculationPeriodMonths { get; set; } = 36;

/// <summary>
/// Default initial budget for examples
/// </summary>
public decimal DefaultInitialBudget { get; set; } = 48_750m;

/// <summary>
/// Enable/disable example data endpoint
/// </summary>
public bool EnableExampleData { get; set; } = true;

/// <summary>
/// Maximum allowed calculation period to prevent abuse
/// </summary>
public int MaxAllowedCalculationPeriod { get; set; } = 120; // 10 years
}
21 changes: 18 additions & 3 deletions src/Predictor.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

var builder = WebApplication.CreateBuilder(args);

// Configure PredictorSettings
builder.Services.Configure<PredictorSettings>(
builder.Configuration.GetSection(PredictorSettings.SectionName));

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Expand All @@ -19,20 +23,31 @@
app.UseAuthorization();
app.MapControllers();

app.MapGet("/example-data", () => ExampleData.CalculateInputExample);
// Get settings
var settings = app.Services.GetRequiredService<IConfiguration>()
.GetSection(PredictorSettings.SectionName)
.Get<PredictorSettings>() ?? new PredictorSettings();

// Conditionally register example data endpoint
if (settings.EnableExampleData)
{
app.MapGet("/example-data", () => ExampleData.GetCalculateInputExample(settings));
}

app.MapPost("/calc", (CalculateInput input) =>
{
var months = new List<MonthOutput>();
var budget = input.InitialBudget;
foreach (var currentMonth in MonthDate.Range(input.StartCalculationMonth, 12 * 3 - 1))
var calculationPeriod = Math.Min(settings.MaxCalculationPeriodMonths, settings.MaxAllowedCalculationPeriod);

foreach (var currentMonth in MonthDate.Range(input.StartCalculationMonth, calculationPeriod - 1))
{
var month = Calculator.CalculateMonth(input, currentMonth, budget);
budget = month.BudgetAfter;
months.Add(month);
}

return new CalculationOutput([.. months]);
return Results.Ok(new CalculationOutput([.. months]));
});

app.Run();
6 changes: 6 additions & 0 deletions src/Predictor.Web/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"PredictorSettings": {
"MaxCalculationPeriodMonths": 24,
"DefaultInitialBudget": 25000,
"EnableExampleData": true,
"MaxAllowedCalculationPeriod": 60
}
}
8 changes: 7 additions & 1 deletion src/Predictor.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"PredictorSettings": {
"MaxCalculationPeriodMonths": 36,
"DefaultInitialBudget": 48750,
"EnableExampleData": true,
"MaxAllowedCalculationPeriod": 120
}
}