| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Command.Input; |
| | | 3 | | using ArturRios.Heimdall.Command.Output; |
| | | 4 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 5 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 6 | | using ArturRios.Heimdall.Domain.Persistence; |
| | | 7 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 8 | | using ArturRios.Heimdall.Shared.Services; |
| | | 9 | | using ArturRios.Mediator.Command.Interfaces; |
| | | 10 | | using ArturRios.Output; |
| | | 11 | | using Microsoft.EntityFrameworkCore; |
| | | 12 | | |
| | | 13 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Handles <see cref="WithdrawScopeOwnerInvitationCommand" /> (UC-58, FR-SC-15): an owner of the scope, or |
| | | 17 | | /// a System Admin, ends an open invitation as withdrawn. All failures are returned as errors on the |
| | | 18 | | /// <see cref="DataOutput{T}" /> rather than thrown. |
| | | 19 | | /// </summary> |
| | | 20 | | /// <remarks> |
| | | 21 | | /// A missing or logically deleted scope is AF-58a and a caller who may not manage it AF-58b, checked |
| | | 22 | | /// before the invitation is looked up so a non-owner learns nothing about which ids exist. The invitation |
| | | 23 | | /// is looked up within the scope, so another scope's is not found (AF-58c), exactly like an ended, an |
| | | 24 | | /// expired or an unknown one, or one that an answer ended between the read and the write. |
| | | 25 | | /// </remarks> |
| | 12 | 26 | | public class WithdrawScopeOwnerInvitationCommandHandler( |
| | 12 | 27 | | IAsyncReadOnlyRepository<Scope, long> scopeReader, |
| | 12 | 28 | | IAsyncReadOnlyRepository<ScopeOwnerInvitation, long> invitationReader, |
| | 12 | 29 | | IScopeOwnerInvitations invitations, |
| | 12 | 30 | | IScopeOwnershipChecker scopeOwnership) |
| | | 31 | | : ICommandHandlerAsync<WithdrawScopeOwnerInvitationCommand, WithdrawScopeOwnerInvitationCommandOutput> |
| | | 32 | | { |
| | | 33 | | public async Task<DataOutput<WithdrawScopeOwnerInvitationCommandOutput?>> HandleAsync( |
| | | 34 | | WithdrawScopeOwnerInvitationCommand command, CancellationToken cancellationToken = default) |
| | | 35 | | { |
| | 12 | 36 | | var output = DataOutput<WithdrawScopeOwnerInvitationCommandOutput?>.New; |
| | | 37 | | |
| | | 38 | | // AF-58a. |
| | 12 | 39 | | var scope = await scopeReader.Query() |
| | 12 | 40 | | .FirstOrDefaultAsync(x => x.PublicId == command.ScopeId && !x.IsDeleted, cancellationToken); |
| | | 41 | | |
| | 12 | 42 | | if (scope is null) |
| | | 43 | | { |
| | 1 | 44 | | return output.WithError(ScopeMessages.ScopeNotFound); |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | // AF-58b. |
| | 11 | 48 | | if (!await scopeOwnership.ActorMayManageScopeAsync(command.ActingRole, command.ActingPersonId, scope.Id)) |
| | | 49 | | { |
| | 2 | 50 | | return output.WithError(ScopeMessages.NotScopeOwner); |
| | | 51 | | } |
| | | 52 | | |
| | 9 | 53 | | var now = DateTime.UtcNow; |
| | | 54 | | |
| | | 55 | | // AF-58c. |
| | 9 | 56 | | var invitation = await invitationReader.Query() |
| | 9 | 57 | | .Where(x => x.PublicId == command.InvitationId && x.ScopeId == scope.Id) |
| | 9 | 58 | | .Where(ScopeOwnerInvitation.OpenAt(now)) |
| | 9 | 59 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 60 | | |
| | 9 | 61 | | if (invitation is null || |
| | 9 | 62 | | !await invitations.TryEndAsync(invitation, ScopeOwnerInvitationOutcomes.Withdrawn, now)) |
| | | 63 | | { |
| | 6 | 64 | | return output.WithError(ScopeInvitationMessages.InvitationNotOpen); |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | // UC-58 main flow: no message — the endpoint answers 204 with no body. |
| | 3 | 68 | | return output.WithData(new WithdrawScopeOwnerInvitationCommandOutput { Id = invitation.PublicId }); |
| | 12 | 69 | | } |
| | | 70 | | } |