Repository: SkillsFundingAgency/das-apim-endpoints
Pull request #2186: FAI-1923 Add application count feature
Author: login: balaji-j
Created: 2025-05-07T11:35:12Z | Merged: 2025-05-19T17:46:40Z

✨ Add application count feature in ApplicationsController

- Introduced a new endpoint to count applications by status.
- Added `Count` method in `ApplicationsController` for handling requests.
- Created `GetApplicationsCountQuery` and its handler for processing.
- Defined response model `GetApplicationsCountQueryResult` for API data.
- Implemented unit tests to validate new functionality and behavior.

Changes made by Balaji Jambulingam


Diff:
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api.UnitTests/Controllers/ApplicationsController/WhenGettingCount.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api.UnitTests/Controllers/ApplicationsController/WhenGettingCount.cs
new file mode 100644
index 0000000000..f3d506f24c
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api.UnitTests/Controllers/ApplicationsController/WhenGettingCount.cs
@@ -0,0 +1,35 @@
+﻿using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount;
+using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.FindAnApprenticeship.Api.UnitTests.Controllers.ApplicationsController
+{
+    [TestFixture]
+    public class WhenGettingCount
+    {
+        [Test, MoqAutoData]
+        public async Task Then_The_Query_Response_Is_Returned(
+            Guid candidateId,
+            ApplicationStatus status,
+            GetApplicationsCountQueryResult queryResult,
+            [Frozen] Mock<IMediator> mediator,
+            [Greedy] Api.Controllers.ApplicationsController controller)
+        {
+            mediator.Setup(x => x.Send(It.Is<GetApplicationsCountQuery>(q =>
+                        q.CandidateId == candidateId && q.Status == status),
+                    It.IsAny<CancellationToken>()))
+                .ReturnsAsync(queryResult);
+
+            var actual = await controller.Count(candidateId, status);
+
+            actual.Should().BeOfType<OkObjectResult>();
+            var actualObject = ((OkObjectResult)actual).Value as GetApplicationsCountQueryResult;
+            actualObject.Should().NotBeNull();
+            actualObject.Should().BeEquivalentTo(queryResult);
+        }
+    }
+}
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api/Controllers/ApplicationsController.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api/Controllers/ApplicationsController.cs
index 9ef23857df..8b1c58091f 100644
--- a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api/Controllers/ApplicationsController.cs
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.Api/Controllers/ApplicationsController.cs
@@ -3,26 +3,19 @@
 using Microsoft.Extensions.Logging;
 using SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplications;
 using System;
+using System.Collections.Generic;
 using System.Net;
 using System.Threading.Tasks;
 using SFA.DAS.FindAnApprenticeship.Api.Models.Applications;
