| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Command.Input; |
| | | 3 | | using ArturRios.Heimdall.Command.Output; |
| | | 4 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 5 | | using ArturRios.Mediator.Command.Interfaces; |
| | | 6 | | using ArturRios.Output; |
| | | 7 | | using Microsoft.EntityFrameworkCore; |
| | | 8 | | using Application = ArturRios.Heimdall.Domain.Entities.Application; |
| | | 9 | | |
| | | 10 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 11 | | |
| | | 12 | | /// <summary> |
| | | 13 | | /// Handles <see cref="HardDeleteApplicationCommand" /> (UC-20, FR-AP-08): locates the application |
| | | 14 | | /// inside the addressed scope in any deletion state (AF-20a) and permanently removes the record. |
| | | 15 | | /// The database cascades the removal to the application's credentials (UC-53), its only |
| | | 16 | | /// dependent rows; the scope and owner its foreign keys point at are left intact. Authorization is entirely the end |
| | | 17 | | /// actor is the System Admin, so no rule is left for the handler to apply. All failures are |
| | | 18 | | /// returned as errors on the <see cref="DataOutput{T}" /> rather than thrown. |
| | | 19 | | /// </summary> |
| | 15 | 20 | | public class HardDeleteApplicationCommandHandler( |
| | 15 | 21 | | IAsyncReadOnlyRepository<Application, long> applicationReader, |
| | 15 | 22 | | IAsyncRepository<Application, long> applicationWriter) |
| | | 23 | | : ICommandHandlerAsync<HardDeleteApplicationCommand, HardDeleteApplicationCommandOutput> |
| | | 24 | | { |
| | | 25 | | public async Task<DataOutput<HardDeleteApplicationCommandOutput?>> HandleAsync( |
| | | 26 | | HardDeleteApplicationCommand command, CancellationToken cancellationToken = default) |
| | | 27 | | { |
| | 16 | 28 | | var output = DataOutput<HardDeleteApplicationCommandOutput?>.New; |
| | | 29 | | |
| | | 30 | | // AF-20a: the application must exist inside the addressed scope. The lookup omits an |
| | | 31 | | // !IsDeleted filter — a logically deleted application, whether by UC-19 or by UC-04's cascade |
| | | 32 | | // from its scope, is exactly what a cleanup pass starts from and must still be purgeable, the |
| | | 33 | | // same call UC-05 and UC-10 make. The route's scopeId qualifies it: an unknown application, |
| | | 34 | | // an unknown scope, and an application living in another scope are all one 404, because the |
| | | 35 | | // addressed resource genuinely does not exist in any of the three cases. A repeated call |
| | | 36 | | // lands here too — the row is already gone, and UC-20 has no idempotent path. |
| | 16 | 37 | | var application = await applicationReader.Query() |
| | 16 | 38 | | .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId); |
| | | 39 | | |
| | 16 | 40 | | if (application is null) |
| | | 41 | | { |
| | 7 | 42 | | return output.WithError(ApplicationMessages.ApplicationNotFound); |
| | | 43 | | } |
| | | 44 | | |
| | | 45 | | // UC-20 step 2: remove the record for good. No dependent is deleted first — no entity carries |
| | | 46 | | // a foreign key to an application, so no foreign key can be violated and there is no total to |
| | | 47 | | // report, unlike the scope and person hard deletes. |
| | 9 | 48 | | var delete = await applicationWriter.DeleteAsync(application); |
| | | 49 | | |
| | 9 | 50 | | if (!delete.Success) |
| | | 51 | | { |
| | 0 | 52 | | return output.WithErrors(delete.Errors); |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | // UC-20 step 3. |
| | 9 | 56 | | return output |
| | 9 | 57 | | .WithData(new HardDeleteApplicationCommandOutput { Id = application.PublicId }) |
| | 9 | 58 | | .WithMessage(ApplicationMessages.ApplicationHardDeletedSuccessfully); |
| | 16 | 59 | | } |
| | | 60 | | } |