Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
47e0ab0
Added development docker files for local dev
owen-lakin Aug 13, 2026
de9571c
Updates for deletion of an application
owen-lakin Aug 14, 2026
167757c
Updates for deletion of an application
owen-lakin Aug 14, 2026
0b4adc8
Merge branch 'main' into feature/ticket-297111
owen-lakin Aug 14, 2026
4abd939
DB Migration updates
owen-lakin Aug 14, 2026
2e46165
Removed the pre deleted and undelete functionality
owen-lakin Aug 17, 2026
5e4c860
Added tests for the delete handler
owen-lakin Aug 18, 2026
697288e
Merge branch 'main' into feature/ticket-297111
owen-lakin Aug 18, 2026
03728bc
Added permission checks for apply search and updated some local dev code
owen-lakin Aug 18, 2026
dbd17f5
Merge branch 'main' into feature/ticket-297111
owen-lakin Aug 18, 2026
b905616
Code tidy up
owen-lakin Aug 19, 2026
51d4c9f
Merge branch 'main' into feature/ticket-297111
owen-lakin Aug 19, 2026
f1904a2
Added to security json for DeleteApplicationAsync endpoint security test
owen-lakin Aug 19, 2026
979f018
Added local development config
owen-lakin Aug 19, 2026
6159b7e
Test updates
owen-lakin Aug 19, 2026
04360f4
Merge branch 'main' into feature/ticket-297111
FrostyApeOne Aug 19, 2026
306f759
Updated the change log
owen-lakin Aug 20, 2026
064c6ef
Changed the change log version
owen-lakin Aug 21, 2026
98cd548
Updated applications delete controller action path
Aug 21, 2026
f99952c
Removed the docker container code
owen-lakin Aug 21, 2026
9b14f4c
Merge branch 'feature/ticket-297111' of https://github.com/DFE-Digita…
owen-lakin Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,6 @@ backend.vars
/Benchmarks/Dfe.PersonsApi.Benchmarks/BenchmarkDotNet.Artifacts/results
/uploads/
uploads/

Directory.Build.targets.user
/src/LocalPackages
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ All notable changes to this service will be documented in this file.
### Notes
- FlexForms (Forms Engine SaaS)

## [2.1.0] - Application delete functionality
### Notes
- Added functionality to soft delete an application, which will be marked with the deleted status in the DB


---------------------------------------------------------------------------

Expand Down Expand Up @@ -140,3 +144,4 @@ All notable changes to this service will be documented in this file.
## [1.5.2] - Filter on status fix
### Notes
- Fix for the filtering not working when a null value for status is encountered in the db

138 changes: 138 additions & 0 deletions src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,144 @@ public string BaseUrl
}
}

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Soft deletes an application, changing its status to Deleted.
/// </summary>
/// <returns>Application deleted successfully.</returns>
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<ApplicationDto> DeleteApplicationAsync(System.Guid applicationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
if (applicationId == null)
throw new System.ArgumentNullException("applicationId");

var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("DELETE");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));

var urlBuilder_ = new System.Text.StringBuilder();
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
// Operation Path: "v1/Applications/{applicationId}"
urlBuilder_.Append("v1/Applications/");
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(applicationId, System.Globalization.CultureInfo.InvariantCulture)));

PrepareRequest(client_, request_, urlBuilder_);

var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

PrepareRequest(client_, request_, url_);

var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
foreach (var item_ in response_.Headers)
headers_[item_.Key] = item_.Value;
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}

ProcessResponse(client_, response_);

