Skip to content

Commit fd19aa3

Browse files
Consumer API: Reject identity deletion process (#535)
* feat: implement rejection of deletion process before approval * chore: fix a typo * feat: add reject identity deletion endpoint * feat: add RejectedAt and RejectedByDevice properties * chore: fix formatting * chore: remove IdentityAddress from RejectDeletionProcessResponse * chore: remove unused directives * chore: line endings * chore: remove unused directives * refactor: use SystemTime.UtcNow * refactor: use SystemTime * refactor: extract into separate method * chore: rename variable * chore: rename variable * refactor: remove ctor input parameter * chore: remove redundant status update * chore: rename input parameter * chore: change the method order in code * feat: add EnsureIdentityOwnsDevice method * test: check CreatedAt value * chore: reorganize lines * test: update condition * chore: reorganize lines * refactor: remove redundant date variable * chore: renamed mock repository variable to fake * test: use identity's own device in test * chore: rename class to plural * test: add reject deletion process test case when device is not owned by identity * test: use own device in test * test: update acting to start deletion process with own device * test: simplify method call * refactor: use hardcoded date * chore: rename variable * refactor: attempt to start deletion process with own device * test: change assertion * refactor: remove redundant handler tests as they are covered by the domain ones * chore: remove unused directives * refactor: reorganize class so that ctors are on top and private methods below their public ones * refactor: remove redundant assertion * feat: ensure deletion process is approved by device owned by identity * chore: move private Approve method beneath its first use * refactor: use device owned by identity and make code uniform in similar tests * refactor: use own device to approve deletion process * test: update assertions * test: add test when process is started by "not owned" device * refactor: remove handler tests covered by domain tests * feat: use RejectedAt instead of CreatedAt in dto * refactor: rename method and update exception thrown * test: update tests * chore: remove redundant call * test: add additional assertion * test: update tests with additional assertions * test: update assertions * refactor: extract method * test: update assertions * fix: fix missing usings * test: avoid having two acting statements * chore: change error code, method name and error message * feat: bring back RejectedAt and RejectedBy properties * refactor: remove two actings * Merge branch 'main' into nmshdb-44-rejection-of-deletion-process * test: fix broken test * chore: formatting * test: fix tests * test: fix tests --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent ed47b99 commit fd19aa3

File tree

21 files changed

+2228
-98
lines changed

21 files changed

+2228
-98
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
2+
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
3+
using Backbone.BuildingBlocks.Domain;
4+
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
5+
using Backbone.Modules.Devices.Domain.Entities.Identities;
6+
using MediatR;
7+
8+
namespace Backbone.Modules.Devices.Application.Identities.Commands.RejectDeletionProcess;
9+
public class Handler : IRequestHandler<RejectDeletionProcessCommand, RejectDeletionProcessResponse>
10+
{
11+
private readonly IIdentitiesRepository _identitiesRepository;
12+
private readonly IUserContext _userContext;
13+
14+
public Handler(IIdentitiesRepository identitiesRepository, IUserContext userContext)
15+
{
16+
_identitiesRepository = identitiesRepository;
17+
_userContext = userContext;
18+
}
19+
20+
public async Task<RejectDeletionProcessResponse> Handle(RejectDeletionProcessCommand request, CancellationToken cancellationToken)
21+
{
22+
var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, track: true) ?? throw new NotFoundException(nameof(Identity));
23+
var deviceId = _userContext.GetDeviceId();
24+
25+
var deletionProcessIdResult = IdentityDeletionProcessId.Create(request.DeletionProcessId);
26+
27+
if (deletionProcessIdResult.IsFailure)
28+
throw new DomainException(deletionProcessIdResult.Error);
29+
30+
var deletionProcessId = deletionProcessIdResult.Value;
31+
var deletionProcess = identity.RejectDeletionProcess(deletionProcessId, deviceId);
32+
await _identitiesRepository.Update(identity, cancellationToken);
33+
34+
return new RejectDeletionProcessResponse(deletionProcess);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using MediatR;
2+
3+
namespace Backbone.Modules.Devices.Application.Identities.Commands.RejectDeletionProcess;
4+
public class RejectDeletionProcessCommand : IRequest<RejectDeletionProcessResponse>
5+
{
6+
public RejectDeletionProcessCommand(string deletionProcessId)
7+
{
8+
DeletionProcessId = deletionProcessId;
9+
}
10+
11+
public string DeletionProcessId { get; set; }
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Backbone.Modules.Devices.Domain.Entities.Identities;
2+
3+
namespace Backbone.Modules.Devices.Application.Identities.Commands.RejectDeletionProcess;
4+
public class RejectDeletionProcessResponse
5+
{
6+
public RejectDeletionProcessResponse(IdentityDeletionProcess deletionProcess)
7+
{
8+
Id = deletionProcess.Id;
9+
Status = deletionProcess.Status;
10+
RejectedAt = deletionProcess.RejectedAt ?? throw new Exception($"The '{nameof(IdentityDeletionProcess.RejectedAt)}' property of the given deletion process must not be null.");
11+
RejectedByDevice = deletionProcess.RejectedByDevice ?? throw new Exception($"The '{nameof(IdentityDeletionProcess.RejectedByDevice)}' property of the given deletion process must not be null.");
12+
}
13+
14+
public string Id { get; }
15+
public DeletionProcessStatus Status { get; }
16+
public DateTime RejectedAt { get; }
17+
public string RejectedByDevice { get; }
18+
}

Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Backbone.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess;
77
using Backbone.Modules.Devices.Application.Identities.Commands.CancelDeletionProcess;
88
using Backbone.Modules.Devices.Application.Identities.Commands.CreateIdentity;
9+
using Backbone.Modules.Devices.Application.Identities.Commands.RejectDeletionProcess;
910
using Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner;
1011
using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcess;
1112
using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcesses;
@@ -80,6 +81,16 @@ public async Task<IActionResult> ApproveDeletionProcess([FromRoute] string id, C
8081
return Ok(response);
8182
}
8283

84+
[HttpPut("Self/DeletionProcesses/{id}/Reject")]
85+
[ProducesResponseType(StatusCodes.Status200OK)]
86+
[ProducesError(StatusCodes.Status400BadRequest)]
87+
[ProducesError(StatusCodes.Status404NotFound)]
88+
public async Task<IActionResult> RejectDeletionProcess([FromRoute] string id, CancellationToken cancellationToken)
89+
{
90+
var response = await _mediator.Send(new RejectDeletionProcessCommand(id), cancellationToken);
91+
return Ok(response);
92+
}
93+
8394
[HttpGet("Self/DeletionProcesses/{id}")]
8495
[ProducesResponseType(StatusCodes.Status200OK)]
8596
[ProducesError(StatusCodes.Status404NotFound)]

Modules/Devices/src/Devices.Domain/DomainErrors.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Backbone.BuildingBlocks.Domain.Errors;
2+
using Backbone.Modules.Devices.Domain.Entities.Identities;
23

34
namespace Backbone.Modules.Devices.Domain;
45

@@ -45,8 +46,8 @@ public static DomainError OnlyOneActiveDeletionProcessAllowed()
4546
return new DomainError("error.platform.validation.device.onlyOneActiveDeletionProcessAllowed", "Only one active deletion process is allowed.");
4647
}
4748

48-
public static DomainError NoDeletionProcessWithRequiredStatusExists()
49+
public static DomainError DeletionProcessMustBeInStatus(DeletionProcessStatus deletionProcessStatus)
4950
{
50-
return new DomainError("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists", "The deletion process does not have the correct status to perform this action.");
51+
return new DomainError($"error.platform.validation.device.deletionProcessMustBeInStatus{deletionProcessStatus}", $"The deletion process must be in status '{deletionProcessStatus}'.");
5152
}
5253
}

Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,6 @@ public IdentityDeletionProcess StartDeletionProcessAsOwner(DeviceId asDevice)
8585
return deletionProcess;
8686
}
8787

