| | | 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.Mediator.Command.Interfaces; |
| | | 9 | | using ArturRios.Output; |
| | | 10 | | using Microsoft.EntityFrameworkCore; |
| | | 11 | | |
| | | 12 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Handles <see cref="HardDeleteGoogleUserCommand" /> (UC-29, FR-GO-16): locates the Google User |
| | | 16 | | /// inside the addressed scope in any deletion state (AF-29a) and permanently removes the record. |
| | | 17 | | /// Nothing cascades — the Deletion Strategy says a hard delete here "simply removes its record", |
| | | 18 | | /// and the scope its foreign key points at is left intact. Authorization is entirely the |
| | | 19 | | /// endpoint's: UC-29's only actor is the System Admin, so no rule is left for the handler to |
| | | 20 | | /// apply. All failures are returned as errors on the <see cref="DataOutput{T}" /> rather than |
| | | 21 | | /// thrown. |
| | | 22 | | /// </summary> |
| | | 23 | | /// <remarks> |
| | | 24 | | /// <para> |
| | | 25 | | /// No self-deletion refusal, unlike <see cref="DeletePersonCommandHandler" /> (AF-09d) and |
| | | 26 | | /// <see cref="HardDeletePersonCommandHandler" /> (AF-10c). A Google User is always |
| | | 27 | | /// <c>User</c>-equivalent (FR-GO-04) and can never hold <c>SystemAdmin</c>, so the only actor |
| | | 28 | | /// who may call this endpoint can never be its target — there is no way to lock yourself out, |
| | | 29 | | /// and a guard against it would be a flow no document defines. |
| | | 30 | | /// </para> |
| | | 31 | | /// <para> |
| | | 32 | | /// Before the record is removed, its <c>PublicId</c> is tombstoned |
| | | 33 | | /// (<see cref="ErasedSubject" />, NFR-21, #195), so the audit entries that name it as their |
| | | 34 | | /// target — this hard delete's own among them — can still be cleared once nothing else says the |
| | | 35 | | /// id was a person's. Written first, a failed delete leaves a tombstone for a live row, which |
| | | 36 | | /// nothing acts on; and a retry writes it again without error. |
| | | 37 | | /// </para> |
| | | 38 | | /// </remarks> |
| | 14 | 39 | | public class HardDeleteGoogleUserCommandHandler( |
| | 14 | 40 | | IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader, |
| | 14 | 41 | | IAsyncRepository<GoogleUser, long> googleUserWriter, |
| | 14 | 42 | | IErasedSubjects erasedSubjects) |
| | | 43 | | : ICommandHandlerAsync<HardDeleteGoogleUserCommand, HardDeleteGoogleUserCommandOutput> |
| | | 44 | | { |
| | | 45 | | public async Task<DataOutput<HardDeleteGoogleUserCommandOutput?>> HandleAsync( |
| | | 46 | | HardDeleteGoogleUserCommand command, CancellationToken cancellationToken = default) |
| | | 47 | | { |
| | 15 | 48 | | var output = DataOutput<HardDeleteGoogleUserCommandOutput?>.New; |
| | | 49 | | |
| | | 50 | | // AF-29a: the Google User must exist inside the addressed scope. The lookup omits an |
| | | 51 | | // !IsDeleted filter — a logically deleted Google User, whether by UC-28 or by UC-04's cascade |
| | | 52 | | // from its scope, is exactly what a cleanup pass starts from and must still be purgeable, the |
| | | 53 | | // same call UC-05, UC-10, and UC-20 make. The route's scopeId qualifies it: an unknown Google |
| | | 54 | | // User, an unknown scope, and a Google User living in another scope are all one 404, because |
| | | 55 | | // the addressed resource genuinely does not exist in any of the three cases. A repeated call |
| | | 56 | | // lands here too — the row is already gone, and UC-29 has no idempotent path as UC-28 does. |
| | 15 | 57 | | var googleUser = await googleUserReader.Query() |
| | 15 | 58 | | .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId); |
| | | 59 | | |
| | 15 | 60 | | if (googleUser is null) |
| | | 61 | | { |
| | 6 | 62 | | return output.WithError(GoogleUserMessages.GoogleUserNotFound); |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | // UC-29 step 2 (NFR-21, #195): the tombstone goes first, so the row is never gone without it. |
| | 9 | 66 | | await erasedSubjects.RecordAsync( |
| | 9 | 67 | | [ |
| | 9 | 68 | | new ErasedSubject |
| | 9 | 69 | | { |
| | 9 | 70 | | Id = googleUser.PublicId, |
| | 9 | 71 | | Kind = (int)ErasedSubjectKinds.GoogleUser, |
| | 9 | 72 | | CreatedAt = DateTime.UtcNow |
| | 9 | 73 | | } |
| | 9 | 74 | | ]); |
| | | 75 | | |
| | | 76 | | // UC-29 step 3: remove the record for good. No dependent is deleted first — a Google User can |
| | | 77 | | // own no application (FR-AP-03) and holds no password reset or email verification tokens, |
| | | 78 | | // since authentication is delegated to Google — so no foreign key can be violated and there |
| | | 79 | | // is no total to report, unlike the scope and person hard deletes. |
| | 9 | 80 | | var delete = await googleUserWriter.DeleteAsync(googleUser); |
| | | 81 | | |
| | 9 | 82 | | if (!delete.Success) |
| | | 83 | | { |
| | 0 | 84 | | return output.WithErrors(delete.Errors); |
| | | 85 | | } |
| | | 86 | | |
| | | 87 | | // UC-29 step 4. |
| | 9 | 88 | | return output |
| | 9 | 89 | | .WithData(new HardDeleteGoogleUserCommandOutput { Id = googleUser.PublicId }) |
| | 9 | 90 | | .WithMessage(GoogleUserMessages.GoogleUserHardDeletedSuccessfully); |
| | 15 | 91 | | } |
| | | 92 | | } |