var status_ = (int)response_.StatusCode;
if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<ApplicationDto>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
return objectResponse_.Object;
}
else
if (status_ == 400)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("Invalid request data or application not found.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 401)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("Unauthorized - no valid user token", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 403)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("User does not have permission to delete this application", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 404)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("Application not found", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 429)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("Too Many Requests.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 500)
{
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ExternalApplicationsException<ExceptionResponse>("Internal server error.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
{
var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false);
throw new ExternalApplicationsException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
}

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Submits an application, changing its status to Submitted.
Expand Down
8 changes: 8 additions & 0 deletions src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ public partial interface IApplicationsClient
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
System.Threading.Tasks.Task<ApplicationDto> GetApplicationByReferenceAsync(string applicationReference, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Soft deletes an application, changing its status to Deleted.
/// </summary>
/// <returns>Application deleted successfully.</returns>
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
System.Threading.Tasks.Task<ApplicationDto> DeleteApplicationAsync(System.Guid applicationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Submits an application, changing its status to Submitted.
Expand Down
93 changes: 93 additions & 0 deletions src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,99 @@
}
}
},
"/v1/Applications/{applicationId}": {
"delete": {
"tags": [
"Applications"
],
"summary": "Soft deletes an application, changing its status to Deleted.",
"operationId": "Applications_DeleteApplication",
"parameters": [
{
"name": "applicationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "guid"
},
"x-position": 1
}
],
"responses": {
"200": {
"description": "Application deleted successfully.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationDto"
}
}
}
},
"400": {
"description": "Invalid request data or application not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"401": {
"description": "Unauthorized - no valid user token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"403": {
"description": "User does not have permission to delete this application",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"404": {
"description": "Application not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"500": {
"description": "Internal server error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"429": {
"description": "Too Many Requests.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
}
}
}
},
"/v1/Applications/{applicationId}/submit": {
"post": {
"tags": [
Expand Down
30 changes: 28 additions & 2 deletions src/GovUK.Dfe.FlexForms.Api/Controllers/ApplicationsController.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
using Asp.Versioning;
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Enums;
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Models.Request;
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Models.Response;
using GovUK.Dfe.CoreLibs.Http.Models;
using GovUK.Dfe.FlexForms.Api.Filters;
using GovUK.Dfe.FlexForms.Api.Models.Applications;
using GovUK.Dfe.FlexForms.Application.Applications.Commands;
using GovUK.Dfe.FlexForms.Application.Applications.Queries;
using GovUK.Dfe.FlexForms.Application.Common.Exceptions;
using GovUK.Dfe.FlexForms.Api.Filters;
using GovUK.Dfe.FlexForms.Infrastructure.Security;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using ApplicationId = GovUK.Dfe.FlexForms.Domain.ValueObjects.ApplicationId;
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Enums;

namespace GovUK.Dfe.FlexForms.Api.Controllers;

Expand Down Expand Up @@ -167,6 +168,31 @@ public async Task<IActionResult> GetApplicationByReferenceAsync(
};
}

/// <summary>
/// Soft deletes an application, changing its status to Deleted.
/// </summary>
[HttpDelete("{applicationId}")]
[SwaggerResponse(200, "Application deleted successfully.", typeof(ApplicationDto))]
[SwaggerResponse(400, "Invalid request data or application not found.", typeof(ExceptionResponse))]
[SwaggerResponse(401, "Unauthorized - no valid user token", typeof(ExceptionResponse))]
[SwaggerResponse(403, "User does not have permission to delete this application", typeof(ExceptionResponse))]
[SwaggerResponse(404, "Application not found", typeof(ExceptionResponse))]
[SwaggerResponse(500, "Internal server error.", typeof(ExceptionResponse))]
[SwaggerResponse(429, "Too Many Requests.", typeof(ExceptionResponse))]
[Authorize(Policy = AuthConstants.TenantAdminUserPolicy)]
public async Task<IActionResult> DeleteApplicationAsync(
[FromRoute] Guid applicationId,
CancellationToken cancellationToken)
{
var command = new DeleteApplicationCommand(applicationId);
var result = await sender.Send(command, cancellationToken);

return new ObjectResult(result)
{
StatusCode = StatusCodes.Status200OK
};
}

/// <summary>
/// Submits an application, changing its status to Submitted.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion src/GovUK.Dfe.FlexForms.Api/GovUK.Dfe.FlexForms.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,8 @@
<Message Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' == '0'" Importance="low" />
<Error Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' != '0'" />
</Target>

<Target Name="BuildDockerImage" AfterTargets="Build" Condition="'$(SkipDockerCompose)' != 'true' and '$(LocalDevelopment)' == 'true'">
<Exec Command="set &quot;REPO_PATH=$(SolutionDir)&quot; &amp;&amp; docker compose -f &quot;$(SolutionDir)docker-compose.yaml&quot; down" />
<Exec Command="set &quot;REPO_PATH=$(SolutionDir)&quot; &amp;&amp; docker compose -f &quot;$(SolutionDir)docker-compose.yaml&quot; up -d --build --force-recreate" />
</Target>
</Project>
Loading
Loading