88-
private void EnsureIdentityOwnsDevice(DeviceId currentDeviceId)
89-
{
90-
if (!Devices.Exists(device => device.Id == currentDeviceId))
91-
throw new DomainException(GenericDomainErrors.NotFound(nameof(Device)));
92-
}
93-
9488
public void DeletionProcessApprovalReminder1Sent()
9589
{
9690
EnsureDeletionProcessInStatusExists(DeletionProcessStatus.WaitingForApproval);
@@ -117,7 +111,9 @@ public void DeletionProcessApprovalReminder3Sent()
117111

118112
public IdentityDeletionProcess ApproveDeletionProcess(IdentityDeletionProcessId deletionProcessId, DeviceId deviceId)
119113
{
120-
var deletionProcess = DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ?? throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
114+
EnsureIdentityOwnsDevice(deviceId);
115+
116+
var deletionProcess = GetDeletionProcess(deletionProcessId);
121117

122118
deletionProcess.Approve(Address, deviceId);
123119

@@ -128,12 +124,28 @@ public IdentityDeletionProcess ApproveDeletionProcess(IdentityDeletionProcessId
128124
return deletionProcess;
129125
}
130126

127+
private IdentityDeletionProcess GetDeletionProcess(IdentityDeletionProcessId deletionProcessId)
128+
{
129+
var deletionProcess = DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ?? throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
130+
return deletionProcess;
131+
}
132+
133+
public IdentityDeletionProcess RejectDeletionProcess(IdentityDeletionProcessId deletionProcessId, DeviceId deviceId)
134+
{
135+
EnsureIdentityOwnsDevice(deviceId);
136+
137+
var deletionProcess = GetDeletionProcess(deletionProcessId);
138+
deletionProcess.Reject(Address, deviceId);
139+
140+
return deletionProcess;
141+
}
142+
131143
private void EnsureDeletionProcessInStatusExists(DeletionProcessStatus status)
132144
{
133145
var deletionProcess = DeletionProcesses.Any(d => d.Status == status);
134146

135147
if (!deletionProcess)
136-
throw new DomainException(DomainErrors.NoDeletionProcessWithRequiredStatusExists());
148+
throw new DomainException(DomainErrors.DeletionProcessMustBeInStatus(status));
137149
}
138150

139151
private void EnsureNoActiveProcessExists()
@@ -144,6 +156,12 @@ private void EnsureNoActiveProcessExists()
144156
throw new DomainException(DomainErrors.OnlyOneActiveDeletionProcessAllowed());
145157
}
146158

159+
private void EnsureIdentityOwnsDevice(DeviceId currentDeviceId)
160+
{
161+
if (!Devices.Exists(device => device.Id == currentDeviceId))
162+
throw new DomainException(GenericDomainErrors.NotFound(nameof(Device)));
163+
}
164+
147165
public void DeletionGracePeriodReminder1Sent()
148166
{
149167
EnsureDeletionProcessInStatusExists(DeletionProcessStatus.Approved);
@@ -193,7 +211,8 @@ public enum DeletionProcessStatus
193211
{
194212
WaitingForApproval = 0,
195213
Approved = 1,
196-
Cancelled = 2
214+
Cancelled = 2,
215+
Rejected = 3
197216
}
198217

199218
public enum IdentityStatus

Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,6 @@ private IdentityDeletionProcess()
1616
Id = null!;
1717
}
1818

19-
public static IdentityDeletionProcess StartAsSupport(IdentityAddress createdBy)
20-
{
21-
return new IdentityDeletionProcess(createdBy, DeletionProcessStatus.WaitingForApproval);
22-
}
23-
24-
public static IdentityDeletionProcess StartAsOwner(IdentityAddress createdBy, DeviceId createdByDeviceId)
25-
{
26-
return new IdentityDeletionProcess(createdBy, createdByDeviceId);
27-
}
28-
2919
private IdentityDeletionProcess(IdentityAddress createdBy, DeletionProcessStatus status)
3020
{
3121
Id = IdentityDeletionProcessId.Generate();
@@ -53,6 +43,16 @@ private void Approve(DeviceId createdByDevice)
5343
GracePeriodEndsAt = SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.LengthOfGracePeriod);
5444
}
5545