+using SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount;
 using SFA.DAS.FindAnApprenticeship.Domain.Models;
 
 namespace SFA.DAS.FindAnApprenticeship.Api.Controllers
 {
     [ApiController]
     [Route("[controller]")]
-    public class ApplicationsController : Controller
+    public class ApplicationsController(IMediator mediator, ILogger<ApplicationsController> logger) : Controller
     {
-        private readonly IMediator _mediator;
-        private readonly ILogger<ApplicationsController> _logger;
-
-        public ApplicationsController(IMediator mediator, ILogger<ApplicationsController> logger)
-        {
-            _mediator = mediator;
-            _logger = logger;
-        }
-
         [HttpGet]
         [ProducesResponseType<GetApplicationsApiResponse>((int)HttpStatusCode.OK)]
         [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
@@ -30,7 +23,7 @@ public async Task<IActionResult> Index([FromQuery] Guid candidateId, [FromQuery]
         {
             try
             {
-                var result = await _mediator.Send(new GetApplicationsQuery
+                var result = await mediator.Send(new GetApplicationsQuery
                 {
                     CandidateId = candidateId,
                     Status = status
@@ -40,9 +33,27 @@ public async Task<IActionResult> Index([FromQuery] Guid candidateId, [FromQuery]
             }
             catch (Exception e)
             {
-                _logger.LogError(e, "Get Applications : An error occurred");
+                logger.LogError(e, "Get Applications : An error occurred");
                 return new StatusCodeResult((int) HttpStatusCode.InternalServerError);
             }
         }
+
+        [HttpGet("count")]
+        [ProducesResponseType<GetApplicationsCountQueryResult>((int)HttpStatusCode.OK)]
+        [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
+        public async Task<IActionResult> Count([FromQuery] Guid candidateId, [FromQuery] ApplicationStatus status)
+        {
+            try
+            {
+                var result = await mediator.Send(new GetApplicationsCountQuery(candidateId, status));
+
+                return Ok(result);
+            }
+            catch (Exception e)
+            {
+                logger.LogError(e, "Get Applications count : An error occurred");
+                return new StatusCodeResult((int)HttpStatusCode.InternalServerError);
+            }
+        }
     }
 }
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Queries/Applications/WhenHandlingGetApplicationsCountQuery.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Queries/Applications/WhenHandlingGetApplicationsCountQuery.cs
new file mode 100644
index 0000000000..20a83d34eb
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Queries/Applications/WhenHandlingGetApplicationsCountQuery.cs
@@ -0,0 +1,34 @@
+﻿using SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount;
+using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests;
+using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Responses;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using SFA.DAS.SharedOuterApi.Models;
+using System.Net;
+
+namespace SFA.DAS.FindAnApprenticeship.UnitTests.Application.Queries.Applications
+{
+    [TestFixture]
+    public class WhenHandlingGetApplicationsCountQuery
+    {
+        [Test, MoqAutoData]
+        public async Task Then_The_QueryResult_Is_Returned_As_Expected(
+            GetApplicationsCountQuery query,
+            GetApplicationsCountApiResponse applicationsCountApiResponse,
+            [Frozen] Mock<ICandidateApiClient<CandidateApiConfiguration>> candidateApiClient,
+            GetApplicationsCountQueryHandler handler)
+        {
+            var expectedRequest = new GetApplicationsCountApiRequest(query.CandidateId, query.Status);
+
+            candidateApiClient
+                .Setup(client => client.GetWithResponseCode<GetApplicationsCountApiResponse>(
+                    It.Is<GetApplicationsCountApiRequest>(r => r.GetUrl == expectedRequest.GetUrl)))
+                .ReturnsAsync(new ApiResponse<GetApplicationsCountApiResponse>(applicationsCountApiResponse, HttpStatusCode.OK, string.Empty));
+            
+            var result = await handler.Handle(query, CancellationToken.None);
+
+            using var scope = new AssertionScope();
+            result.Should().BeEquivalentTo(applicationsCountApiResponse);
+        }
+    }
+}
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/InnerApi/CandidateApi/Requests/WhenBuildingGetApplicationsCountApiRequest.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/InnerApi/CandidateApi/Requests/WhenBuildingGetApplicationsCountApiRequest.cs
new file mode 100644
index 0000000000..184aea1390
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/InnerApi/CandidateApi/Requests/WhenBuildingGetApplicationsCountApiRequest.cs
@@ -0,0 +1,21 @@
+﻿using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests;
+
+namespace SFA.DAS.FindAnApprenticeship.UnitTests.InnerApi.CandidateApi.Requests
+{
+    [TestFixture]
+    public class WhenBuildingGetApplicationsCountApiRequest
+    {
+        [Test, MoqAutoData]
+        public void Then_The_Get_Url_Is_Correct(
+            Guid candidateId,
+            ApplicationStatus status)
+        {
+            // Act
+            var request = new GetApplicationsCountApiRequest(candidateId, status);
+            var actual = request.GetUrl;
+            // Assert
+            actual.Should().Be($"api/candidates/{candidateId}/applications/count?status={status}");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQuery.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQuery.cs
new file mode 100644
index 0000000000..41ce8339c1
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQuery.cs
@@ -0,0 +1,9 @@
+﻿using MediatR;
+using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using System;
+
+namespace SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount
+{
+    public record GetApplicationsCountQuery(Guid CandidateId, ApplicationStatus Status)
+        : IRequest<GetApplicationsCountQueryResult>;
+}
\ No newline at end of file
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryHandler.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryHandler.cs
new file mode 100644
index 0000000000..fac86d2a9c
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryHandler.cs
@@ -0,0 +1,20 @@
+﻿using MediatR;
+using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests;
+using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Responses;
+using SFA.DAS.SharedOuterApi.Configuration;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount
+{
+    public class GetApplicationsCountQueryHandler(ICandidateApiClient<CandidateApiConfiguration> candidateApiClient)
+        : IRequestHandler<GetApplicationsCountQuery, GetApplicationsCountQueryResult>
+    {
+        public async Task<GetApplicationsCountQueryResult> Handle(GetApplicationsCountQuery request, CancellationToken cancellationToken)
+        {
+            var response = await candidateApiClient.GetWithResponseCode<GetApplicationsCountApiResponse>(new GetApplicationsCountApiRequest(request.CandidateId, request.Status));
+            return response.Body;
+        }
+    }
+}
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryResult.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryResult.cs
new file mode 100644
index 0000000000..7d3dd9836c
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Queries/Applications/GetApplicationsCount/GetApplicationsCountQueryResult.cs
@@ -0,0 +1,23 @@
+﻿using SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Responses;
+using System;
+using System.Collections.Generic;
+
+namespace SFA.DAS.FindAnApprenticeship.Application.Queries.Applications.GetApplicationsCount
+{
+    public record GetApplicationsCountQueryResult
+    {
+        public List<Guid> ApplicationIds { get; set; } = [];
+        public string Status { get; set; }
+        public int Count { get; set; }
+
+        public static implicit operator GetApplicationsCountQueryResult(GetApplicationsCountApiResponse source)
+        {
+            return new GetApplicationsCountQueryResult
+            {
+                Status = source.Status,
+                Count = source.Count,
+                ApplicationIds = source.ApplicationIds
+            };
+        }
+    }
+}
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Requests/GetApplicationsCountApiRequest.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Requests/GetApplicationsCountApiRequest.cs
new file mode 100644
index 0000000000..749de65a9e
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Requests/GetApplicationsCountApiRequest.cs
@@ -0,0 +1,11 @@
+﻿using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using System;
+
+namespace SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests
+{
+    public record GetApplicationsCountApiRequest(Guid CandidateId, ApplicationStatus Status) : IGetApiRequest
+    {
+        public string GetUrl => $"api/candidates/{CandidateId}/applications/count?status={Status}";
+    }
+}
\ No newline at end of file
diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Responses/GetApplicationsCountApiResponse.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Responses/GetApplicationsCountApiResponse.cs
new file mode 100644
index 0000000000..aa08b578a3
--- /dev/null
+++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Responses/GetApplicationsCountApiResponse.cs
@@ -0,0 +1,12 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Responses
+{
+    public record GetApplicationsCountApiResponse
+    {
+        public List<Guid> ApplicationIds { get; set; } = [];
+        public string Status { get; set; }
+        public int Count { get; set; }
+    }
+}
\ No newline at end of file


Reviews:
user: login: dashton82
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T21:47:52Z
user: login: balaji-j
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T09:08:19Z
user: login: dashton82
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:49:21Z

Review comments:
diff_hunk: @@ -0,0 +1,11 @@
+﻿using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using System;
+
+namespace SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests
+{
+    public record GetApplicationsCountApiRequest(Guid CandidateId, ApplicationStatus Status) : IGetApiRequest
path: src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Requests/PostApplicationsCountApiRequest.cs
user: login: dashton82
body: Can we rename the filename to match please?
created_at: 2025-05-12T21:47:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+﻿using SFA.DAS.FindAnApprenticeship.Domain.Models;
+using SFA.DAS.SharedOuterApi.Interfaces;
+using System;
+
+namespace SFA.DAS.FindAnApprenticeship.InnerApi.CandidateApi.Requests
+{
+    public record GetApplicationsCountApiRequest(Guid CandidateId, ApplicationStatus Status) : IGetApiRequest
path: src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/InnerApi/CandidateApi/Requests/PostApplicationsCountApiRequest.cs
user: login: balaji-j
body: Renamed the file as appropiate
created_at: 2025-05-13T09:08:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-14T13:39:01Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186) **Quality Gate passed for 'das-apim-endpoints-FindAnApprenticeship'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [85.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=SkillsFundingAgency_das-apim-endpoints_FindAnApprenticeship&pullRequest=2186)


author_association: NONE