diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml index d4d4a7f..9e48c74 100644 --- a/.github/workflows/build-and-push-image.yml +++ b/.github/workflows/build-and-push-image.yml @@ -2,8 +2,7 @@ name: Deploy to environment on: push: - branches: - - main + branches: [ main ] workflow_dispatch: inputs: environment: @@ -14,10 +13,6 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.inputs.environment }} -env: - DOCKER_IMAGE: identifiersapi-app - NODE_VERSION: 18 - jobs: set-env: name: Determine environment @@ -25,57 +20,26 @@ jobs: outputs: environment: ${{ steps.var.outputs.environment }} branch: ${{ steps.var.outputs.branch }} - release: ${{ steps.var.outputs.release }} - checked-out-sha: ${{ steps.var.outputs.checked-out-sha }} + release: ${{steps.var.outputs.release}} steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} + - name: Get branch name for push/dispatch event + run: | + GIT_REF=${{ github.ref_name }} + echo "branch_ref=${GIT_REF}" >> $GITHUB_ENV - id: var run: | - GIT_REF=${{ github.ref }} + GIT_REF=${{ env.branch_ref }} GIT_BRANCH=${GIT_REF##*/} INPUT=${{ github.event.inputs.environment }} ENVIRONMENT=${INPUT:-"development"} RELEASE=${ENVIRONMENT,,}-`date +%Y-%m-%d`.${{ github.run_number }} - CHECKED_OUT_SHA="$(git log -1 '--format=format:%H')" echo "environment=${ENVIRONMENT,,}" >> $GITHUB_OUTPUT echo "branch=$GIT_BRANCH" >> $GITHUB_OUTPUT - echo "release=${RELEASE}" >> $GITHUB_OUTPUT - echo "checked-out-sha=${CHECKED_OUT_SHA}" >> $GITHUB_OUTPUT - - build-and-push-image: - name: Build and push to ACR - needs: set-env - runs-on: ubuntu-22.04 - environment: ${{ needs.set-env.outputs.environment }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Azure Container Registry login - uses: docker/login-action@v3 - with: - username: ${{ secrets.AZURE_ACR_CLIENTID }} - password: ${{ secrets.AZURE_ACR_SECRET }} - registry: ${{ secrets.AZURE_ACR_URL }} - - - name: Build and push docker image - uses: docker/build-push-action@v5 - with: - context: . - file: Dockerfile - build-args: COMMIT_SHA=${{ needs.set-env.outputs.checked-out-sha }} - tags: | - ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.branch }} - ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.release }} - ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:sha-${{ needs.set-env.outputs.checked-out-sha }} - ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:latest - push: true + echo "release=${RELEASE,,}" >> $GITHUB_OUTPUT create-tag: + if: needs.set-env.outputs.environment == 'production' name: Tag and release needs: set-env runs-on: ubuntu-22.04 @@ -96,11 +60,9 @@ jobs: script: | try { await github.rest.repos.createRelease({ - draft: ${{ needs.set-env.outputs.environment == 'test' }}, generate_release_notes: true, name: "${{ needs.set-env.outputs.release }}", owner: context.repo.owner, - prerelease: ${{ needs.set-env.outputs.environment == 'test' }}, repo: context.repo.repo, tag_name: "${{ needs.set-env.outputs.release }}", }); @@ -109,58 +71,17 @@ jobs: } deploy-image: - name: Deploy to ${{ needs.set-env.outputs.environment }} - needs: [ build-and-push-image, set-env ] - runs-on: ubuntu-22.04 - environment: ${{ needs.set-env.outputs.environment }} - steps: - - name: Azure login with ACA credentials - uses: azure/login@v2 - with: - creds: ${{ secrets.AZURE_ACA_CREDENTIALS }} - - - name: Update Azure Container Apps Revision - uses: azure/CLI@v2 - id: azure - with: - azcliversion: 2.45.0 - inlineScript: | - az config set extension.use_dynamic_install=yes_without_prompt - az containerapp update \ - --name ${{ secrets.AZURE_ACA_NAME }} \ - --resource-group ${{ secrets.AZURE_ACA_RESOURCE_GROUP }} \ - --image ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.release }} \ - --output none - - cypress-tests: - name: Run Cypress Tests - if: needs.set-env.outputs.environment == 'test' || needs.set-env.outputs.environment == 'development' - needs: [ deploy-image, set-env ] - runs-on: ubuntu-22.04 - environment: ${{ needs.set-env.outputs.environment }} - defaults: - run: - working-directory: CypressTests - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Setup node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Npm install - run: npm install - - - name: Run cypress - run: npm run cy:run -- --env apiKey="${{ secrets.IDENTIFIERS_API_KEY }}",url="${{ secrets.IDENTIFIERS_API_BASE_URL }}" - - - name: Upload screenshots - if: ${{ failure() }} - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ needs.set-env.outputs.environment }} - path: screenshots + name: Deploy to environment + needs: [ set-env ] + uses: DFE-Digital/deploy-azure-container-apps-action/.github/workflows/build-push-deploy.yml@main + with: + docker-image-name: 'identapi-app' + docker-build-file-name: 'docker/Dockerfile' + environment: ${{ needs.set-env.outputs.environment }} + secrets: + azure-acr-client-id: ${{ secrets.AZURE_ACR_CLIENTID }} + azure-acr-secret: ${{ secrets.AZURE_ACR_SECRET }} + azure-acr-url: ${{ secrets.AZURE_ACR_URL }} + azure-aca-credentials: ${{ secrets.AZURE_ACA_CREDENTIALS }} + azure-aca-name: ${{ secrets.AZURE_ACA_NAME }} + azure-aca-resource-group: ${{ secrets.AZURE_ACA_RESOURCE_GROUP }} diff --git a/.github/workflows/continuous-integration-dotnet.yml b/.github/workflows/continuous-integration-dotnet.yml index 8983992..75aa393 100644 --- a/.github/workflows/continuous-integration-dotnet.yml +++ b/.github/workflows/continuous-integration-dotnet.yml @@ -71,8 +71,14 @@ jobs: run: | dotnet-sonarscanner begin /k:"DFE-Digital_identifiers-api" /o:"dfe-digital" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.coverageReportPaths=CoverageReport/SonarQube.xml dotnet build --no-restore +<<<<<<< HEAD dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" # Todo add this back to previous step once tests in place -# reportgenerator -reports:./**/coverage.cobertura.xml -targetdir:./CoverageReport -reporttypes:SonarQube \ No newline at end of file +# reportgenerator -reports:./**/coverage.cobertura.xml -targetdir:./CoverageReport -reporttypes:SonarQube +======= + dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" + reportgenerator -reports:./**/coverage.cobertura.xml -targetdir:./CoverageReport -reporttypes:SonarQube + dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" +>>>>>>> 322be4b39eb8a99247ce69050f07091f2867ef62 diff --git a/.github/workflows/continuous-integration-terraform.yml b/.github/workflows/continuous-integration-terraform.yml index ffba413..8f48c08 100644 --- a/.github/workflows/continuous-integration-terraform.yml +++ b/.github/workflows/continuous-integration-terraform.yml @@ -28,7 +28,7 @@ jobs: fi - name: Validate Terraform docs - uses: terraform-docs/gh-actions@v1.0.0 + uses: terraform-docs/gh-actions@v1.1.0 with: working-dir: terraform config-file: .terraform-docs.yml diff --git a/.gitignore b/.gitignore index 418ffd9..a46d43e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ !/.idea/.idea.*/.idea /.idea/.idea.*/.idea/* !/.idea/.idea.*/.idea/runConfigurations +.idea ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. .vscode diff --git a/Dfe.Identifiers.Api.Test/ApiTestFixture.cs b/Dfe.Identifiers.Api.Test/ApiTestFixture.cs new file mode 100644 index 0000000..5d46658 --- /dev/null +++ b/Dfe.Identifiers.Api.Test/ApiTestFixture.cs @@ -0,0 +1,37 @@ +using System.Net.Mime; +using Dfe.Identifiers.Infrastructure.Context; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Dfe.Identifiers.Api.Test +{ + public class ApiTestFixture : IDisposable + { + private const string connectionStringKey = "ConnectionStrings:Default"; + private DbContextOptions _dbContextOptions { get; init; } + + public ApiTestFixture() + { + var builder = new ConfigurationBuilder() + .AddUserSecrets(); + + Configuration = builder.Build(); + _dbContextOptions = new DbContextOptionsBuilder() + .UseSqlServer(Configuration[connectionStringKey]) + .Options; + using var context = GetMstrContext(); + context.Database.EnsureDeleted(); + context.Database.Migrate(); + } + + private IConfigurationRoot Configuration { get; init; } + + public MstrContext GetMstrContext() => new(_dbContextOptions); + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Api.Test/DatabaseModelBuilder.cs b/Dfe.Identifiers.Api.Test/DatabaseModelBuilder.cs new file mode 100644 index 0000000..8779807 --- /dev/null +++ b/Dfe.Identifiers.Api.Test/DatabaseModelBuilder.cs @@ -0,0 +1,39 @@ +using AutoFixture; +using Dfe.Identifiers.Domain.Models; + +namespace Dfe.Identifiers.Api.Test +{ + public static class DatabaseModelBuilder + { + private static readonly Fixture _fixture = new Fixture(); + + public static Trust BuildTrust() + { + var result = _fixture.Create(); + result.SK = null; + result.TrustStatus = "Open"; + result.TrustTypeId = 30; + result.TrustType = null; + result.TrustStatusId = null; + result.RegionId = null; + result.TrustBandingId = null; + result.RID = result.RID.Substring(0, 10); + + return result; + } + + public static Establishment BuildEstablishment() + { + var result = _fixture.Create(); + result.SK = null; + result.LocalAuthority = null; + result.EstablishmentType = null; + result.PK_GIAS_URN = _fixture.Create().ToString(); + // Only 224 or 228 are valid in this subset of test data used (see mstr context) + result.EstablishmentTypeId = 224; + result.LocalAuthorityId = 1; + + return result; + } + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Api.Test/Dfe.Identifiers.Api.Test.csproj b/Dfe.Identifiers.Api.Test/Dfe.Identifiers.Api.Test.csproj new file mode 100644 index 0000000..61c8677 --- /dev/null +++ b/Dfe.Identifiers.Api.Test/Dfe.Identifiers.Api.Test.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + true + 29f2c83e-d293-4cdb-b0bf-7ec5ed71bee5 + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + true + PreserveNewest + PreserveNewest + + + + diff --git a/Dfe.Identifiers.Api.Test/Extensions/ActionResultExtension.cs b/Dfe.Identifiers.Api.Test/Extensions/ActionResultExtension.cs new file mode 100644 index 0000000..81eafb6 --- /dev/null +++ b/Dfe.Identifiers.Api.Test/Extensions/ActionResultExtension.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Dfe.Identifiers.Api.Test.Extensions; + +public static class ActionResultExtension +{ + public static Domain.Identifiers.IdentifiersCollection? GetIdentifiers(this ActionResult actionResult) + { + return (Domain.Identifiers.IdentifiersCollection?)((OkObjectResult?)actionResult.Result)?.Value; + } + + public static int? GetStatusCode(this ActionResult actionResult) + { + return ((ObjectResult?)actionResult.Result)?.StatusCode; + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Api.Test/GlobalUsings.cs b/Dfe.Identifiers.Api.Test/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Dfe.Identifiers.Api.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Dfe.Identifiers.Api.Test/IdentifiersControllerTests.cs b/Dfe.Identifiers.Api.Test/IdentifiersControllerTests.cs new file mode 100644 index 0000000..0998691 --- /dev/null +++ b/Dfe.Identifiers.Api.Test/IdentifiersControllerTests.cs @@ -0,0 +1,412 @@ +using System.Net; +using Dfe.Identifiers.Api.Controllers; +using Dfe.Identifiers.Api.Test.Extensions; +using Dfe.Identifiers.Application; +using Dfe.Identifiers.Domain.Identifiers; +using Dfe.Identifiers.Domain.Models; +using Dfe.Identifiers.Infrastructure.Context; +using Dfe.Identifiers.Infrastructure.Repositories; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Dfe.Identifiers.Api.Test; + +public class IdentifiersControllerTests : IClassFixture +{ + private ApiTestFixture Fixture { get; } + private IdentifiersController Sut { get; } + + private const string MixedSameUkprn = "SameUKPRN"; + + public IdentifiersControllerTests(ApiTestFixture fixture) + { + Fixture = fixture; + var logger = new Mock>(); + var context = fixture.GetMstrContext(); + Sut = new IdentifiersController(logger.Object, new IdentifiersQuery(new TrustRepository(context), + new EstablishmentRepository(context))); + } + + // TRUSTS + + [Theory] + [InlineData(TrustIdTypes.GroupID)] + [InlineData(TrustIdTypes.UKPRN)] + [InlineData(TrustIdTypes.GroupUID)] + public async Task Get_TrustIdentifiers_AndTrustExists_Returns_Ok(TrustIdTypes trustIdType) + { + using var context = Fixture.GetMstrContext(); + + var trustData = await BuildTrustSet(context); + + var selectedTrust = trustData.First(); + + var identifier = trustIdType switch + { + TrustIdTypes.GroupID => selectedTrust.GroupID, + TrustIdTypes.UKPRN => selectedTrust.UKPRN, + TrustIdTypes.GroupUID => selectedTrust.GroupUID, + _ => throw new ArgumentOutOfRangeException(nameof(trustIdType), trustIdType, null) + }; + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers(identifier!, cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var trusts = content!.Trusts; + trusts.Length.Should().Be(1); + AssertTrustIdentifierResponse(trusts.First(), selectedTrust); + } + + [Theory] + [InlineData(TrustIdTypes.GroupID)] + [InlineData(TrustIdTypes.UKPRN)] + [InlineData(TrustIdTypes.GroupUID)] + public async Task Get_TrustIdentifiers_AndDuplicateTrustsExists_Returns_Ok(TrustIdTypes trustIdType) + { + using var context = Fixture.GetMstrContext(); + + var trustData = await BuildDuplicateTrustSet(context, trustIdType); + + var identifier = trustIdType switch + { + TrustIdTypes.GroupID => "GroupID", + TrustIdTypes.UKPRN => "UKPRN", + TrustIdTypes.GroupUID => "GroupUID", + _ => throw new ArgumentOutOfRangeException(nameof(trustIdType), trustIdType, null) + }; + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers(identifier, cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var trusts = content!.Trusts; + trusts.Length.Should().Be(3); + AssertTrustIdentifierResponse(trusts.First(), trustData.First()); + AssertTrustIdentifierResponse(trusts[1], trustData[1]); + AssertTrustIdentifierResponse(trusts[2], trustData[2]); + } + + [Theory] + [InlineData(TrustIdTypes.GroupID)] + [InlineData(TrustIdTypes.UKPRN)] + [InlineData(TrustIdTypes.GroupUID)] + public async Task Get_TrustIdentifiers_AndNoOtherIdentifiersExist_Returns_Ok(TrustIdTypes trustIdType) + { + using var context = Fixture.GetMstrContext(); + + var trustData = await BuildTrustSetWithEmptyData(context, trustIdType); + var selectedTrust = trustData.First(); + var identifier = trustIdType switch + { + TrustIdTypes.GroupID => selectedTrust.GroupID, + TrustIdTypes.UKPRN => selectedTrust.UKPRN, + TrustIdTypes.GroupUID => selectedTrust.GroupUID, + _ => throw new ArgumentOutOfRangeException(nameof(trustIdType), trustIdType, null) + }; + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers(identifier!, cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var trusts = content!.Trusts; + trusts.Length.Should().Be(1); + AssertTrustIdentifierResponse(trusts.First(), selectedTrust); + } + + [Fact] + public async Task Get_TrustIdentifiers_AndTrustDoesNotExist_Returns_EmptyList() + { + using var context = Fixture.GetMstrContext(); + + await BuildTrustSet(context); + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers("NoTrustExists", cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var trusts = content!.Trusts; + trusts.Length.Should().Be(0); + } + + // ESTABLISHMENTS + + [Theory] + [InlineData(EstablishmentsIdTypes.URN)] + [InlineData(EstablishmentsIdTypes.UKPRN)] + [InlineData(EstablishmentsIdTypes.LAESTAB)] + public async Task Get_EstablishmentIdentifiers_AndEstablishmentExists_Returns_Ok(EstablishmentsIdTypes idType) + { + using var context = Fixture.GetMstrContext(); + + var trustData = CreateEstablishmentSet(context); + + var selectedEstablishment = trustData.Establishments.First(); + var identifier = idType switch + { + EstablishmentsIdTypes.URN => $"{selectedEstablishment.URN}", + EstablishmentsIdTypes.UKPRN => selectedEstablishment.UKPRN, + EstablishmentsIdTypes.LAESTAB => + $"{selectedEstablishment.LocalAuthority.Code}%2F{selectedEstablishment.EstablishmentNumber}", + _ => throw new ArgumentOutOfRangeException(nameof(idType), idType, null) + }; + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers(identifier!, cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var establishments = content!.Establishments; + establishments.Length.Should().Be(1); + AssertEstablishmentsIdentifierResponse(establishments.First(), selectedEstablishment); + } + + // Mixed + [Fact] + public async Task Get_Identifiers_AndEstablishmentAndTrustExists_Returns_Ok() + { + using var context = Fixture.GetMstrContext(); + + var mixedData = CreateSameUKPRNDataSet(context); + + var selectedEstablishment = mixedData.Establishments.First(); + var selectedTrust = mixedData.Trust; + + var cancellationToken = new CancellationToken(); + var response = await Sut.GetIdentifiers(MixedSameUkprn, cancellationToken); + + response.Result.Should().NotBeNull(); + response.Result.Should().BeOfType(typeof(OkObjectResult)); + response.GetStatusCode().Should().Be((int)HttpStatusCode.OK); + + var content = response.GetIdentifiers(); + content.Should().NotBeNull(); + var establishments = content!.Establishments; + establishments.Length.Should().Be(1); + AssertEstablishmentsIdentifierResponse(establishments.First(), selectedEstablishment); + var trusts = content.Trusts; + trusts.Length.Should().Be(1); + AssertTrustIdentifierResponse(trusts.First(), selectedTrust); + } + + private static async Task> BuildTrustSet(MstrContext context) + { + var trusts = new List(); + + for (var idx = 0; idx < 3; idx++) + { + var trust = DatabaseModelBuilder.BuildTrust(); + trusts.Add(trust); + } + + context.Trusts.AddRange(trusts); + await context.SaveChangesAsync(); + return trusts; + } + + private static async Task> BuildDuplicateTrustSet(MstrContext context, TrustIdTypes trustId) + { + var trusts = new List(); + + for (var idx = 0; idx < 3; idx++) + { + var trust = DatabaseModelBuilder.BuildTrust(); + switch (trustId) + { + case TrustIdTypes.GroupID: + trust.GroupID = "GroupID"; + break; + case TrustIdTypes.UKPRN: + trust.UKPRN = "UKPRN"; + break; + case TrustIdTypes.GroupUID: + trust.GroupUID = "GroupUID"; + break; + } + + trusts.Add(trust); + } + + context.Trusts.AddRange(trusts); + await context.SaveChangesAsync(); + return trusts; + } + + private static async Task> BuildTrustSetWithEmptyData(MstrContext context, + TrustIdTypes trustIdTypeToKeep) + { + var trusts = await BuildTrustSet(context); + foreach (var trust in trusts) + { + switch (trustIdTypeToKeep) + { + case TrustIdTypes.GroupID: + trust.UKPRN = null; + break; + case TrustIdTypes.UKPRN: + trust.GroupID = null; + break; + case TrustIdTypes.GroupUID: + trust.GroupID = null; + trust.UKPRN = null; + break; + default: + throw new ArgumentOutOfRangeException(nameof(trustIdTypeToKeep), trustIdTypeToKeep, null); + } + } + + context.Trusts.UpdateRange(trusts); + await context.SaveChangesAsync(); + return trusts; + } + + private static TrustDataSet CreateEstablishmentSet(MstrContext context) + { + var trust = DatabaseModelBuilder.BuildTrust(); + context.Add(trust); + context.SaveChanges(); + + var establishments = new List(); + + for (var idx = 0; idx < 3; idx++) + { + var localAuthority = context.LocalAuthorities.First(la => la.SK % 3 == idx); + var establishmentDataSet = CreateEstablishment(localAuthority); + + context.Establishments.Add(establishmentDataSet); + + establishments.Add(establishmentDataSet); + } + + context.SaveChanges(); + + var trustToEstablishmentLinks = + LinkTrustToEstablishments(trust, establishments); + + context.EducationEstablishmentTrusts.AddRange(trustToEstablishmentLinks); + + context.SaveChanges(); + + var result = new TrustDataSet(Trust: trust, Establishments: establishments); + + return result; + } + + private static List LinkTrustToEstablishments(Trust trust, + List establishments) + { + var result = new List(); + + establishments.ForEach(establishment => + { + var educationEstablishmentTrust = new EducationEstablishmentTrust() + { + TrustId = (int)trust.SK, + EducationEstablishmentId = (int)establishment.SK + }; + + result.Add(educationEstablishmentTrust); + }); + + return result; + } + + private static TrustDataSet CreateSameUKPRNDataSet(MstrContext context) + { + var trust = DatabaseModelBuilder.BuildTrust(); + trust.UKPRN = MixedSameUkprn; + context.Add(trust); + context.SaveChanges(); + + //Establishment + var establishments = new List(); + + var establishment = CreateEstablishment(context.LocalAuthorities.First()); + establishment.UKPRN = MixedSameUkprn; + + context.Establishments.Add(establishment); + + establishments.Add(establishment); + + context.SaveChanges(); + + var trustToEstablishmentLinks = + LinkTrustToEstablishments(trust, establishments); + + context.EducationEstablishmentTrusts.AddRange(trustToEstablishmentLinks); + + context.SaveChanges(); + + var result = new TrustDataSet(Trust: trust, Establishments: establishments); + + return result; + } + + private static Establishment CreateEstablishment(LocalAuthority la) + { + var establishment = DatabaseModelBuilder.BuildEstablishment(); + + establishment.LocalAuthority = la; + + return establishment; + } + + private static void AssertTrustIdentifierResponse(TrustIdentifiers actual, Trust expected) + { + actual.TrustReference.Should().Be(expected.GroupID); + actual.UKPRN.Should().Be(expected.UKPRN); + actual.UID.Should().Be(expected.GroupUID); + } + + private static void AssertEstablishmentsIdentifierResponse(EstablishmentIdentifiers actual, Establishment expected) + { + actual.URN.Should().Be(expected.URN.ToString()); + actual.UKPRN.Should().Be(expected.UKPRN); + actual.LAESTAB.Should().Be($"{expected.LocalAuthority.Code}/{expected.EstablishmentNumber}"); + } + + public enum TrustIdTypes + { + GroupID, + UKPRN, + GroupUID + } + + public enum EstablishmentsIdTypes + { + URN, + UKPRN, + LAESTAB + } + + private record TrustDataSet( + Trust Trust, + List Establishments + ); +} \ No newline at end of file diff --git a/Dfe.Identifiers.Api.Test/appsettings.test.json b/Dfe.Identifiers.Api.Test/appsettings.test.json new file mode 100644 index 0000000..64f2d8a --- /dev/null +++ b/Dfe.Identifiers.Api.Test/appsettings.test.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "" + } +} diff --git a/Dfe.Identifiers.Api/Controllers/IdentifierController.cs b/Dfe.Identifiers.Api/Controllers/IdentifierController.cs new file mode 100644 index 0000000..4911a56 --- /dev/null +++ b/Dfe.Identifiers.Api/Controllers/IdentifierController.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using System.Text.RegularExpressions; +using Dfe.Identifiers.Application; +using Dfe.Identifiers.Domain.Identifiers; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Dfe.Identifiers.Api.Controllers; + +/// +/// Handles operations related to Identifiers. +/// +[ApiController] +[Route("api/")] +[SwaggerTag("Identifiers Endpoints")] +public class IdentifiersController(ILogger logger, IIdentifiersQuery identifiersQuery) + : ControllerBase +{ + /// + /// Retrieves an object's other identifiers based on one of its identifiers. Currently supports UKPRN, UID and Trust Reference for trusts and UKPRN, URN and LAESTAB for establishments + /// + /// The identifier (UKPRN, UID, URN, LAESTAB or Trust Reference). + /// + /// A list of matching objects or empty lists if not found. + [HttpGet] + [Route("identifier/{identifier}")] + [SwaggerOperation(Summary = "Retrieves an object's Identifiers based on one of its identifiers.", + Description = "Returns an objects identifiers found in the database.")] + [SwaggerResponse(200, "Successfully found and returned the objects identifiers.")] + public async Task> GetIdentifiers(string identifier, CancellationToken cancellationToken) + { + var loggableIdentifier = + Regex.Replace(identifier, "[^a-zA-Z0-9-]", "", RegexOptions.None, TimeSpan.FromSeconds(2)); + logger.LogInformation("Attempting to get object identifiers by identifier {identifier}", loggableIdentifier); + var results = await identifiersQuery.GetIdentifiers(identifier, cancellationToken); + logger.LogInformation("Returning objects found by identifier {identifier}", loggableIdentifier); + logger.LogDebug("{output}", JsonSerializer.Serialize(results)); + return Ok(results); + } + +} diff --git a/Dfe.Identifiers.Api/Dfe.Identifiers.Api.csproj b/Dfe.Identifiers.Api/Dfe.Identifiers.Api.csproj new file mode 100644 index 0000000..2794f11 --- /dev/null +++ b/Dfe.Identifiers.Api/Dfe.Identifiers.Api.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Dfe.Identifiers.Api/Program.cs b/Dfe.Identifiers.Api/Program.cs new file mode 100644 index 0000000..56e92a1 --- /dev/null +++ b/Dfe.Identifiers.Api/Program.cs @@ -0,0 +1,39 @@ +using Dfe.Identifiers.Api.Interfaces; +using Dfe.Identifiers.Application; +using Dfe.Identifiers.Infrastructure.Context; +using Dfe.Identifiers.Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +ConfigureServices(builder.Services, builder.Configuration); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + +void ConfigureServices(IServiceCollection services, IConfiguration config) +{ + services.AddControllers(); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddDbContext(options => + options.UseSqlServer(config.GetConnectionString("DefaultConnection"))); +} \ No newline at end of file diff --git a/Dfe.Identifiers.Api/Properties/launchSettings.json b/Dfe.Identifiers.Api/Properties/launchSettings.json new file mode 100644 index 0000000..71e7444 --- /dev/null +++ b/Dfe.Identifiers.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:17697", + "sslPort": 44397 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5032", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7207;http://localhost:5032", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Dfe.Identifiers.Api/appsettings.Development.json b/Dfe.Identifiers.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Dfe.Identifiers.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Dfe.Identifiers.Api/appsettings.json b/Dfe.Identifiers.Api/appsettings.json new file mode 100644 index 0000000..64f2d8a --- /dev/null +++ b/Dfe.Identifiers.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "" + } +} diff --git a/Dfe.Identifiers.Application/Dfe.Identifiers.Application.csproj b/Dfe.Identifiers.Application/Dfe.Identifiers.Application.csproj new file mode 100644 index 0000000..5d4ca9c --- /dev/null +++ b/Dfe.Identifiers.Application/Dfe.Identifiers.Application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Dfe.Identifiers.Application/IIdentifiersQuery.cs b/Dfe.Identifiers.Application/IIdentifiersQuery.cs new file mode 100644 index 0000000..0c3427b --- /dev/null +++ b/Dfe.Identifiers.Application/IIdentifiersQuery.cs @@ -0,0 +1,8 @@ +using Dfe.Identifiers.Domain.Identifiers; + +namespace Dfe.Identifiers.Application; + +public interface IIdentifiersQuery +{ + Task GetIdentifiers(string identifier, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Dfe.Identifiers.Application/IdentifiersQuery.cs b/Dfe.Identifiers.Application/IdentifiersQuery.cs new file mode 100644 index 0000000..ffb9498 --- /dev/null +++ b/Dfe.Identifiers.Application/IdentifiersQuery.cs @@ -0,0 +1,21 @@ +using Dfe.Identifiers.Api.Interfaces; +using Dfe.Identifiers.Domain.Identifiers; +using static Dfe.Identifiers.Domain.Helpers.IdentifierMapping; + +namespace Dfe.Identifiers.Application; + +public class IdentifiersQuery(ITrustRepository trustRepository, IEstablishmentRepository establishmentRepository) : IIdentifiersQuery +{ + private ITrustRepository _trustRepository { get; } = trustRepository; + private IEstablishmentRepository _establishmentRepository { get; } = establishmentRepository; + + public async Task GetIdentifiers(string identifier, CancellationToken cancellationToken) + { + var trusts = await _trustRepository.GetTrustsByIdentifier(identifier, cancellationToken).ConfigureAwait(false); + var establishments = await _establishmentRepository.GetEstablishmentsByIdentifier(identifier, cancellationToken) + .ConfigureAwait(false); + var results = new IdentifiersCollection(trusts.Select(MapTrustToIdentifiers).ToArray(), + establishments.Select(MapEstablishmentToIdentifiers).ToArray()); + return results; + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Dfe.Identifiers.Domain.csproj b/Dfe.Identifiers.Domain/Dfe.Identifiers.Domain.csproj new file mode 100644 index 0000000..b58b2a7 --- /dev/null +++ b/Dfe.Identifiers.Domain/Dfe.Identifiers.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Dfe.Identifiers.Domain/Enums/TrustStatus.cs b/Dfe.Identifiers.Domain/Enums/TrustStatus.cs new file mode 100644 index 0000000..be5ff82 --- /dev/null +++ b/Dfe.Identifiers.Domain/Enums/TrustStatus.cs @@ -0,0 +1,7 @@ +namespace Dfe.Identifiers.Domain.Enums; + +public enum TrustStatus +{ + Open, + All +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Helpers/IdentifierMapping.cs b/Dfe.Identifiers.Domain/Helpers/IdentifierMapping.cs new file mode 100644 index 0000000..be761f3 --- /dev/null +++ b/Dfe.Identifiers.Domain/Helpers/IdentifierMapping.cs @@ -0,0 +1,18 @@ +using Dfe.Identifiers.Domain.Identifiers; +using Dfe.Identifiers.Domain.Models; + +namespace Dfe.Identifiers.Domain.Helpers; + +public static class IdentifierMapping +{ + public static EstablishmentIdentifiers MapEstablishmentToIdentifiers(Establishment establishment) + { + return new EstablishmentIdentifiers(UKPRN: establishment.UKPRN, URN: establishment.URN.ToString(), + LAESTAB: $"{establishment.LocalAuthority.Code}/{establishment.EstablishmentNumber}"); + } + + public static TrustIdentifiers MapTrustToIdentifiers(Trust trust) + { + return new TrustIdentifiers(UID: trust.GroupUID, UKPRN: trust.UKPRN, TrustReference: trust.GroupID); + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Helpers/QueryMapping.cs b/Dfe.Identifiers.Domain/Helpers/QueryMapping.cs new file mode 100644 index 0000000..3cc1ff0 --- /dev/null +++ b/Dfe.Identifiers.Domain/Helpers/QueryMapping.cs @@ -0,0 +1,16 @@ +using Dfe.Identifiers.Domain.Models; +using Dfe.Identifiers.Domain.Query; + +namespace Dfe.Identifiers.Domain.Helpers; + +public static class QueryMapping +{ + public static Establishment ConvertQueryResultToEstablishment(EstablishmentQueryResult queryResult) + { + var result = queryResult.Establishment; + result.LocalAuthority = queryResult.LocalAuthority; + result.EstablishmentType = queryResult.EstablishmentType; + + return result; + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Identifiers/EstablishmentIdentifiers.cs b/Dfe.Identifiers.Domain/Identifiers/EstablishmentIdentifiers.cs new file mode 100644 index 0000000..656f394 --- /dev/null +++ b/Dfe.Identifiers.Domain/Identifiers/EstablishmentIdentifiers.cs @@ -0,0 +1,7 @@ +namespace Dfe.Identifiers.Domain.Identifiers; + +public record EstablishmentIdentifiers( + string? LAESTAB, + string? UKPRN, + string? URN +); \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Identifiers/IdentifiersCollection.cs b/Dfe.Identifiers.Domain/Identifiers/IdentifiersCollection.cs new file mode 100644 index 0000000..bf5cf15 --- /dev/null +++ b/Dfe.Identifiers.Domain/Identifiers/IdentifiersCollection.cs @@ -0,0 +1,3 @@ +namespace Dfe.Identifiers.Domain.Identifiers; + +public record IdentifiersCollection(TrustIdentifiers[] Trusts, EstablishmentIdentifiers[] Establishments); \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Identifiers/TrustIdentifiers.cs b/Dfe.Identifiers.Domain/Identifiers/TrustIdentifiers.cs new file mode 100644 index 0000000..77c066b --- /dev/null +++ b/Dfe.Identifiers.Domain/Identifiers/TrustIdentifiers.cs @@ -0,0 +1,7 @@ +namespace Dfe.Identifiers.Domain.Identifiers; + +public record TrustIdentifiers( + string? UID, + string? UKPRN, + string? TrustReference +); \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Models/EducationEstablishmentTrust.cs b/Dfe.Identifiers.Domain/Models/EducationEstablishmentTrust.cs new file mode 100644 index 0000000..667920a --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/EducationEstablishmentTrust.cs @@ -0,0 +1,13 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class EducationEstablishmentTrust + { + // Primary Key + public int SK { get; set; } + + // Foreign keys + public int TrustId { get; set; } + public int EducationEstablishmentId { get; set; } + } + +} diff --git a/Dfe.Identifiers.Domain/Models/Establishment.cs b/Dfe.Identifiers.Domain/Models/Establishment.cs new file mode 100644 index 0000000..c3d42a7 --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/Establishment.cs @@ -0,0 +1,34 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class Establishment + { + // Primary key + public long? SK { get; set; } + + // Foreign keys + public string? PK_GIAS_URN { get; set; } + public long? PK_CDM_ID { get; set; } + public int? URN { get; set; } + public long? LocalAuthorityId { get; set; } + public long? EstablishmentTypeId { get; set; } + public long? EstablishmentGroupTypeId { get; set; } + public long? EstablishmentStatusId { get; set; } + public long? RegionId { get; set; } + public int? EstablishmentNumber { get; set; } + public string? EstablishmentName { get; set; } + + public DateTime? Modified { get; set; } + public string? ModifiedBy { get; set; } + + public string? UKPRN { get; set; } + + public int? URNAtCurrentFullInspection { get; set; } + public int? URNAtPreviousFullInspection { get; set; } + public int? URNAtSection8Inspection { get; set; } + public string? GORregion { get; set; } + public string? GORregionCode { get; set; } + + public LocalAuthority? LocalAuthority { get; set; } + public EstablishmentType? EstablishmentType { get; set; } + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Models/EstablishmentType.cs b/Dfe.Identifiers.Domain/Models/EstablishmentType.cs new file mode 100644 index 0000000..f873b4e --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/EstablishmentType.cs @@ -0,0 +1,10 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class EstablishmentType + { + // Primary key + public long SK { get; set; } + public string? Name { get; set; } + public string? Code { get; set; } + } +} diff --git a/Dfe.Identifiers.Domain/Models/LocalAuthority.cs b/Dfe.Identifiers.Domain/Models/LocalAuthority.cs new file mode 100644 index 0000000..27ff468 --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/LocalAuthority.cs @@ -0,0 +1,10 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class LocalAuthority + { + // Primary key + public long SK { get; set; } + public string? Name { get; set; } + public string? Code { get; set; } + } +} diff --git a/Dfe.Identifiers.Domain/Models/Trust.cs b/Dfe.Identifiers.Domain/Models/Trust.cs new file mode 100644 index 0000000..9cbc9d0 --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/Trust.cs @@ -0,0 +1,23 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class Trust + { + // Primary key + public long? SK { get; set; } + public long? TrustTypeId { get; set; } + public long? RegionId { get; set; } + public long? TrustBandingId { get; set; } + public long? TrustStatusId { get; set; } + public string? GroupUID { get; set; } + public string? GroupID { get; set; } + public string? RID { get; set; } + public string? Name { get; set; } + public string? CompaniesHouseNumber { get; set; } + public string? TrustStatus { get; set; } + public DateTime? Modified { get; set; } + public string? ModifiedBy { get; set; } + public string? UKPRN { get; set; } + public string? UPIN { get; set; } + public TrustType? TrustType { get; set; } + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Domain/Models/TrustType.cs b/Dfe.Identifiers.Domain/Models/TrustType.cs new file mode 100644 index 0000000..d0ae1da --- /dev/null +++ b/Dfe.Identifiers.Domain/Models/TrustType.cs @@ -0,0 +1,10 @@ +namespace Dfe.Identifiers.Domain.Models +{ + public class TrustType + { + // Primary key + public long SK { get; set; } + public string? Name { get; set; } + public string? Code { get; set; } + } +} diff --git a/Dfe.Identifiers.Domain/Query/EstablishmentQueryResult.cs b/Dfe.Identifiers.Domain/Query/EstablishmentQueryResult.cs new file mode 100644 index 0000000..0cc513b --- /dev/null +++ b/Dfe.Identifiers.Domain/Query/EstablishmentQueryResult.cs @@ -0,0 +1,10 @@ +using Dfe.Identifiers.Domain.Models; + +namespace Dfe.Identifiers.Domain.Query; + +public record EstablishmentQueryResult +{ + public Establishment Establishment; + public LocalAuthority LocalAuthority; + public EstablishmentType? EstablishmentType; +}; \ No newline at end of file diff --git a/Dfe.Identifiers.Infrastructure/Context/MstrContext.cs b/Dfe.Identifiers.Infrastructure/Context/MstrContext.cs new file mode 100644 index 0000000..6bd91bc --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Context/MstrContext.cs @@ -0,0 +1,159 @@ +using Dfe.Identifiers.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Dfe.Identifiers.Infrastructure.Context; + +public class MstrContext : DbContext +{ + const string DEFAULT_SCHEMA = "mstr"; + + public MstrContext() + { + + } + + public MstrContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Trusts { get; set; } = null!; + public DbSet TrustTypes { get; set; } = null!; + public DbSet Establishments { get; set; } = null!; + public DbSet EstablishmentTypes { get; set; } = null!; + public DbSet EducationEstablishmentTrusts { get; set; } = null!; + public DbSet LocalAuthorities { get; set; } = null!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlServer("Server=localhost;Database=sip;Integrated Security=true;TrustServerCertificate=True"); + } + optionsBuilder.LogTo(Console.WriteLine); + } + + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(ConfigureTrust); + modelBuilder.Entity(ConfigureTrustType); + + modelBuilder.Entity(ConfigureEstablishment); + modelBuilder.Entity(ConfigureEstablishmentType); + modelBuilder.Entity(ConfigureEducationEstablishmentTrust); + modelBuilder.Entity(ConfigureLocalAuthority); + + base.OnModelCreating(modelBuilder); + } + + private void ConfigureEstablishment(EntityTypeBuilder establishmentConfiguration) + { + establishmentConfiguration.HasKey(e => e.SK); + + establishmentConfiguration.ToTable("EducationEstablishment", DEFAULT_SCHEMA); + + + establishmentConfiguration.Property(e => e.EstablishmentName).HasColumnName("EstablishmentName"); + establishmentConfiguration.Property(e => e.EstablishmentNumber).HasColumnName("EstablishmentNumber"); + establishmentConfiguration.Property(e => e.EstablishmentGroupTypeId).HasColumnName("FK_EstablishmentGroupType"); + establishmentConfiguration.Property(e => e.EstablishmentStatusId).HasColumnName("FK_EstablishmentStatus"); + establishmentConfiguration.Property(e => e.EstablishmentTypeId).HasColumnName("FK_EstablishmentType"); + establishmentConfiguration.Property(e => e.LocalAuthorityId).HasColumnName("FK_LocalAuthority"); + establishmentConfiguration.Property(e => e.RegionId).HasColumnName("FK_Region"); + establishmentConfiguration.Property(e => e.GORregion).HasColumnName("GORregion"); + establishmentConfiguration.Property(e => e.Modified).HasColumnName("Modified"); + establishmentConfiguration.Property(e => e.ModifiedBy).HasColumnName("Modified By"); + establishmentConfiguration.Property(e => e.PK_CDM_ID).HasColumnName("PK_CDM_ID"); + establishmentConfiguration.Property(e => e.PK_GIAS_URN).HasColumnName("PK_GIAS_URN").HasConversion(); + establishmentConfiguration.Property(e => e.UKPRN).HasColumnName("UKPRN"); + establishmentConfiguration.Property(e => e.URN).HasColumnName("URN"); + establishmentConfiguration.Property(e => e.URNAtCurrentFullInspection).HasColumnName("URN at Current full inspection"); + establishmentConfiguration.Property(e => e.URNAtPreviousFullInspection).HasColumnName("URN at Previous full inspection"); + establishmentConfiguration.Property(e => e.URNAtSection8Inspection).HasColumnName("URN at Section 8 inspection"); + establishmentConfiguration.Property(e => e.GORregionCode).HasColumnName("GORregion(code)"); + + establishmentConfiguration + .HasOne(x => x.EstablishmentType) + .WithMany() + .HasForeignKey(x => x.EstablishmentTypeId) + .IsRequired(false); + + establishmentConfiguration + .HasOne(x => x.LocalAuthority) + .WithMany() + .HasForeignKey(x => x.LocalAuthorityId) + .IsRequired(false); + } + + /// + /// New mapping for refactoring + /// + /// + + void ConfigureTrust(EntityTypeBuilder trustConfiguration) + { + trustConfiguration.HasKey(e => e.SK); + + trustConfiguration.ToTable("Trust", DEFAULT_SCHEMA); + + trustConfiguration.Property(e => e.TrustTypeId).HasColumnName("FK_TrustType"); + trustConfiguration.Property(e => e.RegionId).HasColumnName("FK_Region"); + trustConfiguration.Property(e => e.TrustBandingId).HasColumnName("FK_TrustBanding"); + trustConfiguration.Property(e => e.TrustStatusId).HasColumnName("FK_TrustStatus"); + trustConfiguration.Property(e => e.GroupUID).HasColumnName("Group UID").IsRequired(); + trustConfiguration.Property(e => e.GroupID).HasColumnName("Group ID"); + trustConfiguration.Property(e => e.RID).HasColumnName("RID"); + trustConfiguration.Property(e => e.Name).HasColumnName("Name").IsRequired(); + trustConfiguration.Property(e => e.CompaniesHouseNumber).HasColumnName("Companies House Number"); + trustConfiguration.Property(e => e.TrustStatus).HasColumnName("Trust Status"); + trustConfiguration.Property(e => e.Modified).HasColumnName("Modified"); + trustConfiguration.Property(e => e.ModifiedBy).HasColumnName("Modified By"); + trustConfiguration.Property(e => e.UKPRN).HasColumnName("UKPRN"); + trustConfiguration.Property(e => e.UPIN).HasColumnName("UPIN"); + + trustConfiguration + .HasOne(x => x.TrustType) + .WithMany() + .HasForeignKey(x => x.TrustTypeId); + } + + private void ConfigureTrustType(EntityTypeBuilder trustTypeConfiguration) + { + trustTypeConfiguration.HasKey(e => e.SK); + + trustTypeConfiguration.ToTable("Ref_TrustType", DEFAULT_SCHEMA); + + trustTypeConfiguration.HasData(new TrustType() { SK = 30, Code = "06", Name = "Multi-academy trust" }); + trustTypeConfiguration.HasData(new TrustType() { SK = 32, Code = "10", Name = "Single-academy trust" }); + } + private void ConfigureEducationEstablishmentTrust(EntityTypeBuilder entityBuilder) + { + entityBuilder.HasKey(e => e.SK); + entityBuilder.ToTable("EducationEstablishmentTrust", DEFAULT_SCHEMA); + + entityBuilder.Property(e => e.EducationEstablishmentId).HasColumnName("FK_EducationEstablishment"); + entityBuilder.Property(e => e.TrustId).HasColumnName("FK_Trust"); + } + + private void ConfigureLocalAuthority(EntityTypeBuilder localAuthorityConfiguration) + { + localAuthorityConfiguration.HasKey(e => e.SK); + localAuthorityConfiguration.ToTable("Ref_LocalAuthority", DEFAULT_SCHEMA); + + localAuthorityConfiguration.HasData(new LocalAuthority() { SK = 1, Code = "202", Name = "Barnsley" }); + localAuthorityConfiguration.HasData(new LocalAuthority() { SK = 2, Code = "203", Name = "Birmingham" }); + localAuthorityConfiguration.HasData(new LocalAuthority() { SK = 3, Code = "204", Name = "Bradford" }); + } + + private void ConfigureEstablishmentType(EntityTypeBuilder establishmentTypeConfiguration) + { + establishmentTypeConfiguration.HasKey(e => e.SK); + establishmentTypeConfiguration.ToTable("Ref_EducationEstablishmentType", DEFAULT_SCHEMA); + + establishmentTypeConfiguration.HasData(new EstablishmentType() { SK = 224, Code = "35", Name = "Free schools" }); + establishmentTypeConfiguration.HasData(new EstablishmentType() { SK = 228, Code = "18", Name = "Further education" }); + } + +} diff --git a/Dfe.Identifiers.Infrastructure/Dfe.Identifiers.Infrastructure.csproj b/Dfe.Identifiers.Infrastructure/Dfe.Identifiers.Infrastructure.csproj new file mode 100644 index 0000000..5eb4b23 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Dfe.Identifiers.Infrastructure.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Dfe.Identifiers.Infrastructure/Interfaces/IEstablishmentRepository.cs b/Dfe.Identifiers.Infrastructure/Interfaces/IEstablishmentRepository.cs new file mode 100644 index 0000000..0ec4ffd --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Interfaces/IEstablishmentRepository.cs @@ -0,0 +1,9 @@ +using Dfe.Identifiers.Domain.Models; + +namespace Dfe.Identifiers.Api.Interfaces; + +public interface IEstablishmentRepository +{ + Task> GetEstablishmentsByIdentifier(string identifier, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Dfe.Identifiers.Infrastructure/Interfaces/ITrustRepository.cs b/Dfe.Identifiers.Infrastructure/Interfaces/ITrustRepository.cs new file mode 100644 index 0000000..4f48664 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Interfaces/ITrustRepository.cs @@ -0,0 +1,8 @@ +using Dfe.Identifiers.Domain.Models; + +namespace Dfe.Identifiers.Api.Interfaces; + +public interface ITrustRepository +{ + Task> GetTrustsByIdentifier(string identifier, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.Designer.cs b/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.Designer.cs new file mode 100644 index 0000000..1226fa6 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.Designer.cs @@ -0,0 +1,730 @@ +// +using System; +using Dfe.Identifiers.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dfe.Identifiers.Infrastructure.Migrations +{ + [DbContext(typeof(MstrContext))] + [Migration("20240328120816_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.EducationEstablishmentTrust", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("EducationEstablishmentId") + .HasColumnType("int") + .HasColumnName("FK_EducationEstablishment"); + + b.Property("TrustId") + .HasColumnType("int") + .HasColumnName("FK_Trust"); + + b.HasKey("SK"); + + b.ToTable("EducationEstablishmentTrust", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Establishment", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("AddressLine1") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line1"); + + b.Property("AddressLine2") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line2"); + + b.Property("AddressLine3") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line3"); + + b.Property("AdministrativeDistrict") + .HasColumnType("nvarchar(max)") + .HasColumnName("Administrative District"); + + b.Property("BehaviourAndAttitudes") + .HasColumnType("int") + .HasColumnName("Behaviour and attitudes"); + + b.Property("CategoryOfConcern") + .HasColumnType("nvarchar(max)") + .HasColumnName("Category of concern"); + + b.Property("CloseDate") + .HasColumnType("datetime2") + .HasColumnName("CloseDate"); + + b.Property("County") + .HasColumnType("nvarchar(max)") + .HasColumnName("County"); + + b.Property("DateOfLatestShortInspection") + .HasColumnType("datetime2") + .HasColumnName("Date of latest short inspection"); + + b.Property("DidTheLatestShortInspectionConvertToAFullInspection") + .HasColumnType("nvarchar(max)") + .HasColumnName("Did the latest short inspection convert to a full inspection?"); + + b.Property("Diocese") + .HasColumnType("nvarchar(max)") + .HasColumnName("Diocese"); + + b.Property("DioceseCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Diocese(code)"); + + b.Property("EarlyYearsProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Early years provision (where applicable)"); + + b.Property("EffectivenessOfLeadershipAndManagement") + .HasColumnType("int") + .HasColumnName("Effectiveness of leadership and management"); + + b.Property("Email") + .HasColumnType("nvarchar(max)") + .HasColumnName("Email"); + + b.Property("EstablishmentGroupTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentGroupType"); + + b.Property("EstablishmentName") + .HasColumnType("nvarchar(max)") + .HasColumnName("EstablishmentName"); + + b.Property("EstablishmentNumber") + .HasColumnType("int") + .HasColumnName("EstablishmentNumber"); + + b.Property("EstablishmentStatusId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentStatus"); + + b.Property("EstablishmentTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentType"); + + b.Property("GORregion") + .HasColumnType("nvarchar(max)") + .HasColumnName("GORregion"); + + b.Property("GORregionCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("GORregion(code)"); + + b.Property("GiasLastChangedDate") + .HasColumnType("datetime2") + .HasColumnName("GiasLastChangedDate"); + + b.Property("HeadFirstName") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadFirstName"); + + b.Property("HeadLastName") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadLastName"); + + b.Property("HeadPreferredJobTitle") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadPreferredJobTitle"); + + b.Property("HeadTitle") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadTitle"); + + b.Property("InspectionEndDate") + .HasColumnType("datetime2") + .HasColumnName("Inspection end date"); + + b.Property("InspectionStartDate") + .HasColumnType("datetime2") + .HasColumnName("Inspection start date"); + + b.Property("InspectionType") + .HasColumnType("nvarchar(max)") + .HasColumnName("Inspection type"); + + b.Property("IsSafeguardingEffective") + .HasColumnType("nvarchar(max)") + .HasColumnName("Is safeguarding effective?"); + + b.Property("Latitude") + .HasColumnType("float") + .HasColumnName("Latitude"); + + b.Property("LocalAuthorityId") + .HasColumnType("bigint") + .HasColumnName("FK_LocalAuthority"); + + b.Property("Longitude") + .HasColumnType("float") + .HasColumnName("Longitude"); + + b.Property("MainPhone") + .HasColumnType("nvarchar(max)") + .HasColumnName("Main Phone"); + + b.Property("Modified") + .HasColumnType("datetime2") + .HasColumnName("Modified"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)") + .HasColumnName("Modified By"); + + b.Property("NumberOfBoys") + .HasColumnType("int") + .HasColumnName("NumberOfBoys"); + + b.Property("NumberOfGirls") + .HasColumnType("int") + .HasColumnName("NumberOfGirls"); + + b.Property("NumberOfOtherSection8InspectionsSinceLastFullInspection") + .HasColumnType("int") + .HasColumnName("Number of other section 8 inspections since last full inspection"); + + b.Property("NumberOfPupils") + .HasColumnType("nvarchar(max)") + .HasColumnName("NumberOfPupils"); + + b.Property("NumberOfShortInspectionsSinceLastFullInspection") + .HasColumnType("int") + .HasColumnName("Number of short inspections since last full inspection"); + + b.Property("OfstedLastInspection") + .HasColumnType("nvarchar(max)") + .HasColumnName("OfstedLastInspection"); + + b.Property("OfstedRating") + .HasColumnType("nvarchar(max)") + .HasColumnName("OfstedRating"); + + b.Property("OpenDate") + .HasColumnType("nvarchar(max)") + .HasColumnName("OpenDate"); + + b.Property("OverallEffectiveness") + .HasColumnType("int") + .HasColumnName("Overall effectiveness"); + + b.Property("PK_CDM_ID") + .HasColumnType("bigint") + .HasColumnName("PK_CDM_ID"); + + b.Property("PK_GIAS_URN") + .HasColumnType("int") + .HasColumnName("PK_GIAS_URN"); + + b.Property("ParliamentaryConstituency") + .HasColumnType("nvarchar(max)") + .HasColumnName("Parliamentary constituency"); + + b.Property("ParliamentaryConstituencyCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("ParliamentaryConstituency(code)"); + + b.Property("PercentageFSM") + .HasColumnType("nvarchar(max)") + .HasColumnName("PercentageFSM"); + + b.Property("PersonalDevelopment") + .HasColumnType("int") + .HasColumnName("Personal development"); + + b.Property("PhaseOfEducation") + .HasColumnType("nvarchar(max)") + .HasColumnName("PhaseOfEducation"); + + b.Property("PhaseOfEducationCode") + .HasColumnType("int") + .HasColumnName("PhaseOfEducation(code)"); + + b.Property("Postcode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Postcode"); + + b.Property("PreviousCategoryOfConcern") + .HasColumnType("nvarchar(max)") + .HasColumnName("Previous category of concern"); + + b.Property("PreviousEarlyYearsProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Previous early years provision (where applicable)"); + + b.Property("PreviousFullInspectionOverallEffectiveness") + .HasColumnType("int") + .HasColumnName("Previous full inspection overall effectiveness"); + + b.Property("PreviousInspectionEndDate") + .HasColumnType("datetime2") + .HasColumnName("Previous inspection end date"); + + b.Property("PreviousInspectionStartDate") + .HasColumnType("datetime2") + .HasColumnName("Previous inspection start date"); + + b.Property("PreviousIsSafeguardingEffective") + .HasColumnType("nvarchar(max)") + .HasColumnName("Previous is safeguarding effective?"); + + b.Property("PreviousPublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Previous publication date"); + + b.Property("ProjectLead") + .HasColumnType("nvarchar(max)") + .HasColumnName("Project Lead"); + + b.Property("PublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Publication date"); + + b.Property("QualityOfEducation") + .HasColumnType("int") + .HasColumnName("Quality of education"); + + b.Property("ReasonEstablishmentClosed") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReasonEstablishmentClosed"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("FK_Region"); + + b.Property("ReligiousCharacter") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousCharacter"); + + b.Property("ReligiousCharacterCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousCharacter(code)"); + + b.Property("ReligiousEthos") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousEthos"); + + b.Property("RouteOfProject") + .HasColumnType("nvarchar(max)") + .HasColumnName("Route of Project"); + + b.Property("SFSOTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("SFSO Territory"); + + b.Property("SchoolCapacity") + .HasColumnType("nvarchar(max)") + .HasColumnName("SchoolCapacity"); + + b.Property("SenUnitCapacity") + .HasColumnType("int") + .HasColumnName("SenUnitCapacity"); + + b.Property("SenUnitOnRoll") + .HasColumnType("int") + .HasColumnName("SenUnitOnRoll"); + + b.Property("ShortInspectionOverallOutcome") + .HasColumnType("nvarchar(max)") + .HasColumnName("Short inspection overall outcome"); + + b.Property("ShortInspectionPublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Short inspection publication date"); + + b.Property("SixthFormProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Sixth form provision (where applicable)"); + + b.Property("StatutoryHighAge") + .HasColumnType("nvarchar(max)") + .HasColumnName("StatutoryHighAge"); + + b.Property("StatutoryLowAge") + .HasColumnType("nvarchar(max)") + .HasColumnName("StatutoryLowAge"); + + b.Property("TheIncomeDeprivationAffectingChildrenIndexIDACIQuintile") + .HasColumnType("int") + .HasColumnName("The income deprivation affecting children index (IDACI) quintile"); + + b.Property("Town") + .HasColumnType("nvarchar(max)") + .HasColumnName("Town"); + + b.Property("UKPRN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UKPRN"); + + b.Property("URN") + .HasColumnType("int") + .HasColumnName("URN"); + + b.Property("URNAtCurrentFullInspection") + .HasColumnType("int") + .HasColumnName("URN at Current full inspection"); + + b.Property("URNAtPreviousFullInspection") + .HasColumnType("int") + .HasColumnName("URN at Previous full inspection"); + + b.Property("URNAtSection8Inspection") + .HasColumnType("int") + .HasColumnName("URN at Section 8 inspection"); + + b.Property("Website") + .HasColumnType("nvarchar(max)") + .HasColumnName("Website"); + + b.HasKey("SK"); + + b.HasIndex("EstablishmentTypeId"); + + b.HasIndex("LocalAuthorityId"); + + b.ToTable("EducationEstablishment", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.EstablishmentType", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_EducationEstablishmentType", "mstr"); + + b.HasData( + new + { + SK = 224L, + Code = "35", + Name = "Free schools" + }, + new + { + SK = 228L, + Code = "18", + Name = "Further education" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.LocalAuthority", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_LocalAuthority", "mstr"); + + b.HasData( + new + { + SK = 1L, + Code = "202", + Name = "Barnsley" + }, + new + { + SK = 2L, + Code = "203", + Name = "Birmingham" + }, + new + { + SK = 3L, + Code = "204", + Name = "Bradford" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Trust", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("AMSDTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("AMSD Territory"); + + b.Property("AddressLine1") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line1"); + + b.Property("AddressLine2") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line2"); + + b.Property("AddressLine3") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line3"); + + b.Property("ClosedDate") + .HasColumnType("datetime2") + .HasColumnName("Closed Date"); + + b.Property("CompaniesHouseNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("Companies House Number"); + + b.Property("County") + .HasColumnType("nvarchar(max)") + .HasColumnName("County"); + + b.Property("CurrentSingleListGrouping") + .HasColumnType("nvarchar(max)") + .HasColumnName("Current Single List Grouping"); + + b.Property("DateActionPlannedFor") + .HasColumnType("datetime2") + .HasColumnName("Date Action Planned For"); + + b.Property("DateEnteredOntoSingleList") + .HasColumnType("datetime2") + .HasColumnName("Date Entered Onto Single List"); + + b.Property("DateOfGroupingDecision") + .HasColumnType("datetime2") + .HasColumnName("Date of Grouping Decision"); + + b.Property("DateOfTrustReviewMeeting") + .HasColumnType("datetime2") + .HasColumnName("Date of Trust Review Meeting"); + + b.Property("EfficiencyICFPReviewCompleted") + .HasColumnType("nvarchar(max)") + .HasColumnName("Efficiency ICFP Review Completed"); + + b.Property("EfficiencyICFPReviewOther") + .HasColumnType("nvarchar(max)") + .HasColumnName("Efficiency ICFP Review Other"); + + b.Property("ExternalGovernanceReviewDate") + .HasColumnType("datetime2") + .HasColumnName("External Governance Review Date"); + + b.Property("FollowUpLetterSent") + .HasColumnType("nvarchar(max)") + .HasColumnName("Follow Up Letter Sent"); + + b.Property("GroupID") + .HasColumnType("nvarchar(max)") + .HasColumnName("Group ID"); + + b.Property("GroupUID") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Group UID"); + + b.Property("IncorporatedOnOpenDate") + .HasColumnType("datetime2") + .HasColumnName("Incorporated on (open date)"); + + b.Property("JoinedDate") + .HasColumnType("datetime2") + .HasColumnName("Joined Date"); + + b.Property("LeadAMSDTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("Lead AMSD Territory"); + + b.Property("LinkToWorkplaceForEfficiencyICFPReview") + .HasColumnType("nvarchar(max)") + .HasColumnName("Link To Workplace For Efficiency ICFP Review"); + + b.Property("MainPhone") + .HasColumnType("nvarchar(max)") + .HasColumnName("Main Phone"); + + b.Property("Modified") + .HasColumnType("datetime2") + .HasColumnName("Modified"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)") + .HasColumnName("Modified By"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("NumberInTrust") + .HasColumnType("int") + .HasColumnName("Number In Trust"); + + b.Property("Postcode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Postcode"); + + b.Property("PrioritisedForReview") + .HasColumnType("nvarchar(max)") + .HasColumnName("Prioritised for Review"); + + b.Property("RID") + .HasColumnType("nvarchar(max)") + .HasColumnName("RID"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("FK_Region"); + + b.Property("Town") + .HasColumnType("nvarchar(max)") + .HasColumnName("Town"); + + b.Property("TrustBandingId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustBanding"); + + b.Property("TrustPerformanceAndRiskDateOfMeeting") + .HasColumnType("datetime2") + .HasColumnName("Trust Performance And Risk Date Of Meeting"); + + b.Property("TrustReviewWriteUp") + .HasColumnType("nvarchar(max)") + .HasColumnName("Trust Review Write Up"); + + b.Property("TrustStatus") + .HasColumnType("nvarchar(max)") + .HasColumnName("Trust Status"); + + b.Property("TrustStatusId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustStatus"); + + b.Property("TrustTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustType"); + + b.Property("UKPRN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UKPRN"); + + b.Property("UPIN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UPIN"); + + b.Property("WIPSummaryGoesToMinister") + .HasColumnType("nvarchar(max)") + .HasColumnName("WIP Summary Goes To Minister"); + + b.HasKey("SK"); + + b.HasIndex("TrustTypeId"); + + b.ToTable("Trust", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.TrustType", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_TrustType", "mstr"); + + b.HasData( + new + { + SK = 30L, + Code = "06", + Name = "Multi-academy trust" + }, + new + { + SK = 32L, + Code = "10", + Name = "Single-academy trust" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Establishment", b => + { + b.HasOne("Dfe.Identifiers.Domain.Models.EstablishmentType", "EstablishmentType") + .WithMany() + .HasForeignKey("EstablishmentTypeId"); + + b.HasOne("Dfe.Identifiers.Domain.Models.LocalAuthority", "LocalAuthority") + .WithMany() + .HasForeignKey("LocalAuthorityId"); + + b.Navigation("EstablishmentType"); + + b.Navigation("LocalAuthority"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Trust", b => + { + b.HasOne("Dfe.Identifiers.Domain.Models.TrustType", "TrustType") + .WithMany() + .HasForeignKey("TrustTypeId"); + + b.Navigation("TrustType"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.cs b/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.cs new file mode 100644 index 0000000..9416162 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Migrations/20240328120816_Initial.cs @@ -0,0 +1,330 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dfe.Identifiers.Infrastructure.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "mstr"); + + migrationBuilder.CreateTable( + name: "EducationEstablishmentTrust", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FK_Trust = table.Column(type: "int", nullable: false), + FK_EducationEstablishment = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EducationEstablishmentTrust", x => x.SK); + }); + + migrationBuilder.CreateTable( + name: "Ref_EducationEstablishmentType", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Code = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ref_EducationEstablishmentType", x => x.SK); + }); + + migrationBuilder.CreateTable( + name: "Ref_LocalAuthority", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Code = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ref_LocalAuthority", x => x.SK); + }); + + migrationBuilder.CreateTable( + name: "Ref_TrustType", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Code = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ref_TrustType", x => x.SK); + }); + + migrationBuilder.CreateTable( + name: "EducationEstablishment", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PK_GIAS_URN = table.Column(type: "int", nullable: true), + PK_CDM_ID = table.Column(type: "bigint", nullable: true), + URN = table.Column(type: "int", nullable: true), + FK_LocalAuthority = table.Column(type: "bigint", nullable: true), + FK_EstablishmentType = table.Column(type: "bigint", nullable: true), + FK_EstablishmentGroupType = table.Column(type: "bigint", nullable: true), + FK_EstablishmentStatus = table.Column(type: "bigint", nullable: true), + FK_Region = table.Column(type: "bigint", nullable: true), + EstablishmentNumber = table.Column(type: "int", nullable: true), + EstablishmentName = table.Column(type: "nvarchar(max)", nullable: true), + Latitude = table.Column(type: "float", nullable: true), + Longitude = table.Column(type: "float", nullable: true), + MainPhone = table.Column(name: "Main Phone", type: "nvarchar(max)", nullable: true), + AddressLine1 = table.Column(name: "Address Line1", type: "nvarchar(max)", nullable: true), + AddressLine2 = table.Column(name: "Address Line2", type: "nvarchar(max)", nullable: true), + AddressLine3 = table.Column(name: "Address Line3", type: "nvarchar(max)", nullable: true), + Town = table.Column(type: "nvarchar(max)", nullable: true), + County = table.Column(type: "nvarchar(max)", nullable: true), + Postcode = table.Column(type: "nvarchar(max)", nullable: true), + Email = table.Column(type: "nvarchar(max)", nullable: true), + Website = table.Column(type: "nvarchar(max)", nullable: true), + StatutoryLowAge = table.Column(type: "nvarchar(max)", nullable: true), + StatutoryHighAge = table.Column(type: "nvarchar(max)", nullable: true), + SchoolCapacity = table.Column(type: "nvarchar(max)", nullable: true), + NumberOfPupils = table.Column(type: "nvarchar(max)", nullable: true), + OfstedLastInspection = table.Column(type: "nvarchar(max)", nullable: true), + OfstedRating = table.Column(type: "nvarchar(max)", nullable: true), + OpenDate = table.Column(type: "nvarchar(max)", nullable: true), + Modified = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(name: "Modified By", type: "nvarchar(max)", nullable: true), + TheincomedeprivationaffectingchildrenindexIDACIquintile = table.Column(name: "The income deprivation affecting children index (IDACI) quintile", type: "int", nullable: true), + Numberofshortinspectionssincelastfullinspection = table.Column(name: "Number of short inspections since last full inspection", type: "int", nullable: true), + Dateoflatestshortinspection = table.Column(name: "Date of latest short inspection", type: "datetime2", nullable: true), + Shortinspectionpublicationdate = table.Column(name: "Short inspection publication date", type: "datetime2", nullable: true), + Didthelatestshortinspectionconverttoafullinspection = table.Column(name: "Did the latest short inspection convert to a full inspection?", type: "nvarchar(max)", nullable: true), + Shortinspectionoveralloutcome = table.Column(name: "Short inspection overall outcome", type: "nvarchar(max)", nullable: true), + Numberofothersection8inspectionssincelastfullinspection = table.Column(name: "Number of other section 8 inspections since last full inspection", type: "int", nullable: true), + Inspectiontype = table.Column(name: "Inspection type", type: "nvarchar(max)", nullable: true), + Inspectionstartdate = table.Column(name: "Inspection start date", type: "datetime2", nullable: true), + Inspectionenddate = table.Column(name: "Inspection end date", type: "datetime2", nullable: true), + Publicationdate = table.Column(name: "Publication date", type: "datetime2", nullable: true), + Overalleffectiveness = table.Column(name: "Overall effectiveness", type: "int", nullable: true), + Categoryofconcern = table.Column(name: "Category of concern", type: "nvarchar(max)", nullable: true), + Earlyyearsprovisionwhereapplicable = table.Column(name: "Early years provision (where applicable)", type: "int", nullable: true), + Effectivenessofleadershipandmanagement = table.Column(name: "Effectiveness of leadership and management", type: "int", nullable: true), + Issafeguardingeffective = table.Column(name: "Is safeguarding effective?", type: "nvarchar(max)", nullable: true), + Previousinspectionstartdate = table.Column(name: "Previous inspection start date", type: "datetime2", nullable: true), + Previousinspectionenddate = table.Column(name: "Previous inspection end date", type: "datetime2", nullable: true), + Previouspublicationdate = table.Column(name: "Previous publication date", type: "datetime2", nullable: true), + Previousfullinspectionoveralleffectiveness = table.Column(name: "Previous full inspection overall effectiveness", type: "int", nullable: true), + Previouscategoryofconcern = table.Column(name: "Previous category of concern", type: "nvarchar(max)", nullable: true), + Previousearlyyearsprovisionwhereapplicable = table.Column(name: "Previous early years provision (where applicable)", type: "int", nullable: true), + Previousissafeguardingeffective = table.Column(name: "Previous is safeguarding effective?", type: "nvarchar(max)", nullable: true), + HeadTitle = table.Column(type: "nvarchar(max)", nullable: true), + HeadFirstName = table.Column(type: "nvarchar(max)", nullable: true), + HeadLastName = table.Column(type: "nvarchar(max)", nullable: true), + HeadPreferredJobTitle = table.Column(type: "nvarchar(max)", nullable: true), + PhaseOfEducation = table.Column(type: "nvarchar(max)", nullable: true), + PercentageFSM = table.Column(type: "nvarchar(max)", nullable: true), + UKPRN = table.Column(type: "nvarchar(max)", nullable: true), + ReligiousCharacter = table.Column(type: "nvarchar(max)", nullable: true), + ReligiousEthos = table.Column(type: "nvarchar(max)", nullable: true), + Diocese = table.Column(type: "nvarchar(max)", nullable: true), + ReasonEstablishmentClosed = table.Column(type: "nvarchar(max)", nullable: true), + CloseDate = table.Column(type: "datetime2", nullable: true), + ProjectLead = table.Column(name: "Project Lead", type: "nvarchar(max)", nullable: true), + Parliamentaryconstituency = table.Column(name: "Parliamentary constituency", type: "nvarchar(max)", nullable: true), + Qualityofeducation = table.Column(name: "Quality of education", type: "int", nullable: true), + Behaviourandattitudes = table.Column(name: "Behaviour and attitudes", type: "int", nullable: true), + Personaldevelopment = table.Column(name: "Personal development", type: "int", nullable: true), + Sixthformprovisionwhereapplicable = table.Column(name: "Sixth form provision (where applicable)", type: "int", nullable: true), + URNatCurrentfullinspection = table.Column(name: "URN at Current full inspection", type: "int", nullable: true), + URNatPreviousfullinspection = table.Column(name: "URN at Previous full inspection", type: "int", nullable: true), + URNatSection8inspection = table.Column(name: "URN at Section 8 inspection", type: "int", nullable: true), + AdministrativeDistrict = table.Column(name: "Administrative District", type: "nvarchar(max)", nullable: true), + RouteofProject = table.Column(name: "Route of Project", type: "nvarchar(max)", nullable: true), + GORregion = table.Column(type: "nvarchar(max)", nullable: true), + SFSOTerritory = table.Column(name: "SFSO Territory", type: "nvarchar(max)", nullable: true), + GiasLastChangedDate = table.Column(type: "datetime2", nullable: true), + NumberOfBoys = table.Column(type: "int", nullable: true), + NumberOfGirls = table.Column(type: "int", nullable: true), + Diocesecode = table.Column(name: "Diocese(code)", type: "nvarchar(max)", nullable: true), + GORregioncode = table.Column(name: "GORregion(code)", type: "nvarchar(max)", nullable: true), + ReligiousCharactercode = table.Column(name: "ReligiousCharacter(code)", type: "nvarchar(max)", nullable: true), + ParliamentaryConstituencycode = table.Column(name: "ParliamentaryConstituency(code)", type: "nvarchar(max)", nullable: true), + PhaseOfEducationcode = table.Column(name: "PhaseOfEducation(code)", type: "int", nullable: true), + SenUnitCapacity = table.Column(type: "int", nullable: true), + SenUnitOnRoll = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EducationEstablishment", x => x.SK); + table.ForeignKey( + name: "FK_EducationEstablishment_Ref_EducationEstablishmentType_FK_EstablishmentType", + column: x => x.FK_EstablishmentType, + principalSchema: "mstr", + principalTable: "Ref_EducationEstablishmentType", + principalColumn: "SK"); + table.ForeignKey( + name: "FK_EducationEstablishment_Ref_LocalAuthority_FK_LocalAuthority", + column: x => x.FK_LocalAuthority, + principalSchema: "mstr", + principalTable: "Ref_LocalAuthority", + principalColumn: "SK"); + }); + + migrationBuilder.CreateTable( + name: "Trust", + schema: "mstr", + columns: table => new + { + SK = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FK_TrustType = table.Column(type: "bigint", nullable: true), + FK_Region = table.Column(type: "bigint", nullable: true), + FK_TrustBanding = table.Column(type: "bigint", nullable: true), + FK_TrustStatus = table.Column(type: "bigint", nullable: true), + GroupUID = table.Column(name: "Group UID", type: "nvarchar(max)", nullable: false), + GroupID = table.Column(name: "Group ID", type: "nvarchar(max)", nullable: true), + RID = table.Column(type: "nvarchar(max)", nullable: true), + Name = table.Column(type: "nvarchar(max)", nullable: false), + CompaniesHouseNumber = table.Column(name: "Companies House Number", type: "nvarchar(max)", nullable: true), + ClosedDate = table.Column(name: "Closed Date", type: "datetime2", nullable: true), + TrustStatus = table.Column(name: "Trust Status", type: "nvarchar(max)", nullable: true), + JoinedDate = table.Column(name: "Joined Date", type: "datetime2", nullable: true), + MainPhone = table.Column(name: "Main Phone", type: "nvarchar(max)", nullable: true), + AddressLine1 = table.Column(name: "Address Line1", type: "nvarchar(max)", nullable: true), + AddressLine2 = table.Column(name: "Address Line2", type: "nvarchar(max)", nullable: true), + AddressLine3 = table.Column(name: "Address Line3", type: "nvarchar(max)", nullable: true), + Town = table.Column(type: "nvarchar(max)", nullable: true), + County = table.Column(type: "nvarchar(max)", nullable: true), + Postcode = table.Column(type: "nvarchar(max)", nullable: true), + PrioritisedforReview = table.Column(name: "Prioritised for Review", type: "nvarchar(max)", nullable: true), + CurrentSingleListGrouping = table.Column(name: "Current Single List Grouping", type: "nvarchar(max)", nullable: true), + DateofGroupingDecision = table.Column(name: "Date of Grouping Decision", type: "datetime2", nullable: true), + DateEnteredOntoSingleList = table.Column(name: "Date Entered Onto Single List", type: "datetime2", nullable: true), + TrustReviewWriteUp = table.Column(name: "Trust Review Write Up", type: "nvarchar(max)", nullable: true), + DateofTrustReviewMeeting = table.Column(name: "Date of Trust Review Meeting", type: "datetime2", nullable: true), + FollowUpLetterSent = table.Column(name: "Follow Up Letter Sent", type: "nvarchar(max)", nullable: true), + DateActionPlannedFor = table.Column(name: "Date Action Planned For", type: "datetime2", nullable: true), + WIPSummaryGoesToMinister = table.Column(name: "WIP Summary Goes To Minister", type: "nvarchar(max)", nullable: true), + ExternalGovernanceReviewDate = table.Column(name: "External Governance Review Date", type: "datetime2", nullable: true), + EfficiencyICFPReviewCompleted = table.Column(name: "Efficiency ICFP Review Completed", type: "nvarchar(max)", nullable: true), + EfficiencyICFPReviewOther = table.Column(name: "Efficiency ICFP Review Other", type: "nvarchar(max)", nullable: true), + LinkToWorkplaceForEfficiencyICFPReview = table.Column(name: "Link To Workplace For Efficiency ICFP Review", type: "nvarchar(max)", nullable: true), + NumberInTrust = table.Column(name: "Number In Trust", type: "int", nullable: true), + Modified = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(name: "Modified By", type: "nvarchar(max)", nullable: true), + AMSDTerritory = table.Column(name: "AMSD Territory", type: "nvarchar(max)", nullable: true), + LeadAMSDTerritory = table.Column(name: "Lead AMSD Territory", type: "nvarchar(max)", nullable: true), + UKPRN = table.Column(type: "nvarchar(max)", nullable: true), + TrustPerformanceAndRiskDateOfMeeting = table.Column(name: "Trust Performance And Risk Date Of Meeting", type: "datetime2", nullable: true), + UPIN = table.Column(type: "nvarchar(max)", nullable: true), + Incorporatedonopendate = table.Column(name: "Incorporated on (open date)", type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Trust", x => x.SK); + table.ForeignKey( + name: "FK_Trust_Ref_TrustType_FK_TrustType", + column: x => x.FK_TrustType, + principalSchema: "mstr", + principalTable: "Ref_TrustType", + principalColumn: "SK"); + }); + + migrationBuilder.InsertData( + schema: "mstr", + table: "Ref_EducationEstablishmentType", + columns: new[] { "SK", "Code", "Name" }, + values: new object[,] + { + { 224L, "35", "Free schools" }, + { 228L, "18", "Further education" } + }); + + migrationBuilder.InsertData( + schema: "mstr", + table: "Ref_LocalAuthority", + columns: new[] { "SK", "Code", "Name" }, + values: new object[,] + { + { 1L, "202", "Barnsley" }, + { 2L, "203", "Birmingham" }, + { 3L, "204", "Bradford" } + }); + + migrationBuilder.InsertData( + schema: "mstr", + table: "Ref_TrustType", + columns: new[] { "SK", "Code", "Name" }, + values: new object[,] + { + { 30L, "06", "Multi-academy trust" }, + { 32L, "10", "Single-academy trust" } + }); + + migrationBuilder.CreateIndex( + name: "IX_EducationEstablishment_FK_EstablishmentType", + schema: "mstr", + table: "EducationEstablishment", + column: "FK_EstablishmentType"); + + migrationBuilder.CreateIndex( + name: "IX_EducationEstablishment_FK_LocalAuthority", + schema: "mstr", + table: "EducationEstablishment", + column: "FK_LocalAuthority"); + + migrationBuilder.CreateIndex( + name: "IX_Trust_FK_TrustType", + schema: "mstr", + table: "Trust", + column: "FK_TrustType"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EducationEstablishment", + schema: "mstr"); + + migrationBuilder.DropTable( + name: "EducationEstablishmentTrust", + schema: "mstr"); + + migrationBuilder.DropTable( + name: "Trust", + schema: "mstr"); + + migrationBuilder.DropTable( + name: "Ref_EducationEstablishmentType", + schema: "mstr"); + + migrationBuilder.DropTable( + name: "Ref_LocalAuthority", + schema: "mstr"); + + migrationBuilder.DropTable( + name: "Ref_TrustType", + schema: "mstr"); + } + } +} diff --git a/Dfe.Identifiers.Infrastructure/Migrations/MstrContextModelSnapshot.cs b/Dfe.Identifiers.Infrastructure/Migrations/MstrContextModelSnapshot.cs new file mode 100644 index 0000000..817e835 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Migrations/MstrContextModelSnapshot.cs @@ -0,0 +1,727 @@ +// +using System; +using Dfe.Identifiers.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dfe.Identifiers.Infrastructure.Migrations +{ + [DbContext(typeof(MstrContext))] + partial class MstrContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.EducationEstablishmentTrust", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("EducationEstablishmentId") + .HasColumnType("int") + .HasColumnName("FK_EducationEstablishment"); + + b.Property("TrustId") + .HasColumnType("int") + .HasColumnName("FK_Trust"); + + b.HasKey("SK"); + + b.ToTable("EducationEstablishmentTrust", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Establishment", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("AddressLine1") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line1"); + + b.Property("AddressLine2") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line2"); + + b.Property("AddressLine3") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line3"); + + b.Property("AdministrativeDistrict") + .HasColumnType("nvarchar(max)") + .HasColumnName("Administrative District"); + + b.Property("BehaviourAndAttitudes") + .HasColumnType("int") + .HasColumnName("Behaviour and attitudes"); + + b.Property("CategoryOfConcern") + .HasColumnType("nvarchar(max)") + .HasColumnName("Category of concern"); + + b.Property("CloseDate") + .HasColumnType("datetime2") + .HasColumnName("CloseDate"); + + b.Property("County") + .HasColumnType("nvarchar(max)") + .HasColumnName("County"); + + b.Property("DateOfLatestShortInspection") + .HasColumnType("datetime2") + .HasColumnName("Date of latest short inspection"); + + b.Property("DidTheLatestShortInspectionConvertToAFullInspection") + .HasColumnType("nvarchar(max)") + .HasColumnName("Did the latest short inspection convert to a full inspection?"); + + b.Property("Diocese") + .HasColumnType("nvarchar(max)") + .HasColumnName("Diocese"); + + b.Property("DioceseCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Diocese(code)"); + + b.Property("EarlyYearsProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Early years provision (where applicable)"); + + b.Property("EffectivenessOfLeadershipAndManagement") + .HasColumnType("int") + .HasColumnName("Effectiveness of leadership and management"); + + b.Property("Email") + .HasColumnType("nvarchar(max)") + .HasColumnName("Email"); + + b.Property("EstablishmentGroupTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentGroupType"); + + b.Property("EstablishmentName") + .HasColumnType("nvarchar(max)") + .HasColumnName("EstablishmentName"); + + b.Property("EstablishmentNumber") + .HasColumnType("int") + .HasColumnName("EstablishmentNumber"); + + b.Property("EstablishmentStatusId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentStatus"); + + b.Property("EstablishmentTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_EstablishmentType"); + + b.Property("GORregion") + .HasColumnType("nvarchar(max)") + .HasColumnName("GORregion"); + + b.Property("GORregionCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("GORregion(code)"); + + b.Property("GiasLastChangedDate") + .HasColumnType("datetime2") + .HasColumnName("GiasLastChangedDate"); + + b.Property("HeadFirstName") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadFirstName"); + + b.Property("HeadLastName") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadLastName"); + + b.Property("HeadPreferredJobTitle") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadPreferredJobTitle"); + + b.Property("HeadTitle") + .HasColumnType("nvarchar(max)") + .HasColumnName("HeadTitle"); + + b.Property("InspectionEndDate") + .HasColumnType("datetime2") + .HasColumnName("Inspection end date"); + + b.Property("InspectionStartDate") + .HasColumnType("datetime2") + .HasColumnName("Inspection start date"); + + b.Property("InspectionType") + .HasColumnType("nvarchar(max)") + .HasColumnName("Inspection type"); + + b.Property("IsSafeguardingEffective") + .HasColumnType("nvarchar(max)") + .HasColumnName("Is safeguarding effective?"); + + b.Property("Latitude") + .HasColumnType("float") + .HasColumnName("Latitude"); + + b.Property("LocalAuthorityId") + .HasColumnType("bigint") + .HasColumnName("FK_LocalAuthority"); + + b.Property("Longitude") + .HasColumnType("float") + .HasColumnName("Longitude"); + + b.Property("MainPhone") + .HasColumnType("nvarchar(max)") + .HasColumnName("Main Phone"); + + b.Property("Modified") + .HasColumnType("datetime2") + .HasColumnName("Modified"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)") + .HasColumnName("Modified By"); + + b.Property("NumberOfBoys") + .HasColumnType("int") + .HasColumnName("NumberOfBoys"); + + b.Property("NumberOfGirls") + .HasColumnType("int") + .HasColumnName("NumberOfGirls"); + + b.Property("NumberOfOtherSection8InspectionsSinceLastFullInspection") + .HasColumnType("int") + .HasColumnName("Number of other section 8 inspections since last full inspection"); + + b.Property("NumberOfPupils") + .HasColumnType("nvarchar(max)") + .HasColumnName("NumberOfPupils"); + + b.Property("NumberOfShortInspectionsSinceLastFullInspection") + .HasColumnType("int") + .HasColumnName("Number of short inspections since last full inspection"); + + b.Property("OfstedLastInspection") + .HasColumnType("nvarchar(max)") + .HasColumnName("OfstedLastInspection"); + + b.Property("OfstedRating") + .HasColumnType("nvarchar(max)") + .HasColumnName("OfstedRating"); + + b.Property("OpenDate") + .HasColumnType("nvarchar(max)") + .HasColumnName("OpenDate"); + + b.Property("OverallEffectiveness") + .HasColumnType("int") + .HasColumnName("Overall effectiveness"); + + b.Property("PK_CDM_ID") + .HasColumnType("bigint") + .HasColumnName("PK_CDM_ID"); + + b.Property("PK_GIAS_URN") + .HasColumnType("int") + .HasColumnName("PK_GIAS_URN"); + + b.Property("ParliamentaryConstituency") + .HasColumnType("nvarchar(max)") + .HasColumnName("Parliamentary constituency"); + + b.Property("ParliamentaryConstituencyCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("ParliamentaryConstituency(code)"); + + b.Property("PercentageFSM") + .HasColumnType("nvarchar(max)") + .HasColumnName("PercentageFSM"); + + b.Property("PersonalDevelopment") + .HasColumnType("int") + .HasColumnName("Personal development"); + + b.Property("PhaseOfEducation") + .HasColumnType("nvarchar(max)") + .HasColumnName("PhaseOfEducation"); + + b.Property("PhaseOfEducationCode") + .HasColumnType("int") + .HasColumnName("PhaseOfEducation(code)"); + + b.Property("Postcode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Postcode"); + + b.Property("PreviousCategoryOfConcern") + .HasColumnType("nvarchar(max)") + .HasColumnName("Previous category of concern"); + + b.Property("PreviousEarlyYearsProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Previous early years provision (where applicable)"); + + b.Property("PreviousFullInspectionOverallEffectiveness") + .HasColumnType("int") + .HasColumnName("Previous full inspection overall effectiveness"); + + b.Property("PreviousInspectionEndDate") + .HasColumnType("datetime2") + .HasColumnName("Previous inspection end date"); + + b.Property("PreviousInspectionStartDate") + .HasColumnType("datetime2") + .HasColumnName("Previous inspection start date"); + + b.Property("PreviousIsSafeguardingEffective") + .HasColumnType("nvarchar(max)") + .HasColumnName("Previous is safeguarding effective?"); + + b.Property("PreviousPublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Previous publication date"); + + b.Property("ProjectLead") + .HasColumnType("nvarchar(max)") + .HasColumnName("Project Lead"); + + b.Property("PublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Publication date"); + + b.Property("QualityOfEducation") + .HasColumnType("int") + .HasColumnName("Quality of education"); + + b.Property("ReasonEstablishmentClosed") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReasonEstablishmentClosed"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("FK_Region"); + + b.Property("ReligiousCharacter") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousCharacter"); + + b.Property("ReligiousCharacterCode") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousCharacter(code)"); + + b.Property("ReligiousEthos") + .HasColumnType("nvarchar(max)") + .HasColumnName("ReligiousEthos"); + + b.Property("RouteOfProject") + .HasColumnType("nvarchar(max)") + .HasColumnName("Route of Project"); + + b.Property("SFSOTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("SFSO Territory"); + + b.Property("SchoolCapacity") + .HasColumnType("nvarchar(max)") + .HasColumnName("SchoolCapacity"); + + b.Property("SenUnitCapacity") + .HasColumnType("int") + .HasColumnName("SenUnitCapacity"); + + b.Property("SenUnitOnRoll") + .HasColumnType("int") + .HasColumnName("SenUnitOnRoll"); + + b.Property("ShortInspectionOverallOutcome") + .HasColumnType("nvarchar(max)") + .HasColumnName("Short inspection overall outcome"); + + b.Property("ShortInspectionPublicationDate") + .HasColumnType("datetime2") + .HasColumnName("Short inspection publication date"); + + b.Property("SixthFormProvisionWhereApplicable") + .HasColumnType("int") + .HasColumnName("Sixth form provision (where applicable)"); + + b.Property("StatutoryHighAge") + .HasColumnType("nvarchar(max)") + .HasColumnName("StatutoryHighAge"); + + b.Property("StatutoryLowAge") + .HasColumnType("nvarchar(max)") + .HasColumnName("StatutoryLowAge"); + + b.Property("TheIncomeDeprivationAffectingChildrenIndexIDACIQuintile") + .HasColumnType("int") + .HasColumnName("The income deprivation affecting children index (IDACI) quintile"); + + b.Property("Town") + .HasColumnType("nvarchar(max)") + .HasColumnName("Town"); + + b.Property("UKPRN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UKPRN"); + + b.Property("URN") + .HasColumnType("int") + .HasColumnName("URN"); + + b.Property("URNAtCurrentFullInspection") + .HasColumnType("int") + .HasColumnName("URN at Current full inspection"); + + b.Property("URNAtPreviousFullInspection") + .HasColumnType("int") + .HasColumnName("URN at Previous full inspection"); + + b.Property("URNAtSection8Inspection") + .HasColumnType("int") + .HasColumnName("URN at Section 8 inspection"); + + b.Property("Website") + .HasColumnType("nvarchar(max)") + .HasColumnName("Website"); + + b.HasKey("SK"); + + b.HasIndex("EstablishmentTypeId"); + + b.HasIndex("LocalAuthorityId"); + + b.ToTable("EducationEstablishment", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.EstablishmentType", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_EducationEstablishmentType", "mstr"); + + b.HasData( + new + { + SK = 224L, + Code = "35", + Name = "Free schools" + }, + new + { + SK = 228L, + Code = "18", + Name = "Further education" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.LocalAuthority", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_LocalAuthority", "mstr"); + + b.HasData( + new + { + SK = 1L, + Code = "202", + Name = "Barnsley" + }, + new + { + SK = 2L, + Code = "203", + Name = "Birmingham" + }, + new + { + SK = 3L, + Code = "204", + Name = "Bradford" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Trust", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("AMSDTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("AMSD Territory"); + + b.Property("AddressLine1") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line1"); + + b.Property("AddressLine2") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line2"); + + b.Property("AddressLine3") + .HasColumnType("nvarchar(max)") + .HasColumnName("Address Line3"); + + b.Property("ClosedDate") + .HasColumnType("datetime2") + .HasColumnName("Closed Date"); + + b.Property("CompaniesHouseNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("Companies House Number"); + + b.Property("County") + .HasColumnType("nvarchar(max)") + .HasColumnName("County"); + + b.Property("CurrentSingleListGrouping") + .HasColumnType("nvarchar(max)") + .HasColumnName("Current Single List Grouping"); + + b.Property("DateActionPlannedFor") + .HasColumnType("datetime2") + .HasColumnName("Date Action Planned For"); + + b.Property("DateEnteredOntoSingleList") + .HasColumnType("datetime2") + .HasColumnName("Date Entered Onto Single List"); + + b.Property("DateOfGroupingDecision") + .HasColumnType("datetime2") + .HasColumnName("Date of Grouping Decision"); + + b.Property("DateOfTrustReviewMeeting") + .HasColumnType("datetime2") + .HasColumnName("Date of Trust Review Meeting"); + + b.Property("EfficiencyICFPReviewCompleted") + .HasColumnType("nvarchar(max)") + .HasColumnName("Efficiency ICFP Review Completed"); + + b.Property("EfficiencyICFPReviewOther") + .HasColumnType("nvarchar(max)") + .HasColumnName("Efficiency ICFP Review Other"); + + b.Property("ExternalGovernanceReviewDate") + .HasColumnType("datetime2") + .HasColumnName("External Governance Review Date"); + + b.Property("FollowUpLetterSent") + .HasColumnType("nvarchar(max)") + .HasColumnName("Follow Up Letter Sent"); + + b.Property("GroupID") + .HasColumnType("nvarchar(max)") + .HasColumnName("Group ID"); + + b.Property("GroupUID") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Group UID"); + + b.Property("IncorporatedOnOpenDate") + .HasColumnType("datetime2") + .HasColumnName("Incorporated on (open date)"); + + b.Property("JoinedDate") + .HasColumnType("datetime2") + .HasColumnName("Joined Date"); + + b.Property("LeadAMSDTerritory") + .HasColumnType("nvarchar(max)") + .HasColumnName("Lead AMSD Territory"); + + b.Property("LinkToWorkplaceForEfficiencyICFPReview") + .HasColumnType("nvarchar(max)") + .HasColumnName("Link To Workplace For Efficiency ICFP Review"); + + b.Property("MainPhone") + .HasColumnType("nvarchar(max)") + .HasColumnName("Main Phone"); + + b.Property("Modified") + .HasColumnType("datetime2") + .HasColumnName("Modified"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)") + .HasColumnName("Modified By"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("NumberInTrust") + .HasColumnType("int") + .HasColumnName("Number In Trust"); + + b.Property("Postcode") + .HasColumnType("nvarchar(max)") + .HasColumnName("Postcode"); + + b.Property("PrioritisedForReview") + .HasColumnType("nvarchar(max)") + .HasColumnName("Prioritised for Review"); + + b.Property("RID") + .HasColumnType("nvarchar(max)") + .HasColumnName("RID"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("FK_Region"); + + b.Property("Town") + .HasColumnType("nvarchar(max)") + .HasColumnName("Town"); + + b.Property("TrustBandingId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustBanding"); + + b.Property("TrustPerformanceAndRiskDateOfMeeting") + .HasColumnType("datetime2") + .HasColumnName("Trust Performance And Risk Date Of Meeting"); + + b.Property("TrustReviewWriteUp") + .HasColumnType("nvarchar(max)") + .HasColumnName("Trust Review Write Up"); + + b.Property("TrustStatus") + .HasColumnType("nvarchar(max)") + .HasColumnName("Trust Status"); + + b.Property("TrustStatusId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustStatus"); + + b.Property("TrustTypeId") + .HasColumnType("bigint") + .HasColumnName("FK_TrustType"); + + b.Property("UKPRN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UKPRN"); + + b.Property("UPIN") + .HasColumnType("nvarchar(max)") + .HasColumnName("UPIN"); + + b.Property("WIPSummaryGoesToMinister") + .HasColumnType("nvarchar(max)") + .HasColumnName("WIP Summary Goes To Minister"); + + b.HasKey("SK"); + + b.HasIndex("TrustTypeId"); + + b.ToTable("Trust", "mstr"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.TrustType", b => + { + b.Property("SK") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SK")); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SK"); + + b.ToTable("Ref_TrustType", "mstr"); + + b.HasData( + new + { + SK = 30L, + Code = "06", + Name = "Multi-academy trust" + }, + new + { + SK = 32L, + Code = "10", + Name = "Single-academy trust" + }); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Establishment", b => + { + b.HasOne("Dfe.Identifiers.Domain.Models.EstablishmentType", "EstablishmentType") + .WithMany() + .HasForeignKey("EstablishmentTypeId"); + + b.HasOne("Dfe.Identifiers.Domain.Models.LocalAuthority", "LocalAuthority") + .WithMany() + .HasForeignKey("LocalAuthorityId"); + + b.Navigation("EstablishmentType"); + + b.Navigation("LocalAuthority"); + }); + + modelBuilder.Entity("Dfe.Identifiers.Domain.Models.Trust", b => + { + b.HasOne("Dfe.Identifiers.Domain.Models.TrustType", "TrustType") + .WithMany() + .HasForeignKey("TrustTypeId"); + + b.Navigation("TrustType"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Dfe.Identifiers.Infrastructure/Repositories/EstablishmentRepository.cs b/Dfe.Identifiers.Infrastructure/Repositories/EstablishmentRepository.cs new file mode 100644 index 0000000..591ed3e --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Repositories/EstablishmentRepository.cs @@ -0,0 +1,32 @@ +using Dfe.Identifiers.Api.Interfaces; +using Dfe.Identifiers.Domain.Models; +using Dfe.Identifiers.Domain.Query; +using Dfe.Identifiers.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; +using static Dfe.Identifiers.Domain.Helpers.QueryMapping; + +namespace Dfe.Identifiers.Infrastructure.Repositories; + +public class EstablishmentRepository(MstrContext context) : IEstablishmentRepository +{ + public async Task> GetEstablishmentsByIdentifier(string identifier, + CancellationToken cancellationToken) + { + var establishments = await BaseQuery().Where(establishment => + establishment.Establishment.UKPRN == identifier || establishment.Establishment.URN.ToString() == identifier || + identifier.StartsWith(establishment.LocalAuthority.Code) && identifier.EndsWith(establishment.Establishment.EstablishmentNumber.ToString()) + ) + .ToListAsync(cancellationToken); + var results = establishments.Select(ConvertQueryResultToEstablishment).ToList(); + return results; + } + private IQueryable BaseQuery() + { + var query = from establishment in context.Establishments + from establishmentType in context.EstablishmentTypes.Where(establishmentType => establishmentType.SK == establishment.EstablishmentTypeId).DefaultIfEmpty() + from localAuthority in context.LocalAuthorities.Where(localAuthority => localAuthority.SK == establishment.LocalAuthorityId).DefaultIfEmpty() + select new EstablishmentQueryResult { Establishment = establishment, LocalAuthority = localAuthority, EstablishmentType = establishmentType }; + + return query; + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.Infrastructure/Repositories/TrustRepository.cs b/Dfe.Identifiers.Infrastructure/Repositories/TrustRepository.cs new file mode 100644 index 0000000..c888616 --- /dev/null +++ b/Dfe.Identifiers.Infrastructure/Repositories/TrustRepository.cs @@ -0,0 +1,27 @@ +using Dfe.Identifiers.Api.Interfaces; +using Dfe.Identifiers.Domain.Models; +using Dfe.Identifiers.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; + +namespace Dfe.Identifiers.Infrastructure.Repositories +{ + public class TrustRepository(MstrContext context) : ITrustRepository + { + public async Task> GetTrustsByIdentifier(string identifier, CancellationToken cancellationToken) + { + var trusts = await DefaultIncludes().AsNoTracking().Where(trust => + identifier.Equals(trust.UKPRN) || identifier.Equals(trust.GroupID) || identifier.Equals(trust.GroupUID)) + .ToListAsync(cancellationToken).ConfigureAwait(false); + return trusts; + } + + private IQueryable DefaultIncludes() + { + var query = context.Trusts + .Include(trust => trust.TrustType) + .AsQueryable(); + + return query; + } + } +} \ No newline at end of file diff --git a/Dfe.Identifiers.sln b/Dfe.Identifiers.sln new file mode 100644 index 0000000..8850765 --- /dev/null +++ b/Dfe.Identifiers.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Identifiers.Api", "Dfe.Identifiers.Api\Dfe.Identifiers.Api.csproj", "{1A202DB5-F4D6-4E57-B433-CEEC22827EED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Identifiers.Api.Test", "Dfe.Identifiers.Api.Test\Dfe.Identifiers.Api.Test.csproj", "{F14F6DE7-F898-425E-BD58-274F52D875E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Identifiers.Application", "Dfe.Identifiers.Application\Dfe.Identifiers.Application.csproj", "{0FE90D55-11EA-490C-98EC-9DE1FEB4F01A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Identifiers.Domain", "Dfe.Identifiers.Domain\Dfe.Identifiers.Domain.csproj", "{B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Identifiers.Infrastructure", "Dfe.Identifiers.Infrastructure\Dfe.Identifiers.Infrastructure.csproj", "{F686E187-424E-4ED1-A623-2357C07CF2E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{4454B268-9107-4F0D-8A36-7C77500C61A1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{4808E126-5301-4E06-86B3-33D7A4208BA0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{E44B3F03-C939-4CE1-BF35-4413F37133CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A202DB5-F4D6-4E57-B433-CEEC22827EED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A202DB5-F4D6-4E57-B433-CEEC22827EED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A202DB5-F4D6-4E57-B433-CEEC22827EED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A202DB5-F4D6-4E57-B433-CEEC22827EED}.Release|Any CPU.Build.0 = Release|Any CPU + {F14F6DE7-F898-425E-BD58-274F52D875E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F14F6DE7-F898-425E-BD58-274F52D875E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F14F6DE7-F898-425E-BD58-274F52D875E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F14F6DE7-F898-425E-BD58-274F52D875E5}.Release|Any CPU.Build.0 = Release|Any CPU + {0FE90D55-11EA-490C-98EC-9DE1FEB4F01A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FE90D55-11EA-490C-98EC-9DE1FEB4F01A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FE90D55-11EA-490C-98EC-9DE1FEB4F01A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FE90D55-11EA-490C-98EC-9DE1FEB4F01A}.Release|Any CPU.Build.0 = Release|Any CPU + {B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB}.Release|Any CPU.Build.0 = Release|Any CPU + {F686E187-424E-4ED1-A623-2357C07CF2E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F686E187-424E-4ED1-A623-2357C07CF2E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F686E187-424E-4ED1-A623-2357C07CF2E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F686E187-424E-4ED1-A623-2357C07CF2E7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {84562030-666E-437E-B821-8D8B85EBE41A} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0FE90D55-11EA-490C-98EC-9DE1FEB4F01A} = {4454B268-9107-4F0D-8A36-7C77500C61A1} + {B633146F-1E4A-4CFD-A5EA-D64D7F25E1FB} = {4808E126-5301-4E06-86B3-33D7A4208BA0} + {F686E187-424E-4ED1-A623-2357C07CF2E7} = {E44B3F03-C939-4CE1-BF35-4413F37133CA} + EndGlobalSection +EndGlobal diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2ddceca..0000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim AS build -WORKDIR /build - -ENV DEBIAN_FRONTEND=noninteractive - -COPY . . - -# Build project... - - -ARG ASPNET_IMAGE_TAG -FROM mcr.microsoft.com/dotnet/aspnet:7.0.17-bullseye-slim AS final - -COPY --from=build /app /app - -WORKDIR /app -COPY ./script/web-docker-entrypoint.sh ./docker-entrypoint.sh -RUN chmod +x ./docker-entrypoint.sh -EXPOSE 80/tcp diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..242c627 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,18 @@ +ARG ASPNET_IMAGE_TAG=7.0-bullseye-slim-amd64 +ARG DOTNET_SDK=7.0 + +# Stage 2 - Build and publish dotnet application +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_SDK} AS publish +COPY . . +RUN dotnet restore DfE.IdentifiersApi +RUN dotnet build DfE.IdentifiersApi -c Release +RUN dotnet publish DfE.IdentifiersApi -c Release -o /app --no-build + +COPY ./docker/docker-entrypoint.sh /app/docker-entrypoint.sh + +# Stage 3 - Put into Docker container that will actually be run +FROM mcr.microsoft.com/dotnet/aspnet:${ASPNET_IMAGE_TAG} AS final +COPY --from=publish /app /app +WORKDIR /app +RUN chmod +x ./docker-entrypoint.sh +EXPOSE 80/tcp diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..c6f71fd --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" +services: + webapp: + build: + context: .. + dockerfile: docker/Dockerfile + command: /bin/bash -c "./docker-entrypoint.sh dotnet DfE.IdentifiersApi.dll" + ports: + - 80:80/tcp + env_file: + - .env diff --git a/script/web-docker-entrypoint.sh b/docker/docker-entrypoint.sh similarity index 100% rename from script/web-docker-entrypoint.sh rename to docker/docker-entrypoint.sh diff --git a/terraform/.terraform-docs.yml b/terraform/.terraform-docs.yml new file mode 100644 index 0000000..a691780 --- /dev/null +++ b/terraform/.terraform-docs.yml @@ -0,0 +1,26 @@ +--- +formatter: "markdown table" +version: "~> 0.16" +settings: + anchor: true + default: true + description: false + escape: true + hide-empty: false + html: true + indent: 2 + lockfile: true + read-comments: true + required: true + sensitive: true + type: true +sort: + enabled: true + by: name +output: + file: README.md + mode: inject + template: |- + + {{ .Content }} + diff --git a/terraform/.terraform-version b/terraform/.terraform-version new file mode 100644 index 0000000..6a126f4 --- /dev/null +++ b/terraform/.terraform-version @@ -0,0 +1 @@ +1.7.5 diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..ae80823 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,118 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/azure/azapi" { + version = "1.12.1" + constraints = ">= 1.6.0" + hashes = [ + "h1:EaQL7pQCRm5iL2zy/dG7rOe2OZ0ZypuyVnpQAiAwJmM=", + "zh:1cf52e685ceb04e73e13fbf3f3036bff23a3274a4ceda8693c0612076a588166", + "zh:321b59c2a67c6cb4e5cf0dbe2cc978f5389d781e8b391f9b75bf4d830abd2ffe", + "zh:49046bd8020c3b44c6b5dc67041f181e4fff45e3bc1a9ff0646dd20c21c8ce47", + "zh:5784d0c326ec4825571577bc39b253019bd3b1030c19d67ca3436df2d7ba01c8", + "zh:5ad7e18d26f170c01888d8e65dab7aa475089aac7bf0106526fd57cdd56533bc", + "zh:6695854f4f655673bea85e37444bf0c070b440dba4bc269aa144d0f6b7c1cc5f", + "zh:7f372c897da6b9ad90869a8eb85b37dad4dff2d5d311b3eca1a2e6373e2271ed", + "zh:8afa1a2be1dada4e8be4ab72d9d56f36af1e486c9353d04aabf6e79db7310125", + "zh:90809364619238c45185bff25c7d9c4fde34253561d8183ebbe797456c44bc9c", + "zh:9338d44650c9e68e10a6bc2d69f7beacd5059e6ac681d2e388e80a1652d9c183", + "zh:c94ee6fb1df2c1d35f338107b5e73cdba86c4ecf9dcde95e2ca0132cbbd4bd7c", + "zh:de231d363b1a664c6b5d3af8d3b9cf542d04d4506fb9458ba6c8ebf94e0e32ae", + ] +} + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.47.0" + constraints = ">= 2.37.1" + hashes = [ + "h1:g8+gBFM4QVOEQFqAEs5pR6iXpbGvgPvcEi1evHwziyw=", + "zh:1372d81eb24ef3b4b00ea350fe87219f22da51691b8e42ce91d662f6c2a8af5e", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:1e654a74d171d6ff8f9f6f67e3ff1421d4c5e56a18607703626bf12cd23ba001", + "zh:35227fad617a0509c64ab5759a8b703b10d244877f1aa5416bfbcc100c96996f", + "zh:357f553f0d78d46a96c7b2ed06d25ee0fc60fc5be19812ccb5d969fa47d62e17", + "zh:58faa2940065137e3e87d02eba59ab5cd7137d7a18caf225e660d1788f274569", + "zh:7308eda0339620fa24f47cedd22221fc2c02cab9d5be1710c09a783aea84eb3a", + "zh:863eabf7f908a8263e28d8aa2ad1381affd6bb5c67755216781f674ef214100e", + "zh:8b95b595a7c14ed7b56194d03cdec253527e7a146c1c58961be09e6b5c50baee", + "zh:afbca6b4fac9a0a488bc22ff9e51a8f14e986137d25275068fd932f379a51d57", + "zh:c6aadec4c81a44c3ffc22c2d90ffc6706bf5a9a903a395d896477516f4be6cbb", + "zh:e54a59de7d4ef0f3a18f91fed0b54a2bce18257ae2ee1df8a88226e1023c5811", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.96.0" + constraints = ">= 3.52.0, >= 3.67.0, >= 3.76.0" + hashes = [ + "h1:o1BGLLHL33WaMjlUYSCr6zo7nuw4mKrpcLee14fSLc0=", + "zh:2fb3f3c309bc8b040cd63f3a5711d4a6fc107e653a760063ec3ee6417912d14d", + "zh:45b83f492bd371c837df6d68e96ee3ab89faa00f740bca915187b344fd795ae3", + "zh:4a8b9f31da14ae824b2358fe772bb03ee79283d3294985f2acb48a0d4cd950bb", + "zh:4ab3c38b6141a0bd52d9216383d256771c0bfdc1869dccf52f414ed04290ed35", + "zh:6772d182dde23ff3fe10497f104a866cfc1cb848988f830100247363f9dd9ef7", + "zh:85875de128bc2d119c63f16116773594345ad5d0e8a3b464f7612479900df640", + "zh:9cd696005f4cfab4662d7db81039a64fc4c66d6eeedddf0808f2e97bc8af25f4", + "zh:bdc8921161253d3bff8f951cbf63f73f856bbda0ee2e9f51af60d74464059d21", + "zh:d7320767f7cde3796906f453a99ba80284fe8479ce127a4703ecf45dd9ef1321", + "zh:e0c28b79c0bf5004a9d094a68ec0c887c7df307f2cedeed2cbbef567c61443c6", + "zh:f069aa8e951508ea812cb8fef73f79594212864014eb85db39cdea2c648f69ee", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.2" + constraints = ">= 3.2.1" + hashes = [ + "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", + "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", + "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", + "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", + "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", + "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", + "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", + "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", + "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", + "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", + "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", + ] +} + +provider "registry.terraform.io/statuscakedev/statuscake" { + version = "2.2.2" + constraints = ">= 2.1.0" + hashes = [ + "h1:+BzbpPyJvjkn34pRYswz7K/a1v9LqcaH/tXeVlU0oH0=", + "h1:2y6sc3WpkhqovLq2zZh994HPQYfnX7cmHOH3NKb8EqA=", + "h1:GoqF8hHJHM2EceT1oCZRvGX0xkmwYrgEmcGvYWrKyaQ=", + "h1:OoqL/K/eNLahbfMwJvYZHo9kacafjtrJKhd6cLrubZ4=", + "h1:PFVKpIeHsABv8YGW6tRcs/sDTIpCI5+cfc/fx+daymU=", + "h1:QBfk+RVQhzJM0eQDzCDF82baabL23bVN23YuXJ+x/hI=", + "h1:SJiQusSTE2fC3lPNbtW0aExYwSQAZTe920yzux2iwkY=", + "h1:WU3zNJtKJmskgNhHnJzjLS0UnrlO//MJQ6G3EbKcWVA=", + "h1:eZCJ+mvkgqzB6JobTWLyZmN6hCjLcHIddbyUpZnBCwA=", + "h1:latCWLUpxz+tlByrUhC5SpekEJSJo7SpEEfFq+cj1Ug=", + "h1:nVaJkDBk4sv0yWFzg3p+yeJGzE8mB4KJv3Q6/UgU164=", + "h1:phIY76XC5ij8dZUSHJqU4Aj/vq17wq3rQFZ9guLtVF4=", + "h1:v9W0xSeWF2bz3b3txV0s43el5DCxCZGZO13hiVBkz9c=", + "h1:wFoZJfmNvG6XTf65NLai67geSHqYV1Tilx7OITrHilE=", + "zh:0916313344c579d6e05d70f88129a10fe48f7dabe0e61cad17874d6c496f288d", + "zh:0d491ff72c2eda6482855033ca2146c5ace1663d07cb3da7253b59ed2e2ec6f4", + "zh:11fffbce18eb3d3c283e877242f477e0c561342c19090240b60af7d948bd84ac", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:1c6116092c59bc0010e147dc7832ae981d528f235cef563e5ae05a93ef8bac5c", + "zh:1f13a543b1d32cc1f1e3d2ed5ca83445f088787c335690fe20dee1203488a8bc", + "zh:23f55fd0714696c3863f892646a79a780cea1923c0c4d2b1064df735ba4156f5", + "zh:288fbb4431ac12014aa5aab10ee7166dfe71fd1158464e06e8a527aa4919e64e", + "zh:2a0746d3c8cdc7d0875df0f4605d81130e6da0ce6196d0f5f8661fe9191bc2ab", + "zh:31d660756abc53d252906fe1239fad58ecb6c1d0dbb087408a8af266be1f9ae4", + "zh:6765281d1b7efb41085c5375660b6c6b271babc5d09b2c030bec7176f7a7bfb6", + "zh:6d7204eadb667c1f2cab762a7a97234cf47452f0ecad680f5106c8fd02524c87", + "zh:b70a97b0eba471d683e23ce8744a5c67ef9952086e2d6f5825b72f32b6caff89", + "zh:df89ee4aaba88faea33d33384a36ea52588b5514644d335f724378a682d8d9da", + "zh:ffe2e1e7224ea5f4dd65b9eef45464e3124cfc824473b04e7af429f9177f2375", + ] +} diff --git a/terraform/Brewfile b/terraform/Brewfile new file mode 100644 index 0000000..000ace3 --- /dev/null +++ b/terraform/Brewfile @@ -0,0 +1,6 @@ +brew "tfenv" +brew "terraform-docs" +brew "tfsec" +brew "az" +brew "coreutils" +brew "jq" diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..3a2e8f7 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,196 @@ +This documentation covers the deployment of the infrastructure to host the app. + +## Azure infrastructure + +The infrastructure is managed using [Terraform](https://www.terraform.io/).
+The state is stored remotely in encrypted Azure storage.
+[Terraform workspaces](https://www.terraform.io/docs/state/workspaces.html) are used to separate environments. + +#### Configuring the storage backend + +The Terraform state is stored remotely in Azure, this allows multiple team members to +make changes and means the state file is backed up. The state file contains +sensitive information so access to it should be restricted, and it should be stored +encrypted at rest. + +##### Create a new storage backend + +This step only needs to be done once per project (eg. not per environment). +If it has already been created, obtain the storage backend attributes and skip to the next step. + +The [Azure tutorial](https://docs.microsoft.com/en-us/azure/developer/terraform/store-state-in-azure-storage) outlines the steps to create a storage account and container for the state file. You will need: + +- resource_group_name: The name of the resource group used for the Azure Storage account. +- storage_account_name: The name of the Azure Storage account. +- container_name: The name of the blob container. +- key: The name of the state store file to be created. + +##### Create a backend configuration file + +Create a new file named `backend.vars` with the following content: + +``` +resource_group_name = [the name of the Azure resource group] +storage_account_name = [the name of the Azure Storage account] +container_name = [the name of the blob container] +key = "terraform.tstate" +``` + +##### Install dependencies + +We can use [Homebrew](https://brew.sh) to install the dependecies we need to deploy the infrastructure (eg. tfenv, Azure cli). +These are listed in the `Brewfile` + +to install, run: + +``` +$ brew bundle +``` + +##### Log into azure with the Azure CLI + +Log in to your account: + +``` +$ az login +``` + +Confirm which account you are currently using: + +``` +$ az account show +``` + +To list the available subscriptions, run: + +``` +$ az account list +``` + +Then if needed, switch to it using the 'id': + +``` +$ az account set --subscription +``` + +##### Initialise Terraform + +Install the required terraform version with the Terraform version manager `tfenv`: + +``` +$ tfenv install +``` + +Initialize Terraform to download the required Terraform modules and configure the remote state backend +to use the settings you specified in the previous step. + +`$ terraform init -backend-config=backend.vars` + +##### Create a Terraform variables file + +Each environment will need it's own `tfvars` file. + +Copy the `terraform.tfvars.example` to `environment-name.tfvars` and modify the contents as required + +##### Create the infrastructure + +Now Terraform has been initialised you can create a workspace if needed: + +`$ terraform workspace new staging` + +Or to check what workspaces already exist: + +`$ terraform workspace list` + +Switch to the new or existing workspace: + +`$ terraform workspace select staging` + +Plan the changes: + +`$ terraform plan -var-file=staging.tfvars` + +Terraform will ask you to provide any variables not specified in an `*.auto.tfvars` file. +Now you can run: + +`$ terraform apply -var-file=staging.tfvars` + +If everything looks good, answer `yes` and wait for the new infrastructure to be created. + +##### Azure resources + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.7 | +| [azurerm](#requirement\_azurerm) | >= 3.67.0 | +| [statuscake](#requirement\_statuscake) | >= 2.1.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [azure\_container\_apps\_hosting](#module\_azure\_container\_apps\_hosting) | github.com/DFE-Digital/terraform-azurerm-container-apps-hosting | v1.5.0 | +| [statuscake-tls-monitor](#module\_statuscake-tls-monitor) | github.com/dfe-digital/terraform-statuscake-tls-monitor | v0.1.3 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_location](#input\_azure\_location) | Azure location in which to launch resources. | `string` | n/a | yes | +| [cdn\_frontdoor\_custom\_domains](#input\_cdn\_frontdoor\_custom\_domains) | Azure CDN Front Door custom domains. If they are within the DNS zone (optionally created), the Validation TXT records and ALIAS/CNAME records will be created | `list(string)` | n/a | yes | +| [cdn\_frontdoor\_enable\_rate\_limiting](#input\_cdn\_frontdoor\_enable\_rate\_limiting) | Enable CDN Front Door Rate Limiting. This will create a WAF policy, and CDN security policy. For pricing reasons, there will only be one WAF policy created. | `bool` | n/a | yes | +| [cdn\_frontdoor\_forwarding\_protocol](#input\_cdn\_frontdoor\_forwarding\_protocol) | Azure CDN Front Door forwarding protocol | `string` | `"HttpOnly"` | no | +| [cdn\_frontdoor\_health\_probe\_path](#input\_cdn\_frontdoor\_health\_probe\_path) | Specifies the path relative to the origin that is used to determine the health of the origin. | `string` | `"/"` | no | +| [cdn\_frontdoor\_health\_probe\_protocol](#input\_cdn\_frontdoor\_health\_probe\_protocol) | Use Http or Https | `string` | `"Http"` | no | +| [cdn\_frontdoor\_host\_add\_response\_headers](#input\_cdn\_frontdoor\_host\_add\_response\_headers) | List of response headers to add at the CDN Front Door `[{ "name" = "Strict-Transport-Security", "value" = "max-age=31536000" }]` | `list(map(string))` | n/a | yes | +| [cdn\_frontdoor\_origin\_fqdn\_override](#input\_cdn\_frontdoor\_origin\_fqdn\_override) | Manually specify the hostname that the CDN Front Door should target. Defaults to the Container App FQDN | `string` | `""` | no | +| [cdn\_frontdoor\_origin\_host\_header\_override](#input\_cdn\_frontdoor\_origin\_host\_header\_override) | Manually specify the host header that the CDN sends to the target. Defaults to the recieved host header. Set to null to set it to the host\_name (`cdn_frontdoor_origin_fqdn_override`) | `string` | `""` | no | +| [container\_apps\_allow\_ips\_inbound](#input\_container\_apps\_allow\_ips\_inbound) | Restricts access to the Container Apps by creating a network security group rule that only allow inbound traffic from the provided list of IPs | `list(string)` | `[]` | no | +| [container\_command](#input\_container\_command) | Container command | `list(any)` | n/a | yes | +| [container\_health\_probe\_path](#input\_container\_health\_probe\_path) | Specifies the path that is used to determine the liveness of the Container | `string` | `"/"` | no | +| [container\_health\_probe\_protocol](#input\_container\_health\_probe\_protocol) | Use HTTPS or a TCP connection for the Container liveness probe | `string` | `"tcp"` | no | +| [container\_scale\_http\_concurrency](#input\_container\_scale\_http\_concurrency) | When the number of concurrent HTTP requests exceeds this value, then another replica is added. Replicas continue to add to the pool up to the max-replicas amount. | `number` | `10` | no | +| [container\_secret\_environment\_variables](#input\_container\_secret\_environment\_variables) | Container secret environment variables | `map(string)` | n/a | yes | +| [dns\_ns\_records](#input\_dns\_ns\_records) | DNS NS records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
records : list(string)
})
)
| n/a | yes | +| [dns\_txt\_records](#input\_dns\_txt\_records) | DNS TXT records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
records : list(string)
})
)
| n/a | yes | +| [dns\_zone\_domain\_name](#input\_dns\_zone\_domain\_name) | DNS zone domain name. If created, records will automatically be created to point to the CDN. | `string` | n/a | yes | +| [enable\_cdn\_frontdoor](#input\_enable\_cdn\_frontdoor) | Enable Azure CDN FrontDoor. This will use the Container Apps endpoint as the origin. | `bool` | n/a | yes | +| [enable\_container\_health\_probe](#input\_enable\_container\_health\_probe) | Enable liveness probes for the Container | `bool` | `true` | no | +| [enable\_container\_registry](#input\_enable\_container\_registry) | Set to true to create a container registry | `bool` | n/a | yes | +| [enable\_dns\_zone](#input\_enable\_dns\_zone) | Conditionally create a DNS zone | `bool` | n/a | yes | +| [enable\_monitoring](#input\_enable\_monitoring) | Create an App Insights instance and notification group for the Container App | `bool` | n/a | yes | +| [environment](#input\_environment) | Environment name. Will be used along with `project_name` as a prefix for all resources. | `string` | n/a | yes | +| [existing\_logic\_app\_workflow](#input\_existing\_logic\_app\_workflow) | Name, and Resource Group of an existing Logic App Workflow. Leave empty to create a new Resource |
object({
name : string
resource_group_name : string
})
|
{
"name": "",
"resource_group_name": ""
}
| no | +| [existing\_network\_watcher\_name](#input\_existing\_network\_watcher\_name) | Use an existing network watcher to add flow logs. | `string` | n/a | yes | +| [existing\_network\_watcher\_resource\_group\_name](#input\_existing\_network\_watcher\_resource\_group\_name) | Existing network watcher resource group. | `string` | n/a | yes | +| [image\_name](#input\_image\_name) | Image name | `string` | n/a | yes | +| [image\_tag](#input\_image\_tag) | Default image tag for the primary container | `string` | `"web-latest"` | no | +| [monitor\_email\_receivers](#input\_monitor\_email\_receivers) | A list of email addresses that should be notified by monitoring alerts | `list(string)` | n/a | yes | +| [monitor\_endpoint\_healthcheck](#input\_monitor\_endpoint\_healthcheck) | Specify a route that should be monitored for a 200 OK status | `string` | n/a | yes | +| [project\_name](#input\_project\_name) | Project name. Will be used along with `environment` as a prefix for all resources. | `string` | n/a | yes | +| [registry\_admin\_enabled](#input\_registry\_admin\_enabled) | Do you want to enable access key based authentication for your Container Registry? | `bool` | `true` | no | +| [registry\_managed\_identity\_assign\_role](#input\_registry\_managed\_identity\_assign\_role) | Assign the 'AcrPull' Role to the Container App User-Assigned Managed Identity. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity | `bool` | `false` | no | +| [registry\_use\_managed\_identity](#input\_registry\_use\_managed\_identity) | Create a User-Assigned Managed Identity for the Container App. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity | `bool` | `true` | no | +| [statuscake\_api\_token](#input\_statuscake\_api\_token) | API token for StatusCake | `string` | `"00000000000000000000000000000"` | no | +| [statuscake\_contact\_group\_email\_addresses](#input\_statuscake\_contact\_group\_email\_addresses) | List of email address that should receive notifications from StatusCake | `list(string)` | `[]` | no | +| [statuscake\_contact\_group\_integrations](#input\_statuscake\_contact\_group\_integrations) | List of Integration IDs to connect to your Contact Group | `list(string)` | `[]` | no | +| [statuscake\_contact\_group\_name](#input\_statuscake\_contact\_group\_name) | Name of the contact group in StatusCake | `string` | `""` | no | +| [statuscake\_monitored\_resource\_addresses](#input\_statuscake\_monitored\_resource\_addresses) | The URLs to perform TLS checks on | `list(string)` | `[]` | no | +| [tags](#input\_tags) | Tags to be applied to all resources | `map(string)` | n/a | yes | +| [virtual\_network\_address\_space](#input\_virtual\_network\_address\_space) | Virtual network address space CIDR | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..6602f20 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,3 @@ +terraform { + backend "azurerm" {} +} diff --git a/terraform/backend.vars.example b/terraform/backend.vars.example new file mode 100644 index 0000000..bc23821 --- /dev/null +++ b/terraform/backend.vars.example @@ -0,0 +1,5 @@ +resource_group_name = "" +storage_account_name = "" +container_name = "" +key = "terraform.tstate" +subscription_id = "" diff --git a/terraform/container-apps-hosting.tf b/terraform/container-apps-hosting.tf new file mode 100644 index 0000000..dd00caf --- /dev/null +++ b/terraform/container-apps-hosting.tf @@ -0,0 +1,48 @@ +module "azure_container_apps_hosting" { + source = "github.com/DFE-Digital/terraform-azurerm-container-apps-hosting?ref=v1.5.0" + + environment = local.environment + project_name = local.project_name + azure_location = local.azure_location + tags = local.tags + + virtual_network_address_space = local.virtual_network_address_space + + enable_container_registry = local.enable_container_registry + registry_admin_enabled = local.registry_admin_enabled + registry_use_managed_identity = local.registry_use_managed_identity + registry_managed_identity_assign_role = local.registry_managed_identity_assign_role + + image_name = local.image_name + image_tag = local.image_tag + container_command = local.container_command + container_secret_environment_variables = local.container_secret_environment_variables + container_scale_http_concurrency = local.container_scale_http_concurrency + container_health_probe_protocol = local.container_health_probe_protocol + + enable_dns_zone = local.enable_dns_zone + dns_zone_domain_name = local.dns_zone_domain_name + dns_ns_records = local.dns_ns_records + dns_txt_records = local.dns_txt_records + + enable_cdn_frontdoor = local.enable_cdn_frontdoor + cdn_frontdoor_forwarding_protocol = local.cdn_frontdoor_forwarding_protocol + cdn_frontdoor_enable_rate_limiting = local.cdn_frontdoor_enable_rate_limiting + cdn_frontdoor_host_add_response_headers = local.cdn_frontdoor_host_add_response_headers + cdn_frontdoor_custom_domains = local.cdn_frontdoor_custom_domains + cdn_frontdoor_origin_fqdn_override = local.cdn_frontdoor_origin_fqdn_override + cdn_frontdoor_origin_host_header_override = local.cdn_frontdoor_origin_host_header_override + container_apps_allow_ips_inbound = local.container_apps_allow_ips_inbound + + enable_monitoring = local.enable_monitoring + monitor_email_receivers = local.monitor_email_receivers + container_health_probe_path = local.container_health_probe_path + cdn_frontdoor_health_probe_path = local.cdn_frontdoor_health_probe_path + monitor_endpoint_healthcheck = local.monitor_endpoint_healthcheck + enable_container_health_probe = local.enable_container_health_probe + cdn_frontdoor_health_probe_protocol = local.cdn_frontdoor_health_probe_protocol + + existing_logic_app_workflow = local.existing_logic_app_workflow + existing_network_watcher_name = local.existing_network_watcher_name + existing_network_watcher_resource_group_name = local.existing_network_watcher_resource_group_name +} diff --git a/terraform/key-vault-tfvars-secrets.tf b/terraform/key-vault-tfvars-secrets.tf new file mode 100644 index 0000000..019415f --- /dev/null +++ b/terraform/key-vault-tfvars-secrets.tf @@ -0,0 +1,15 @@ +# module "azurerm_key_vault" { +# source = "github.com/DFE-Digital/terraform-azurerm-key-vault-tfvars?ref=v0.4.1" + +# environment = local.environment +# project_name = local.project_name +# existing_resource_group = module.azure_container_apps_hosting.azurerm_resource_group_default.name +# azure_location = local.azure_location +# key_vault_access_use_rbac_authorization = true +# key_vault_access_users = [] +# key_vault_access_ipv4 = local.key_vault_access_ipv4 +# tfvars_filename = local.tfvars_filename +# diagnostic_log_analytics_workspace_id = module.azure_container_apps_hosting.azurerm_log_analytics_workspace_container_app.id +# diagnostic_eventhub_name = "" +# tags = local.tags +# } diff --git a/terraform/locals.tf b/terraform/locals.tf new file mode 100644 index 0000000..5daeddc --- /dev/null +++ b/terraform/locals.tf @@ -0,0 +1,45 @@ +locals { + environment = var.environment + project_name = var.project_name + azure_location = var.azure_location + tags = var.tags + virtual_network_address_space = var.virtual_network_address_space + enable_container_registry = var.enable_container_registry + registry_admin_enabled = var.registry_admin_enabled + registry_use_managed_identity = var.registry_use_managed_identity + registry_managed_identity_assign_role = var.registry_managed_identity_assign_role + image_name = var.image_name + image_tag = var.image_tag + container_command = var.container_command + container_secret_environment_variables = var.container_secret_environment_variables + container_scale_http_concurrency = var.container_scale_http_concurrency + container_health_probe_protocol = var.container_health_probe_protocol + enable_dns_zone = var.enable_dns_zone + dns_zone_domain_name = var.dns_zone_domain_name + dns_ns_records = var.dns_ns_records + dns_txt_records = var.dns_txt_records + enable_cdn_frontdoor = var.enable_cdn_frontdoor + container_apps_allow_ips_inbound = var.container_apps_allow_ips_inbound + cdn_frontdoor_enable_rate_limiting = var.cdn_frontdoor_enable_rate_limiting + cdn_frontdoor_host_add_response_headers = var.cdn_frontdoor_host_add_response_headers + cdn_frontdoor_custom_domains = var.cdn_frontdoor_custom_domains + cdn_frontdoor_origin_fqdn_override = var.cdn_frontdoor_origin_fqdn_override + cdn_frontdoor_origin_host_header_override = var.cdn_frontdoor_origin_host_header_override + cdn_frontdoor_forwarding_protocol = var.cdn_frontdoor_forwarding_protocol + # key_vault_access_ipv4 = var.key_vault_access_ipv4 + # tfvars_filename = var.tfvars_filename + enable_monitoring = var.enable_monitoring + monitor_email_receivers = var.monitor_email_receivers + enable_container_health_probe = var.enable_container_health_probe + container_health_probe_path = var.container_health_probe_path + cdn_frontdoor_health_probe_path = var.cdn_frontdoor_health_probe_path + cdn_frontdoor_health_probe_protocol = var.cdn_frontdoor_health_probe_protocol + monitor_endpoint_healthcheck = var.monitor_endpoint_healthcheck + existing_logic_app_workflow = var.existing_logic_app_workflow + existing_network_watcher_name = var.existing_network_watcher_name + existing_network_watcher_resource_group_name = var.existing_network_watcher_resource_group_name + statuscake_monitored_resource_addresses = var.statuscake_monitored_resource_addresses + statuscake_contact_group_name = var.statuscake_contact_group_name + statuscake_contact_group_integrations = var.statuscake_contact_group_integrations + statuscake_contact_group_email_addresses = var.statuscake_contact_group_email_addresses +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..c06a45c --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,8 @@ +provider "azurerm" { + features {} + skip_provider_registration = true +} + +provider "statuscake" { + api_token = var.statuscake_api_token +} diff --git a/terraform/statuscake-tls-monitor.tf b/terraform/statuscake-tls-monitor.tf new file mode 100644 index 0000000..5799330 --- /dev/null +++ b/terraform/statuscake-tls-monitor.tf @@ -0,0 +1,11 @@ +module "statuscake-tls-monitor" { + source = "github.com/dfe-digital/terraform-statuscake-tls-monitor?ref=v0.1.3" + + statuscake_monitored_resource_addresses = local.statuscake_monitored_resource_addresses + statuscake_alert_at = [ # days to alert on + 40, 20, 5 + ] + statuscake_contact_group_name = local.statuscake_contact_group_name + statuscake_contact_group_integrations = local.statuscake_contact_group_integrations + statuscake_contact_group_email_addresses = local.statuscake_contact_group_email_addresses +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..4714926 --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,15 @@ +environment = "development" +project_name = "myproject" +azure_location = "uksouth" +enable_container_registry = true +image_name = "myimage" +enable_mssql_database = true +mssql_server_admin_password = "S3crEt" +mssql_database_name = "mydatabase" +container_command = ["/bin/bash", "-c", "echo hello && sleep 86400"] +container_environment_variables = { + "ASPNETCORE_ENVIRONMENT" = "production" +} +key_vault_access_users = [ + "someone_example.com#EXT#@tenantname.onmicrosoft.com", +] diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..0920312 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,258 @@ +variable "environment" { + description = "Environment name. Will be used along with `project_name` as a prefix for all resources." + type = string +} + +# variable "key_vault_access_ipv4" { +# description = "List of IPv4 Addresses that are permitted to access the Key Vault" +# type = list(string) +# } + +# variable "tfvars_filename" { +# description = "tfvars filename. This ensures that tfvars are kept up to date in Key Vault." +# type = string +# } + +variable "project_name" { + description = "Project name. Will be used along with `environment` as a prefix for all resources." + type = string +} + +variable "azure_location" { + description = "Azure location in which to launch resources." + type = string +} + +variable "tags" { + description = "Tags to be applied to all resources" + type = map(string) +} + +variable "virtual_network_address_space" { + description = "Virtual network address space CIDR" + type = string +} + +variable "enable_container_registry" { + description = "Set to true to create a container registry" + type = bool +} + +variable "image_name" { + description = "Image name" + type = string +} + +variable "registry_admin_enabled" { + description = "Do you want to enable access key based authentication for your Container Registry?" + type = bool + default = true +} + +variable "registry_use_managed_identity" { + description = "Create a User-Assigned Managed Identity for the Container App. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity" + type = bool + default = true +} + +variable "registry_managed_identity_assign_role" { + description = "Assign the 'AcrPull' Role to the Container App User-Assigned Managed Identity. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity" + type = bool + default = false +} + +variable "container_command" { + description = "Container command" + type = list(any) +} + +variable "container_health_probe_protocol" { + description = "Use HTTPS or a TCP connection for the Container liveness probe" + type = string + default = "tcp" +} + +variable "container_secret_environment_variables" { + description = "Container secret environment variables" + type = map(string) + sensitive = true +} + +variable "container_scale_http_concurrency" { + description = "When the number of concurrent HTTP requests exceeds this value, then another replica is added. Replicas continue to add to the pool up to the max-replicas amount." + type = number + default = 10 +} + +variable "enable_cdn_frontdoor" { + description = "Enable Azure CDN FrontDoor. This will use the Container Apps endpoint as the origin." + type = bool +} + +variable "cdn_frontdoor_origin_fqdn_override" { + description = "Manually specify the hostname that the CDN Front Door should target. Defaults to the Container App FQDN" + type = string + default = "" +} + +variable "cdn_frontdoor_origin_host_header_override" { + description = "Manually specify the host header that the CDN sends to the target. Defaults to the recieved host header. Set to null to set it to the host_name (`cdn_frontdoor_origin_fqdn_override`)" + type = string + default = "" + nullable = true +} + +variable "cdn_frontdoor_enable_rate_limiting" { + description = "Enable CDN Front Door Rate Limiting. This will create a WAF policy, and CDN security policy. For pricing reasons, there will only be one WAF policy created." + type = bool +} + +variable "cdn_frontdoor_host_add_response_headers" { + description = "List of response headers to add at the CDN Front Door `[{ \"name\" = \"Strict-Transport-Security\", \"value\" = \"max-age=31536000\" }]`" + type = list(map(string)) +} + +variable "enable_monitoring" { + description = "Create an App Insights instance and notification group for the Container App" + type = bool +} + +variable "monitor_email_receivers" { + description = "A list of email addresses that should be notified by monitoring alerts" + type = list(string) +} + +variable "existing_logic_app_workflow" { + description = "Name, and Resource Group of an existing Logic App Workflow. Leave empty to create a new Resource" + type = object({ + name : string + resource_group_name : string + }) + default = { + name = "" + resource_group_name = "" + } +} + +variable "container_health_probe_path" { + description = "Specifies the path that is used to determine the liveness of the Container" + type = string + default = "/" +} + +variable "cdn_frontdoor_health_probe_path" { + description = "Specifies the path relative to the origin that is used to determine the health of the origin." + type = string + default = "/" +} + +variable "cdn_frontdoor_custom_domains" { + description = "Azure CDN Front Door custom domains. If they are within the DNS zone (optionally created), the Validation TXT records and ALIAS/CNAME records will be created" + type = list(string) +} + +variable "monitor_endpoint_healthcheck" { + description = "Specify a route that should be monitored for a 200 OK status" + type = string +} + +variable "existing_network_watcher_name" { + description = "Use an existing network watcher to add flow logs." + type = string +} + +variable "existing_network_watcher_resource_group_name" { + description = "Existing network watcher resource group." + type = string +} + +variable "container_apps_allow_ips_inbound" { + description = "Restricts access to the Container Apps by creating a network security group rule that only allow inbound traffic from the provided list of IPs" + type = list(string) + default = [] +} + +variable "enable_dns_zone" { + description = "Conditionally create a DNS zone" + type = bool +} + +variable "cdn_frontdoor_forwarding_protocol" { + description = "Azure CDN Front Door forwarding protocol" + type = string + default = "HttpOnly" +} + +variable "dns_zone_domain_name" { + description = "DNS zone domain name. If created, records will automatically be created to point to the CDN." + type = string +} + +variable "dns_ns_records" { + description = "DNS NS records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + records : list(string) + }) + ) +} + +variable "dns_txt_records" { + description = "DNS TXT records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + records : list(string) + }) + ) +} + +variable "enable_container_health_probe" { + description = "Enable liveness probes for the Container" + type = bool + default = true +} + +variable "cdn_frontdoor_health_probe_protocol" { + description = "Use Http or Https" + type = string + default = "Http" +} + +variable "image_tag" { + description = "Default image tag for the primary container" + type = string + default = "web-latest" +} + +variable "statuscake_api_token" { + description = "API token for StatusCake" + type = string + sensitive = true + default = "00000000000000000000000000000" +} + +variable "statuscake_contact_group_name" { + description = "Name of the contact group in StatusCake" + type = string + default = "" +} + +variable "statuscake_contact_group_integrations" { + description = "List of Integration IDs to connect to your Contact Group" + type = list(string) + default = [] +} + +variable "statuscake_monitored_resource_addresses" { + description = "The URLs to perform TLS checks on" + type = list(string) + default = [] +} + +variable "statuscake_contact_group_email_addresses" { + description = "List of email address that should receive notifications from StatusCake" + type = list(string) + default = [] +} diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 0000000..d098126 --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.5.7" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.67.0" + } + statuscake = { + source = "StatusCakeDev/statuscake" + version = ">= 2.1.0" + } + } +}