46+
public static IdentityDeletionProcess StartAsSupport(IdentityAddress createdBy)
47+
{
48+
return new IdentityDeletionProcess(createdBy, DeletionProcessStatus.WaitingForApproval);
49+
}
50+
51+
public static IdentityDeletionProcess StartAsOwner(IdentityAddress createdBy, DeviceId createdByDeviceId)
52+
{
53+
return new IdentityDeletionProcess(createdBy, createdByDeviceId);
54+
}
55+
5656
public IdentityDeletionProcessId Id { get; }
5757
public IReadOnlyList<IdentityDeletionProcessAuditLogEntry> AuditLog => _auditLog;
5858
public DeletionProcessStatus Status { get; private set; }
@@ -65,6 +65,9 @@ private void Approve(DeviceId createdByDevice)
6565
public DateTime? ApprovedAt { get; private set; }
6666
public DeviceId? ApprovedByDevice { get; private set; }
6767

68+
public DateTime? RejectedAt { get; private set; }
69+
public DeviceId? RejectedByDevice { get; private set; }
70+
6871
public DateTime? CancelledAt { get; private set; }
6972
public DeviceId? CancelledByDevice { get; private set; }
7073

@@ -74,7 +77,6 @@ private void Approve(DeviceId createdByDevice)
7477
public DateTime? GracePeriodReminder2SentAt { get; private set; }
7578
public DateTime? GracePeriodReminder3SentAt { get; private set; }
7679

