diff --git a/src/APIM.sln b/src/APIM.sln
index e3b02d8e07..fabd91055a 100644
--- a/src/APIM.sln
+++ b/src/APIM.sln
@@ -504,6 +504,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.SharedOuterApi.Appr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.SharedOuterApi.Apprentice.GovUK.Auth.UnitTests", "Shared\SFA.DAS.SharedOuterApi.Apprentice.GovUK.Auth.UnitTests\SFA.DAS.SharedOuterApi.Apprentice.GovUK.Auth.UnitTests.csproj", "{86F8DB4C-5EB3-4B5E-A232-7C09095ADEA2}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Payments", "Payments", "{1AC73DE4-60FE-4E67-82DC-05656F74D7B3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments", "Payments\SFA.DAS.Payments\SFA.DAS.Payments.csproj", "{20D6BE73-2729-4B9D-BC69-C60663964486}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.Api", "Payments\SFA.DAS.Payments.Api\SFA.DAS.Payments.Api.csproj", "{91EA5AEF-58FB-4930-9736-FE05E6DFBF64}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.Api.UnitTests", "Payments\SFA.DAS.Payments.Api.UnitTests\SFA.DAS.Payments.Api.UnitTests.csproj", "{9C81947D-6B60-4019-A799-9B66F2166D1F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.UnitTests", "Payments\SFA.DAS.Payments.UnitTests\SFA.DAS.Payments.UnitTests.csproj", "{5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1318,6 +1328,22 @@ Global
{86F8DB4C-5EB3-4B5E-A232-7C09095ADEA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86F8DB4C-5EB3-4B5E-A232-7C09095ADEA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86F8DB4C-5EB3-4B5E-A232-7C09095ADEA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {20D6BE73-2729-4B9D-BC69-C60663964486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {20D6BE73-2729-4B9D-BC69-C60663964486}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {20D6BE73-2729-4B9D-BC69-C60663964486}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {20D6BE73-2729-4B9D-BC69-C60663964486}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91EA5AEF-58FB-4930-9736-FE05E6DFBF64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91EA5AEF-58FB-4930-9736-FE05E6DFBF64}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91EA5AEF-58FB-4930-9736-FE05E6DFBF64}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91EA5AEF-58FB-4930-9736-FE05E6DFBF64}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9C81947D-6B60-4019-A799-9B66F2166D1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9C81947D-6B60-4019-A799-9B66F2166D1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9C81947D-6B60-4019-A799-9B66F2166D1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9C81947D-6B60-4019-A799-9B66F2166D1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1525,6 +1551,10 @@ Global
{F014EBD4-A9FA-4196-A39C-BD71805449D6} = {2FBDD130-2DBC-4BBB-8219-D9AF89DE0BF8}
{3BA94F50-4E8B-463E-87FA-03A9C060FC36} = {9C327515-DF6A-4277-BF53-4C86D510C718}
{86F8DB4C-5EB3-4B5E-A232-7C09095ADEA2} = {9C327515-DF6A-4277-BF53-4C86D510C718}
+ {20D6BE73-2729-4B9D-BC69-C60663964486} = {1AC73DE4-60FE-4E67-82DC-05656F74D7B3}
+ {91EA5AEF-58FB-4930-9736-FE05E6DFBF64} = {1AC73DE4-60FE-4E67-82DC-05656F74D7B3}
+ {9C81947D-6B60-4019-A799-9B66F2166D1F} = {1AC73DE4-60FE-4E67-82DC-05656F74D7B3}
+ {5F6EE783-C6B9-4E8B-BA5F-C2CD327245A5} = {1AC73DE4-60FE-4E67-82DC-05656F74D7B3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2353A4D0-2859-4472-B2D4-BBD8087B4E22}
diff --git a/src/Payments/README.md b/src/Payments/README.md
new file mode 100644
index 0000000000..3bf8b624c3
--- /dev/null
+++ b/src/Payments/README.md
@@ -0,0 +1,54 @@
+## ⛔Never push sensitive information such as client id's, secrets or keys into repositories including in the README file⛔
+
+# Payments Outer API
+
+
+
+[![Build Status](https://dev.azure.com/sfa-gov-uk/Digital%20Apprenticeship%20Service/_apis/build/status/das-apim-endpoints-Apprenticeships?branchName=master)](https://dev.azure.com/sfa-gov-uk/Digital%20Apprenticeship%20Service/_build/latest?definitionId=das-apim-endpoints-Payments&branchName=master)
+[![Jira Project](https://img.shields.io/badge/Jira-Project-blue)](https://skillsfundingagency.atlassian.net/jira/software/c/projects/FLP/boards/753)
+[![Confluence Project](https://img.shields.io/badge/Confluence-Project-blue)](https://skillsfundingagency.atlassian.net/wiki/spaces/NDL/pages/3480354918/Flexible+Payments+Models)
+[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg?longCache=true&style=flat-square)](https://en.wikipedia.org/wiki/MIT_License)
+
+The Payments outer provides endpoints used by das-funding-payments and should not be used by any other applications.
+
+## How It Works
+
+The Outer API orchestrates calls to multiple inner APIs in order to provide the functionality required by the UI. Each UI page operation (GET, POST etc) should call a single outer API endpoint only.
+
+## 🚀 Installation
+
+### Pre-Requisites
+
+* A clone of this repository
+* A code editor that supports .Net8
+* Azure Storage Emulator (Azureite)
+
+### Config
+
+Most of the application configuration is taken from the [das-employer-config repository](https://github.com/SkillsFundingAgency/das-employer-config) and the default values can be used in most cases. The config json will need to be added to the local Azure Storage instance with a a PartitionKey of LOCAL and a RowKey of SFA.DAS.Payments.OuterAPI_1.0.
+
+| Name | Description | Stub Value |
+| ------------------------------------------------------------- | ------------------------------------------------- |-------------------------------------------|
+| LearnerDataApiConfiguration:Url | Url of the data endpoint | https://localhost:4000/learner-data-api |
+| LearnerDataApiConfiguration:TokenSettings:Url | Url of the token endpoint | |
+| LearnerDataApiConfiguration:TokenSettings:Scope | Token settings | |
+| LearnerDataApiConfiguration:TokenSettings:ClientId | Token settings | |
+| LearnerDataApiConfiguration:TokenSettings:Tenant | Token settings | |
+| LearnerDataApiConfiguration:TokenSettings:ClientSecret | Token settings | |
+| LearnerDataApiConfiguration:TokenSettings:GrantType | Token settings | |
+| LearnerDataApiConfiguration:TokenSettings:ShouldSkipForLocal | For local use only, skips calling token endpoint | true |
+
+
+
+
+## 🔗 External Dependencies
+
+The Outer API has many external dependancies which can all be configured to use stubs by following the config above.
+
+## Running Locally
+
+* Make sure Azure Storage Emulator (Azureite) is running
+* Make sure the config has been updated to call any Stub APIs required
+* Run the [Commitments Stubs](https://github.com/SkillsFundingAgency/das-commitments-stubs)
+* Run the Apprenticeships Inner API
+* Run the application
\ No newline at end of file
diff --git a/src/Payments/SFA.DAS.Payments.Api.UnitTests/AssertExtensions.cs b/src/Payments/SFA.DAS.Payments.Api.UnitTests/AssertExtensions.cs
new file mode 100644
index 0000000000..afe6479497
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api.UnitTests/AssertExtensions.cs
@@ -0,0 +1,18 @@
+using FluentAssertions;
+
+namespace SFA.DAS.Payments.Api.UnitTests
+{
+ public static class AssertExtensions
+ {
+ //extension that asserts that the object is of the expected type and returns it
+ public static T ShouldBeOfType(this object? actual)
+ {
+ if (actual == null)
+ throw new AssertionException($"Expected object of type {typeof(T).Name} but was null");
+
+ actual.Should().BeOfType();
+ return (T)actual;
+ }
+
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/CollectionCalendar/WhenGettingAcademicYear.cs b/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/CollectionCalendar/WhenGettingAcademicYear.cs
new file mode 100644
index 0000000000..4fb708f600
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/CollectionCalendar/WhenGettingAcademicYear.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+using SFA.DAS.Payments.Api.Controllers;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.InnerApi.Requests.CollectionCalendar;
+using SFA.DAS.SharedOuterApi.InnerApi.Responses.CollectionCalendar;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.Testing.AutoFixture;
+
+namespace SFA.DAS.Payments.Api.UnitTests.Controllers.CollectionCalendar;
+
+public class WhenGettingAcademicYear
+{
+ [Test, MoqAutoData]
+ public async Task Then_Gets_ApprenticeshipKey_From_ApiClient(
+ GetAcademicYearsResponse expectedResponse,
+ DateTime searchDate,
+ Mock> mockCollectionCalendarApiClient)
+ {
+ // Arrange
+ mockCollectionCalendarApiClient.Setup(x => x.Get(It.IsAny())).ReturnsAsync(expectedResponse);
+
+ var controller = new CollectionCalendarController(mockCollectionCalendarApiClient.Object);
+
+ // Act
+ var result = await controller.GetAcademicYear(searchDate);
+
+ // Assert
+ var okObjectResult = result.ShouldBeOfType();
+ var actualResponse = okObjectResult.Value.ShouldBeOfType();
+ actualResponse.Should().Be(expectedResponse);
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/WhenGetLearnerReferences.cs b/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/WhenGetLearnerReferences.cs
new file mode 100644
index 0000000000..c87c5e9cae
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api.UnitTests/Controllers/WhenGetLearnerReferences.cs
@@ -0,0 +1,71 @@
+using AutoFixture;
+using FluentAssertions;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using SFA.DAS.Payments.Api.Controllers;
+using SFA.DAS.Payments.Api.Models;
+using SFA.DAS.Payments.Application.Learners;
+using SFA.DAS.Payments.Models.Responses;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.Payments.Api.UnitTests.Controllers
+{
+ [TestFixture]
+ internal class WhenGetLearnerReferences
+ {
+#pragma warning disable CS8618 // Non-nullable field is uninitialized. - nUnit initializes fields in SetUp
+ private Fixture _fixture;
+ private Mock> _loggerMock;
+ private Mock _mediatorMock;
+ private IlrController _controller;
+#pragma warning restore CS8618 // Non-nullable field is uninitialized.
+
+ [SetUp]
+ public void SetUp()
+ {
+ _fixture = new Fixture();
+ _loggerMock = new Mock>();
+ _mediatorMock = new Mock();
+ _controller = new IlrController(_loggerMock.Object, _mediatorMock.Object);
+ }
+
+ [Test]
+ public async Task Then_ReturnsOkResult_WithLearnerReferences()
+ {
+ // Arrange
+ var ukprn = _fixture.Create();
+ var academicYear = _fixture.Create();
+ var learnersQueryResult = _fixture.Create>();
+
+ _mediatorMock.Setup(m => m.Send(It.IsAny(), default)).ReturnsAsync(learnersQueryResult);
+
+ // Act
+ var result = await _controller.GetLearnerReferences(ukprn, academicYear);
+
+ // Assert
+ result.Should().BeOfType();
+ var okResult = result.Should().BeOfType().Subject;
+ okResult.Value.Should().BeEquivalentTo(learnersQueryResult.ToLearnerReferenceResponse());
+ }
+
+ [Test]
+ public async Task Then_ReturnsBadRequest_OnException()
+ {
+ // Arrange
+ var ukprn = _fixture.Create();
+ var academicYear = _fixture.Create();
+ _mediatorMock.Setup(m => m.Send(It.IsAny(), default))
+ .ThrowsAsync(new Exception("Test exception"));
+
+ // Act
+ var result = await _controller.GetLearnerReferences(ukprn, academicYear);
+
+ // Assert
+ result.Should().BeOfType();
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api.UnitTests/SFA.DAS.Payments.Api.UnitTests.csproj b/src/Payments/SFA.DAS.Payments.Api.UnitTests/SFA.DAS.Payments.Api.UnitTests.csproj
new file mode 100644
index 0000000000..605a807daa
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api.UnitTests/SFA.DAS.Payments.Api.UnitTests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ enable
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Payments/SFA.DAS.Payments.Api/AppStart/AddServiceRegistrationExtensions.cs b/src/Payments/SFA.DAS.Payments.Api/AppStart/AddServiceRegistrationExtensions.cs
new file mode 100644
index 0000000000..fcb96d9ada
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/AppStart/AddServiceRegistrationExtensions.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.Options;
+using SFA.DAS.Api.Common.Configuration;
+using SFA.DAS.Api.Common.Infrastructure;
+using SFA.DAS.Api.Common.Interfaces;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Infrastructure;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.SharedOuterApi.Services;
+using System.Diagnostics.CodeAnalysis;
+
+namespace SFA.DAS.Payments.Api.AppStart;
+
+[ExcludeFromCodeCoverage]
+public static class AddServiceRegistrationExtensions
+{
+ public static void AddServiceRegistration(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddHttpClient();
+ services.AddTransient();
+ services.AddTransient(typeof(IInternalApiClient<>), typeof(InternalApiClient<>));
+ services.AddTransient(typeof(IAccessTokenApiClient<>), typeof(AccessTokenApiClient<>));
+ services.AddTransient, LearnerDataApiClient>();
+ services.AddTransient, CollectionCalendarApiClient>();
+ }
+}
+
+[ExcludeFromCodeCoverage]
+public static class AddConfigurationOptionsExtension
+{
+ public static void AddConfigurationOptions(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddOptions();
+
+ services.Configure(configuration.GetSection("AzureAd"));
+ services.AddSingleton(cfg => cfg.GetService>()!.Value);
+
+ services.Configure(configuration.GetSection(nameof(LearnerDataApiConfiguration)));
+ services.AddSingleton(cfg => cfg.GetService>()!.Value);
+
+ services.Configure(configuration.GetSection(nameof(CollectionCalendarApiConfiguration)));
+ services.AddSingleton(cfg => cfg.GetService>()!.Value);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Payments/SFA.DAS.Payments.Api/Controllers/CollectionCalendarController.cs b/src/Payments/SFA.DAS.Payments.Api/Controllers/CollectionCalendarController.cs
new file mode 100644
index 0000000000..aea21e5a33
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Controllers/CollectionCalendarController.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Mvc;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.InnerApi.Requests.CollectionCalendar;
+using SFA.DAS.SharedOuterApi.InnerApi.Responses.CollectionCalendar;
+using SFA.DAS.SharedOuterApi.Interfaces;
+
+namespace SFA.DAS.Payments.Api.Controllers;
+
+[ApiController]
+[Route("[controller]")]
+public class CollectionCalendarController : ControllerBase
+{
+ private readonly ICollectionCalendarApiClient _collectionCalendarApiClient;
+
+ public CollectionCalendarController(ICollectionCalendarApiClient collectionCalendarApiClient)
+ {
+ _collectionCalendarApiClient = collectionCalendarApiClient;
+ }
+
+ [HttpGet]
+ [Route("academicYear/{searchDate}")]
+ public async Task GetAcademicYear(DateTime searchDate)
+ {
+ return Ok(await _collectionCalendarApiClient.Get(new GetAcademicYearByDateRequest(searchDate)));
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api/Controllers/IlrController.cs b/src/Payments/SFA.DAS.Payments.Api/Controllers/IlrController.cs
new file mode 100644
index 0000000000..2054b40235
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Controllers/IlrController.cs
@@ -0,0 +1,38 @@
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using SFA.DAS.Payments.Api.Models;
+using SFA.DAS.Payments.Application.Learners;
+
+namespace SFA.DAS.Payments.Api.Controllers
+{
+ [ApiController]
+ [Route("ILR")]
+ public class IlrController : ControllerBase
+ {
+ private readonly ILogger _logger;
+ private readonly IMediator _mediator;
+
+ public IlrController(ILogger logger, IMediator mediator)
+ {
+ _logger = logger;
+ _mediator = mediator;
+ }
+
+ [HttpGet]
+ [Route("{ukprn}/{academicYear}")]
+ public async Task GetLearnerReferences(string ukprn, short academicYear)
+ {
+ try
+ {
+ var result = await _mediator.Send(new GetLearnersQuery(ukprn, academicYear));
+ var learnerReferences = result.ToLearnerReferenceResponse();
+ return Ok(learnerReferences);
+ }
+ catch(Exception e)
+ {
+ _logger.LogError(e, "Error attempting to get Learner References");
+ return BadRequest();
+ }
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api/Models/LearnerReferencesResponse.cs b/src/Payments/SFA.DAS.Payments.Api/Models/LearnerReferencesResponse.cs
new file mode 100644
index 0000000000..2bfce12d75
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Models/LearnerReferencesResponse.cs
@@ -0,0 +1,18 @@
+using SFA.DAS.Payments.Models.Responses;
+
+namespace SFA.DAS.Payments.Api.Models
+{
+ public class LearnerReferenceResponse
+ {
+ public long Uln { get; set; }
+ public string LearnerRefNumber { get; set; } = string.Empty;
+ }
+
+ public static class LearnerReferenceResponseExtensions
+ {
+ public static IEnumerable ToLearnerReferenceResponse(this IEnumerable learners)
+ {
+ return learners.Select(x=> new LearnerReferenceResponse { Uln = x.Uln, LearnerRefNumber = x.LearnRefNumber });
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api/Program.cs b/src/Payments/SFA.DAS.Payments.Api/Program.cs
new file mode 100644
index 0000000000..67e2944f6b
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Program.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace SFA.DAS.Payments.Api;
+
+[ExcludeFromCodeCoverage]
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); });
+}
\ No newline at end of file
diff --git a/src/Payments/SFA.DAS.Payments.Api/Properties/launchSettings.json b/src/Payments/SFA.DAS.Payments.Api/Properties/launchSettings.json
new file mode 100644
index 0000000000..7850330879
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Properties/launchSettings.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:53085",
+ "sslPort": 44373
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7157;http://localhost:5157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api/SFA.DAS.Payments.Api.csproj b/src/Payments/SFA.DAS.Payments.Api/SFA.DAS.Payments.Api.csproj
new file mode 100644
index 0000000000..102f2657cd
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/SFA.DAS.Payments.Api.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Payments/SFA.DAS.Payments.Api/Startup.cs b/src/Payments/SFA.DAS.Payments.Api/Startup.cs
new file mode 100644
index 0000000000..866c231a17
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/Startup.cs
@@ -0,0 +1,110 @@
+using Microsoft.AspNetCore.Mvc.Authorization;
+using Microsoft.OpenApi.Models;
+using SFA.DAS.Api.Common.AppStart;
+using SFA.DAS.Api.Common.Configuration;
+using SFA.DAS.Payments.Api.AppStart;
+using SFA.DAS.Payments.Application.Learners;
+using SFA.DAS.SharedOuterApi.AppStart;
+using SFA.DAS.SharedOuterApi.Infrastructure.HealthCheck;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace SFA.DAS.Payments.Api;
+
+[ExcludeFromCodeCoverage]
+public class Startup
+{
+ private readonly IWebHostEnvironment _env;
+ private readonly IConfiguration _configuration;
+
+ public Startup(IConfiguration configuration, IWebHostEnvironment env)
+ {
+ _env = env;
+ _configuration = configuration.BuildSharedConfiguration();
+ }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddOptions();
+ services.AddSingleton(_env);
+
+ services.AddConfigurationOptions(_configuration);
+
+ if (!_configuration.IsLocalOrDev())
+ {
+ var azureAdConfiguration = _configuration
+ .GetSection("AzureAd")
+ .Get();
+ var policies = new Dictionary
+ {
+ {"default", "APIM"}
+ };
+
+ services.AddAuthentication(azureAdConfiguration, policies);
+ }
+
+ services.AddMediatR(configuration => configuration.RegisterServicesFromAssembly(typeof(GetLearnersQueryHandler).Assembly));
+ services.AddServiceRegistration(_configuration);
+
+ services
+ .AddMvc(o =>
+ {
+ if (!_configuration.IsLocalOrDev())
+ {
+ o.Filters.Add(new AuthorizeFilter("default"));
+ }
+ });
+
+ if (_configuration["Environment"] != "DEV")
+ {
+ services.AddHealthChecks()
+ .AddCheck(LocationsApiHealthCheck.HealthCheckResultDescription);
+ }
+
+ services.AddControllers().AddJsonOptions(options =>
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
+
+ services.AddOpenTelemetryRegistration(_configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]);
+
+ services.AddSwaggerGen(c =>
+ {
+ c.SwaggerDoc("v1", new OpenApiInfo { Title = "PaymentsOuterApi", Version = "v1" });
+ });
+
+ services.AddLogging();
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+ else
+ {
+ app.UseExceptionHandler("/Error");
+ }
+
+ app.UseAuthentication();
+
+ if (!_configuration["Environment"].Equals("DEV", StringComparison.CurrentCultureIgnoreCase))
+ {
+ app.UseHealthChecks();
+ }
+
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllerRoute(
+ name: "default",
+ pattern: "{controller}/{action}/{id}");
+ });
+
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "PaymentsOuterApi");
+ c.RoutePrefix = string.Empty;
+ });
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.Api/appsettings.json b/src/Payments/SFA.DAS.Payments.Api/appsettings.json
new file mode 100644
index 0000000000..c0fa768c79
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.Api/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConfigNames": "SFA.DAS.Payments.OuterApi"
+}
diff --git a/src/Payments/SFA.DAS.Payments.UnitTests/Application/Learners/WhenGetLearnersQueryHandler.cs b/src/Payments/SFA.DAS.Payments.UnitTests/Application/Learners/WhenGetLearnersQueryHandler.cs
new file mode 100644
index 0000000000..9d1bd3da4b
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.UnitTests/Application/Learners/WhenGetLearnersQueryHandler.cs
@@ -0,0 +1,111 @@
+using AutoFixture;
+using FluentAssertions;
+using Microsoft.Extensions.Logging;
+using Moq;
+using SFA.DAS.Payments.Application.Learners;
+using SFA.DAS.Payments.Models.Requests;
+using SFA.DAS.Payments.Models.Responses;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.SharedOuterApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.Payments.UnitTests.Application.Learners
+{
+ [TestFixture]
+ public class WhenGetLearnersQueryHandler
+ {
+#pragma warning disable CS8618 // Non-nullable field is uninitialized. - nUnit initializes fields in SetUp
+ private Fixture _fixture;
+ private Mock> _apiClientMock;
+ private Mock> _loggerMock;
+ private GetLearnersQueryHandler _handler;
+#pragma warning restore CS8618 // Non-nullable field is uninitialized.
+
+ [SetUp]
+ public void SetUp()
+ {
+ _fixture = new Fixture();
+ _apiClientMock = new Mock>();
+ _loggerMock = new Mock>();
+ _handler = new GetLearnersQueryHandler(_loggerMock.Object, _apiClientMock.Object);
+ }
+
+ [Test]
+ public async Task And_OnePageOfDataReturned_ThenShouldReturnLearners()
+ {
+ // Arrange
+ var query = _fixture.Create();
+ var learners = _fixture.CreateMany(9).ToList();
+
+ MockResponse(learners, 1, 1);
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().BeEquivalentTo(learners);
+ }
+
+ [Test]
+ public async Task And_MultiplePagesOfDataReturned_ThenShouldReturnLearners()
+ {
+ // Arrange
+ var query = _fixture.Create();
+ var learners = _fixture.CreateMany(9).ToList();
+
+ MockResponse(learners, 1, 3); // page 1 of 3
+ MockResponse(learners, 2, 3); // page 2 of 3
+ MockResponse(learners, 3, 3); // page 3 of 3
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().BeEquivalentTo(learners);
+ }
+
+ [Test]
+ public async Task And_NoDataReturned_ThenShouldReturnEmptyList()
+ {
+ // Arrange
+ var query = _fixture.Create();
+ var emptyList = new List();
+ var apiResponse = new ApiResponse>(null, System.Net.HttpStatusCode.NoContent, null, new Dictionary>());
+
+ _apiClientMock.Setup(x => x.GetWithResponseCode>(It.IsAny())).ReturnsAsync(apiResponse);
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().BeEquivalentTo(emptyList);
+ }
+
+ private void MockResponse(List learners, int pageNumber, int totalPages)
+ {
+ var itemsPerPage = learners.Count / totalPages;
+ var startIndex = (pageNumber - 1) * itemsPerPage;
+
+ var sliceOfLearners = learners.GetRange(startIndex, itemsPerPage);
+
+ var paginationHeaderValue = $"{{\"TotalItems\":{learners.Count},\"PageNumber\":{pageNumber},\"PageSize\":{itemsPerPage},\"TotalPages\":{totalPages}}}";
+ var paginationHeader = new Dictionary> { { "X-Pagination", new[] { paginationHeaderValue } } };
+ var apiResponse = new ApiResponse>(sliceOfLearners, System.Net.HttpStatusCode.OK, null, paginationHeader);
+
+ _apiClientMock.Setup(x => x.GetWithResponseCode>(It.Is(r => RequestPageNumberIs(r,pageNumber)))).ReturnsAsync(apiResponse);
+ }
+
+ private static bool RequestPageNumberIs(GetLearnersRequest request, int expectedPageNumber)
+ {
+ if(request.GetUrl.Contains($"pageNumber={expectedPageNumber}"))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments.UnitTests/SFA.DAS.Payments.UnitTests.csproj b/src/Payments/SFA.DAS.Payments.UnitTests/SFA.DAS.Payments.UnitTests.csproj
new file mode 100644
index 0000000000..78954fddd3
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.UnitTests/SFA.DAS.Payments.UnitTests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ false
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Payments/SFA.DAS.Payments.sln b/src/Payments/SFA.DAS.Payments.sln
new file mode 100644
index 0000000000..0845940dd9
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments.sln
@@ -0,0 +1,59 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35222.181
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments", "SFA.DAS.Payments\SFA.DAS.Payments.csproj", "{58BF78ED-6961-4BCD-9E6E-68FACFBC36C4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.Api", "SFA.DAS.Payments.Api\SFA.DAS.Payments.Api.csproj", "{88FC840A-7A97-4411-8CE7-894E58DD0564}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.UnitTests", "SFA.DAS.Payments.UnitTests\SFA.DAS.Payments.UnitTests.csproj", "{720C6265-EF1B-4C49-B524-00CEFC4C5D87}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.Payments.Api.UnitTests", "SFA.DAS.Payments.Api.UnitTests\SFA.DAS.Payments.Api.UnitTests.csproj", "{90332082-C132-43AE-A664-7F65F9F0F52E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{65EE386C-94BB-4A55-BDCF-6D0573AD229F}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{268E0FE2-9159-48C2-A3BF-EDB6502E0011}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SFA.DAS.SharedOuterApi", "..\Shared\SFA.DAS.SharedOuterApi\SFA.DAS.SharedOuterApi.csproj", "{FBFA8D08-A342-4106-8805-5E4F424C3AB7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {58BF78ED-6961-4BCD-9E6E-68FACFBC36C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58BF78ED-6961-4BCD-9E6E-68FACFBC36C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58BF78ED-6961-4BCD-9E6E-68FACFBC36C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58BF78ED-6961-4BCD-9E6E-68FACFBC36C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88FC840A-7A97-4411-8CE7-894E58DD0564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {88FC840A-7A97-4411-8CE7-894E58DD0564}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88FC840A-7A97-4411-8CE7-894E58DD0564}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {88FC840A-7A97-4411-8CE7-894E58DD0564}.Release|Any CPU.Build.0 = Release|Any CPU
+ {720C6265-EF1B-4C49-B524-00CEFC4C5D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {720C6265-EF1B-4C49-B524-00CEFC4C5D87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {720C6265-EF1B-4C49-B524-00CEFC4C5D87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {720C6265-EF1B-4C49-B524-00CEFC4C5D87}.Release|Any CPU.Build.0 = Release|Any CPU
+ {90332082-C132-43AE-A664-7F65F9F0F52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {90332082-C132-43AE-A664-7F65F9F0F52E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {90332082-C132-43AE-A664-7F65F9F0F52E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {90332082-C132-43AE-A664-7F65F9F0F52E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBFA8D08-A342-4106-8805-5E4F424C3AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBFA8D08-A342-4106-8805-5E4F424C3AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBFA8D08-A342-4106-8805-5E4F424C3AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBFA8D08-A342-4106-8805-5E4F424C3AB7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {FBFA8D08-A342-4106-8805-5E4F424C3AB7} = {268E0FE2-9159-48C2-A3BF-EDB6502E0011}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1FBA9471-0084-450A-961B-4B248AA9922B}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Payments/SFA.DAS.Payments/Application/Learners/GetLearnersQueryHandler.cs b/src/Payments/SFA.DAS.Payments/Application/Learners/GetLearnersQueryHandler.cs
new file mode 100644
index 0000000000..0bd1dc0c53
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments/Application/Learners/GetLearnersQueryHandler.cs
@@ -0,0 +1,104 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using SFA.DAS.Payments.Models.Requests;
+using SFA.DAS.Payments.Models.Responses;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.SharedOuterApi.Models;
+
+namespace SFA.DAS.Payments.Application.Learners
+{
+ public class GetLearnersQuery : IRequest>
+ {
+ public string Ukprn { get; set; }
+ public short AcademicYear { get; set; }
+ public GetLearnersQuery(string ukprn, short academicYear)
+ {
+ Ukprn = ukprn;
+ AcademicYear = academicYear;
+ }
+ }
+
+ public class GetLearnersQueryHandler : IRequestHandler>
+ {
+ private readonly ILearnerDataApiClient _apiClient;
+ private readonly ILogger _logger;
+
+ public GetLearnersQueryHandler(
+ ILogger logger, ILearnerDataApiClient apiClient)
+ {
+ _apiClient = apiClient;
+ _logger = logger;
+ }
+
+ public async Task> Handle(GetLearnersQuery request, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Getting learners for UKPRN: {ukprn} and Academic Year: {academicYear}", request.Ukprn, request.AcademicYear);
+ List learners = new List();
+
+ var initialResponse = await _apiClient.GetWithResponseCode>(new GetLearnersRequest(request.Ukprn, request.AcademicYear, 1));
+ if(initialResponse.StatusCode == System.Net.HttpStatusCode.NoContent)
+ {
+ _logger.LogWarning("No learners found for UKPRN: {ukprn} and Academic Year: {academicYear}", request.Ukprn, request.AcademicYear);
+ return learners;
+ }
+
+ learners.AddRange(initialResponse.Body);
+
+ var paginationHeader = GetPaginationHeader(initialResponse);
+
+ for (var i = 1; i < paginationHeader.TotalPages; i++) // Start at 1 because we've already got the first page
+ {
+ var pageNumber = i + 1; // The API is 1-based
+ var additionalPage = await _apiClient.GetWithResponseCode>(new GetLearnersRequest(request.Ukprn, request.AcademicYear, pageNumber));
+ learners.AddRange(additionalPage.Body);
+ }
+
+ _logger.LogInformation("Retrieved {learnerCount} learners", learners.Count);
+ return learners;
+ }
+
+ private PaginationHeader GetPaginationHeader(ApiResponse> apiResponse)
+ {
+ PaginationHeader? paginationHeader;
+
+ try
+ {
+ if(!apiResponse.Headers.ContainsKey("X-Pagination"))
+ {
+ _logger.LogWarning("No X-Pagination header returned from LearnerData endpoint. This is acceptable when working with Stub only");
+ return new PaginationHeader
+ {
+ TotalItems = apiResponse.Body.Count,
+ PageNumber = 1,
+ PageSize = apiResponse.Body.Count,
+ TotalPages = 1
+ };
+ }
+
+ var paginationHeaderString = apiResponse.Headers["X-Pagination"].Single();
+ paginationHeader = Newtonsoft.Json.JsonConvert.DeserializeObject(paginationHeaderString);
+ }
+ catch(Exception ex)
+ {
+ _logger.LogError(ex, "Failed to parse pagination header");
+ throw;
+ }
+
+ if (paginationHeader == null)
+ {
+ throw new Exception("Failed to parse pagination header");
+ }
+
+ return paginationHeader;
+ }
+
+ private class PaginationHeader
+ {
+ public int TotalItems { get; set; }
+ public int PageNumber { get; set; }
+ public int PageSize { get; set; }
+ public int TotalPages { get; set; }
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments/Models/Requests/GetLearnersRequest.cs b/src/Payments/SFA.DAS.Payments/Models/Requests/GetLearnersRequest.cs
new file mode 100644
index 0000000000..f743689e58
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments/Models/Requests/GetLearnersRequest.cs
@@ -0,0 +1,20 @@
+using SFA.DAS.SharedOuterApi.Interfaces;
+
+namespace SFA.DAS.Payments.Models.Requests
+{
+ public class GetLearnersRequest : IGetApiRequest
+ {
+ private readonly string _ukprn;
+ private readonly short _academicYear;
+ private readonly int _pageNumber;
+
+ public string GetUrl => $"learners/{_academicYear}?ukprn={_ukprn}&pageNumber={_pageNumber}&pageSize=1000";
+
+ public GetLearnersRequest(string ukprn, short academicYear, int pageNumber)
+ {
+ _ukprn = ukprn;
+ _academicYear = academicYear;
+ _pageNumber = pageNumber;
+ }
+ }
+}
diff --git a/src/Payments/SFA.DAS.Payments/Models/Responses/LearnerResponse.cs b/src/Payments/SFA.DAS.Payments/Models/Responses/LearnerResponse.cs
new file mode 100644
index 0000000000..f3bd38f26d
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments/Models/Responses/LearnerResponse.cs
@@ -0,0 +1,33 @@
+namespace SFA.DAS.Payments.Models.Responses
+{
+ public class LearnerResponse
+ {
+ public int Ukprn { get; set; }
+ public string LearnRefNumber { get; set; }
+ public long Uln { get; set; }
+ public string FamilyName { get; set; }
+ public string GivenNames { get; set; }
+ public DateTime DateOfBirth { get; set; }
+ public string NiNumber { get; set; }
+ public Learningdelivery[] LearningDeliveries { get; set; }
+ }
+
+ public class Learningdelivery
+ {
+ public int AimType { get; set; }
+ public DateTime LearnStartDate { get; set; }
+ public DateTime LearnPlanEndDate { get; set; }
+ public int FundModel { get; set; }
+ public int StdCode { get; set; }
+ public string DelLocPostCode { get; set; }
+ public string EpaOrgID { get; set; }
+ public int CompStatus { get; set; }
+ public DateTime LearnActEndDate { get; set; }
+ public int WithdrawReason { get; set; }
+ public int Outcome { get; set; }
+ public DateTime AchDate { get; set; }
+ public string OutGrade { get; set; }
+ public int ProgType { get; set; }
+ }
+
+}
diff --git a/src/Payments/SFA.DAS.Payments/SFA.DAS.Payments.csproj b/src/Payments/SFA.DAS.Payments/SFA.DAS.Payments.csproj
new file mode 100644
index 0000000000..5441276dc8
--- /dev/null
+++ b/src/Payments/SFA.DAS.Payments/SFA.DAS.Payments.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Payments/azure-pipelines.yml b/src/Payments/azure-pipelines.yml
new file mode 100644
index 0000000000..7e7d3e6f51
--- /dev/null
+++ b/src/Payments/azure-pipelines.yml
@@ -0,0 +1,53 @@
+trigger:
+ batch: true
+ branches:
+ include:
+ - "master"
+ paths:
+ include:
+ - src/Payments
+ exclude:
+ - azure
+ - pipeline-templates
+ - deployments
+
+pr:
+ autoCancel: true
+ branches:
+ include:
+ - "master"
+ paths:
+ include:
+ - src/Payments
+ exclude:
+ - azure
+ - pipeline-templates
+ - deployments
+
+variables:
+- group: Release Management Resources
+- group: RELEASE das-apim-endpoints
+
+resources:
+ repositories:
+ - repository: self
+ - repository: das-platform-building-blocks
+ type: github
+ name: SkillsFundingAgency/das-platform-building-blocks
+ ref: refs/tags/2.2.0
+ endpoint: SkillsFundingAgency
+ - repository: das-platform-automation
+ type: github
+ name: SkillsFundingAgency/das-platform-automation
+ ref: refs/tags/5.1.10
+ endpoint: SkillsFundingAgency
+ pipelines:
+ - pipeline: das-employer-config
+ project: Digital Apprenticeship Service
+ source: das-employer-config
+ branch: master
+
+stages:
+- template: ../../pipeline-templates/stage/outerapi-pipeline.yml
+ parameters:
+ OuterApiName: Payments
diff --git a/src/Payments/pipeline-templates-payments/job/deploy.yml b/src/Payments/pipeline-templates-payments/job/deploy.yml
new file mode 100644
index 0000000000..e79f582224
--- /dev/null
+++ b/src/Payments/pipeline-templates-payments/job/deploy.yml
@@ -0,0 +1,162 @@
+parameters:
+ ServiceConnection:
+ AppRoleAssignmentsServiceConnection:
+ Environment:
+ AADGroupObjectIdArray:
+ SchemaFilePath:
+ ConfigurationSecrets:
+ DeploymentName:
+ AppServiceName:
+ CustomHostName:
+ AppServiceKeyVaultCertificateName:
+ ConfigNames:
+ DeploymentPackagePath:
+ ApiVersionSetName:
+ ApiPath:
+ ApiBaseUrl:
+ ProductId:
+ ApplicationIdentifierUri:
+ SandboxEnabled: $False
+ AddXForwardedAuthorization: $False
+
+jobs:
+- deployment: DeployInfrastructureNew
+ condition: eq( variables.platformBuildBlockVersion, 'refs/tags/2.2.0')
+ pool:
+ name: DAS - Continuous Deployment Agents
+ environment: ${{ parameters.Environment }}
+ workspace:
+ clean: all
+ variables:
+ AppServiceName: ${{ parameters.AppServiceName }}
+ CustomHostName: ${{ parameters.CustomHostName }}
+ AppServiceKeyVaultCertificateName: ${{ parameters.AppServiceKeyVaultCertificateName }}
+ ConfigNames: ${{ parameters.ConfigNames }}
+ strategy:
+ runOnce:
+ deploy:
+ steps:
+ - template: azure-pipelines-templates/deploy/step/wait-azure-devops-deployment.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ EnvironmentId: $(Environment.Id)
+ PipelineName: $(Build.DefinitionName)
+ RunId: $(Build.BuildId)
+ - template: azure-pipelines-templates/deploy/step/arm-deploy.yml@das-platform-building-blocks
+ parameters:
+ Location: $(ResourceGroupLocation)
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ SubscriptionId: $(SubscriptionId)
+ TemplatePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/azure/template.json
+ ParametersPath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/azure/template.parameters.json
+ IsMultiRepoCheckout: true
+ TemplateSecrets:
+ LoggingRedisConnectionString: $(LoggingRedisConnectionString)
+ ConfigurationStorageConnectionString: $(ConfigurationStorageConnectionString)
+ - template: azure-pipelines-templates/deploy/step/generate-config.yml@das-platform-building-blocks
+ parameters:
+ EnvironmentName: $(EnvironmentName)
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ SourcePath: $(Pipeline.Workspace)/das-employer-config/Configuration/das-apim-endpoints
+ StorageAccountName: $(ConfigurationStorageAccountName)
+ StorageAccountResourceGroup: $(SharedEnvResourceGroup)
+ TargetFileName: ${{ parameters.SchemaFilePath }}
+ TableName: Configuration
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ - template: azure-pipelines-templates/deploy/step/app-deploy.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ AppServiceName: ${{ parameters.AppServiceName }}
+ DeploymentPackagePath: ${{ parameters.DeploymentPackagePath }}
+ - checkout: das-platform-automation
+ - task: AzurePowerShell@5
+ displayName: Import-ApimSwaggerApiDefinition - ${{ parameters.AppServiceName }}
+ inputs:
+ azureSubscription: ${{ parameters.ServiceConnection }}
+ ScriptPath: das-platform-automation/Infrastructure-Scripts/Import-ApimSwaggerApiDefinition.ps1
+ ScriptArguments:
+ -ApimResourceGroup $(ApimResourceGroup) `
+ -InstanceName $(InstanceName) `
+ -AppServiceResourceGroup $(ResourceGroupName) `
+ -ApiVersionSetName ${{ parameters.ApiVersionSetName }} `
+ -ApiPath ${{ parameters.ApiPath }} `
+ -ApiBaseUrl ${{ parameters.ApiBaseUrl }} `
+ -ProductId ${{ parameters.ProductId }} `
+ -ApplicationIdentifierUri ${{ parameters.ApplicationIdentifierUri }} `
+ -SandboxEnabled ${{ parameters.SandboxEnabled }} `
+ -AddXForwardedAuthorization ${{ parameters.AddXForwardedAuthorization }} `
+ -Verbose
+ azurePowerShellVersion: LatestVersion
+ pwsh: true
+
+- deployment: DeployInfrastructureOld
+ condition: ne( variables.platformBuildBlockVersion, 'refs/tags/2.2.0')
+ pool:
+ name: DAS - Continuous Deployment Agents
+ environment: ${{ parameters.Environment }}
+ workspace:
+ clean: all
+ variables:
+ AppServiceName: ${{ parameters.AppServiceName }}
+ CustomHostName: ${{ parameters.CustomHostName }}
+ AppServiceKeyVaultCertificateName: ${{ parameters.AppServiceKeyVaultCertificateName }}
+ ConfigNames: ${{ parameters.ConfigNames }}
+ strategy:
+ runOnce:
+ deploy:
+ steps:
+ - template: azure-pipelines-templates/deploy/step/wait-azure-devops-deployment.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ EnvironmentId: $(Environment.Id)
+ PipelineName: $(Build.DefinitionName)
+ RunId: $(Build.BuildId)
+ - template: azure-pipelines-templates/deploy/step/arm-deploy.yml@das-platform-building-blocks
+ parameters:
+ Location: $(ResourceGroupLocation)
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ SubscriptionId: $(SubscriptionId)
+ TemplatePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/azure/template.json
+ ParametersPath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/azure/template.parameters.json
+ IsMultiRepoCheckout: true
+ TemplateSecrets:
+ LoggingRedisConnectionString: $(LoggingRedisConnectionString)
+ ConfigurationStorageConnectionString: $(ConfigurationStorageConnectionString)
+ - template: azure-pipelines-templates/deploy/step/generate-config.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ SourcePath: $(Pipeline.Workspace)/das-employer-config/Configuration/das-apim-endpoints
+ TargetFileName: ${{ parameters.SchemaFilePath }}
+ TableName: Configuration
+ - template: azure-pipelines-templates/deploy/step/app-role-assignments.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.AppRoleAssignmentsServiceConnection }}
+ ResourceName: ${{ parameters.AppServiceName }}
+ Tenant: $(Tenant)
+ AADGroupObjectIdArray: ${{ parameters.AADGroupObjectIdArray }}
+ IsMultiRepoCheckout: true
+ - template: azure-pipelines-templates/deploy/step/app-deploy.yml@das-platform-building-blocks
+ parameters:
+ ServiceConnection: ${{ parameters.ServiceConnection }}
+ AppServiceName: ${{ parameters.AppServiceName }}
+ DeploymentPackagePath: ${{ parameters.DeploymentPackagePath }}
+ - checkout: das-platform-automation
+ - task: AzurePowerShell@5
+ displayName: Import-ApimSwaggerApiDefinition - ${{ parameters.AppServiceName }}
+ inputs:
+ azureSubscription: ${{ parameters.ServiceConnection }}
+ ScriptPath: das-platform-automation/Infrastructure-Scripts/Import-ApimSwaggerApiDefinition.ps1
+ ScriptArguments:
+ -ApimResourceGroup $(ApimResourceGroup) `
+ -InstanceName $(InstanceName) `
+ -AppServiceResourceGroup $(ResourceGroupName) `
+ -ApiVersionSetName ${{ parameters.ApiVersionSetName }} `
+ -ApiPath ${{ parameters.ApiPath }} `
+ -ApiBaseUrl ${{ parameters.ApiBaseUrl }} `
+ -ProductId ${{ parameters.ProductId }} `
+ -ApplicationIdentifierUri ${{ parameters.ApplicationIdentifierUri }} `
+ -SandboxEnabled ${{ parameters.SandboxEnabled }} `
+ -AddXForwardedAuthorization ${{ parameters.AddXForwardedAuthorization }} `
+ -Verbose
+ azurePowerShellVersion: LatestVersion
+ pwsh: true
diff --git a/src/Payments/pipeline-templates-payments/stage/outerapi-pipeline.yml b/src/Payments/pipeline-templates-payments/stage/outerapi-pipeline.yml
new file mode 100644
index 0000000000..3a1add0ecd
--- /dev/null
+++ b/src/Payments/pipeline-templates-payments/stage/outerapi-pipeline.yml
@@ -0,0 +1,250 @@
+parameters:
+ OuterApiName:
+ SandboxEnabled: $False
+ AddXForwardedAuthorization: $False
+ SharedOuterApiProjectPathToInclude: 'src/Shared/SFA.DAS.SharedOuterApi/SFA.DAS.SharedOuterApi.csproj'
+ SharedOuterApiTestProjectPathToInclude: 'src/Shared/SFA.DAS.SharedOuterApi.UnitTests/SFA.DAS.SharedOuterApi.UnitTests.csproj'
+ AdditionalProjectPathToInclude: ''
+ AdditionalTestProjectPathToInclude: ''
+ ConfigurationSecrets: {}
+
+stages:
+- stage: Build
+ jobs:
+ - template: ../../../../pipeline-templates/job/code-build.yml
+ parameters:
+ SonarCloudProjectKey: SkillsFundingAgency_das-apim-endpoints_${{ parameters.OuterApiName }}
+ TargetProjects: |
+ src/${{ parameters.OuterApiName }}/**/*.csproj
+ ${{ parameters.SharedOuterApiProjectPathToInclude }}
+ ${{ parameters.SharedOuterApiTestProjectPathToInclude }}
+ ${{ parameters.AdditionalProjectPathToInclude }}
+ ${{ parameters.AdditionalTestProjectPathToInclude }}
+ UnitTestProjects: |
+ src/${{ parameters.OuterApiName }}/**/*UnitTests.csproj
+ ${{ parameters.SharedOuterApiTestProjectPathToInclude }}
+ ${{ parameters.AdditionalTestProjectPathToInclude }}
+ AcceptanceTestProjects: 'src/${{ parameters.OuterApiName }}/**/*AcceptanceTests.csproj'
+ PublishProject: 'src/${{ parameters.OuterApiName }}/SFA.DAS.${{ parameters.OuterApiName }}.Api/SFA.DAS.${{ parameters.OuterApiName }}.Api.csproj'
+
+- stage: Deploy_AT
+ dependsOn: Build
+ displayName: Deploy to AT
+ variables:
+ - group: DevTest Management Resources
+ - group: AT DevTest Shared Resources
+ - group: AT das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DAS-DevTest-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-CDS
+ Environment: AT
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AADGroupObjectIdArray: $(AdminAADGroupObjectId),$(DevAADGroupObjectId)
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_TEST
+ dependsOn: Build
+ displayName: Deploy to TEST
+ variables:
+ - group: DevTest Management Resources
+ - group: TEST DevTest Shared Resources
+ - group: TEST das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DAS-DevTest-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-CDS
+ Environment: TEST
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AADGroupObjectIdArray: $(AdminAADGroupObjectId),$(DevAADGroupObjectId)
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_TEST2
+ dependsOn: Build
+ displayName: Deploy to TEST2
+ variables:
+ - group: DevTest Management Resources
+ - group: TEST2 DevTest Shared Resources
+ - group: TEST2 das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DAS-DevTest-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-CDS
+ Environment: TEST2
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AADGroupObjectIdArray: $(AdminAADGroupObjectId),$(DevAADGroupObjectId)
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_PP
+ dependsOn: Build
+ displayName: Deploy to PP
+ variables:
+ - group: PREPROD Management Resources
+ - group: PREPROD Shared Resources
+ - group: PREPROD das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DIG-PreProd-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-FCS
+ Environment: PP
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AADGroupObjectIdArray: $(AdminAADGroupObjectId),$(DevAADGroupObjectId)
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_PROD
+ dependsOn: Build
+ displayName: Deploy to PROD
+ variables:
+ - group: PROD Management Resources
+ - group: PROD Shared Resources
+ - group: PROD das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DIG-Prod-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-FCS
+ Environment: PROD
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_MO
+ dependsOn: Build
+ displayName: Deploy to MO
+ variables:
+ - group: MO Management Resources
+ - group: MO Shared Resources
+ - group: MO das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-ASM-ModelOffice-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-FCS
+ Environment: MO
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
+
+- stage: Deploy_DEMO
+ dependsOn: Build
+ displayName: Deploy to DEMO
+ variables:
+ - group: DevTest Management Resources
+ - group: DEMO DevTest Shared Resources
+ - group: DEMO das-apim-endpoints
+ - name: platformBuildBlockVersion
+ value: $[ resources.repositories['das-platform-building-blocks'].ref ]
+ jobs:
+ - template: ../../../../src/Payments/pipeline-templates-payments/job/deploy.yml
+ parameters:
+ ServiceConnection: SFA-DAS-DevTest-ARM
+ AppRoleAssignmentsServiceConnection: das-app-role-assignments-CDS
+ Environment: DEMO
+ SchemaFilePath: $(${{ parameters.OuterApiName }}OuterApiSchemaFilePath)
+ ConfigurationSecrets: ${{ parameters.ConfigurationSecrets }}
+ DeploymentName: Deploy_${{ parameters.OuterApiName }}
+ AADGroupObjectIdArray: $(AdminAADGroupObjectId),$(DevAADGroupObjectId)
+ AppServiceName: $(${{ parameters.OuterApiName }}OuterApiAppServiceName)
+ CustomHostName: $(${{ parameters.OuterApiName }}OuterApiCustomHostname)
+ AppServiceKeyVaultCertificateName: $(${{ parameters.OuterApiName }}OuterApiKeyVaultCertificateName)
+ ConfigNames: $(${{ parameters.OuterApiName }}OuterApiConfigNames)
+ DeploymentPackagePath: $(Pipeline.Workspace)/ApimEndpointsArtifacts/SFA.DAS.${{ parameters.OuterApiName }}.Api.zip
+ ApiVersionSetName: $(${{ parameters.OuterApiName }}OuterApiVersionSetName)
+ ApiPath: $(${{ parameters.OuterApiName }}OuterApiPath)
+ ApiBaseUrl: $(${{ parameters.OuterApiName }}OuterApiBaseUrl)
+ ProductId: $(${{ parameters.OuterApiName }}ProductId)
+ ApplicationIdentifierUri: $(${{ parameters.OuterApiName }}OuterApiIdentifierUri)
+ SandboxEnabled: ${{ parameters.SandboxEnabled }}
+ AddXForwardedAuthorization: ${{ parameters.AddXForwardedAuthorization }}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Configuration/AccessTokenProviderApiConfiguration.cs b/src/Shared/SFA.DAS.SharedOuterApi/Configuration/AccessTokenProviderApiConfiguration.cs
new file mode 100644
index 0000000000..aeff43a704
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Configuration/AccessTokenProviderApiConfiguration.cs
@@ -0,0 +1,13 @@
+namespace SFA.DAS.SharedOuterApi.Configuration
+{
+ public class AccessTokenProviderApiConfiguration
+ {
+ public string Url { get; set; }
+ public string Scope { get; set; }
+ public string ClientId { get; set; }
+ public string Tenant { get; set; }
+ public string ClientSecret { get; set; }
+ public string GrantType { get; set; }
+ public bool ShouldSkipForLocal { get; set; }
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Configuration/LearnerDataApiConfiguration.cs b/src/Shared/SFA.DAS.SharedOuterApi/Configuration/LearnerDataApiConfiguration.cs
new file mode 100644
index 0000000000..e70ab41a48
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Configuration/LearnerDataApiConfiguration.cs
@@ -0,0 +1,10 @@
+using SFA.DAS.SharedOuterApi.Interfaces;
+
+namespace SFA.DAS.SharedOuterApi.Configuration
+{
+ public class LearnerDataApiConfiguration : IAccessTokenApiConfiguration
+ {
+ public string Url { get; set; }
+ public AccessTokenProviderApiConfiguration TokenSettings { get; set; }
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/AccessTokenApiClient.cs b/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/AccessTokenApiClient.cs
new file mode 100644
index 0000000000..8a8185600c
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/AccessTokenApiClient.cs
@@ -0,0 +1,76 @@
+using Microsoft.Extensions.Logging;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.SharedOuterApi.Infrastructure
+{
+ public class AccessTokenApiClient : ApiClient, IAccessTokenApiClient where T : IAccessTokenApiConfiguration
+ {
+ private readonly HttpClient _tokenClient;
+ private readonly ILogger> _logger;
+
+ public AccessTokenApiClient(
+ ILogger> logger,
+ IHttpClientFactory httpClientFactory,
+ T apiConfiguration) : base(httpClientFactory, apiConfiguration)
+ {
+ _logger = logger;
+ _tokenClient = httpClientFactory.CreateClient();
+ _tokenClient.BaseAddress = new Uri(apiConfiguration.TokenSettings.Url);
+ }
+
+ protected override async Task AddAuthenticationHeader(HttpRequestMessage httpRequestMessage)
+ {
+ if (Configuration.TokenSettings.ShouldSkipForLocal)
+ {
+ _logger.LogWarning("Token acquisition is skipped. This should not happen in a production environment.");
+ return;
+ }
+
+ var token = string.Empty;
+
+ try
+ {
+ token = await GetAccessToken();
+ }
+ catch (Exception e)
+ {
+ throw new UnauthorizedAccessException("Could not retrieve access token", e);
+ }
+
+ httpRequestMessage.Headers.Add("Authorization", $"Bearer {token}");
+ }
+
+ private async Task GetAccessToken()
+ {
+ var tokenMessage = new HttpRequestMessage(HttpMethod.Get, Configuration.TokenSettings.Tenant);
+ tokenMessage.Content = new FormUrlEncodedContent(new[]
+ {
+ new KeyValuePair("client_id", Configuration.TokenSettings.ClientId),
+ new KeyValuePair("scope", Configuration.TokenSettings.Scope),
+ new KeyValuePair("client_secret", Configuration.TokenSettings.ClientSecret),
+ new KeyValuePair("grant_type", "client_credentials")
+ });
+
+ var response = await _tokenClient.SendAsync(tokenMessage).ConfigureAwait(false);
+ var json = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new Exception($"Could not retrieve access token. Status code: {response.StatusCode}, Response: {json}");
+ }
+
+ var tokenResponse = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
+
+ return tokenResponse.access_token;
+ }
+
+ private class TokenResponse
+ {
+ public string access_token { get; set; }
+ }
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/GetApiClient.cs b/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/GetApiClient.cs
index 256f85acea..4a17724173 100644
--- a/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/GetApiClient.cs
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Infrastructure/GetApiClient.cs
@@ -6,6 +6,8 @@
using System.Threading.Tasks;
using SFA.DAS.SharedOuterApi.Models;
using System.Text.Json.Serialization;
+using System.Collections.Generic;
+using System.Linq;
namespace SFA.DAS.SharedOuterApi.Infrastructure
{
@@ -77,7 +79,9 @@ public async Task> GetWithResponseCode(IGetApi
responseBody = JsonSerializer.Deserialize(json, options);
}
- var getWithResponseCode = new ApiResponse(responseBody, response.StatusCode, errorContent);
+ var headers = response.Headers;
+
+ var getWithResponseCode = new ApiResponse(responseBody, response.StatusCode, errorContent, GetHeaders(response));
return getWithResponseCode;
}
@@ -89,5 +93,14 @@ private static bool IsNot200RangeResponseCode(HttpStatusCode statusCode)
protected abstract Task AddAuthenticationHeader(HttpRequestMessage httpRequestMessage);
+ private static Dictionary> GetHeaders(HttpResponseMessage httpResponseMessage)
+ {
+ if(httpResponseMessage?.Headers == null && !httpResponseMessage.Headers.Any())
+ {
+ return new Dictionary>();
+ }
+
+ return httpResponseMessage.Headers.ToDictionary(h => h.Key, h => h.Value);
+ }
}
}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiClient.cs b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiClient.cs
new file mode 100644
index 0000000000..f0eed6aaeb
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiClient.cs
@@ -0,0 +1,6 @@
+namespace SFA.DAS.SharedOuterApi.Interfaces
+{
+ public interface IAccessTokenApiClient : IApiClient
+ {
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiConfiguration.cs b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiConfiguration.cs
new file mode 100644
index 0000000000..7383204313
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/IAccessTokenApiConfiguration.cs
@@ -0,0 +1,9 @@
+using SFA.DAS.SharedOuterApi.Configuration;
+
+namespace SFA.DAS.SharedOuterApi.Interfaces
+{
+ public interface IAccessTokenApiConfiguration : IApiConfiguration
+ {
+ AccessTokenProviderApiConfiguration TokenSettings { get; set; }
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/ILearnerDataApiClient.cs b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/ILearnerDataApiClient.cs
new file mode 100644
index 0000000000..7cab632fb4
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Interfaces/ILearnerDataApiClient.cs
@@ -0,0 +1,6 @@
+namespace SFA.DAS.SharedOuterApi.Interfaces
+{
+ public interface ILearnerDataApiClient : IGetApiClient
+ {
+ }
+}
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Models/ApiResponse.cs b/src/Shared/SFA.DAS.SharedOuterApi/Models/ApiResponse.cs
index 3aeb6e1506..a91b0955fc 100644
--- a/src/Shared/SFA.DAS.SharedOuterApi/Models/ApiResponse.cs
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Models/ApiResponse.cs
@@ -1,4 +1,5 @@
-using System.Net;
+using System.Collections.Generic;
+using System.Net;
namespace SFA.DAS.SharedOuterApi.Models
{
@@ -7,12 +8,19 @@ public class ApiResponse
public TResponse Body { get; }
public HttpStatusCode StatusCode { get; }
public string ErrorContent { get ; }
+ public Dictionary> Headers { get; }
- public ApiResponse (TResponse body, HttpStatusCode statusCode, string errorContent)
+ public ApiResponse(TResponse body, HttpStatusCode statusCode, string errorContent) :this(body, statusCode, errorContent, new Dictionary>())
+ {
+
+ }
+
+ public ApiResponse (TResponse body, HttpStatusCode statusCode, string errorContent, Dictionary> headers)
{
Body = body;
StatusCode = statusCode;
ErrorContent = errorContent;
+ Headers = headers;
}
}
}
\ No newline at end of file
diff --git a/src/Shared/SFA.DAS.SharedOuterApi/Services/LearnerDataApiClient.cs b/src/Shared/SFA.DAS.SharedOuterApi/Services/LearnerDataApiClient.cs
new file mode 100644
index 0000000000..c97728fd31
--- /dev/null
+++ b/src/Shared/SFA.DAS.SharedOuterApi/Services/LearnerDataApiClient.cs
@@ -0,0 +1,33 @@
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.SharedOuterApi.Models;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.SharedOuterApi.Services
+{
+ public class LearnerDataApiClient : ILearnerDataApiClient
+ {
+ private readonly IAccessTokenApiClient _apiClient;
+
+ public LearnerDataApiClient(IAccessTokenApiClient apiClient)
+ {
+ _apiClient = apiClient;
+ }
+
+ public async Task Get(IGetApiRequest request)
+ {
+ return await _apiClient.Get(request);
+ }
+
+ public async Task> GetWithResponseCode(IGetApiRequest request)
+ {
+ return await _apiClient.GetWithResponseCode(request);
+ }
+
+ public async Task GetResponseCode(IGetApiRequest request)
+ {
+ return await _apiClient.GetResponseCode(request);
+ }
+ }
+}