|
| 1 | +# Apply to transfer an academy – External Applications Web |
| 2 | + |
| 3 | +This repository contains the Razor Pages frontend for submitting applications to transfer academies into another trust. It uses a Clean Architecture layout (Domain ➜ Application ➜ Infrastructure ➜ Web) and drives every screen from JSON form templates delivered by the External Applications API. Template-driven pages, conditional logic, file uploads, and contributor management are all orchestrated in the web layer while persistence and validation live in the API. |
| 4 | + |
| 5 | +## Features |
| 6 | +- 🧩 Template-driven form engine: tasks/pages/fields come from template schemas fetched via `ITemplatesClient` and parsed into domain models. |
| 7 | +- 🔀 Dynamic logic: conditional visibility/requirements, collection flows, and derived collection flows handled server-side in `RenderForm`. |
| 8 | +- 👥 Contributor management: invite/remove contributors before entering the form and keep application state in session + API. |
| 9 | +- 🔐 Secure authentication: DfE Sign-in (OIDC) plus optional internal/test schemes; session-based token refresh and permissions caching. |
| 10 | +- 🛡️ File uploads with AV protection: files posted to the API, scanned asynchronously, and cleaned/blacklisted when `ScanResultConsumer` receives an infected result. |
| 11 | +- 📢 Notifications and events: publishes `TransferApplicationSubmittedEvent` to Azure Service Bus after successful submission; renders API-sourced notifications to users. |
| 12 | +- 🚀 Production-ready concerns: redis + memory hybrid caching, gzip compression, Application Insights, GovUK Frontend rebrand, and MassTransit transport setup. |
| 13 | +- 🛠️ Admin tools: role-restricted admin pages let you view the current template metadata, clear caches/sessions, and publish a new template version directly through the UI (with validation and cache invalidation). |
| 14 | + |
| 15 | +## Architecture Overview 🧱 |
| 16 | +- **Web (Razor Pages)**: UI, request pipeline, auth, cookies, session, and form rendering (`Pages/FormEngine`, `Pages/Applications`, feedback, notifications). |
| 17 | +- **Application**: Interfaces for form orchestration, state, validation, uploads, and event mapping. |
| 18 | +- **Domain**: Template and form primitives (`FormTemplate`, `TaskGroup`, `Task`, `Page`, `Field`, `ConditionalLogic`, `EventMapping`). |
| 19 | +- **Infrastructure**: Implementations that call the External Applications API (`IApplicationsClient`, `ITemplatesClient`, `INotificationsClient`), template stores/parsers, MassTransit consumers, redis-backed caching, and file upload handling. |
| 20 | +- **Tests**: Unit tests for infrastructure/web and Cypress end-to-end specs. |
| 21 | + |
| 22 | +## System Flow (happy path) 🔄 |
| 23 | +1. **Sign in** via DfE Sign-in (or test/internal auth in non-prod). Permissions are cached and refreshed via `TokenRefresh` settings. |
| 24 | +2. **Dashboard** shows the user’s applications for the configured template; “Create” calls `CreateApplicationAsync`, stores IDs in session, and clears cached form data. |
| 25 | +3. **Contributors** page lets the lead applicant manage collaborators before entering the form. |
| 26 | +4. **Form engine** loads the current template (cached via `FormTemplateProvider` + `ApiTemplateStore`), restores response data from the API/session, and renders pages with conditional logic, collection flows, and complex fields (autocomplete, upload). |
| 27 | +5. **File uploads** are sent to the API; scan results arrive on Azure Service Bus and `ScanResultConsumer` removes infected files, clears redis cache, and raises a user notification. |
| 28 | +6. **Validation and navigation** are handled server-side; task completion is tracked per-task and persisted back through `IApplicationResponseService`. |
| 29 | +7. **Submit** posts final responses to the API and publishes `TransferApplicationSubmittedEvent` to the Service Bus using the configured event mapping for the transfer template. |
| 30 | + |
| 31 | +Example event publication in the form engine: |
| 32 | + |
| 33 | +```3554:3592:src/DfE.ExternalApplications.Web/Pages/FormEngine/RenderForm.cshtml.cs |
| 34 | + /// Publishes the TransferApplicationSubmittedEvent to the service bus |
| 35 | + /// Uses the event data mapper to extract and transform form data according to the configured mapping |
| 36 | + private async Task PublishApplicationSubmittedEventAsync(ApplicationDto application) |
| 37 | + { |
| 38 | + var eventData = await _eventDataMapper.MapToEventAsync<TransferApplicationSubmittedEvent>( |
| 39 | + FormData, |
| 40 | + Template, |
| 41 | + "transfer-application-submitted-v1", |
| 42 | + application.ApplicationId, |
| 43 | + application.ApplicationReference); |
| 44 | + await publishEndpoint.PublishAsync(eventData, messageProperties, CancellationToken.None); |
| 45 | + } |
| 46 | +``` |
| 47 | + |
| 48 | +## Key configuration ⚙️ |
| 49 | +All settings are standard ASP.NET Core configuration keys (appsettings or environment variables). Important ones: |
| 50 | + |
| 51 | +| Key | Purpose | Dev value / notes | |
| 52 | +| --- | --- | --- | |
| 53 | +| `DfESignIn:Authority` / `ClientId` / `ClientSecret` / `RedirectUri` | OIDC login | Dev authority `https://test-oidc.signin.education.gov.uk`; set secrets locally. | |
| 54 | +| `ExternalApplicationsApiClient:BaseUrl` | Backend API endpoint | `https://api.dev.apply-transfer-academy.service.gov.uk` (see `appsettings.Development.json`). | |
| 55 | +| `ExternalApplicationsApiClient:ClientId` / `ClientSecret` / `Authority` / `Scope` | API auth (Azure AD) | ClientId preset; supply `ClientSecret` via user-secrets/env. | |
| 56 | +| `Template:Id` | Template to render | Default transfer template `9A4E9C58-9135-468C-B154-7B966F7ACFB7`. | |
| 57 | +| `FormEngine:ComplexFields` | External search endpoints | Dev APIs for trusts and establishments are prefilled in `appsettings.Development.json`. | |
| 58 | +| `MassTransit:Transport` / `AzureServiceBus:ConnectionString` | Service Bus for events & AV scan results | Provide connection string to receive scan results and publish submissions. | |
| 59 | +| `ConnectionStrings:Redis` | Redis for hybrid caching and sessions | Default `localhost:6379`. | |
| 60 | +| `ApplicationInsights:ConnectionString` | Telemetry | Optional locally; required in cloud. | |
| 61 | +| `TokenRefresh:*` | Session/token refresh windows | Defaults provided in `appsettings.json`. | |
| 62 | +| `InternalServiceAuth:*` | Service-to-service auth | Used for internal APIs and virus-scan cleanup. | |
| 63 | + |
| 64 | +## Running locally 🖥️ |
| 65 | +Prerequisites: .NET 8 SDK, Redis (local or container), Node/npm if running Cypress, and access to the dev External Applications API + Azure AD app registration. |
| 66 | + |
| 67 | +1) Clone and restore |
| 68 | +```bash |
| 69 | +dotnet restore DfE.ExternalApplications.Web.sln |
| 70 | +``` |
| 71 | + |
| 72 | +2) Configure secrets (examples) |
| 73 | +```bash |
| 74 | +# Auth & token refresh |
| 75 | +dotnet user-secrets set "DfESignIn:ClientSecret" "<oidc-client-secret>" --project src/DfE.ExternalApplications.Web |
| 76 | +dotnet user-secrets set "TokenRefresh:ClientSecret" "<oidc-client-secret>" --project src/DfE.ExternalApplications.Web |
| 77 | + |
| 78 | +# External Applications API auth |
| 79 | +dotnet user-secrets set "ExternalApplicationsApiClient:ClientSecret" "<api-client-secret>" --project src/DfE.ExternalApplications.Web |
| 80 | + |
| 81 | +# Messaging / telemetry / cache |
| 82 | +dotnet user-secrets set "MassTransit:AzureServiceBus:ConnectionString" "<sb-connection>" --project src/DfE.ExternalApplications.Web |
| 83 | +dotnet user-secrets set "ApplicationInsights:ConnectionString" "<ai-connection>" --project src/DfE.ExternalApplications.Web |
| 84 | +dotnet user-secrets set "ConnectionStrings:Redis" "localhost:6379" --project src/DfE.ExternalApplications.Web |
| 85 | + |
| 86 | +# Internal service-to-service auth |
| 87 | +dotnet user-secrets set "InternalServiceAuth:SecretKey" "<internal-signing-key>" --project src/DfE.ExternalApplications.Web |
| 88 | +dotnet user-secrets set "InternalServiceAuth:Services:0:ApiKey" "<internal-api-key>" --project src/DfE.ExternalApplications.Web |
| 89 | + |
| 90 | +# Optional: secured downstream APIs for complex fields (if required) 🔒 |
| 91 | +# Matching structure: |
| 92 | +# { |
| 93 | +# "Id": "TrustComplexField", |
| 94 | +# "ApiEndpoint": "https://api.dev.academies.education.gov.uk/trusts?page=1&count=10&groupname={0}&ukprn={0}&companieshousenumber={0}&status=Open", |
| 95 | +# "ApiKey": "<trusts-api-key>" |
| 96 | +# }, |
| 97 | +# { |
| 98 | +# "Id": "EstablishmentComplexField", |
| 99 | +# "ApiEndpoint": "https://api.dev.academies.education.gov.uk/v4/establishments?page=1&count=10&name={0}&urn={0}&ukprn={0}&matchAny=true&excludeClosed=true", |
| 100 | +# "ApiKey": "<establishments-api-key>" |
| 101 | +# } |
| 102 | +dotnet user-secrets set "FormEngine:ComplexFields:0:ApiKey" "<trusts-api-key>" --project src/DfE.ExternalApplications.Web |
| 103 | +dotnet user-secrets set "FormEngine:ComplexFields:1:ApiKey" "<establishments-api-key>" --project src/DfE.ExternalApplications.Web |
| 104 | +``` |
| 105 | + |
| 106 | +3) Use the dev API configuration |
| 107 | +`ASPNETCORE_ENVIRONMENT=Development` uses `appsettings.Development.json`, which already points to `https://api.dev.apply-transfer-academy.service.gov.uk` and the dev academies search endpoints. |
| 108 | + |
| 109 | +4) Run the web app |
| 110 | +```bash |
| 111 | +dotnet run --project src/DfE.ExternalApplications.Web/DfE.ExternalApplications.Web.csproj |
| 112 | +``` |
| 113 | +Browse to `https://localhost:5001` (or the HTTPS port shown in the console) and sign in with a dev DfE Sign-in account. |
| 114 | + |
| 115 | +5) Optional: receive virus-scan events |
| 116 | +Ensure the Service Bus connection string is set so `ScanResultConsumer` can process `ScanResultEvent` messages and clean infected uploads. |
| 117 | + |
| 118 | +## Tests ✅ |
| 119 | +- Unit tests: `dotnet test DfE.ExternalApplications.Web.sln` |
| 120 | +- Cypress (E2E): from `Tests/DfE.ExternalApplications.CypressTests`, install deps then run `npm test` or `npx cypress run`. |
| 121 | + |
| 122 | +## Example workflow 📋 |
| 123 | +1. Create a new application from the dashboard (uses the configured template ID). |
| 124 | +2. Add contributors and proceed to the form. |
| 125 | +3. Complete tasks/pages; upload supporting documents (they will be virus-scanned). |
| 126 | +4. Submit; the app persists responses to the External Applications API and publishes `TransferApplicationSubmittedEvent` to Service Bus for downstream processing. |
| 127 | + |
| 128 | +## Admin pages (template management) 🛠️ |
| 129 | +- Access: available to users in role `Admin`; entry point at `/Admin/Admin` with a link to `/Admin/TemplateManager`. |
| 130 | +- Admin dashboard: shows the current template ID/name/description/version, task group count, cache key status, and tokens; provides a “clear all” to wipe session and template cache. |
| 131 | +- Template Manager: |
| 132 | + - Displays the current template JSON and latest version (fetched from the API; cache is cleared before loading). |
| 133 | + - “Add template version” flow validates the provided JSON against the form template schema, base64-encodes it, and calls `CreateTemplateVersionAsync`. |
| 134 | + - Auto-suggests the next patch version, and after creating a version, clears and verifies cache so the new schema is served immediately. |
| 135 | + - Includes “clear all” to drop session and cache and return to dashboard if the template ID is lost. |
| 136 | + |
| 137 | +## Notes 🧠 |
| 138 | +- Clean Architecture boundaries are enforced: web depends on application interfaces, implementations live in Infrastructure, and templates are domain-driven JSON schemas. |
| 139 | +- If you change template IDs or event mappings, update `Template:Id` and the mapping under `EventMappings/<template>/`. |
| 140 | + |
0 commit comments