77-
7880
public bool IsActive()
7981
{
8082
return Status is DeletionProcessStatus.Approved or DeletionProcessStatus.WaitingForApproval;
@@ -123,17 +125,37 @@ public void GracePeriodReminder3Sent(IdentityAddress address)
123125

124126
public void Approve(IdentityAddress address, DeviceId approvedByDevice)
125127
{
126-
if (Status != DeletionProcessStatus.WaitingForApproval)
127-
throw new DomainException(DomainErrors.NoDeletionProcessWithRequiredStatusExists());
128+
EnsureStatus(DeletionProcessStatus.WaitingForApproval);
128129

129130
Approve(approvedByDevice);
130131
_auditLog.Add(IdentityDeletionProcessAuditLogEntry.ProcessApproved(Id, address, approvedByDevice));
131132
}
132133

134+
public void Reject(IdentityAddress address, DeviceId rejectedByDevice)
135+
{
136+
EnsureStatus(DeletionProcessStatus.WaitingForApproval);
137+
138+
Reject(rejectedByDevice);
139+
_auditLog.Add(IdentityDeletionProcessAuditLogEntry.ProcessRejected(Id, address, rejectedByDevice));
140+
}
141+
142+
private void EnsureStatus(DeletionProcessStatus deletionProcessStatus)
143+
{
144+
if (Status != deletionProcessStatus)
145+
throw new DomainException(DomainErrors.DeletionProcessMustBeInStatus(deletionProcessStatus));
146+
}
147+
148+
private void Reject(DeviceId rejectedByDevice)
149+
{
150+
Status = DeletionProcessStatus.Rejected;
151+
RejectedAt = SystemTime.UtcNow;
152+
RejectedByDevice = rejectedByDevice;
153+
}
154+
133155
public void Cancel(IdentityAddress address, DeviceId cancelledByDevice)
134156
{
135157
if (Status != DeletionProcessStatus.Approved)
136-
throw new DomainException(DomainErrors.NoDeletionProcessWithRequiredStatusExists());
158+
throw new DomainException(DomainErrors.DeletionProcessMustBeInStatus(DeletionProcessStatus.Approved));
137159

138160
Status = DeletionProcessStatus.Cancelled;
139161
CancelledAt = SystemTime.UtcNow;

Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static IdentityDeletionProcessAuditLogEntry ProcessApproved(IdentityDelet
2020
return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was approved.", Hasher.HashUtf8(identityAddress.StringValue), Hasher.HashUtf8(deviceId.StringValue), DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.Approved);
2121
}
2222

23+
public static IdentityDeletionProcessAuditLogEntry ProcessRejected(IdentityDeletionProcessId processId, IdentityAddress identityAddress, DeviceId deviceId)
24+
{
25+
return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was rejected.", Hasher.HashUtf8(identityAddress.StringValue), Hasher.HashUtf8(deviceId.StringValue), DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.Rejected);
26+
}
27+
2328
public static IdentityDeletionProcessAuditLogEntry ProcessCancelled(IdentityDeletionProcessId processId, IdentityAddress identityAddress, DeviceId deviceId)
2429
{
2530
return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was cancelled.", Hasher.HashUtf8(identityAddress.StringValue), Hasher.HashUtf8(deviceId.StringValue), DeletionProcessStatus.Approved, DeletionProcessStatus.Cancelled);

0 commit comments

Comments
 (0)