Added documentation, restructured solution

This commit is contained in:
2026-10-30 10:57:35 +02:00
parent 58f8006ab2
commit 4fa379f74e
29 changed files with 203 additions and 20 deletions
+163
View File
@@ -0,0 +1,163 @@
<mxfile host="Electron">
<diagram name="Page-1" id="lj056Mnt25KX5hBf4p9v">
<mxGraphModel dx="1063" dy="686" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="G6m14uD4gVQkRd8Mz5xj-42" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;movable=1;resizable=1;rotatable=1;deletable=1;editable=1;locked=0;connectable=1;" value="" vertex="1">
<mxGeometry height="700" width="952" x="340" y="130" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" value="&lt;b style=&quot;color: rgb(255, 255, 255);&quot;&gt;Persistance&lt;/b&gt;" vertex="1">
<mxGeometry height="70" width="650" x="475" y="730" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="PostgresSQL" vertex="1">
<mxGeometry height="45" width="120" x="500" y="742.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-3" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Redis" vertex="1">
<mxGeometry height="45" width="120" x="740" y="742.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-4" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Garage S3" vertex="1">
<mxGeometry height="45" width="120" x="980" y="742.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-5" parent="1" style="rounded=0;whiteSpace=wrap;html=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fillColor=#e51400;fontColor=#ffffff;strokeColor=#B20000;" value="&lt;span style=&quot;color: rgb(255, 255, 255);&quot;&gt;&lt;b&gt;Enterprise Integration Elements&lt;/b&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="70" width="650" x="475" y="580" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-7" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Skoruba Identity API" vertex="1">
<mxGeometry height="45" width="120" x="500" y="592.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-8" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Skoruba STS" vertex="1">
<mxGeometry height="45" width="120" x="658" y="592.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-10" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Email" vertex="1">
<mxGeometry height="45" width="120" x="815" y="592.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-55" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-11" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="G6m14uD4gVQkRd8Mz5xj-10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-56" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-11" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-57" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-11" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-58" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-11" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-11" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Quartz Scheduler" vertex="1">
<mxGeometry height="45" width="120" x="980" y="592.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-12" parent="1" style="rounded=0;whiteSpace=wrap;html=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fillColor=#fa6800;fontColor=#000000;strokeColor=#C73500;" value="&lt;b style=&quot;color: rgb(255, 255, 255);&quot;&gt;Integration &amp;amp; Services&lt;/b&gt;" vertex="1">
<mxGeometry height="70" width="650" x="475" y="440" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-51" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-13" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-52" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-13" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-53" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-13" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.538;entryY=0.073;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-61" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-13" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=0.333;entryY=0.167;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-62" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-13" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-13" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="PFM API" vertex="1">
<mxGeometry height="45" width="62" x="500" y="452.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-14" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;dashed=1;fillColor=none;fontColor=#000000;strokeColor=#000000;strokeWidth=2;" value="PFM MCP" vertex="1">
<mxGeometry height="45" width="72" x="580" y="452.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-15" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="" vertex="1">
<mxGeometry height="45" width="284" x="816" y="452.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-16" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Governance" vertex="1">
<mxGeometry height="20" width="80" x="830" y="465" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-17" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Evidence" vertex="1">
<mxGeometry height="20" width="62" x="918" y="465" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-18" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Grants" vertex="1">
<mxGeometry height="20" width="42" x="988" y="465" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-19" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Projects" vertex="1">
<mxGeometry height="20" width="52" x="1038" y="465" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-20" parent="1" style="rounded=0;whiteSpace=wrap;html=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" value="&lt;b&gt;Presentation&lt;/b&gt;" vertex="1">
<mxGeometry height="70" width="650" x="475" y="300" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-49" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-21" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-21" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="PFM Console (Web)" vertex="1">
<mxGeometry height="45" width="120" x="500" y="312.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-48" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-22" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;startArrow=classic;startFill=1;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-50" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-22" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-22" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="PFM Partner (Web)" vertex="1">
<mxGeometry height="45" width="120" x="640" y="312.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-23" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;dashed=1;fillColor=none;fontColor=#000000;strokeColor=#000000;strokeWidth=2;" value="PFM Console Mobile" vertex="1">
<mxGeometry height="45" width="130" x="830" y="312.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-24" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;dashed=1;fillColor=none;fontColor=#000000;strokeColor=#000000;strokeWidth=2;" value="PFM Partner Mobile" vertex="1">
<mxGeometry height="45" width="120" x="980" y="312.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-26" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Khongisa CDN" vertex="1">
<mxGeometry height="45" width="102" x="668" y="452.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-34" parent="1" style="rounded=0;whiteSpace=wrap;html=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" value="&lt;span style=&quot;color: rgb(255, 255, 255);&quot;&gt;&lt;b&gt;Consumers / Users&lt;/b&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="70" width="650" x="475" y="160" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-25" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="3rd Party AI" vertex="1">
<mxGeometry height="45" width="80" x="1020" y="172.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-37" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;dashed=1;fillColor=none;fontColor=#000000;strokeColor=#000000;strokeWidth=2;" value="PFM AI" vertex="1">
<mxGeometry height="45" width="62" x="950" y="172.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-46" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-38" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.33;exitY=0.996;exitDx=0;exitDy=0;entryX=0.333;entryY=0.167;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-21">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-38" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Private Funding Organisations" vertex="1">
<mxGeometry height="45" width="180" x="500" y="172.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-47" edge="1" parent="1" source="G6m14uD4gVQkRd8Mz5xj-39" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=1 4;" target="G6m14uD4gVQkRd8Mz5xj-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-39" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;" value="Funding Applicants" vertex="1">
<mxGeometry height="45" width="120" x="690" y="172.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-40" parent="1" style="rounded=1;whiteSpace=wrap;html=1;roundedPerimeter=1;dashed=1;fillColor=none;fontColor=#000000;strokeColor=#000000;strokeWidth=2;" value="Data Aggregators" vertex="1">
<mxGeometry height="45" width="110" x="820" y="172.5" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-41" edge="1" parent="1" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;rounded=0;fillColor=#1ba1e2;strokeColor=#006EAF;movable=0;resizable=0;rotatable=0;deletable=0;editable=0;locked=1;connectable=0;" value="">
<mxGeometry height="100" relative="1" width="100" as="geometry">
<mxPoint x="430" y="800" as="sourcePoint" />
<mxPoint x="430" y="160" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-44" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;rotation=-90;fontSize=15;movable=0;resizable=0;rotatable=0;deletable=0;editable=0;locked=1;connectable=0;" value="Interaction / Integration / Data Flow" vertex="1">
<mxGeometry height="30" width="450" x="170" y="465" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-63" edge="1" parent="1" style="endArrow=none;dashed=1;html=1;strokeWidth=2;rounded=0;" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="340" y="550" as="sourcePoint" />
<mxPoint x="1290" y="550" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-64" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Public" vertex="1">
<mxGeometry height="30" width="102" x="1190" y="510" as="geometry" />
</mxCell>
<mxCell id="G6m14uD4gVQkRd8Mz5xj-65" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Private" vertex="1">
<mxGeometry height="30" width="102" x="1190" y="570" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+342
View File
@@ -0,0 +1,342 @@
# PostFundManagement (PFM) Platform — Architectural Specification
> **Document Version**: 2.0
> **Target Runtimes**: .NET 10.0 (C# 13) | React 19 / TypeScript 5.8 / Vite
> **Scope**: System Architecture, Security Topology, RBAC Enforcement, and Maker-Checker Pattern
> **Technology Partner & Technical Authorship**: **Lite Charms (PTY) Ltd** is the official technology partner and author of every technical implementation, architecture, and maintenance of the Post-Fund Management (PFM) Platform. **Ifri Technologies** is an initiative and entity within **Lite Charms (PTY) Ltd**.
---
## 1. System Topology & Architectural Layers
The PFM platform is constructed using **Clean Architecture** and **Domain-Driven Design (DDD)** principles to guarantee strict decoupling of business rules, persistence concerns, and user interfaces.
```
┌────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION TIER │
│ PostFundManagement.ConsoleUI │ PostFundManagement.PartnerUI │
│ (Internal Staff & Governance) │ (External Recipient Portal) │
│ React 19 + TypeScript + Vite │ React 19 + WebGL (OGL) + Vite │
│ │ │
│ PostFundManagement.Mcp (External MCP Server Host - HTTP/SSE & Stdio) │
└───────────────────────────────────┬────────────────────────────────────┘
│ HTTPS / REST (v1/api/*) + OIDC PKCE
┌───────────────────────────────────▼────────────────────────────────────┐
│ API GATEWAY │
│ PostFundManagement.Api (ASP.NET Core Minimal APIs / Carter) │
│ Authentication: JWT Bearer + OpenTelemetry Observability │
│ Quartz Client: Scheduler-Only (ZeroSizeThreadPool / Enqueues Jobs) │
└───────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────────────▼────────────────────────────────────┐
│ APPLICATION LAYER │
│ PostFundManagement.Application │
│ CQRS (Mediator), Business Validators, Domain Services, Event Handlers │
└──────────────────┬─────────────────────────────────┬───────────────────┘
│ │
┌──────────────────▼──────────────────┐ ┌──────────▼───────────────────┐
│ DOMAIN LAYER │ │ INFRASTRUCTURE LAYER │
│ PostFundManagement.Domain │ │ PostFundManagement.Infrastr.│
│ Entities, Value Objects, Enums, │ │ PostgreSQL (EF Core 10), │
│ Domain Invariants, Audit Models │ │ Skoruba STS, S3 Garage, │
└─────────────────────────────────────┘ │ 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) │
└────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Security & Identity Architecture
Authentication and authorization are separated cleanly across system boundaries:
* **Security Token Service (STS)**: Duende IdentityServer / Skoruba Administration (`https://sts.security.khongisa.co.za`) issues cryptographically signed JWT access tokens using OpenID Connect (OIDC) with Proof Key for Code Exchange (PKCE).
* **Audience & Scopes**: Web clients request standard OIDC scopes (`openid`, `profile`, `roles`, `email`, `address`) along with the target API resource scope (`pfm-api-dev`).
* **Tenant Isolation**: Every authenticated principal carries a `tenant_id` claim. Multitenant query filters in EF Core enforce cross-tenant partition boundaries at the data access layer.
* **Partner Isolation**: Grantees and external recipients possess the `partner` role. The `ConsoleUI` enforces immediate lockout for `partner` roles, restricting external users exclusively to `PartnerUI`.
---
## 3. Role-Based Access Control (RBAC) Architecture
PFM implements a fine-grained, claim-based RBAC model where system permissions are decomposed into **15 operational domains** and **5 progressive action capabilities**.
### 3.1 Role Claims Inventory
| Claim Key | Operational Domain | Functional Scope |
| :--- | :--- | :--- |
| `dashboard` | Executive Dashboard | Macro portfolio KPIs, disbursed capital, and ISO 31000 risk heatmap |
| `governance` | Governance & Structure | Holding entities, funder portfolios, programmes, rules, and instruments |
| `projects_milestones` | Projects & Milestones | Work breakdown structure (WBS), contractual milestones, and activities |
| `grant_financials` | Grants & Financials | Statutory funding awards, budget lines, contracts, and disbursement tranches |
| `evidence_risks` | M&E, Evidence & Risks | Deliverable evidence verification queue, indicators, and risk registers |
| `beneficiaries` | Beneficiaries & Impact | Demographic targets, POPIA compliance items, and outcome deliverables |
| `change_management` | Change Management | Project issues, contractual change requests, and budget variations |
| `site_visits` | Site Visits | Physical field inspections, checklist observations, and inspector findings |
| `settings_invites` | Settings Invites | Onboarding invitations and candidate notifications dispatch |
| `settings_users` | Settings Users | User provisioning, role assignments, and account lockouts |
| `settings_auditlog` | Settings Audit Log | Immutable audit ledger and system telemetry records |
| `settings_notifications`| Settings Notifications | SMTP engines, notification channels, and dispatch telemetry |
| `tenant_self_service` | Tenant Self-Service | Tenant profile configuration and delegated sub-user management |
| `tenant_management` | Tenant Management | Multi-tenant directory, partition allocation, and solution ownership |
| `unrestricted` | System Administration | Root administrative bypass across all system partitions |
### 3.2 Claim Actions & Capability Hierarchy
Action capabilities within a claim domain follow a strict subset hierarchy:
$$\text{view} \subset \text{add} \subset \text{update} \quad \text{and} \quad \text{view} \subset \text{export}$$
* **`view`**: Read-only access to entity lists, detailed records, metrics, and progress scores.
* **`add`**: Can create new records, edit draft records, submit milestone evidence, and change workflow statuses *other than approval* (includes `view`).
* **`update`**: Authoritative governance capability. Can execute **approval** status transitions (`Approved`, `Active` contracts) on records (includes `view` and `add`).
* **`export`**: Can generate and download structured spreadsheets (`.xlsx`) and formal PDF reports (includes `view`).
* **`all`**: Comprehensive authority (`view`, `add`, `update`, `export`) over the claim domain.
### 3.3 Authoritative Role-to-Claims Matrix
| Role | Assigned Claim Domains | Standard Capabilities | Record Approval Authority (`update`) |
| :--- | :--- | :--- | :---: |
| **`Admin`** | `unrestricted` | `all` | **Yes (System-wide)** |
| **`Executive`** | `dashboard`, `governance`, `projects_milestones`, `grant_financials`, `evidence_risks`, `tenant_self_service` | `all` | **Yes** |
| **`Manager`** | `dashboard`, `projects_milestones`, `grant_financials`, `evidence_risks`, `beneficiaries`, `change_management`, `site_visits`, `settings_invites`, `settings_users`, `settings_auditlog`, `settings_notifications`, `tenant_self_service` | `all` | **Yes** |
| **`Supervisor`** | `dashboard`, `projects_milestones`, `grant_financials`, `evidence_risks`, `beneficiaries`, `change_management`, `site_visits`, `settings_invites`, `settings_users` | `view, add, update, export`<br>*(Dashboard: `view, export`)* | **Yes** |
| **`Contributor`** | `projects_milestones`, `grant_financials`, `evidence_risks`, `beneficiaries`, `change_management`, `site_visits`, `settings_invites` | **`view, add`** | ❌ **Strictly Prohibited** |
---
## 4. Maker-Checker Pattern Architecture
To eliminate fraud risk, self-dealing, and conflicts of interest, PFM enforces the **Maker-Checker Pattern** (Segregation of Duties / Dual Authorization).
### 4.1 Core Principle
> **The user who creates or drafts a record (the "Maker") is strictly prohibited from verifying or approving that record (the "Checker"), regardless of their administrative role or RBAC claim privileges.**
Even if an `Executive`, `Manager`, or `Supervisor` possesses the `update` claim action on a domain, they cannot approve a record if they are identified as the record's creator.
```mermaid
flowchart LR
A["Maker (User A)<br/>Creates Record"] --> B["Record Persisted<br/>with createdBy = User A"]
B --> C{"Checker Evaluation<br/>(Approver Login)"}
C -->|"Checker is User A"| D["Approval BLOCKED: Maker-Checker Violation"]
C -->|"Checker is User B with update claim"| E["Approval GRANTED: Status Approved"]
```
### 4.2 Record Attribution Model
Every operational domain entity immutably captures the creator identity:
* `createdBy`: Cryptographic user identifier (Guid / Subject ID) populated from the verified token claim `context.GetUserId()` upon creation.
* `approvedBy`: User identifier of the independent verifier populated upon successful transition to `Approved`.
### 4.3 Client-Side Enforcement Hooks & Helpers
In `PostFundManagement.ConsoleUI`, the Maker-Checker rule is evaluated dynamically via [`useRbac()`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.ConsoleUI/src/auth/rbac.ts):
* **`isMaker(createdBy)`**: Compares `createdBy` against current user `user.id`, `sub`, and `user.email`.
* **`canApproveRecord(claim, createdBy)`**: Evaluates:
$$\text{canApproveRecord} = \text{canApprove}(\text{claim}) \land \neg\text{isMaker}(\text{createdBy})$$
* **`getApprovalRestrictionReason(claim, createdBy)`**: Surfaces explanatory feedback:
- `"Requires update/approval permission"` if the claim lacks `update`.
- `"Maker-Checker policy: Record creators cannot approve their own records"` if `isMaker` is true.
### 4.4 UI Guard Implementations
1. **Status Dropdowns**: The `Approved` option in status `<select>` controls is disabled with contextual labels:
```tsx
<option value={ApprovalStatus.Approved} disabled={!canApproveRecord('projects_milestones', record.createdBy)}>
Approved {isMaker(record.createdBy) ? '(Maker-Checker: Cannot approve own record)' : ''}
</option>
```
2. **Action Buttons**: Fast-track "Approve" buttons (e.g. Budget Approval, Disbursement Authorisation, Evidence Review) are hidden or accompanied by a Maker-Checker indicator badge when viewed by the creator.
3. **Export Controls**: `<ExportButton>` enforces domain claim checks explicitly (`claim="grant_financials"`), rejecting unauthenticated exports.
---
## 5. Caching Strategy, Mutation Versioning & Performance Optimization
To achieve sub-50ms page transitions while guaranteeing data freshness across mutations in a distributed architecture, PFM employs a multi-tiered caching topology.
```
┌────────────────────────────────────────────────────────────────────────┐
│ REACT QUERY CACHE (In-Memory Browser Tier) │
│ staleTime: 2m (dynamic) | 5m (taxonomy) | gcTime: 10m │
└───────────────────────────────────┬────────────────────────────────────┘
│ Cache Miss / Background Refetch
┌───────────────────────────────────▼────────────────────────────────────┐
│ AXIOS CLIENT INTERCEPTOR & MUTATION VERSIONING (_v) │
│ Reads/Browsing: attaches active `_v` epoch token │
│ Mutations (POST/PUT/PATCH/DELETE): auto-bumps epoch -> new `_v` token │
└───────────────────────────────────┬────────────────────────────────────┘
│ HTTP GET /v1/api/*?_v=<epoch>
┌───────────────────────────────────▼────────────────────────────────────┐
│ ASP.NET CORE OUTPUTCACHE (Redis / Memory Tier) │
│ Base Policy: 2-minute expiration partitioned by URI + QueryString │
│ Hits same `_v` -> 2ms response | New `_v` -> DB query & cache refresh │
└────────────────────────────────────────────────────────────────────────┘
```
### 5.1 The OutputCache Stale Data Challenge
ASP.NET Core Minimal APIs in `PostFundManagement.Api` attach `.CacheOutput()` to GET endpoints, using a 2-minute base sliding expiration backed by Redis / Memory cache.
Because OutputCache keys are generated from the HTTP Method, Host, Path, and **Query String**, a naive client refetch after a mutation (e.g. `queryClient.invalidateQueries({ queryKey: ['projects'] })`) calls `GET /v1/api/projects`. OutputCache intercepts this identical URI, returning the pre-mutation response cached earlier. The updated project status or approved milestone does not reflect in the UI until the 2-minute cache expires.
### 5.2 Client-Side Mutation Epoch Versioning
PFM solves this without modifying backend endpoints via **Client-Side Mutation Versioning**:
1. **Epoch Tracking (`activeMutationEpoch`)**: `httpClient.ts` tracks an active mutation timestamp initialized from `sessionStorage` or `Date.now()`.
2. **Automatic Mutation Interceptor**: Every successful mutating HTTP response (`POST`, `PUT`, `PATCH`, `DELETE` with HTTP 2xx) triggers `bumpMutationEpoch()`.
3. **Query Parameter Injection**: Outgoing `GET` requests to internal APIs automatically append `_v: getMutationEpoch()`.
4. **Instant Invalidation**: When a mutation succeeds, the epoch is bumped. Subsequent React Query refetches send the new `_v`, producing an immediate **cache miss** in ASP.NET Core OutputCache. The backend queries PostgreSQL, returns fresh data, and caches the new state under the new `_v` key.
5. **Cache Hit Snappiness**: Navigations between pages while no mutations have occurred reuse the stable `_v`, yielding sub-5ms responses from Redis/Memory OutputCache and 0ms from React Query in-memory cache.
6. **Explicit Freshness (`_t`)**: When `Cache-Control: no-cache` is specified or during explicit refresh operations, a unique timestamp `_t` is attached to force a guaranteed fresh fetch.
### 5.3 RBAC Startup Pipeline Acceleration
Following the introduction of fine-grained RBAC and Maker-Checker checks, startup performance was streamlined to eliminate sequential network waterfalls:
* **Cache-First Role Claims**: `fetchAndCacheSystemRoles` and `fetchAndCacheRoleClaims` check in-memory and `localStorage` caches *before* dispatching network calls.
* **Parallel User Resolution**: `getUserRolesFromApi` and `getUserClaimsFromApi` are resolved concurrently using `Promise.allSettled`.
* **Parallel Claim Resolution**: Claims across all assigned roles are retrieved concurrently rather than in serial loops.
* **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.
---
## 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.
---
## 11. Model Context Protocol (MCP) Architectural Specification
### 11.1 Architectural Shift & Philosophy
The PFM platform integrates the **Model Context Protocol (MCP)** standard (v2.3.0) to expose domain capabilities both internally and externally while preventing token exhaustion and eliminating network call overhead:
1. **Dual Realisation Pattern**:
* **In-App MCP (`PostFundManagement.Api` & `PostFundManagement.Jobs`)**: Internal features, background jobs, and workflows obtain in-process access to MCP tooling via `IInAppMcpClient`. Calls are executed directly against scoped services with zero network latency, zero serialization overhead, and no external web requests.
* **External MCP Server (`PostFundManagement.Mcp`)**: A dedicated standalone presentation host exposes the exact same tools over standard external transports (HTTP/SSE at `/mcp` and command-line Stdio via `--stdio`) for external LLMs, agentic workbenches, and client IDEs.
2. **Application.Services Proxying Pattern**:
All tools reside in `PostFundManagement.Application/Mcp` and proxy established `Application.Services` interfaces (`IGrantsService`, `IProjectService`, `IExecutionService`, `IGovernanceService`, `IEvidenceService`, `ICommunicationsService`, `IUserService`). This ensures:
* **Single Source of Truth (BR-005)**: Business validation, entity mapping, and audit logging happen identically whether called from an HTTP endpoint, an internal in-app MCP consumer, or an external MCP server.
* **Domain Groupings**: Tools are partitioned by service domains (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`), providing clean tool namespaces (e.g., `grants_create_award`, `execution_approve_disbursement`).
* **Token Usage Optimization**: Responses are wrapped in concise `McpToolResult<T>` records, filtering out internal framework metadata and returning concise JSON structures to minimize LLM context window consumption.
3. **Current Agentic Boundary**:
The platform establishes the full tooling layer and in-app client contracts; no autonomous agents are defined in this release, preserving clear architectural boundaries for scheduled future agent implementations.
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.
Binary file not shown.
+247
View File
@@ -0,0 +1,247 @@
# PostFundManagement (PFM) Platform — Operational Processes & Governance (Processes.md)
> **Document Context**: Authoritative operational manual and process specification for the PFM Platform. Derived from the PFM SRS v2.0 and governed by **Lite Charms (PTY) Ltd** (official technology partner and author of every technical implementation, architecture, and maintenance of the system, with **Ifri Technologies** operating within Lite Charms (PTY) Ltd).
---
## 1. PFM End-to-End Operational Lifecycle
The PFM platform governs the lifecycle of grant, development, innovation, and impact funding portfolios from the moment an award is approved through long-term post-fund monitoring.
```
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ POST-FUND LIFECYCLE │
│ │
│ [1. Award Intake] ──> [2. Contracting] ──> [3. Onboarding] │
│ External Approval Draft Agreement KYC & Banking Gate │
│ Recorded (BR-001) Auto-created (BR-002) Required for Disb. (BR-003) │
│ │ │
│ ▼ │
│ [4. Project & WBS] ──> [5. Budget & Lines] ──> [6. Disbursement Gate] │
│ Milestones & Tasks Allocations & Caps Requires Verified Evidence │
│ Progress Tracking No Over-commitment & Satisfied Conditions (BR-004) │
│ │ │
│ ▼ │
│ [7. M&E & Evidence] ─> [8. Change & Risk] ──> [9. Close-Out & Impact] │
│ Indicator Actuals Variations & Issues Post-Fund Monitoring │
│ Field Site Visits Audit Trail (BR-007) Portfolio Roll-Up (BR-010) │
└────────────────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Core Operational Processes
### 2.1 Process 1: Award Intake & Funding Registration (Module M01)
* **Pre-condition**: Award has been approved by external sanctioning committee (BR-001).
* **Actor**: Funding Administrator (`grant_financials: add`).
* **Workflow**:
1. Capture external reference number, award amount ($> 0$), funding period, programme, and recipient organisation.
2. System enforces duplicate check against external reference numbers.
3. Upon successful award creation, the system **automatically initializes** the corresponding legal `Contract` in `Draft` status (BR-002).
### 2.2 Process 2: Contracting & Agreement Management (Module M02)
* **Pre-condition**: Valid award record exists.
* **Actors**: Legal / Programme Manager (`grant_financials: add`), Executive / Authorised Signatory (`grant_financials: update`).
* **Workflow**:
1. Author attaches legal terms, clauses, total value, and effective/expiry dates.
2. Upload signed bilateral agreement PDF.
3. Transition contract status to `Active`.
4. **Maker-Checker Rule**: The user who uploaded or created the contract cannot be the signatory who transitions it to `Active`.
### 2.3 Process 3: Project WBS & Milestones Management (Module M04 & M07)
* **Pre-condition**: Project is in `Active` status.
* **Actors**: Contributor / Project Manager (`projects_milestones: add`), Supervisor / Manager (`projects_milestones: update`).
* **Workflow**:
1. Project Manager defines contractual milestones with due dates and priority levels.
2. Contributors log ground activities against milestones.
3. Contributor uploads completion evidence.
4. Status transitions from `Planned` $\to$ `In Progress` $\to$ `Submitted` $\to$ `Under Review` $\to$ `Verified & Approved`.
5. **Verification Gate**: Only users with `projects_milestones: update` who **did not create** the milestone can verify and approve it.
### 2.4 Process 4: Disbursement Control & Payment Release (Module M05 & M06)
* **Pre-condition**: Recipient onboarding complete, banking details verified, and linked milestone deliverable verified.
* **Actors**: Finance Officer (`grant_financials: add`), Finance Director / Manager (`grant_financials: update`).
* **Workflow**:
1. Finance Officer drafts disbursement tranche drawdown against verified milestone.
2. System validates conditions precedent and budget caps (commits + expenditures $\le$ approved budget line).
3. Authoriser verifies evidence and approves tranche release.
4. **Maker-Checker Rule**: The user who captured the payment request cannot approve the disbursement.
### 2.5 Process 5: M&E Indicators, Risks & Evidence Review (Module M08 & M09)
* **Pre-condition**: Approved project indicators and risk registers configured.
* **Actors**: M&E Officer (`evidence_risks: add`), Programme Supervisor (`evidence_risks: update`).
* **Workflow**:
1. Field staff or recipients submit indicator actuals and supporting verification files (`EvidenceType`).
2. Evidence enters the `EvidenceReviewQueue`.
3. Supervisor reviews document authenticity, geotags, and deliverable match.
4. Decision: `Approve & Verify` or `Reject` (with mandatory rework reasoning).
5. **Maker-Checker Rule**: The officer who uploaded the evidence document cannot approve their own submission.
### 2.6 Process 6: Change Management & Variations (Module M15 & M16)
* **Pre-condition**: Active project with identified issue or required contractual variance.
* **Actors**: Contributor (`change_management: add`), Manager (`change_management: update`).
* **Workflow**:
1. Contributor logs project issue or drafts contractual change request (budget reallocation, timeline extension, scope shift).
2. Impact assessment conducted.
3. Governance deliberation.
4. Resolution: `Approved & Enacted` or `Rejected`.
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.1 Policy Definition
Under PFM enterprise governance standards, **dual authorization (Maker-Checker)** is mandatory for all material lifecycle decisions:
$$\text{Approval Granted} \iff (\text{User has 'update' permission}) \land (\text{User ID} \neq \text{Record Creator ID})$$
### 3.2 Matrix of Segregated Actions
| Operational Domain | Maker Action (`add`) | Checker Action (`update`) | Maker-Checker Invariant |
| :--- | :--- | :--- | :--- |
| **Projects & Governance** | Register project, update scope | Approve project governance status | Creator cannot approve project |
| **Contractual Milestones** | Define milestone, submit evidence | Verify deliverable, approve milestone | Milestone author cannot verify |
| **Disbursements** | Capture tranche drawdown request | Authorize payment release | Requestor cannot release payment |
| **Budgets** | Define budget line, allocate funds | Approve statutory budget line | Allocator cannot approve budget |
| **Evidence Queue** | Upload proof, ingest documentation | Approve & verify evidence artifact | Submitter cannot verify evidence |
| **Change Requests** | Draft scope / budget variation | Approve & enact variation | Initiator cannot enact change |
| **Site Visits** | Log field checklist & observations | Approve site inspection report | Inspector cannot self-sign report |
| **Programmes** | Create programme structure | Approve programme governance | Creator cannot approve programme |
### 3.3 System Behavior on Maker-Checker Collisions
When an authorised approver navigates to a record they personally created:
1. **Dropdown Controls**: The `Approved` option is disabled with the label:
`Approved (Maker-Checker: Cannot approve own record)`
2. **Action Buttons**: Fast-track approve buttons are hidden, and a visual tag `Maker-Checker (Creator)` is displayed.
3. **Audit Trail**: Any attempt to bypass client controls logs a security violation in the immutable audit log (`settings_auditlog`).
---
## 4. RBAC Operational Roles & Boundaries
### 4.1 Role Assignment Rules
1. **`Contributor`**: Operational field execution. Strictly restricted to `view` and `add`. Contributors can create activities, submit evidence, and draft change requests, but **never approve records or export datasets**.
2. **`Supervisor`**: Operational oversight. Possesses `view, add, update, export`. Verifies milestones, inspects evidence, reviews site visits, and exports reports, subject to the Maker-Checker rule.
3. **`Manager`**: Comprehensive operational management. Possesses `all` across operational domains. Manages audit logs, notifications, and tenancy.
4. **`Executive`**: Strategic leadership. Possesses `all` across strategic domains (`dashboard`, `governance`, `grant_financials`, `evidence_risks`).
5. **`Admin`**: System administrator. Possesses `unrestricted: all`. Governs tenant boundaries, partitions, and system security.
6. **`Partner`**: External grantee / recipient. Strictly confined to `PartnerUI`. Any attempt to access `ConsoleUI` triggers an immediate security lockout.
---
## 5. Caching & Mutation Invalidation Workflow
To ensure that caching never impedes record lifecycle updates or displays stale data while maximizing UI responsiveness, the following operational flow is enforced across ConsoleUI:
```mermaid
sequenceDiagram
autonumber
actor User as User / Maker
participant UI as ConsoleUI React Query
participant Client as Axios HttpClient
participant OutputCache as Backend OutputCache Redis/Memory
participant DB as PostgreSQL Database
User->>UI: Submits Record Mutation Create/Update/Approve
UI->>Client: Dispatches POST / PUT / PATCH / DELETE
Client->>DB: Executes Mutation in Database
DB-->>Client: Returns 200 OK / 201 Created
Client->>Client: Interceptor detects mutation: bumps activeMutationEpoch
UI->>UI: onSuccess triggers invalidateQueries
UI->>Client: Refetches active queries with _v = activeMutationEpoch
Client->>OutputCache: GET /v1/api/* with new _v epoch
Note over OutputCache: Cache Miss: new _v key never seen before
OutputCache->>DB: Queries PostgreSQL for fresh data
DB-->>OutputCache: Returns updated entities
OutputCache-->>UI: Caches fresh data under _v and returns to UI
UI-->>User: UI updates immediately with new status / record
```
### 5.1 Operating Rules for Record Modifications
1. **Zero Manual Cache Eviction Required**: Frontend developers do not need to manually configure cache busters for standard operations. The Axios response interceptor in `httpClient.ts` automatically increments `activeMutationEpoch` on any successful mutating call (`POST`, `PUT`, `PATCH`, `DELETE`).
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.
---
## 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB