8 Commits
Author SHA1 Message Date
hermes 6114d8f457 Updated documentation 2026-10-28 09:28:06 +02:00
hermes b8c3201c58 Refactored quartz healthcheck on api 2026-10-27 23:59:58 +02:00
hermes a8daea830f Refactored quartz client registration 2026-10-27 23:48:36 +02:00
hermes 5be7c8c56a Removed defaults from feature flags 2026-10-27 23:17:18 +02:00
hermes d24b0dafb1 Added http accessor to scheduler 2026-10-27 23:04:50 +02:00
hermes cd47a2b93a Fixed scheduler and api configuration 2026-10-27 22:08:12 +02:00
hermes c11535ebfc Added scheduler 2026-10-27 21:31:27 +02:00
hermes 129d10c7d1 Added jobs project and updated nuget packages 2026-10-27 08:28:24 +02:00
22 changed files with 607 additions and 123 deletions
+131 -5
View File
@@ -22,12 +22,13 @@ The PFM platform is constructed using **Clean Architecture** and **Domain-Driven
│ API GATEWAY │ │ API GATEWAY │
│ PostFundManagement.Api (ASP.NET Core Minimal APIs / Carter) │ │ PostFundManagement.Api (ASP.NET Core Minimal APIs / Carter) │
│ Authentication: JWT Bearer + OpenTelemetry Observability │ │ Authentication: JWT Bearer + OpenTelemetry Observability │
│ Quartz Client: Scheduler-Only (ZeroSizeThreadPool / Enqueues Jobs) │
└───────────────────────────────────┬────────────────────────────────────┘ └───────────────────────────────────┬────────────────────────────────────┘
│ │
┌───────────────────────────────────▼────────────────────────────────────┐ ┌───────────────────────────────────▼────────────────────────────────────┐
│ APPLICATION LAYER │ │ APPLICATION LAYER │
│ PostFundManagement.Application │ │ PostFundManagement.Application │
│ CQRS (Mediator), Business Validators, Domain Services, Quartz Jobs │ │ CQRS (Mediator), Business Validators, Domain Services, Event Handlers │
└──────────────────┬─────────────────────────────────┬───────────────────┘ └──────────────────┬─────────────────────────────────┬───────────────────┘
│ │ │ │
┌──────────────────▼──────────────────┐ ┌──────────▼───────────────────┐ ┌──────────────────▼──────────────────┐ ┌──────────▼───────────────────┐
@@ -35,8 +36,15 @@ The PFM platform is constructed using **Clean Architecture** and **Domain-Driven
│ PostFundManagement.Domain │ │ PostFundManagement.Infrastr.│ │ PostFundManagement.Domain │ │ PostFundManagement.Infrastr.│
│ Entities, Value Objects, Enums, │ │ PostgreSQL (EF Core 10), │ │ Entities, Value Objects, Enums, │ │ PostgreSQL (EF Core 10), │
│ Domain Invariants, Audit Models │ │ Skoruba STS, S3 Garage, │ │ Domain Invariants, Audit Models │ │ Skoruba STS, S3 Garage, │
└─────────────────────────────────────┘ │ Redis HybridCache │ └─────────────────────────────────────┘ │ Redis, Quartz (pfm-scheduler)│
└──────────────────────────────┘ └──────────────┬───────────────┘
│
┌────────────────────────────────────────────────────────▼───────────────┐
│ BACKGROUND JOBS WORKER HOST │
│ PostFundManagement.Jobs (.NET 10 Background Host / Quartz Runner) │
│ Recurring Job Registration (JobsWorker) & Mediated Execution Engine │
│ Quartz Runner: Full Execution (DefaultThreadPool MaxConcurrency = 5) │
└────────────────────────────────────────────────────────────────────────┘
``` ```
--- ---
@@ -113,8 +121,8 @@ Even if an `Executive`, `Manager`, or `Supervisor` possesses the `update` claim
flowchart LR flowchart LR
A["Maker (User A)<br/>Creates Record"] --> B["Record Persisted<br/>with createdBy = User A"] A["Maker (User A)<br/>Creates Record"] --> B["Record Persisted<br/>with createdBy = User A"]
B --> C{"Checker Evaluation<br/>(Approver Login)"} B --> C{"Checker Evaluation<br/>(Approver Login)"}
C -->|Checker == User A| D["Approval BLOCKED<br/>Maker-Checker Violation"] C -->|"Checker is User A"| D["Approval BLOCKED: Maker-Checker Violation"]
C -->|Checker == User B<br/>with 'update' claim| E["Approval GRANTED<br/>Status -> Approved"] C -->|"Checker is User B with update claim"| E["Approval GRANTED: Status Approved"]
``` ```
### 4.2 Record Attribution Model ### 4.2 Record Attribution Model
@@ -189,4 +197,122 @@ Following the introduction of fine-grained RBAC and Maker-Checker checks, startu
* **Immediate Non-Blocking Session Render**: If valid session tokens and user profiles exist in `localStorage`, `AuthProvider` immediately unblocks UI rendering (`isLoading: false`) and runs tenant lockout checks, account synchronization, and claim revalidation asynchronously in the background. * **Immediate Non-Blocking Session Render**: If valid session tokens and user profiles exist in `localStorage`, `AuthProvider` immediately unblocks UI rendering (`isLoading: false`) and runs tenant lockout checks, account synchronization, and claim revalidation asynchronously in the background.
* **In-Memory User Sync & Lockout Caching**: `isUserSyncedInBrowser` and `verifyTenantLockout` maintain in-memory references to eliminate synchronous `JSON.parse` operations from the request pipeline. * **In-Memory User Sync & Lockout Caching**: `isUserSyncedInBrowser` and `verifyTenantLockout` maintain in-memory references to eliminate synchronous `JSON.parse` operations from the request pipeline.
---
## 6. Distributed Background Job Processing Architecture (PostFundManagement.Jobs)
The PFM platform decouples synchronous API request handling from asynchronous, long-running, and scheduled workloads using a distributed **Quartz.NET (v4.6+)** clustering topology backed by PostgreSQL (`pfm-scheduler`).
### 6.1 Split-Node Topology: Scheduler Client vs. Execution Host
To maintain high API throughput, low memory footprint, and complete process isolation, PFM implements a split-node architecture between `PostFundManagement.Api` and `PostFundManagement.Jobs`:
```
┌──────────────────────────────────────────────┐
│ PostFundManagement.Api │
│ (API Gateway / Scheduler Client) │
│ │
│ * AddQuartzSchedulerClient() │
│ * ZeroSizeThreadPool (0 execution threads) │
│ * SchedulerStatus.Created (Standby mode) │
│ * Strictly schedules / enqueues events │
│ * Never acquires triggers or executes jobs │
└──────────────────────┬───────────────────────┘
│ Schedules IEvent jobs via IJobOrchestrator
▼
┌──────────────────────────────────────────────┐
│ PostgreSQL JobStore │
│ (pfm-scheduler) │
│ │
│ * Clustered tables (QRTZ_TRIGGERS, etc.) │
│ * System.Text.Json serialized IEvent payload│
│ * Distributed locking & check-in heartbeat │
└──────────────────────▲───────────────────────┘
│
│ Acquires triggers & executes (Polling loop)
┌──────────────────────┴───────────────────────┐
│ PostFundManagement.Jobs │
│ (Dedicated Worker Service) │
│ │
│ * AddQuartzScheduler() │
│ * DefaultThreadPool (MaxConcurrency = 5) │
│ * SchedulerStatus.Running (Started loop) │
│ * JobsWorker background service: │
│ - Registers recurring cron jobs on start │
│ - Enqueues one-time jobs when needed │
│ * Executes MediatorJob<TNotification> │
│ * Deserializes and publishes via IMediator │
└──────────────────────────────────────────────┘
```
| Characteristic | `PostFundManagement.Api` | `PostFundManagement.Jobs` |
| :--- | :--- | :--- |
| **Architectural Role** | API Gateway & Event Producer | Dedicated Execution Daemon & Consumer |
| **Registration Extension** | `AddQuartzSchedulerClient(...)` | `AddQuartzScheduler(...)` |
| **Thread Pool** | `ZeroSizeThreadPool` (0 threads) | `DefaultThreadPool` (`MaxConcurrency = 5`) |
| **Hosted Service** | None (`AddQuartzHostedService` omitted) | `AddQuartzHostedService(WaitForJobsToComplete = true)` |
| **Lifecycle State** | `SchedulerStatus.Created` (Standby) | `SchedulerStatus.Running` (Active execution loop) |
| **Readiness Check** | Verifies DB connectivity without requiring `Running` | Verifies active execution engine & DB connectivity |
| **Trigger Acquisition** | ❌ **Disabled** (Cannot acquire triggers or run jobs) | ✅ **Active** (Polls DB, acquires triggers, runs jobs) |
### 6.2 Event-Driven Mediated Orchestration (`IEvent` & `MediatorJob`)
Background tasks are modeled as strongly-typed domain events implementing `IEvent : INotification` (`PostFundManagement.Domain.Abstractions`).
```mermaid
sequenceDiagram
autonumber
participant API as PostFundManagement.Api
participant Orchestrator as IJobOrchestrator
participant DB as PostgreSQL pfm-scheduler
participant Host as PostFundManagement.Jobs
participant QuartzThread as Quartz Execution Engine
participant Mediator as IMediator MediatorJob
participant Handler as Domain Event Handlers
Note over API,Orchestrator: 1. Event Scheduling (API Node)
API->>Orchestrator: SendAsync(notification) / ScheduleAsync(notification, cron)
Orchestrator->>DB: Stores JobDetail and Trigger with serialized Payload JSON
Note over DB,Host: 2. Distributed Acquisition (Jobs Node)
QuartzThread->>DB: Polls and acquires due trigger with row-level locks
QuartzThread->>Host: Dispatches to local thread pool
Host->>Mediator: Instantiates MediatorJob[TNotification]
Mediator->>Mediator: Deserializes payload from MergedJobDataMap
Mediator->>Handler: Publish(notification, cancellationToken)
Handler-->>Host: Execution complete and updates business data in pfm DB
Host->>DB: Releases trigger and commits execution state
```
1. **Scheduling via `IJobOrchestrator`**:
- `SendAsync<TNotification>(notification)`: Schedules a durable, one-time execution in the `onetime-jobs` group with `StartNow()` and `DisallowConcurrentExecution()`.
- `ScheduleAsync<TNotification>(notification, cronExpression)`: Configures a durable, cron-driven recurring job in the `scheduled-jobs` group with automatic misfire handling (`CronTriggerMisfireInstruction.IgnoreMisfires`).
- `InterruptAsync(eventName, correlationId)`: Allows runtime cancellation of active job executions across the cluster.
2. **Transparent Deserialization & Dispatch (`MediatorJob<TNotification>`)**:
`MediatorJob<TNotification>` resolves `IMediator` from DI, deserializes the JSON payload, initiates OpenTelemetry distributed tracing (`MediatorTelemetry.Source`), and publishes the event to all registered in-process handlers.
### 6.3 Recurring vs. One-Time Job Patterns
* **Recurring Jobs**: Scheduled by `JobsWorker` (`BackgroundService` in `PostFundManagement.Jobs`) during application startup. These jobs govern periodic health sweeps, SLA tracking, notification batching, and risk evaluations on predictable cron intervals.
* **One-Time Jobs**: Primarily scheduled by `PostFundManagement.Api` as immediate or deferred side-effects of HTTP requests and lifecycle mutations (e.g. user invites, award registration triggers). Furthermore, a running recurring job in `PostFundManagement.Jobs` can dispatch a one-time job (`jobOrchestrator.SendAsync`) when handling fan-out tasks or invoking decoupled activities that cannot be triggered directly by the originating job.
### 6.4 Core Background Processing Capabilities
The background job runner powers four essential operational workloads:
1. **Automated Project Monitoring & Reminders**:
- Sweeps active projects, contractual deliverables, and milestone deadlines.
- Computes planned vs. actual progress variance and escalates overdue milestones (`FR-M04-017`, `FR-M07-009`).
- Dispatches automated reminders to Grantees/Partners prior to milestone due dates and submission cut-offs.
2. **Risk Monitoring & Automated Notifications**:
- Evaluates project risk registers, Key Risk Indicators (KRIs), and issue escalation thresholds (`FR-M09-001` - `011`).
- Detects risk score creep (Likelihood $\times$ Impact) and triggers automated alerts when risk tolerance thresholds are breached.
- Monitors overdue mitigation action plans and escalates unresolved high-severity risks to Programme Managers.
3. **Email Notification Processing**:
- Continuously consumes and batches pending notification records (`Platform.Email` / `NotificationPlatform.Email`).
- Dispatches transactional emails via MailKit / MimeKit SMTP with exponential backoff and retry policies.
- Updates delivery status (`Sent`, `Failed`, retry count) and logs dispatch telemetry for audit compliance (`FR-M25-001`).
4. **Platform Extensibility**:
- New background jobs and recurring routines can be introduced by creating an `IEvent` model in `PostFundManagement.Application` and scheduling it via `JobsWorker` or `IJobOrchestrator`, with zero impact on the API surface.
+1
View File
@@ -44,6 +44,7 @@
<Using Include="Microsoft.Extensions.Logging" /> <Using Include="Microsoft.Extensions.Logging" />
<Using Include="Microsoft.Extensions.Options" /> <Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.IdentityModel.Tokens" /> <Using Include="Microsoft.IdentityModel.Tokens" />
<Using Include="Microsoft.FeatureManagement" />
<!-- Domain Constants --> <!-- Domain Constants -->
<Using Static="true" Include="PostFundManagement.Domain.Extensions.Constants" /> <Using Static="true" Include="PostFundManagement.Domain.Extensions.Constants" />
+56 -55
View File
@@ -12,67 +12,68 @@
<PackageVersion Include="Scrutor" Version="7.0.0" /> <PackageVersion Include="Scrutor" Version="7.0.0" />
<!-- Microsoft Extensions & Configuration --> <!-- Microsoft Extensions & Configuration -->
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.10.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.11.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.10.0" /> <PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.11.0" />
<PackageVersion Include="Microsoft.Extensions.Validation" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Validation" Version="10.0.14" />
<PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="4.7.0" /> <PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="4.8.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.14" />
<!-- Security, Authentication & Identity --> <!-- Security, Authentication & Identity -->
<PackageVersion Include="AccessTokenClient.Extensions" Version="5.1.0" /> <PackageVersion Include="AccessTokenClient.Extensions" Version="5.1.0" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="4.2.0" /> <PackageVersion Include="Duende.AccessTokenManagement" Version="4.3.0" />
<PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="4.2.0" /> <PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="4.3.0" />
<PackageVersion Include="IdentityModel" Version="6.2.0" /> <PackageVersion Include="IdentityModel" Version="6.2.0" />
<PackageVersion Include="IdentityModel.AspNetCore" Version="4.3.0" /> <PackageVersion Include="IdentityModel.AspNetCore" Version="4.3.0" />
<PackageVersion Include="IdentityModel.AspNetCore.OAuth2introspection" Version="6.2.0" /> <PackageVersion Include="IdentityModel.AspNetCore.OAuth2introspection" Version="6.2.0" />
<PackageVersion Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageVersion Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageVersion Include="KubernetesClient" Version="19.0.2" /> <PackageVersion Include="KubernetesClient" Version="21.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.14" />
<!-- Upgraded to latest non-vulnerable version --> <!-- Upgraded to latest non-vulnerable version -->
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.13" /> <PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.14" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.13" /> <PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.14" />
<!-- Resilience --> <!-- Resilience -->
<PackageVersion Include="Polly" Version="8.8.0" /> <PackageVersion Include="Polly" Version="8.8.1" />
<PackageVersion Include="Polly.Extensions" Version="8.8.0" /> <PackageVersion Include="Polly.Extensions" Version="8.8.1" />
<!-- API, Web & Documentation --> <!-- API, Web & Documentation -->
<PackageVersion Include="Asp.Versioning.Abstractions" Version="10.2.1" /> <PackageVersion Include="Asp.Versioning.Abstractions" Version="10.2.1" />
<PackageVersion Include="Asp.Versioning.Http" Version="10.2.3" /> <PackageVersion Include="Asp.Versioning.Http" Version="10.2.3" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" /> <PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" Version="10.0.14" />
<PackageVersion Include="Refit.HttpClientFactory" Version="15.2.0" /> <PackageVersion Include="Refit.HttpClientFactory" Version="16.4.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.17.4" /> <PackageVersion Include="Scalar.AspNetCore" Version="2.18.3" />
<!-- Database & Entity Framework Core --> <!-- Database & Entity Framework Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Analyzers" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Analyzers" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.12" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.14" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<!-- Quartz Scheduler --> <!-- Quartz Scheduler -->
<PackageVersion Include="Quartz" Version="4.1.0" /> <PackageVersion Include="Quartz" Version="4.6.2" />
<PackageVersion Include="Quartz.AspNetCore" Version="4.1.0" /> <PackageVersion Include="Quartz.AspNetCore" Version="4.6.2" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="4.1.0" /> <PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="4.6.2" />
<PackageVersion Include="Quartz.Plugins" Version="4.1.0" /> <PackageVersion Include="Quartz.Plugins" Version="4.6.2" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="4.1.0" /> <PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="4.6.2" />
<PackageVersion Include="Quartz.Serialization.SystemTextJson" Version="4.1.0" /> <PackageVersion Include="Quartz.Serialization.SystemTextJson" Version="4.6.2" />
<!-- Health Checks --> <!-- Health Checks -->
<PackageVersion Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" /> <PackageVersion Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
@@ -83,17 +84,17 @@
<PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" /> <PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<!-- Observability & OpenTelemetry --> <!-- Observability & OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.20.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.18.0" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.20.1" />
<!-- Email & Messaging --> <!-- Email & Messaging -->
<PackageVersion Include="MailKit" Version="4.18.0" /> <PackageVersion Include="MailKit" Version="4.18.2" />
<PackageVersion Include="MimeKit" Version="4.18.0" /> <PackageVersion Include="MimeKit" Version="4.18.2" />
<!-- CQRS & Mediator --> <!-- CQRS & Mediator -->
<PackageVersion Include="FluentResults" Version="4.0.0" /> <PackageVersion Include="FluentResults" Version="4.0.0" />
@@ -101,17 +102,17 @@
<PackageVersion Include="Mediator.SourceGenerator" Version="3.0.2" /> <PackageVersion Include="Mediator.SourceGenerator" Version="3.0.2" />
<!-- Cloud Storage --> <!-- Cloud Storage -->
<PackageVersion Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.101.4" /> <PackageVersion Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.102.7" />
<PackageVersion Include="AWSSDK.S3" Version="4.0.103.3" /> <PackageVersion Include="AWSSDK.S3" Version="4.0.104.7" />
<!-- Analyzers --> <!-- Analyzers -->
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.259" /> <PackageVersion Include="Meziantou.Analyzer" Version="3.0.317" />
<!-- Testing --> <!-- Testing -->
<PackageVersion Include="coverlet.collector" Version="6.0.4" /> <PackageVersion Include="coverlet.collector" Version="10.1.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" /> <PackageVersion Include="xunit.runner.visualstudio" Version="4.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+19 -1
View File
@@ -71,6 +71,7 @@ PostFundManagement/
├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL (Npgsql), Migrations, Data Protection ├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL (Npgsql), Migrations, Data Protection
├── PostFundManagement.Application/ # CQRS Handlers, Mediator, DTOs, Validations, S3, Email, Quartz ├── PostFundManagement.Application/ # CQRS Handlers, Mediator, DTOs, Validations, S3, Email, Quartz
├── PostFundManagement.Api/ # Minimal APIs / Carter endpoints, Auth, Swagger/Scalar, Versioning ├── PostFundManagement.Api/ # Minimal APIs / Carter endpoints, Auth, Swagger/Scalar, Versioning
├── PostFundManagement.Jobs/ # Dedicated Quartz.NET Background Worker Host (Recurring & One-Time Execution)
├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet) ├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet)
├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4) ├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4)
├── PostFundManagement.PartnerUI/ # External Grantee / Partner Portal (React 19 + Vite 8 + Tailwind 4) ├── PostFundManagement.PartnerUI/ # External Grantee / Partner Portal (React 19 + Vite 8 + Tailwind 4)
@@ -85,6 +86,7 @@ PostFundManagement/
| **`PostFundManagement.Infrastructure`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Duende.AccessTokenManagement` 4.2.0, EF Core Tools & Design | Houses `PostFundManagementDbContext`, EF Core fluent configurations, entity-to-table mappings, PostgreSQL schema definitions, multitenancy filters, and migrations. | | **`PostFundManagement.Infrastructure`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Duende.AccessTokenManagement` 4.2.0, EF Core Tools & Design | Houses `PostFundManagementDbContext`, EF Core fluent configurations, entity-to-table mappings, PostgreSQL schema definitions, multitenancy filters, and migrations. |
| **`PostFundManagement.Application`** | `net10.0` | `Mediator.SourceGenerator` / `Mediator.Abstractions` 3.0.2, `FluentResults` 4.0.0, `Microsoft.Extensions.Caching.Hybrid` 10.10.0, `Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12, `Refit` 15.2.0, `Duende.AccessTokenManagement.OpenIdConnect` 4.2.0, `Quartz` 4.1.0, `AWSSDK.S3` | Implements CQRS command/query handlers, domain validators, S3 object storage clients (Garage S3), background jobs (Quartz), notification dispatchers, and business logic. | | **`PostFundManagement.Application`** | `net10.0` | `Mediator.SourceGenerator` / `Mediator.Abstractions` 3.0.2, `FluentResults` 4.0.0, `Microsoft.Extensions.Caching.Hybrid` 10.10.0, `Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12, `Refit` 15.2.0, `Duende.AccessTokenManagement.OpenIdConnect` 4.2.0, `Quartz` 4.1.0, `AWSSDK.S3` | Implements CQRS command/query handlers, domain validators, S3 object storage clients (Garage S3), background jobs (Quartz), notification dispatchers, and business logic. |
| **`PostFundManagement.Api`** | `net10.0` | `Microsoft.AspNetCore.OpenApi` 10.0.12, `Scalar.AspNetCore` 2.17.4, `Asp.Versioning.Http` 10.2.3, `IdentityModel.AspNetCore` 4.3.0, `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.12, `AspNetCore.HealthChecks.NpgSql` 9.0.0, `AspNetCore.HealthChecks.UI` | Minimal API endpoint definitions divided by business slices (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Handles routing, auth policies, error problem details, and OpenAPI metadata. | | **`PostFundManagement.Api`** | `net10.0` | `Microsoft.AspNetCore.OpenApi` 10.0.12, `Scalar.AspNetCore` 2.17.4, `Asp.Versioning.Http` 10.2.3, `IdentityModel.AspNetCore` 4.3.0, `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.12, `AspNetCore.HealthChecks.NpgSql` 9.0.0, `AspNetCore.HealthChecks.UI` | Minimal API endpoint definitions divided by business slices (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Handles routing, auth policies, error problem details, and OpenAPI metadata. |
| **`PostFundManagement.Jobs`** | `net10.0` | `Quartz` 4.6.2, `Quartz.Extensions.DependencyInjection` 4.6.2, `Quartz.Serialization.SystemTextJson` 4.6.2, `Mediator.Abstractions` 3.0.2 | Dedicated background execution host. Executes recurring Quartz jobs (registered on startup by `JobsWorker`), processes asynchronous domain events (`IEvent`), and executes jobs concurrently (`MaxConcurrency = 5`). |
| **`PostFundManagement.Tests`** | `net10.0` | `xunit` 2.9.3, `xunit.runner.visualstudio` 3.1.4, `Microsoft.NET.Test.Sdk` 17.14.1, `coverlet.collector` 6.0.4 | Unit and integration test coverage for domain rules, CQRS handlers, financial calculation models, and gate condition validations. | | **`PostFundManagement.Tests`** | `net10.0` | `xunit` 2.9.3, `xunit.runner.visualstudio` 3.1.4, `Microsoft.NET.Test.Sdk` 17.14.1, `coverlet.collector` 6.0.4 | Unit and integration test coverage for domain rules, CQRS handlers, financial calculation models, and gate condition validations. |
### 2.2 Frontend Projects (React 19 / TypeScript) ### 2.2 Frontend Projects (React 19 / TypeScript)
@@ -120,6 +122,22 @@ PostFundManagement/
* **Authentication / STS**: Duende IdentityServer / Skoruba Administration (`https://sts.security.khongisa.co.za`). * **Authentication / STS**: Duende IdentityServer / Skoruba Administration (`https://sts.security.khongisa.co.za`).
* **Observability**: OpenTelemetry exporters streaming traces, metrics, and logs to Aspire Dashboard / OTLP collectors. * **Observability**: OpenTelemetry exporters streaming traces, metrics, and logs to Aspire Dashboard / OTLP collectors.
### 2.4 Distributed Background Processing Architecture (Quartz.NET & PostFundManagement.Jobs)
* **Dedicated Execution Daemon**: `PostFundManagement.Jobs` is a standalone background host responsible for recurring job scheduling, lifecycle sweeps, and asynchronous event execution.
* **API Isolation (Scheduler Client)**: `PostFundManagement.Api` is strictly configured as a scheduler client via `AddQuartzSchedulerClient(...)` using `ZeroSizeThreadPool` (0 worker threads) and running in standby (`SchedulerStatus.Created`). The API enqueues jobs and triggers into the clustered PostgreSQL database (`pfm-scheduler`) but never acquires triggers or executes jobs locally.
* **Worker Execution Engine**: `PostFundManagement.Jobs` configures full Quartz execution via `AddQuartzScheduler(...)` using `DefaultThreadPool` (`MaxConcurrency = 5`) and `AddQuartzHostedService(options => options.WaitForJobsToComplete = true)`.
* **Event-Driven Orchestration (`IEvent` & `MediatorJob`)**:
- The API acts as the primary source of one-time events triggered by user actions and lifecycle transitions (implementing `IEvent : INotification`).
- Events are enqueued via `IJobOrchestrator.SendAsync` or `ScheduleAsync` and persisted in `pfm-scheduler` as `MediatorJob<TNotification>` with serialized JSON payloads.
- When acquired by `PostFundManagement.Jobs`, `MediatorJob` deserializes the event payload, propagates OpenTelemetry distributed traces, and publishes the event via `IMediator.Publish(...)`.
* **Startup Recurring Registration**: On daemon startup, `JobsWorker : BackgroundService` initializes and registers recurring cron jobs across tenant portfolios.
* **Cascading One-Time Execution**: If a recurring job identifies complex follow-up items that cannot be processed in the recurring execution window, it dispatches decoupled one-time jobs (`jobOrchestrator.SendAsync`) across worker threads.
* **Core Job Workloads**:
1. *Automated Project Monitoring & Reminders*: Sweeps milestones, checks contractual SLA deadlines, sets overdue flags (`FR-M07-009`), and sends reminder notices to Grantees/Partners.
2. *Risk Monitoring & Automated Notifications*: Evaluates risk score creep ($\text{Likelihood} \times \text{Impact}$), flags tolerance breaches, and sends escalation alerts for overdue mitigation actions.
3. *Email Notification Processing*: Batches and dispatches pending notification entries (`Platform.Email` / `NotificationPlatform.Email`) via MailKit/MimeKit SMTP and records delivery telemetry (`FR-M25-001`).
4. *Future Jobs*: Seamlessly supports future scheduled workloads (e.g. drawdown reconciliations, compliance expiries, portfolio aggregations) simply by adding `IEvent` handlers.
--- ---
## 3. Master Business Rules (BR-001 to BR-010) ## 3. Master Business Rules (BR-001 to BR-010)
@@ -848,7 +866,7 @@ The platform uses **Hashids** to obfuscate internal sequential database primary
* **Implementation**: Managed by `HashService` (`PostFundManagement.Application/Services/Shared/HashService.cs`) using the `Hashids.net` (v1.7.0) library. * **Implementation**: Managed by `HashService` (`PostFundManagement.Application/Services/Shared/HashService.cs`) using the `Hashids.net` (v1.7.0) library.
* **Shared Configuration (`HasherSettings`)**: * **Shared Configuration (`HasherSettings`)**:
* **Salt**: Sourced from configuration (`HasherSettings:Salt` or `HASHER_SALT`), defaulting to: * **Salt**: Sourced from configuration (`HasherSettings:Salt` or `HASHER_SALT`), defaulting to:
`"TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ=="` `"salt-random-string"`
* **Min Hash Length**: `HasherSettings:MinHashLength` or `HASHER_MIN_HASH_LENGTH` (default: `11`). * **Min Hash Length**: `HasherSettings:MinHashLength` or `HASHER_MIN_HASH_LENGTH` (default: `11`).
* **Core Service Methods**: * **Core Service Methods**:
* `HashEncodeLongId(long id)` / `HashEncodeIntId(int id)`: Encodes integers into obfuscated HashID strings (e.g. `1` $\to$ `kY5aB9m...`). * `HashEncodeLongId(long id)` / `HashEncodeIntId(int id)`: Encodes integers into obfuscated HashID strings (e.g. `1` $\to$ `kY5aB9m...`).
+4 -12
View File
@@ -4,7 +4,6 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Health; using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator; using PostFundManagement.Domain.Mediator;
using PostFundManagement.Infrastructure.Database.Services; using PostFundManagement.Infrastructure.Database.Services;
using static PostFundManagement.Domain.Extensions.Constants;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -45,7 +44,7 @@ builder.Services.AddSkorubaApiService(builder.Configuration);
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>)); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>)); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration); builder.Services.AddQuartzSchedulerClient(DefaultSchedulerName, builder.Configuration);
builder.Services.AddEmailServices(builder.Configuration); builder.Services.AddEmailServices(builder.Configuration);
builder.Services.AddHashServices(builder.Configuration); builder.Services.AddHashServices(builder.Configuration);
builder.Services.AddGarageS3(builder.Configuration); builder.Services.AddGarageS3(builder.Configuration);
@@ -119,7 +118,9 @@ builder.Services.AddCors(options =>
policy.SetIsOriginAllowed(origin => policy.SetIsOriginAllowed(origin =>
{ {
if (string.IsNullOrWhiteSpace(origin)) return false; if (string.IsNullOrWhiteSpace(origin)) return false;
var clean = origin.TrimEnd('/'); var clean = origin.TrimEnd('/');
if (allowedOrigins.Any(a => string.Equals(a.TrimEnd('/'), clean, StringComparison.OrdinalIgnoreCase))) if (allowedOrigins.Any(a => string.Equals(a.TrimEnd('/'), clean, StringComparison.OrdinalIgnoreCase)))
return true; return true;
@@ -133,10 +134,7 @@ builder.Services.AddCors(options =>
return true; return true;
} }
} }
catch catch { /* eat all exceptions */ }
{
// ignore parse failure
}
return false; return false;
}) })
@@ -148,12 +146,6 @@ builder.Services.AddCors(options =>
var app = builder.Build(); var app = builder.Build();
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();
if (scheduler!.Status != SchedulerStatus.Running)
await scheduler.Start();
app.UseCors(CorsPolicy); app.UseCors(CorsPolicy);
app.UseHsts(); app.UseHsts();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
-7
View File
@@ -32,13 +32,6 @@
"Microsoft.EntityFrameworkCore.Update": "Error" "Microsoft.EntityFrameworkCore.Update": "Error"
} }
}, },
"EmailProcessorCron": "0 */5 * * * ?",
"Email": {
"SmtpServer": "smtp.example.com",
"SmtpPort": 587,
"SenderEmail": "noreply@example.com",
"SenderName": "Example"
},
"HasherSettings": { "HasherSettings": {
"MinHashLength": 11 "MinHashLength": 11
}, },
@@ -20,7 +20,7 @@ public static class Quartz
services.AddQuartz(config => services.AddQuartz(config =>
{ {
config.UseSimpleTypeLoader(); config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); config.UseThreadPool<global::Quartz.Impl.ZeroSizeThreadPool>();
config.UseTimeZoneConverter(); config.UseTimeZoneConverter();
config.UsePersistentStore(storage => config.UsePersistentStore(storage =>
@@ -1,9 +1,3 @@
using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using Castle.DynamicProxy;
using FluentResults;
using Microsoft.AspNetCore.Http;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Attributes; using PostFundManagement.Domain.Attributes;
@@ -9,4 +9,6 @@ public sealed class SmtpSettings
public string? Host { get; set; } public string? Host { get; set; }
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
public string? ProcessorJobCron { get; set; }
} }
@@ -11,8 +11,8 @@ public sealed class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHea
if (scheduler == null) if (scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{DefaultSchedulerName}' not found."); return HealthCheckResult.Unhealthy($"Scheduler with name '{DefaultSchedulerName}' not found.");
if (scheduler.Status != SchedulerStatus.Running) if (scheduler.Status is not (SchedulerStatus.Running or SchedulerStatus.Standby or SchedulerStatus.Created))
return HealthCheckResult.Unhealthy($"{DefaultSchedulerName} Quartz scheduler is not running"); return HealthCheckResult.Unhealthy($"{DefaultSchedulerName} Quartz scheduler is not available (Status: {scheduler.Status})");
await scheduler.Exists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); await scheduler.Exists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
@@ -0,0 +1,16 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Infrastructure.Database.Services;
public sealed class HeadlessTenantContextService : ITenantContextService
{
public string? TenantName => "Ifri Technologies";
public long TenantId => 1;
public bool ByPassTenantQueryFilter { get; set; } = true;
public bool IsAdmin => true;
public bool HasTenantFilter => true;
}
+40
View File
@@ -0,0 +1,40 @@
# Stage 1: Runtime base
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
# Stage 2: Build & Restore
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy props and csproj files for layer caching
COPY ["Directory.Build.props", "./"]
COPY ["Directory.Packages.props", "./"]
COPY ["PostFundManagement.snk", "./"]
COPY ["LICENSE", "./"]
COPY ["PostFundManagement.Jobs/PostFundManagement.Jobs.csproj", "PostFundManagement.Jobs/"]
COPY ["PostFundManagement.Application/PostFundManagement.Application.csproj", "PostFundManagement.Application/"]
COPY ["PostFundManagement.Domain/PostFundManagement.Domain.csproj", "PostFundManagement.Domain/"]
COPY ["PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj", "PostFundManagement.Infrastructure/"]
# Restore packages
RUN dotnet restore "PostFundManagement.Jobs/PostFundManagement.Jobs.csproj"
# Copy full source code
COPY . .
# Build from the project folder using relative filename
WORKDIR "/src/PostFundManagement.Jobs"
RUN dotnet build "PostFundManagement.Jobs.csproj" -c Release -o /app/build
# Stage 3: Publish
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "PostFundManagement.Jobs.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 4: Final Runtime
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PostFundManagement.Jobs.dll"]
+21
View File
@@ -0,0 +1,21 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Jobs;
public class JobsWorker(IJobOrchestrator jobOrchestrator, IFeatureManager featureManager,
ILogger<JobsWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if(!await featureManager.IsEnabledAsync(nameof(JobsWorker)))
{
logger.LogInformation("JobsWorker is disabled.");
return;
}
// TODO: scheduler reccuring jobs
logger.LogInformation("JobsWorker is stopping.");
}
}
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-PostFundManagement.Jobs-97b98845-fddb-4212-b877-8dac9f7f86e3</UserSecretsId>
</PropertyGroup>
<!-- File Nesting -->
<ItemGroup>
<ProjectCapability Include="ConfigurableFileNesting" />
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
</ItemGroup>
<!-- CQRS Source Generator -->
<ItemGroup>
<PackageReference Include="Mediator.SourceGenerator">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PostFundManagement.Application\PostFundManagement.Application.csproj" />
</ItemGroup>
</Project>
+40
View File
@@ -0,0 +1,40 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Mediator;
using PostFundManagement.Infrastructure.Database.Services;
using PostFundManagement.Jobs;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddFeatureManagement();
builder.Services.AddSharedServices();
builder.Services.AddHostedService<JobsWorker>();
builder.Services.AddRegisterServicesWithCaching(builder.Configuration);
builder.Services.AddEndpointAndServiceCaching(builder.Configuration);
builder.Services.AddTransient<ITenantContextService, HeadlessTenantContextService>();
builder.Services.AddSkorubaApiService(builder.Configuration);
builder.Services.AddHttpContextAccessor();
builder.Services.AddMediator();
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration);
builder.Services.AddEmailServices(builder.Configuration);
builder.Services.AddHashServices(builder.Configuration);
builder.Services.AddGarageS3(builder.Configuration);
builder.Services.AddHttpClient();
builder.Services.AddDataProtectionDatabase(builder.Configuration);
builder.Services.AddApplicationDbContext(builder.Configuration);
var host = builder.Build();
var schedulerFactory = host.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();
if (scheduler!.Status != SchedulerStatus.Running)
await scheduler.Start();
host.Run();
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"PostFundManagement.Jobs": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
+41
View File
@@ -0,0 +1,41 @@
{
"FeatureManagement": {
"JobsWorker": true,
"EmailProcessorJobEnabled": true,
"ServiceCaching": false
},
"GeneralS3Settings": {
"ServiceUrl": "https://s3.garage.khongisa.co.za",
"Region": "garage",
"BucketName": "pfm",
"CdnBaseUrl": "https://pfm.cdn.khongisa.co.za"
},
"MachineIdentitySettings": {
"Authority": "https://sts.security.khongisa.co.za",
"Scope": "skoruba_identity_admin_api"
},
"Monitoring": {
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
"ServiceName": "Pfm.Jobs.DEV"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Quartz": "Error",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database": "Error",
"Microsoft.EntityFrameworkCore.Update": "Error"
}
},
"Email": {
"ProcessorJobCron": "0 */5 * * * ?",
"SmtpServer": "smtp.example.com",
"SmtpPort": 587,
"SenderEmail": "noreply@example.com",
"SenderName": "Example"
},
"HasherSettings": {
"MinHashLength": 11
},
"AllowedHosts": "*"
}
+12 -5
View File
@@ -1,4 +1,13 @@
<Solution> <Solution>
<Folder Name="/Presentation/">
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" />
<Project Path="PostFundManagement.Jobs/PostFundManagement.Jobs.csproj" Id="81281806-b5ca-4784-87af-3e8feeac988c" />
</Folder>
<Folder Name="/Shared/">
<Project Path="PostFundManagement.Application/PostFundManagement.Application.csproj" Id="750c7754-53a7-4573-abed-5c436246a04d" />
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" />
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
</Folder>
<Folder Name="/Solution Items/"> <Folder Name="/Solution Items/">
<File Path=".editorconfig" /> <File Path=".editorconfig" />
<File Path=".gitignore" /> <File Path=".gitignore" />
@@ -9,9 +18,7 @@
<File Path="PostFundManagement.snk" /> <File Path="PostFundManagement.snk" />
<File Path="README.md" /> <File Path="README.md" />
</Folder> </Folder>
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" /> <Folder Name="/Tests/">
<Project Path="PostFundManagement.Application/PostFundManagement.Application.csproj" Id="750c7754-53a7-4573-abed-5c436246a04d" /> <Project Path="PostFundManagement.Tests/PostFundManagement.Tests.csproj" Id="ce650d27-8a68-436d-b5c1-db25f2286e85" />
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" /> </Folder>
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
<Project Path="PostFundManagement.Tests/PostFundManagement.Tests.csproj" Id="ce650d27-8a68-436d-b5c1-db25f2286e85" />
</Solution> </Solution>
+87 -7
View File
@@ -12,7 +12,7 @@ The PFM platform governs the lifecycle of grant, development, innovation, and im
┌────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────┐
│ POST-FUND LIFECYCLE │ │ POST-FUND LIFECYCLE │
│ │ │ │
│ [1. Award Intake] ──> [2. Contracting] ──> [3. Onboarding] │ │ [1. Award Intake] ──> [2. Contracting] ──> [3. Onboarding] │
│ External Approval Draft Agreement KYC & Banking Gate │ │ External Approval Draft Agreement KYC & Banking Gate │
│ Recorded (BR-001) Auto-created (BR-002) Required for Disb. (BR-003) │ │ Recorded (BR-001) Auto-created (BR-002) Required for Disb. (BR-003) │
│ │ │ │ │ │
@@ -88,6 +88,16 @@ The PFM platform governs the lifecycle of grant, development, innovation, and im
4. Resolution: `Approved & Enacted` or `Rejected`. 4. Resolution: `Approved & Enacted` or `Rejected`.
5. **Maker-Checker Rule**: Change request initiator cannot approve or enact the change. 5. **Maker-Checker Rule**: Change request initiator cannot approve or enact the change.
### 2.7 Process 7: Automated Background Workflows & Scheduled Jobs (PostFundManagement.Jobs)
* **Pre-condition**: Background jobs host (`PostFundManagement.Jobs`) active and connected to `pfm-scheduler`.
* **Actors**: System Engine, `JobsWorker` Daemon, Domain Event Handlers.
* **Workflow**:
1. **Event Triggering**: API operations emit strongly typed events (`IEvent`) and enqueue them into `pfm-scheduler` via `IJobOrchestrator`.
2. **Cron Schedule Execution**: `JobsWorker` initializes recurring schedules on daemon startup for periodic monitoring, risk assessments, and communication batches.
3. **Job Execution**: Quartz cluster dispatches acquired triggers to `MediatorJob<TNotification>` within `PostFundManagement.Jobs` (concurrency limit = 5).
4. **Mediated Processing**: Event payload is deserialized and published via `IMediator` to execute domain logic (project monitoring, risk threshold checks, email dispatch).
5. **Cascading One-Time Jobs**: When a recurring job identifies an action item requiring separate asynchronous execution, it triggers a decoupled one-time job (`jobOrchestrator.SendAsync`).
--- ---
## 3. The Maker-Checker Operational Policy ## 3. The Maker-Checker Operational Policy
@@ -139,20 +149,20 @@ To ensure that caching never impedes record lifecycle updates or displays stale
sequenceDiagram sequenceDiagram
autonumber autonumber
actor User as User / Maker actor User as User / Maker
participant UI as ConsoleUI (React Query) participant UI as ConsoleUI React Query
participant Client as Axios HttpClient participant Client as Axios HttpClient
participant OutputCache as Backend OutputCache (Redis/Memory) participant OutputCache as Backend OutputCache Redis/Memory
participant DB as PostgreSQL Database participant DB as PostgreSQL Database
User->>UI: Submits Record Mutation (Create / Update / Approve) User->>UI: Submits Record Mutation Create/Update/Approve
UI->>Client: Dispatches POST / PUT / PATCH / DELETE UI->>Client: Dispatches POST / PUT / PATCH / DELETE
Client->>DB: Executes Mutation in Database Client->>DB: Executes Mutation in Database
DB-->>Client: Returns 200 OK / 201 Created DB-->>Client: Returns 200 OK / 201 Created
Client->>Client: Interceptor detects mutation: bumps activeMutationEpoch Client->>Client: Interceptor detects mutation: bumps activeMutationEpoch
UI->>UI: onSuccess triggers queryClient.invalidateQueries() UI->>UI: onSuccess triggers invalidateQueries
UI->>Client: Refetches active queries with _v = activeMutationEpoch UI->>Client: Refetches active queries with _v = activeMutationEpoch
Client->>OutputCache: GET /v1/api/*?_v=<newEpoch> Client->>OutputCache: GET /v1/api/* with new _v epoch
Note over OutputCache: Cache Miss (new _v key never seen before) Note over OutputCache: Cache Miss: new _v key never seen before
OutputCache->>DB: Queries PostgreSQL for fresh data OutputCache->>DB: Queries PostgreSQL for fresh data
DB-->>OutputCache: Returns updated entities DB-->>OutputCache: Returns updated entities
OutputCache-->>UI: Caches fresh data under _v and returns to UI OutputCache-->>UI: Caches fresh data under _v and returns to UI
@@ -164,4 +174,74 @@ sequenceDiagram
2. **Deterministic Invalidation**: Calling standard `queryClient.invalidateQueries({ queryKey: [...] })` or `invalidateAndBustCache(queryClient, filters)` guarantees that the subsequent refetch bypasses stale ASP.NET Core OutputCache entries. 2. **Deterministic Invalidation**: Calling standard `queryClient.invalidateQueries({ queryKey: [...] })` or `invalidateAndBustCache(queryClient, filters)` guarantees that the subsequent refetch bypasses stale ASP.NET Core OutputCache entries.
3. **Optimized Read Cache**: When users navigate across unchanged views or switch tabs, identical `_v` tokens are reused, allowing backend OutputCache and React Query in-memory caches to fulfill requests in < 5ms without unnecessary database hits. 3. **Optimized Read Cache**: When users navigate across unchanged views or switch tabs, identical `_v` tokens are reused, allowing backend OutputCache and React Query in-memory caches to fulfill requests in < 5ms without unnecessary database hits.
---
## 6. Background Job Lifecycle & Event Execution Workflows
The dedicated worker host (`PostFundManagement.Jobs`) operates as a decoupled daemon executing recurring maintenance routines and asynchronous domain events dispatched from the API or internally from other jobs.
### 6.1 Event Ingestion & Job Persistence Flow
```mermaid
flowchart TD
subgraph Producer ["PostFundManagement.Api (Event Source)"]
A["User Action / Lifecycle Transition"] --> B["Domain Command / Mutation"]
B --> C["Instantiate IEvent (e.g. SyncUserEvent, ProjectReminderEvent)"]
C --> D["IJobOrchestrator.SendAsync() or ScheduleAsync()"]
end
subgraph Store ["PostgreSQL Clustered Store (pfm-scheduler)"]
D --> E[("QRTZ_JOB_DETAILS and QRTZ_TRIGGERS")]
end
subgraph Consumer ["PostFundManagement.Jobs (Execution Host)"]
E -->|"Acquire Trigger Lock"| F["Quartz Scheduler Engine (MaxConcurrency = 5)"]
F --> G["MediatorJob[TNotification]"]
G --> H["Deserialize JSON Payload from JobDataMap"]
H --> I["IMediator.Publish(notification)"]
I --> J["Domain Event Handler: Business Logic in pfm DB"]
J -.->|"Cascading Sub-Task Needed"| K["IJobOrchestrator.SendAsync: Dispatch One-Time Job"]
K -.-> E
end
```
### 6.2 Standard Background Processes
#### 1. Automated Project Monitoring & Reminders Process
* **Trigger**: Scheduled cron job registered on `JobsWorker` startup.
* **Operational Cycle**:
1. Job queries active projects and non-completed milestones across tenant partitions.
2. Compares milestone contractual due dates against current UTC time:
- **T - 14 / T - 7 / T - 1 Days**: Emits reminder notification events to Grantee/Partner project managers.
- **T + 0 Days (Overdue)**: Automatically flags the milestone as `Overdue` (`FR-M07-009`) and updates project RAG status to `At Risk` (`FR-M04-017`).
3. Escalates overdue notifications to internal Programme Managers for intervention.
#### 2. Risk Monitoring & Automated Escalation Process
* **Trigger**: Periodic recurring schedule.
* **Operational Cycle**:
1. Queries all active project risk registers and Key Risk Indicators (KRIs) (`FR-M09-001` - `011`).
2. Evaluates current severity score ($\text{Risk Score} = \text{Likelihood} \times \text{Impact}$).
3. If risk score breaches the configured programme tolerance threshold:
- Automatically logs a risk escalation event.
- Dispatches alerts to Risk Managers and Portfolio Executives.
4. Sweeps assigned risk mitigation actions; identifies overdue mitigation plans and emits reminders to responsible assignees.
#### 3. Email Notification Processing Workflow (`Platform.Email`)
* **Trigger**: High-frequency recurring polling job or deferred one-time dispatch.
* **Operational Cycle**:
1. Queries pending notifications from the transactional database where `Platform == NotificationPlatform.Email` and `Status == NotificationStatus.Pending` or `Queued`.
2. Resolves tenant context using `HeadlessTenantContextService`.
3. Formats email templates with recipient credentials and branding.
4. Transmits messages via MailKit/MimeKit SMTP engine with rate-limiting and connection pooling.
5. On successful delivery: Updates record status to `Sent`, stamps `DeliveredAt` timestamp, and logs dispatch telemetry (`FR-M25-001`).
6. On failure: Increments retry counter; if retry limit exceeded, marks as `Failed` with diagnostic error trace.
### 6.3 Recurring-to-One-Time Job Cascading Pattern
When a recurring job evaluates a broad batch of records (e.g., scanning thousands of project indicators or verifying compliance expirations), it must not execute long-running operations synchronously in the recurring job thread:
1. The recurring job identifies items requiring heavy processing (e.g. generating an executive PDF digest or calling external webhooks).
2. For each identified item, it enqueues an isolated one-time job via `IJobOrchestrator.SendAsync(specificEvent)`.
3. This releases the recurring job quickly, distributes execution across the 5 available worker threads in `PostFundManagement.Jobs`, and guarantees that a single failed sub-task can be retried independently without stalling the entire recurring cycle.
+23 -10
View File
@@ -41,6 +41,16 @@ The application follows a modular, hierarchical architecture where high-level fu
### **5. Notifications & Platform Services** ### **5. Notifications & Platform Services**
* Multi-channel delivery engine (`NotificationPlatform`) tracking candidate invitations, reminders, and status updates (`NotificationStatus`) across Email, SMS, WhatsApp, In-App, and Webhooks. * Multi-channel delivery engine (`NotificationPlatform`) tracking candidate invitations, reminders, and status updates (`NotificationStatus`) across Email, SMS, WhatsApp, In-App, and Webhooks.
### **6. Background Processing & Distributed Jobs Engine**
* **`PostFundManagement.Jobs`**: Dedicated background execution host powered by **Quartz.NET (v4.6+)** with a PostgreSQL clustered JobStore (`pfm-scheduler`).
* **Decoupled Architecture**: `PostFundManagement.Api` serves strictly as a scheduler client (`ZeroSizeThreadPool`), dispatching events into the cluster while leaving execution exclusively to `PostFundManagement.Jobs` (`MaxConcurrency = 5`).
* **Event-Driven Execution**: Domain events implementing `IEvent : INotification` are mediated and serialized into `MediatorJob<TNotification>`.
* **Core Automated Operations**:
- **Automated Project Monitoring & Reminders**: Tracks milestones, evaluates planned vs actual deadlines, flags overdue statuses, and emits reminders to Grantees/Partners.
- **Risk Monitoring & Automated Notifications**: Evaluates risk registers, tracks Key Risk Indicators (KRIs), flags score creep, and escalates overdue mitigation actions.
- **Email Processing**: Asynchronously processes `NotificationPlatform.Email` records via MailKit/MimeKit SMTP.
- **Recurring & Cascading Jobs**: `JobsWorker` registers recurring cron schedules on startup and can trigger isolated one-time jobs when sub-activities cannot be executed directly by another job.
--- ---
## 🧩 Key System Enums ## 🧩 Key System Enums
@@ -68,6 +78,7 @@ The application follows a modular, hierarchical architecture where high-level fu
* **Object Storage**: S3-compatible storage (Garage S3 / AWS S3) * **Object Storage**: S3-compatible storage (Garage S3 / AWS S3)
* **Caching & OutputCache Invalidation**: Redis StackExchange + ASP.NET Core `OutputCaching` (2-minute base policy) coupled with TanStack Query v5 and Axios client-side mutation versioning (`_v` parameter) for instantaneous reads and guaranteed zero-staleness on mutations * **Caching & OutputCache Invalidation**: Redis StackExchange + ASP.NET Core `OutputCaching` (2-minute base policy) coupled with TanStack Query v5 and Axios client-side mutation versioning (`_v` parameter) for instantaneous reads and guaranteed zero-staleness on mutations
* **Telemetry**: OpenTelemetry Metrics, Traces & Logs with Aspire / OTLP * **Telemetry**: OpenTelemetry Metrics, Traces & Logs with Aspire / OTLP
* **Distributed Background Jobs**: Quartz.NET 4.6+ with clustered PostgreSQL JobStore (`pfm-scheduler`), separating the API (Scheduler Client with `ZeroSizeThreadPool`) from `PostFundManagement.Jobs` (Worker Execution Daemon with `MaxConcurrency = 5`)
--- ---
@@ -105,16 +116,18 @@ For in-depth architectural and operational specifications, see:
```text ```text
PostFundManagement/ PostFundManagement/
├── PostFundManagement.Domain/ # Core Domain Entities, Enums & Interfaces ├── PostFundManagement.Domain/ # Core Domain Entities, Enums, Value Objects & Abstractions (IEvent)
├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL, Skoruba STS, Migrations ├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL (Npgsql), Migrations, Data Protection
├── PostFundManagement.Application/ # CQRS Handlers, DTOs, Business Rules & Validations ├── PostFundManagement.Application/ # CQRS Handlers, Mediator, DTOs, Validations, S3, Email, Job Orchestrator
├── PostFundManagement.Api/ # REST Endpoints, Middleware, Auth & Swagger/Scalar ├── PostFundManagement.Api/ # ASP.NET Core Minimal APIs / Carter, Auth (OIDC), Swagger/Scalar, Versioning
├── PostFundManagement.Tests/ # Unit & Integration Test Suite ├── PostFundManagement.Jobs/ # Dedicated Quartz.NET Background Worker Host (Recurring & Event-Driven Jobs)
├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite) ├── PostFundManagement.Tests/ # Unit & Integration Test Suite (xUnit, Coverlet)
├── PostFundManagement.PartnerUI/ # External Grantee / Recipient Portal (React 19 + Vite) ├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite + Tailwind 4)
├── Architecture.md # System Architecture, RBAC & Maker-Checker Spec ├── PostFundManagement.PartnerUI/ # External Grantee / Recipient Portal (React 19 + Vite + WebGL OGL)
├── Processes.md # Operational Grant Lifecycle & Process Flows ├── Architecture.md # System Architecture, Security, RBAC, Caching & Quartz Spec
└── docker-compose.yml # Container orchestration ├── Processes.md # Operational Grant Lifecycle, Maker-Checker & Background Workflows
├── GEMINI.md # Master Technical & Architecture Roadmap (SRS v2.0 Alignment)
└── docker-compose.yml # Container orchestration for API and Frontends
``` ```
## 🚀 Getting Started ## 🚀 Getting Started
+58 -10
View File
@@ -1,4 +1,62 @@
services: services:
pfm-scheduler:
image: pfm-scheduler
build:
context: .
dockerfile: ./PostFundManagement.Jobs/Dockerfile
network: host
container_name: pfm-scheduler
environment:
# Service layer caching feature flag
- FeatureManagement__JobsWorker=${FEATURE_JOBS_WORKER}
- FeatureManagement__EmailProcessorJobEnabled=${FEATURE_EMAIL_PROCESSOR_JOB_ENABLED}
- FeatureManagement__ServiceCaching=${FEATURE_SERVICE_CACHING}
# Header forwarding overrides
- DOTNET_ENVIRONMENT=${ENVIRONMENT:-Development}
# Postgres Database Overrides
- ConnectionStrings__PfmDatabase=Server=${POSTGRES_HOST:-192.168.1.170};Port=${POSTGRES_PORT:-5432};Database=${POSTGRES_DB:-pfm};User Id=${POSTGRES_USER:-pfm-user};Password=${POSTGRES_PASSWORD:-};
- ConnectionStrings__PfmSchedulerDatabase=Server=${POSTGRES_HOST:-192.168.1.170};Port=${POSTGRES_PORT:-5432};Database=pfm-scheduler;User Id=${POSTGRES_USER:-pfm-user};Password=${POSTGRES_PASSWORD:-};
# Garage S3 Settings
- GeneralS3Settings__ServiceUrl=${GENERAL_S3_SERVICE_URL:-https://s3.garage.khongisa.co.za}
- GeneralS3Settings__Region=${GENERAL_S3_REGION:-garage}
- GeneralS3Settings__BucketName=${GENERAL_S3_BUCKET_NAME:-pfm}
- GeneralS3Settings__CdnBaseUrl=${GENERAL_S3_CDN_BASE_URL:-https://pfm.cdn.khongisa.co.za}
- GeneralS3Settings__AccessKey=${GENERAL_S3_ACCESS_KEY:-}
- GeneralS3Settings__SecretKey=${GENERAL_S3_SECRET_KEY:-}
# Skoruba Duende Administration Security Overrides
- ConnectionStrings__SkorubaApi=${SKORUBA_ADMIN_API:-https://api.security.khongisa.co.za/}
- MachineIdentitySettings__Authority=${ADMIN_SECURITY_AUTHORITY:-https://sts.security.khongisa.co.za}
- MachineIdentitySettings__Scope=${ADMIN_SECURITY_SCOPE:-}
- MachineIdentitySettings__ClientId=${ADMIN_SECURITY_CLIENT_ID:-}
- MachineIdentitySettings__ClientSecret=${ADMIN_SECURITY_CLIENT_SECRET:-}
# HashIds overrides
- HasherSettings__MinHashLength=${HASHER_MIN_HASH_LENGTH:-11}
- HasherSettings__Salt=${HASHER_SALT:-}
# Data Protection Overrides
- DataProtection__Certificate=${DATA_PROTECTION_CERTIFICATE:-}
- DataProtection__Password=${DATA_PROTECTION_CERTIFICATE_PASSWORD:-}
# Monitoring Overrides
- Monitoring__Address=${MONITORING_ADDRESS:-http://aspire-dashboard-service.aspire.svc.cluster.local:18889}
- Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.DEV}
- Monitoring__ApiKey=${MONITORING_API_KEY:-}
# Email Processor Overrides
- Email__ProcessorJobCron=${EMAIL_PROCESSOR_CRON:-0 */5 * * * ?}
- Email__Credentials__Password=${EMAIL_CREDENTIALS_PASSWORD:-}
- Email__Credentials__Username=${EMAIL_CREDENTIALS_USERNAME:-}
- Email__SmtpServer=${EMAIL_SMTP_SERVER:-}
- Email__SmtpPort=${EMAIL_SMTP_PORT:-587}
# Redis overrides
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}
restart: unless-stopped
pfm-api: pfm-api:
image: pfm-api image: pfm-api
build: build:
@@ -9,9 +67,6 @@ services:
ports: ports:
- "8075:8080" - "8075:8080"
environment: environment:
# Service layer caching feature flag
- EnableServiceCaching=false
# Header forwarding overrides # Header forwarding overrides
- ASPNETCORE_ENVIRONMENT=${ENVIRONMENT:-Development} - ASPNETCORE_ENVIRONMENT=${ENVIRONMENT:-Development}
- ForwardedHeadersConfiguration__Enabled=true - ForwardedHeadersConfiguration__Enabled=true
@@ -62,13 +117,6 @@ services:
- Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.DEV} - Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.DEV}
- Monitoring__ApiKey=${MONITORING_API_KEY:-} - Monitoring__ApiKey=${MONITORING_API_KEY:-}
# Email Processor Overrides
- EmailProcessorCron=${EMAIL_PROCESSOR_CRON:-}
- Email__Credentials__Password=${EMAIL_CREDENTIALS_PASSWORD:-}
- Email__Credentials__Username=${EMAIL_CREDENTIALS_USERNAME:-}
- Email__SmtpServer=${EMAIL_SMTP_SERVER:-}
- Email__SmtpPort=${EMAIL_SMTP_PORT:-587}
# Redis overrides # Redis overrides
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false} - ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}
restart: unless-stopped restart: unless-stopped