| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// NFR-12: whether suspending a person would leave a scope they own with no owner at all. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <para> |
| | | 13 | | /// The erasure request (UC-42) and the anonymisation pass (NFR-20) both need this answer — |
| | | 14 | | /// one to decide whether the request can take effect, the other to decide whether a blocked |
| | | 15 | | /// request can now proceed — so it is written once here rather than twice more. |
| | | 16 | | /// </para> |
| | | 17 | | /// <para> |
| | | 18 | | /// Three handlers predating this carry their own copies of the same guard |
| | | 19 | | /// (<c>DeletePersonCommandHandler</c>, <c>HardDeletePersonCommandHandler</c>, |
| | | 20 | | /// <c>UpdatePersonCommandHandler</c>). Consolidating those is worth doing and is deliberately |
| | | 21 | | /// not done here: they are covered by their own tests, and folding four call sites into one |
| | | 22 | | /// belongs in a change about that rather than tacked onto this one. |
| | | 23 | | /// </para> |
| | | 24 | | /// </remarks> |
| | | 25 | | public static class LastScopeOwnerGuard |
| | | 26 | | { |
| | | 27 | | /// <summary> |
| | | 28 | | /// Whether any scope <paramref name="person" /> owns is owned by nobody else who is still |
| | | 29 | | /// live. Requires <c>ScopeOwnerships</c> to have been loaded. |
| | | 30 | | /// </summary> |
| | | 31 | | public static async Task<bool> WouldStripLastOwnerAsync( |
| | | 32 | | Person person, IAsyncReadOnlyRepository<Person, long> personReader) |
| | | 33 | | { |
| | | 34 | | // Whatever the role: a SystemAdmin may own a scope too (FR-SC-08). |
| | 18 | 35 | | if (person.ScopeOwnerships.Count == 0) |
| | | 36 | | { |
| | 9 | 37 | | return false; |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | // Persons already logically deleted are excluded: a suspended owner cannot authenticate, so |
| | | 41 | | // they no longer keep a scope owned. |
| | 9 | 42 | | var coOwnedScopeIds = await personReader.Query() |
| | 9 | 43 | | .Where(other => other.Id != person.Id && !other.IsDeleted) |
| | 9 | 44 | | .SelectMany(other => other.ScopeOwnerships.Select(ownership => ownership.ScopeId)) |
| | 9 | 45 | | .Distinct() |
| | 9 | 46 | | .ToListAsync(); |
| | | 47 | | |
| | 18 | 48 | | return person.ScopeOwnerships.Any(ownership => !coOwnedScopeIds.Contains(ownership.ScopeId)); |
| | 18 | 49 | | } |
| | | 50 | | } |