From 26df42318011da93c3544867ab6212214b49f5be Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 03:04:37 +0200 Subject: [PATCH 1/6] Refactored cache services --- .../Abstractions/Services/IProjectService.cs | 2 +- .../Governance/GovernanceCacheService.cs | 108 +++++- .../Services/Projects/ProjectCacheService.cs | 324 +++++++++++++++--- .../Services/Projects/ProjectService.cs | 2 +- .../Services/Shared/IdentityService.cs | 1 + .../Services/Users/UserService.cs | 8 + 6 files changed, 379 insertions(+), 66 deletions(-) diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 4f6e73e..1883fb5 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -153,5 +153,5 @@ public interface IProjectService Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log] - Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default); + Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index 9f630d8..2061276 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -215,25 +215,101 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantAsync(tenantIdHash, cancellationToken); - public Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"gettenant-{tenantIdHash}").Value; - public Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - public Task> GetTenantsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantAsync(tenantIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenantorganisation-{tenantOrganisationIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantOrganisationsAsync(cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenantorganisations").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantOrganisationsAsync(cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantsAsync(cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenants").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantsAsync(cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => await service.UpdateOrganisationAsync(request, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index 4c952e3..d927741 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -86,45 +86,197 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetBeneficiariesAsync(projectIdHash, cancellationToken); - public Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getbeneficiaries-{projectIdHash}").Value; - public Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - public Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetBeneficiariesAsync(projectIdHash, cancellationToken); - public Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (result.IsFailed) return result; - public Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); - public Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return result.Value; + } - public Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getbeneficiary-{beneficiaryIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getchangerequest-{changeRequestIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetChangeRequestsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getchangerequests-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetChangeRequestsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getcomplianceitem-{complianceItemIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetComplianceItemsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getcomplianceitems-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetComplianceItemsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetIssueAsync(issueIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getissue-{issueIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetIssueAsync(issueIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetIssuesAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getissues-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetIssuesAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -174,15 +326,53 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetOutcomeAsync(outcomeIdHash, cancellationToken); - public Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getoutcome-{outcomeIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetOutcomeAsync(outcomeIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getoutcomes-{beneficiaryIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -376,15 +566,53 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken); - public Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getsitevisit-{siteVisitIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetSiteVisitsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getsitevisits-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetSiteVisitsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateBeneficiaryItemAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index f7fef6d..d2abeb5 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -498,7 +498,7 @@ public sealed class ProjectService(IDbContextFactory conte throw new NotImplementedException(); } - public Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default) + public Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/PostFundManagement.Application/Services/Shared/IdentityService.cs b/PostFundManagement.Application/Services/Shared/IdentityService.cs index cfe216d..7c668ad 100644 --- a/PostFundManagement.Application/Services/Shared/IdentityService.cs +++ b/PostFundManagement.Application/Services/Shared/IdentityService.cs @@ -12,6 +12,7 @@ public sealed class IdentityService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: Ensure TenantUser status is checked var userExistsAndActive = await context.Users .AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken); diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index e76caf9..a7ca325 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -14,8 +14,11 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + // TODO: refactor to use TenantUser + // TODO: Also add to TenantUser if (user is null) context.Users.Add(new Domain.Entities.User { @@ -40,8 +43,10 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var query = context.Users.AsNoTracking(); + // TODO: refactor to use TenantUser if (request.Status.HasValue) query = query.Where(u => u.Status == request.Status.Value); @@ -59,6 +64,7 @@ public sealed class UserService(IDbContextFactory contextF else query = query.OrderBy(u => u.Id); + // TODO: refactor to use TenantUser var users = await query.Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize).ToListAsync(cancellationToken); @@ -70,6 +76,7 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: Update TenantUser status as well var changedTotal = await context.Users .Where(f => f.Id == userId) .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); @@ -82,6 +89,7 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var user = await context.Users.AsNoTracking() .FirstOrDefaultAsync(f => f.Id == userId, cancellationToken); -- 2.47.3 From 4b806a3202171adec092cb5af695a8c3f7eb77cc Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 03:55:20 +0200 Subject: [PATCH 2/6] UserService refactored to integrate into TenantUser --- .../Services/Users/UserService.cs | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index a7ca325..fb8c107 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -1,5 +1,6 @@ using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Domain; +using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; @@ -7,19 +8,19 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Users; -public sealed class UserService(IDbContextFactory contextFactory) : IUserService +public sealed class UserService(IDbContextFactory contextFactory, ITenantContextService tenantContext) : IUserService { public async Task SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - // TODO: refactor to use TenantUser - var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + var tenantUser = await context.TenantUsers + .Include(i => i.User) + .FirstOrDefaultAsync(u => u.UserId == userId, cancellationToken); - // TODO: refactor to use TenantUser - // TODO: Also add to TenantUser - if (user is null) + if (tenantUser is null) + { context.Users.Add(new Domain.Entities.User { Id = userId, @@ -27,12 +28,22 @@ public sealed class UserService(IDbContextFactory contextF Status = ActivityStatus.Active, LastLoginAt = DateTime.UtcNow }); + + context.TenantUsers.Add(new Domain.Entities.TenantUser + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Status = ActivityStatus.Active, + TenantId = tenantContext.TenantId, + UserId = userId + }); + } else { - user!.Email = email; - user.LastLoginAt = DateTime.UtcNow; + tenantUser.User!.Email = email; + tenantUser.User.LastLoginAt = DateTime.UtcNow; - context.Users.Update(user); + context.Users.Update(tenantUser.User); } return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); @@ -43,32 +54,29 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - // TODO: refactor to use TenantUser - var query = context.Users.AsNoTracking(); + var query = context.TenantUsers.Include(i => i.User).AsNoTracking(); - // TODO: refactor to use TenantUser if (request.Status.HasValue) query = query.Where(u => u.Status == request.Status.Value); if (!string.IsNullOrWhiteSpace(request.Email)) - query = query.Where(u => u.Email != null && u.Email.Contains(request.Email)); + query = query.Where(u => u.User!.Email != null && u.User!.Email.Contains(request.Email)); if (!string.IsNullOrWhiteSpace(request.SortBy)) query = request.SortBy.ToLowerInvariant() switch { - "email" => request.SortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email), - "lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt), + "email" => request.SortDescending ? query.OrderByDescending(u => u.User!.Email) : query.OrderBy(u => u.User!.Email), + "lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.User!.LastLoginAt) : query.OrderBy(u => u.User!.LastLoginAt), "status" => request.SortDescending ? query.OrderByDescending(u => u.Status) : query.OrderBy(u => u.Status), _ => request.SortDescending ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id) }; else query = query.OrderBy(u => u.Id); - // TODO: refactor to use TenantUser var users = await query.Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize).ToListAsync(cancellationToken); - return Result.Ok(users.Select(u => u.Map()).ToArray()); + return Result.Ok(users.Select(u => u.User!.Map()).ToArray()); })); public async Task UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) => @@ -76,9 +84,13 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - // TODO: Update TenantUser status as well - var changedTotal = await context.Users - .Where(f => f.Id == userId) + var changedTotal = await context.TenantUsers + .Include(i => i.User) + .Where(f => f.UserId == userId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Status, status), cancellationToken); + + changedTotal += await context.Users.Where(f => f.Id == userId) .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.New); @@ -89,11 +101,11 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - // TODO: refactor to use TenantUser - var user = await context.Users.AsNoTracking() - .FirstOrDefaultAsync(f => f.Id == userId, cancellationToken); + var tenantUser = await context.TenantUsers.AsNoTracking() + .Include(i => i.User) + .FirstOrDefaultAsync(f => f.UserId == userId, cancellationToken); - return Result.FailIf(user == null, NotFoundError.New) - .ToResult(user?.Map()!); + return Result.FailIf(tenantUser == null, NotFoundError.New) + .ToResult(tenantUser!.User?.Map()!); })); } -- 2.47.3 From 24697776d0c7f9ab3f9ea1fb3c279822f1e5a2eb Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 04:01:31 +0200 Subject: [PATCH 3/6] Refactored roganisation routines to use TenantOrganisation --- .../Services/Governance/GovernanceService.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/PostFundManagement.Application/Services/Governance/GovernanceService.cs b/PostFundManagement.Application/Services/Governance/GovernanceService.cs index e118760..04b5524 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceService.cs @@ -56,13 +56,14 @@ public sealed class GovernanceService(IDbContextFactory co { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var organisations = await context.Organisations.AsNoTracking() - .OrderBy(o => o.Name) + var organisations = await context.TenantOrganisations.AsNoTracking() + .Include(i => i.Organisation) + .OrderBy(o => o.Organisation!.Name) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); - return Result.Ok(organisations.Select(o => o.Map()).ToArray()); + return Result.Ok(organisations.Select(o => o.Organisation!.Map()).ToArray()); })); public async Task> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) => @@ -76,11 +77,12 @@ public sealed class GovernanceService(IDbContextFactory co await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var organisation = await context.Organisations.AsNoTracking() - .FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken); + var tenantOrganisation = await context.TenantOrganisations.AsNoTracking() + .Include(i => i.OrganisationId) + .FirstOrDefaultAsync(o => o.OrganisationId == decodeResult.Value, cancellationToken); - return Result.FailIf(organisation is null, NotFoundError.New) - .ToResult(organisation!.Map()); + return Result.FailIf(tenantOrganisation is null, NotFoundError.New) + .ToResult(tenantOrganisation!.Organisation!.Map()); })); public async Task> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => -- 2.47.3 From 1efbf9d99f8432788e8f311b169b262e5f6bcdc2 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 08:20:36 +0200 Subject: [PATCH 4/6] Added TenantId to mapper methods on all affected models --- .../Abstractions/Services/IProjectService.cs | 2 +- .../Communications/CommunicationsService.cs | 40 ++++++++++--------- .../Services/Projects/ProjectCacheService.cs | 2 +- .../Services/Projects/ProjectService.cs | 2 +- .../Services/Projects/Records.cs | 2 +- .../Extensions/Mappers.cs | 19 ++++++++- 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 1883fb5..023f172 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -77,7 +77,7 @@ public interface IProjectService [Log] [Audit] - Task UpdateChangeRequestAsync(UpdatehangeRequest request, Guid userId, CancellationToken cancellationToken = default); + Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs index 824d5ff..712583e 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs @@ -9,14 +9,14 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Communications; public sealed class CommunicationsService(IDbContextFactory contextFactory, ITenantContextService tenantContext) : ICommunicationsService -{ +{ public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var invite = new Domain.Entities.Invite - { + { OrganisationId = request.OrganisationId, InvitedOrganisationId = request.InvitedOrganisationId, AwardId = request.AwardId, @@ -82,23 +82,27 @@ public sealed class CommunicationsService(IDbContextFactory i.Map()).ToArray()); })); - public Task CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + })); - public Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + })); - public Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + })); - public Task> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + })); } diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index d927741..0660080 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -617,7 +617,7 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach public async Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateBeneficiaryItemAsync(request, userId, cancellationToken); - public async Task UpdateChangeRequestAsync(UpdatehangeRequest request, Guid userId, CancellationToken cancellationToken = default) => + public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateChangeRequestAsync(request, userId, cancellationToken); public async Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index d2abeb5..0d615f8 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -388,7 +388,7 @@ public sealed class ProjectService(IDbContextFactory conte throw new NotImplementedException(); } - public Task UpdateChangeRequestAsync(UpdatehangeRequest request, Guid userId, CancellationToken cancellationToken = default) + public Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/PostFundManagement.Application/Services/Projects/Records.cs b/PostFundManagement.Application/Services/Projects/Records.cs index 5934724..17fba04 100644 --- a/PostFundManagement.Application/Services/Projects/Records.cs +++ b/PostFundManagement.Application/Services/Projects/Records.cs @@ -60,7 +60,7 @@ public sealed record CreateComplianceItemRequest([Required] long ProjectId, [Required][MaxLength(LabelLength)] string Title, [Required][MaxLength(TextLength)] string Requirements); -public sealed record UpdatehangeRequest([Required] long ChangeRequestId, +public sealed record UpdateChangeRequest([Required] long ChangeRequestId, [Required][MaxLength(LabelLength)] string Title, [Required][MaxLength(TextLength)] string Justification, [Required] decimal RevisedBudget, diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index 0355088..a49bbbe 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -38,6 +38,7 @@ public static class Mappers public static Activity Map(this Entities.Activity entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, Amount = entity.Amount, ApprovedBy = entity.ApprovedBy, Category = entity.Category, @@ -56,6 +57,7 @@ public static class Mappers public static AuditLog Map(this Entities.AuditLog entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, Action = entity.Action, Changes = entity.Changes, EntityId = entity.EntityId, @@ -83,6 +85,7 @@ public static class Mappers public static Beneficiary Map(this Entities.Beneficiary entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, Category = entity.Category, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, @@ -97,6 +100,7 @@ public static class Mappers public static Budget Map(this Entities.Budget entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, Amount = entity.Amount, ApprovedBy = entity.ApprovedBy, CreatedAt = entity.CreatedAt, @@ -109,6 +113,7 @@ public static class Mappers public static ComplianceItem Map(this Entities.ComplianceItem entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, ProjectId = entity.ProjectId, @@ -122,6 +127,7 @@ public static class Mappers public static Contract Map(this Entities.Contract entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, AwardId = entity.AwardId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, @@ -136,6 +142,7 @@ public static class Mappers public static Disbursement Map(this Entities.Disbursement entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, Amount = entity.Amount, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, @@ -149,6 +156,7 @@ public static class Mappers public static Evidence Map(this Entities.Evidence entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, DocumentUrl = entity.DocumentUrl, @@ -166,6 +174,7 @@ public static class Mappers public static Indicator Map(this Entities.Indicator entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, ActualAmount = entity.ActualAmount, BaselineAmount = entity.BaselineAmount, CreatedAt = entity.CreatedAt, @@ -180,6 +189,7 @@ public static class Mappers { Id = entity.Id, Code = entity.Code, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Description = entity.Description, @@ -191,6 +201,7 @@ public static class Mappers public static Invite Map(this Entities.Invite entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, AwardId = entity.AwardId, ContractId = entity.ContractId, CreatedAt = entity.CreatedAt, @@ -205,6 +216,7 @@ public static class Mappers public static Issue Map(this Entities.Issue entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Description = entity.Description, @@ -220,6 +232,7 @@ public static class Mappers public static Milestone Map(this Entities.Milestone entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, DueAt = entity.DueAt, @@ -233,7 +246,7 @@ public static class Mappers public static Organisation Map(this Entities.Organisation entity) => new() { - Id = entity.Id, + Id = entity.Id, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Email = entity.Email, @@ -246,6 +259,7 @@ public static class Mappers public static Outcome Map(this Entities.Outcome entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, BeneficiaryId = entity.BeneficiaryId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, @@ -317,6 +331,7 @@ public static class Mappers public static Risk Map(this Entities.Risk entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Description = entity.Description, @@ -329,6 +344,7 @@ public static class Mappers public static Rule Map(this Entities.Rule entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Name = entity.Name, @@ -342,6 +358,7 @@ public static class Mappers public static SiteVisit Map(this Entities.SiteVisit entity) => new() { Id = entity.Id, + TenantId = entity.TenantId, ProjectId = entity.ProjectId, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, -- 2.47.3 From 86e2bad4fe6c90313d7fe517af4af59e7df22076 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 19:00:50 +0200 Subject: [PATCH 5/6] Implemented remaining business functionality --- .../GetNotificationsEndpoint.cs | 38 ++ .../Governance/CreateTenantEndpoint.cs | 36 ++ .../Endpoints/Governance/GetTenantEndpoint.cs | 37 ++ .../Governance/GetTenantsEndpoint.cs | 32 ++ .../Governance/UpdateOrganisationEndpoint.cs | 10 +- .../Governance/UpdateTenantEndpoint.cs | 36 ++ .../Projects/CreateBeneficiaryEndpoint.cs | 35 ++ .../Projects/CreateChangeRequestEndpoint.cs | 38 ++ .../Projects/CreateComplianceItemEndpoint.cs | 36 ++ .../Endpoints/Projects/CreateIssueEndpoint.cs | 35 ++ .../Projects/CreateOutcomeEndpoint.cs | 36 ++ .../Projects/CreateSiteVisitEndpoint.cs | 35 ++ .../Projects/GetBeneficiariesEndpoint.cs | 38 ++ .../Projects/GetBeneficiaryEndpoint.cs | 38 ++ .../Projects/GetChangeRequestEndpoint.cs | 38 ++ .../Projects/GetChangeRequestsEndpoint.cs | 38 ++ .../Projects/GetComplianceItemEndpoint.cs | 38 ++ .../Projects/GetComplianceItemsEndpoint.cs | 38 ++ .../Endpoints/Projects/GetIssueEndpoint.cs | 38 ++ .../Endpoints/Projects/GetIssuesEndpoint.cs | 38 ++ .../Endpoints/Projects/GetOutcomeEndpoint.cs | 38 ++ .../Endpoints/Projects/GetOutcomesEndpoint.cs | 38 ++ .../Projects/GetSiteVisitEndpoint.cs | 38 ++ .../Projects/GetSiteVisitsEndpoint.cs | 38 ++ .../Projects/UpdateBeneficiaryEndpoint.cs | 35 ++ .../Projects/UpdateChangeRequestEndpoint.cs | 35 ++ .../Projects/UpdateComplianceItemEndpoint.cs | 35 ++ .../Endpoints/Projects/UpdateIssueEndpoint.cs | 35 ++ .../Projects/UpdateOutcomeEndpoint.cs | 35 ++ .../Projects/UpdateSiteVisitEndpoint.cs | 35 ++ .../Services/ICommunicationsService.cs | 2 +- .../Services/IGovernanceService.cs | 2 +- .../Abstractions/Services/IProjectService.cs | 2 +- .../CommunicationsCacheService.cs | 4 +- .../Communications/CommunicationsService.cs | 78 ++- .../Services/Communications/Records.cs | 4 +- .../Governance/GovernanceCacheService.cs | 4 +- .../Services/Governance/GovernanceService.cs | 222 ++++++-- .../Services/Governance/Records.cs | 7 +- .../Services/Projects/ProjectCacheService.cs | 4 +- .../Services/Projects/ProjectService.cs | 512 ++++++++++++++---- .../Extensions/Mappers.cs | 16 + 42 files changed, 1755 insertions(+), 172 deletions(-) create mode 100644 PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs diff --git a/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs new file mode 100644 index 0000000..5454dba --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using static PostFundManagement.Application.Extensions.General; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Communications; + +[ApiVersionTarget(1)] +public sealed class GetNotificationsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/communications/invites/{organisationId}", async (string organisationId, + ICommunicationsService service, CancellationToken cancellationToken = default) => + await service.GetNotificationsAsync(organisationId, cancellationToken) switch + { + { IsSuccess: true, Value: var notifications } => Results.Ok(notifications), + var r when r.HasError() => Results.Forbid(), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Retrieves the notification system report") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetNotificationsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Communications) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs new file mode 100644 index 0000000..572928e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs @@ -0,0 +1,36 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Governance; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Governance; + +[ApiVersionTarget(1)] +public sealed class CreateTenantEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/governance/tenants", async (CreateTenantRequest request, IGovernanceService service, HttpContext context, + CancellationToken cancellationToken = default) => + await service.CreateTenantAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Create a new tenant") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateTenantEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs new file mode 100644 index 0000000..5ac8bdb --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs @@ -0,0 +1,37 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Governance; + +[ApiVersionTarget(1)] +public sealed class GetTenantEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/governance/tenants/{tenantId}", async (string tenantId, IGovernanceService service, + CancellationToken cancellationToken = default) => + await service.GetTenantAsync(tenantId, cancellationToken) switch + { + { IsSuccess: true, Value: var tenant } => Results.Ok(tenant), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets system tenants") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetTenantsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs new file mode 100644 index 0000000..2286976 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Entities; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Governance; + +[ApiVersionTarget(1)] +public sealed class GetTenantsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/governance/tenants", async (IGovernanceService service, + CancellationToken cancellationToken = default) => + await service.GetTenantsAsync(cancellationToken) switch + { + { IsSuccess: true, Value: var tenants } => Results.Ok(tenants), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets system tenants") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetTenantsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs index 6fa659a..085beac 100644 --- a/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs @@ -15,11 +15,13 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint public void Map(IEndpointRouteBuilder builder) { builder.MapPut("api/governance/organisations", async (UpdateOrganisationRequest request, - IGovernanceService service, CancellationToken cancellationToken = default) => - await service.UpdateOrganisationAsync(request, cancellationToken) switch + IGovernanceService service, HttpContext context, CancellationToken cancellationToken = default) => + await service.UpdateOrganisationAsync(request, context.GetUserId(), cancellationToken) switch { { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()) }) .WithDescription("Update an existing organization profile") @@ -27,10 +29,10 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint .WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status500InternalServerError) - .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Governance); } } diff --git a/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs new file mode 100644 index 0000000..949522f --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs @@ -0,0 +1,36 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Governance; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Governance; + +[ApiVersionTarget(1)] +public sealed class UpdateTenantEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/governance/tenants", async (UpdateTenantRequest request, HttpContext context, + IGovernanceService service, CancellationToken cancellationToken = default) => + await service.UpdateTenantAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Updates a specific tenant") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateTenantEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs new file mode 100644 index 0000000..0787805 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateBeneficiaryEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/beneficiaries", async (CreateBeneficiaryRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.CreateBeneficiaryAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Add a new project beneficiary") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateBeneficiaryEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs new file mode 100644 index 0000000..f78ca4c --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateChangeRequestEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/changerequests", async (CreateChangeRequest request, IProjectService service, + HttpContext context, CancellationToken cancellationToken = default) => + await service.CreateChangeRequestAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Create a new change request against an existing project") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateChangeRequestEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs new file mode 100644 index 0000000..85397c2 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs @@ -0,0 +1,36 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateComplianceItemEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/complianceitems", async (CreateComplianceItemRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.CreateComplianceItemAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Add a new beneficiary compliance item") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateComplianceItemEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs new file mode 100644 index 0000000..2eb631b --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateIssueEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/issues", async (CreateIssueRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.CreateIssueAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Add a new project issue") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateIssueEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs new file mode 100644 index 0000000..e410127 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs @@ -0,0 +1,36 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateOutcomeEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/beneficiaris/outcomes", async (CreateOutcomeRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.CreateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Add a new beneficiary outcome") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateOutcomeEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} + \ No newline at end of file diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs new file mode 100644 index 0000000..5de8bde --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class CreateSiteVisitEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/projects/sitevisits", async (CreateSiteVisitRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.CreateSiteVisitAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Add a new project site visit") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateSiteVisitEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs new file mode 100644 index 0000000..1812765 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetBeneficiariesEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/beneficiaries/{projectId}", async (string projectId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetBeneficiariesAsync(projectId, cancellationToken) switch + { + { IsSuccess: true, Value: var beneficiaries } => Results.Ok(beneficiaries), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project beneficiaries") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs new file mode 100644 index 0000000..0af8e92 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetBeneficiaryEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/beneficiaries/{beneficiaryId}", async (string beneficiaryId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetBeneficiaryAsync(beneficiaryId, cancellationToken) switch + { + { IsSuccess: true, Value: var beneficiary } => Results.Ok(beneficiary), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project beneficiaries") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs new file mode 100644 index 0000000..a71792d --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetChangeRequestEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/changerequests/{changeRequestId}", async (string changeRequestId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetChangeRequestAsync(changeRequestId, cancellationToken) switch + { + { IsSuccess: true, Value: var changeRequest } => Results.Ok(changeRequest), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets a specific project change request") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetChangeRequestEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs new file mode 100644 index 0000000..4d2ba3e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetChangeRequestsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/changerequests/{projectId}", async (string projectId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetChangeRequestsAsync(projectId, cancellationToken) switch + { + { IsSuccess: true, Value: var changeRequests } => Results.Ok(changeRequests), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project change requests") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetChangeRequestsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs new file mode 100644 index 0000000..0a0ed2e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetComplianceItemEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/complianceitems/{complianceItemId}", async (string complianceItemId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetComplianceItemAsync(complianceItemId, cancellationToken) switch + { + { IsSuccess: true, Value: var complianceItem } => Results.Ok(complianceItem), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets a specific project compliance item") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetComplianceItemEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs new file mode 100644 index 0000000..3579d22 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetComplianceItemsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/complianceitems/{projectId}", async (string projectId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetComplianceItemsAsync(projectId, cancellationToken) switch + { + { IsSuccess: true, Value: var complianceItems } => Results.Ok(complianceItems), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project compliance items") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetComplianceItemsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs new file mode 100644 index 0000000..983b642 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetIssueEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/issues/{issueId}", async (string issueId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetIssueAsync(issueId, cancellationToken) switch + { + { IsSuccess: true, Value: var issue } => Results.Ok(issue), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets a specific project issue") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetIssueEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs new file mode 100644 index 0000000..1966815 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetIssuesEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/issues/{projectId}", async (string projectId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetIssuesAsync(projectId, cancellationToken) switch + { + { IsSuccess: true, Value: var issues } => Results.Ok(issues), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project issues") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetIssuesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs new file mode 100644 index 0000000..ac81816 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetOutcomeEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/outcomes/{outcomeId}", async (string outcomeId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetOutcomeAsync(outcomeId, cancellationToken) switch + { + { IsSuccess: true, Value: var outcome } => Results.Ok(outcome), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets a specific project outcome") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetOutcomeEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs new file mode 100644 index 0000000..e2f4e9f --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetOutcomesEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/outcomes/{beneficiatyId}", async (string beneficiatyId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetOutcomesAsync(beneficiatyId, cancellationToken) switch + { + { IsSuccess: true, Value: var outcomes } => Results.Ok(outcomes), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project beneficiary outcomes") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetOutcomesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs new file mode 100644 index 0000000..bbe08f7 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetSiteVisitEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/sitevisits/{siteVisitId}", async (string siteVisitId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetSiteVisitAsync(siteVisitId, cancellationToken) switch + { + { IsSuccess: true, Value: var siteVisit } => Results.Ok(siteVisit), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project site visit") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetSiteVisitEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs new file mode 100644 index 0000000..cb20d17 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs @@ -0,0 +1,38 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class GetSiteVisitsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/sitevisits/{projectId}", async (string projectId, IProjectService service, + CancellationToken cancellationToken = default) => + await service.GetSiteVisitsAsync(projectId, cancellationToken) switch + { + { IsSuccess: true, Value: var siteVisits } => Results.Ok(siteVisits), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Gets project site visits") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetSiteVisitsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs new file mode 100644 index 0000000..255b45b --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateBeneficiaryEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/beneficiaries", async (UpdateBeneficiaryRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateBeneficiaryAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update project beneficiary") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateBeneficiaryEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs new file mode 100644 index 0000000..3368070 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateChangeRequestEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/changerequests", async (UpdateChangeRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateChangeRequestAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update a specific project change request") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateChangeRequestEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs new file mode 100644 index 0000000..34ed912 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateComplianceItemEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/complianceitems", async (UpdateComplianceItemRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateComplianceItemAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update a specific compliance item") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateComplianceItemEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs new file mode 100644 index 0000000..aeae4ac --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateIssueEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/issues", async (UpdateIssueRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateIssueAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update a specific project issue") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateIssueEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs new file mode 100644 index 0000000..4caf386 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateOutcomeEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/outcomes", async (UpdateOutcomeRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update project beneficiary outcome") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateOutcomeEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs new file mode 100644 index 0000000..917a206 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs @@ -0,0 +1,35 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; +using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateSiteVisitEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/sitevisits", async (UpdateSiteVisitRequest request, HttpContext context, + IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateSiteVisitAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Update project site visit") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateSiteVisitEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs index f1a1821..23149ff 100644 --- a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs @@ -26,7 +26,7 @@ public interface ICommunicationsService [Log] [Audit] - Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default); + Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default); [Log] Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs index 98b9624..fc3a708 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs @@ -31,7 +31,7 @@ public interface IGovernanceService [Log] [Audit] - Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); + Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] [Audit] diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 023f172..53249bb 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -105,7 +105,7 @@ public interface IProjectService [Log] [Audit] - Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default); + Task UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs index a12bbb1..613677d 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs @@ -135,6 +135,6 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.SendInviteAsync(request, userId, cancellationToken); - public async Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => - await service.UpdateNotificationAsync(request, userId, cancellationToken); + public async Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) => + await service.UpdateNotificationAsync(request, cancellationToken); } diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs index 712583e..d163422 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs @@ -1,4 +1,5 @@ using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; @@ -8,7 +9,8 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Communications; -public sealed class CommunicationsService(IDbContextFactory contextFactory, ITenantContextService tenantContext) : ICommunicationsService +public sealed class CommunicationsService(IDbContextFactory contextFactory, + HashService hashService, ITenantContextService tenantContext) : ICommunicationsService { public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -23,24 +25,19 @@ public sealed class CommunicationsService(IDbContextFactory>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + context.Notifications.Add(new Domain.Entities.Notification + { + CreatedAt = DateTime.UtcNow, + Recipient = request.Recipient, + OrganisationId = request.OrganisationId, + Destination = request.Destination, + Subject = request.Subject, + Message = request.Message, + Platform = request.Platform, + Status = NotificationStatus.Queued, + TenantId = tenantContext.TenantId + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create entry")); })); - public async Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => + public async Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Notifications + .Where(n => n.Id == request.NotificationId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Destination, request.Destination) + .SetProperty(u => u.SentAt, request.SentAt) + .SetProperty(u => u.Error, request.Error) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) + .SetProperty(u => u.Status, request.Status), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create($"Failed to update notification: {request.NotificationId}")); })); public async Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { + var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notifications = await context.Notifications.AsNoTracking() + .Where(n => n.OrganisationId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(notifications?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { + var decodeResult = hashService.DecodeLongIdHash(notificationIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notification = await context.Notifications.AsNoTracking() + .FirstOrDefaultAsync(n => n.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(notification is null, NotFoundError.New) + .ToResult(notification!.Map()); })); } diff --git a/PostFundManagement.Application/Services/Communications/Records.cs b/PostFundManagement.Application/Services/Communications/Records.cs index b315be1..706df2f 100644 --- a/PostFundManagement.Application/Services/Communications/Records.cs +++ b/PostFundManagement.Application/Services/Communications/Records.cs @@ -3,10 +3,10 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Application.Services.Communications; -public sealed record UpdateNotificationRequest([Required] long NotificationId, +public sealed record UpdateNotificationRequest([Required] long NotificationId, string? Destination, [Required] NotificationStatus Status, [Required] DateTime SentAt, - [Required][MaxLength(TextLength)] string Error); + [MaxLength(TextLength)] string? Error); public sealed record CreateNotificationRequest([Required] long OrganisationId, [Required] Guid Recipient, diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index 2061276..16a7aac 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -311,8 +311,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => - await service.UpdateOrganisationAsync(request, cancellationToken); + public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdateOrganisationAsync(request, userId, cancellationToken); public async Task UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdatePortfolioAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Governance/GovernanceService.cs b/PostFundManagement.Application/Services/Governance/GovernanceService.cs index 04b5524..74391dc 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceService.cs @@ -10,7 +10,7 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Governance; -public sealed class GovernanceService(IDbContextFactory contextFactory, HashService hashService, +public sealed class GovernanceService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : IGovernanceService { public async Task> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => @@ -140,7 +140,7 @@ public sealed class GovernanceService(IDbContextFactory co if (await context.Organisations.AnyAsync(o => o.RegistrationNo == registrationNo, cancellationToken)) return Result.Fail(CreateError.Create("Organisation with the same registration number already exists")); - context.Organisations.Add(new Domain.Entities.Organisation + var organisation = context.Organisations.Add(new Domain.Entities.Organisation { Name = request.Name.Humanize(LetterCasing.Title).Trim(), RegistrationNo = registrationNo, @@ -148,13 +148,18 @@ public sealed class GovernanceService(IDbContextFactory co Type = request.Type, Status = ActivityStatus.Active, CreatedBy = userId, - CreatedAt = DateTime.UtcNow + CreatedAt = DateTime.UtcNow }); - return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); + await context.SaveChangesAsync(cancellationToken); + + var mapResult = await CreateTenantOrganisationAsync(hashService.HashEncodeLongId(organisation.Entity.Id).Value, + hashService.HashEncodeLongId(tenantContext.TenantId).Value, userId, cancellationToken); + + return Result.FailIf(mapResult.IsFailed, CreateError.Create("Failed to create organisation")); })); - public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => + public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -165,9 +170,17 @@ public sealed class GovernanceService(IDbContextFactory co .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.RegistrationNo, request.RegistrationNo.Clean()) .SetProperty(u => u.Email, request.Email.ToLower(CultureInfo.InvariantCulture).Clean()) - .SetProperty(u => u.Type, request.Type), cancellationToken); + .SetProperty(u => u.Type, request.Type) + .SetProperty(u => u.Status, request.Status), cancellationToken); - return Result.OkIf(changedTotal > 0, UpdateError.New); + var tenantOrganisation = await context.TenantOrganisations.AsNoTracking().FirstOrDefaultAsync(f => f.OrganisationId == request.OrganisationId, cancellationToken); + + if (tenantOrganisation is null) + return Result.Fail(ValidationError.Create("Failed to find linked tenant organisation")); + + var updateResult = await UpdateTenantOrganisationAsync(hashService.HashEncodeLongId(tenantOrganisation.Id).Value, request.Status, userId, cancellationToken); + + return Result.OkIf(changedTotal > 0 && updateResult.IsFailed, UpdateError.Create("Failed to update organisation")); })); public async Task CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => @@ -255,48 +268,171 @@ public sealed class GovernanceService(IDbContextFactory co .ToResult(portfolio!.Map()); })); - public Task CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetTenantsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var requestedName = request.Name.Humanize(LetterCasing.Title).Trim(); - public Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Tenants.AnyAsync(t => t.Name == requestedName, cancellationToken)) + return Result.Fail(ValidationError.Create("A tenant with an equivalent name exists.")); - public Task UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + context.Tenants.Add(new Domain.Entities.Tenant + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Name = requestedName, + Status = ActivityStatus.Active + }); - public Task UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create new tenant")); + })); - public Task CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var tenants = await context.Tenants.AsNoTracking() + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); - public Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(tenants?.Select(t => t.Map()).ToArray() ?? []); + })); - public Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(tenantIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var tenant = await context.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(tenant is null, NotFoundError.Create("Failed to retrieve tenants")) + .ToResult(tenant!.Map()); + })); + + public async Task UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var requestedName = request.Name.Humanize(LetterCasing.Title).Trim(); + + if (await context.Tenants.AnyAsync(t => t.Name == requestedName, cancellationToken)) + return Result.Fail(ValidationError.Create("A tenant with an equivalent name exists.")); + + var changedTotal = await context.Tenants + .Where(f => f.Id == request.TenantId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) + .SetProperty(u => u.UpdatedBy, userId) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.Name, requestedName), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records updated")); + })); + + public async Task UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.TenantUsers + .Where(f => f.UserId == request.UserId) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, request.Status), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records updated")); + })); + + public async Task CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var organisationDecodeResult = hashService.DecodeLongIdHash(organisationIdHash); + + if (organisationDecodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(organisationDecodeResult.Errors)); + + var tenantDecodeResult = hashService.DecodeLongIdHash(tenantIdHash); + + if (tenantDecodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(tenantDecodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.TenantOrganisations.AnyAsync(to => to.OrganisationId == organisationDecodeResult.Value && to.TenantId == tenantDecodeResult.Value, cancellationToken)) + return Result.Fail(ValidationError.Create("Already exists")); + + context.TenantOrganisations.Add(new Domain.Entities.TenantOrganisation + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + OrganisationId = organisationDecodeResult.Value, + TenantId = tenantDecodeResult.ValueOrDefault + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create tenant organisation map")); + })); + + public async Task UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(tenantOrganisationIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.TenantOrganisations + .Where(f => f.Id == decodeResult.Value) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update tenant organisation map")); + })); + + public async Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var tenantOrganisations = await context.TenantOrganisations.AsNoTracking() + .Include(i => i.Organisation) + .Include(i => i.Tenant) + .Where(f => f.TenantId == tenantContext.TenantId) + .ToListAsync(); + + return Result.Ok(tenantOrganisations?.Select(i => i.Map()).ToArray() ?? []); + })); + + public async Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(tenantOrganisationIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var tenantOrganisation = await context.TenantOrganisations.AsNoTracking() + .Include(i => i.Organisation) + .Include(i => i.Tenant) + .FirstOrDefaultAsync(f => f.TenantId == tenantContext.TenantId, cancellationToken); + + return Result.FailIf(tenantOrganisation is null, NotFoundError.Create("Failed to find tenant organisation map")) + .ToResult(tenantOrganisation!.Map()); + })); } diff --git a/PostFundManagement.Application/Services/Governance/Records.cs b/PostFundManagement.Application/Services/Governance/Records.cs index 949e824..3b85c12 100644 --- a/PostFundManagement.Application/Services/Governance/Records.cs +++ b/PostFundManagement.Application/Services/Governance/Records.cs @@ -3,6 +3,8 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Application.Services.Governance; +public sealed record CreateTenantRequest([Required][MaxLength(LabelLength)] string Name); + public sealed record UpdateTenantUserRequest([Required] Guid UserId, [Required] ActivityStatus Status); public sealed record CreateTenantUserRequest([Required] long TenantId, [Required] Guid UserId); @@ -11,8 +13,6 @@ public sealed record UpdateTenantRequest([Required] long TenantId, [Required][MaxLength(LabelLength)] string Name, [Required] ActivityStatus Status); -public sealed record CreateTenantRequest([Required] long TenantId, [Required][MaxLength(LabelLength)] string Name); - public sealed record UpdatePortfolioRequest([Required] long PortfolioId, [Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(TextLength)] string Description, @@ -27,7 +27,8 @@ public sealed record UpdateOrganisationRequest([Required] long OrganisationId, [Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(LabelLength)] string RegistrationNo, [Required][EmailAddress][MaxLength(LabelLength)] string Email, - [Required] OrganisationType Type); + [Required] OrganisationType Type, + [Required] ActivityStatus Status); public sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(LabelLength)] string RegistrationNo, diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index 0660080..9e0e3ed 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -614,8 +614,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public async Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => - await service.UpdateBeneficiaryItemAsync(request, userId, cancellationToken); + public async Task UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdateBeneficiaryAsync(request, userId, cancellationToken); public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateChangeRequestAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index 0d615f8..f37bb77 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -383,123 +383,443 @@ public sealed class ProjectService(IDbContextFactory conte return Result.Ok(projects.Select(i => i.Map()).ToArray()); })); - public Task CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) + return Result.Fail(ValidationError.Create("Project does not exist")); - public Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + context.ChangeRequests.Add(new Domain.Entities.ChangeRequest + { + ProjectId = request.ProjectId, + Title = request.Title, + Justification = request.Justification, + RevisedBudget = request.RevisedBudget, + RevisedEndedAt = request.RevisionDate, + Status = ApprovalStatus.Pending, + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + TenantId = tenantContext.TenantId, + }); - public Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create change request")); + })); - public Task CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changedTotal = await context.ChangeRequests + .Where(c => c.Id == request.ChangeRequestId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.RevisedBudget, request.RevisedBudget) + .SetProperty(u => u.RevisedEndedAt, DateTime.UtcNow) + .SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Justification, request.Justification), cancellationToken); - public Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records were changed")); + })); - public Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); - public Task CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); - public Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changeRequests = await context.ChangeRequests.AsNoTracking() + .Where(c => c.ProjectId == decodeResult.Value) + .ToListAsync(cancellationToken); - public Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(changeRequests?.Select(i => i.Map()).ToArray() ?? []); + })); - public Task CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(changeRequestIdHash); - public Task UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); - public Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changeRequest = await context.ChangeRequests.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken); - public Task CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.FailIf(changeRequest is null, NotFoundError.New) + .ToResult(changeRequest!.Map()); + })); - public Task UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) + return Result.Fail(ValidationError.Create("Project does not exist")); - public Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + context.ComplianceItems.Add(new Domain.Entities.ComplianceItem + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + ProjectId = request.ProjectId, + Requirements = request.Requirements, + Status = ApprovalStatus.Pending, + TenantId = tenantContext.TenantId, + Title = request.Title.Humanize(LetterCasing.Title).Trim() + }); - public Task CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create compliance item")); + })); - public Task UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changedTotal = await context.ComplianceItems + .Where(f => f.Id == request.ComplianceItemId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Requirements, request.Requirements) + .SetProperty(u => u.Status, request.Status), cancellationToken); - public Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); + })); + + public async Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var complianceItems = await context.ComplianceItems.AsNoTracking() + .Where(c => c.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(complianceItems?.Select(i => i.Map()).ToArray() ?? []); + })); + + public async Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(complianceItemIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var compliancItem = await context.ComplianceItems.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(compliancItem is null, NotFoundError.New) + .ToResult(compliancItem!.Map()); + })); + + public async Task CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + context.Beneficiaries.Add(new Domain.Entities.Beneficiary + { + Category = request.Category, + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Location = request.Location, + Name = request.Name.Humanize(LetterCasing.Title).Trim(), + ProjectId = request.ProjectId, + ReachedCount = request.ReachedCount, + TargetCount = request.TargetCount, + UserId = userId, + TenantId = tenantContext.TenantId + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create beneficiary")); + })); + + public async Task UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Beneficiaries + .Where(f => f.Id == request.BeneficiaryId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.ReachedCount, request.ReachedCount) + .SetProperty(u => u.TargetCount, request.TargetCount) + .SetProperty(u => u.Location, request.Location) + .SetProperty(u => u.Category, request.Category.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); + })); + + public async Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var beneficiaries = await context.Beneficiaries.AsNoTracking() + .Where(b => b.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(beneficiaries?.Select(i => i.Map()).ToArray() ?? []); + })); + + public async Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var beneficiary = await context.Beneficiaries.AsNoTracking() + .FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(beneficiary is null, NotFoundError.New) + .ToResult(beneficiary!.Map()); + })); + + public async Task CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + context.Issues.Add(new Domain.Entities.Issue + { + AssignedTo = request.AssignedTo, + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Description = request.Description, + Priority = request.Priority, + ProjectId = request.ProjectId, + Status = ActivityStatus.Active, + TenantId = tenantContext.TenantId, + Title = request.Title.Humanize(LetterCasing.Title).Trim() + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create issue")); + })); + + public async Task UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Issues + .Where(i => i.Id == request.IssueId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Description, request.Description) + .SetProperty(u => u.Resolution, request.Resolution) + .SetProperty(u => u.ResolvedAt, DateTime.UtcNow) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.AssignedTo, request.AssignedTo), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changes")); + })); + + public async Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var issues = await context.Issues.AsNoTracking() + .Where(i => i.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(issues?.Select(i => i.Map()).ToArray() ?? []); + })); + + public async Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(issueIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var issue = await context.Issues.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(issue is null, NotFoundError.New) + .ToResult(issue!.Map()); + })); + + public async Task CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + context.Outcomes.Add(new Domain.Entities.Outcome + { + BeneficiaryId = request.BeneficiaryId, + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Description = request.Description, + Name = request.Name.Humanize(LetterCasing.Title).Trim(), + TenantId = tenantContext.TenantId + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create outcome")); + })); + + public async Task UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Outcomes + .Where(o => o.Id == request.OutcomeId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Description, request.Description) + .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); + })); + + public async Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var outcomes = await context.Outcomes.AsNoTracking() + .Where(o => o.BeneficiaryId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(outcomes?.Select(o => o.Map()).ToArray() ?? []); + })); + + public async Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(outcomeIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var outcome = await context.Outcomes.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(outcome is null, NotFoundError.New) + .ToResult(outcome!.Map()); + })); + + public async Task CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + context.SiteVisits.Add(new Domain.Entities.SiteVisit + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + InspectorName = request.InspectorName.Humanize(LetterCasing.Title).Trim(), + Findings = request.Findings, + VisitedAt = request.VisitedAt + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create site visit")); + })); + + public async Task UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.SiteVisits + .Where(v => v.Id == request.SiteVisitId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.InspectorName, request.InspectorName.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Findings, request.Findings) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.VisitedAt, request.VisitedAt), cancellationToken); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update site visit")); + })); + + public async Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var visits = await context.SiteVisits.AsNoTracking() + .Where(v => v.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(visits?.Select(i => i.Map()).ToArray() ?? []); + })); + + public async Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(siteVisitIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var visit = await context.SiteVisits.AsNoTracking().FirstOrDefaultAsync(v => v.Id == decodeResult.Value, cancellationToken); + + return Result.FailIf(visit is null, NotFoundError.New) + .ToResult(visit!.Map()); + })); } diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index a49bbbe..13808aa 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -110,6 +110,22 @@ public static class Mappers UpdatedAt = entity.UpdatedAt }; + public static ChangeRequest Map(this Entities.ChangeRequest entity) => new() + { + ApprovedBy = entity.ApprovedBy, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Id = entity.Id, + Justification = entity.Justification, + ProjectId = entity.ProjectId, + RevisedBudget = entity.RevisedBudget, + RevisedEndedAt = entity.RevisedEndedAt, + Status = entity.Status, + TenantId = entity.TenantId, + Title = entity.Title, + UpdatedAt = entity.UpdatedAt + }; + public static ComplianceItem Map(this Entities.ComplianceItem entity) => new() { Id = entity.Id, -- 2.47.3 From 0f903a58b57665519f0ca1b35c8af051138c2b6e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 19:06:03 +0200 Subject: [PATCH 6/6] Recolved endpoint name ambiguity --- .../Endpoints/Governance/GetTenantsEndpoint.cs | 2 +- .../Endpoints/Projects/GetBeneficiariesEndpoint.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs index 2286976..865a037 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs @@ -22,7 +22,7 @@ public sealed class GetTenantsEndpoint : IEndpoint }) .WithDescription("Gets system tenants") .RequireAuthorization(Constants.MainScope) - .WithName(typeof(GetTenantsEndpoint).ToEndpointName()) + .WithName(typeof(GetTenantsEndpoint).ToEndpointName("Retrieval")) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) diff --git a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs index 1812765..b01e257 100644 --- a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs @@ -25,7 +25,7 @@ public sealed class GetBeneficiariesEndpoint : IEndpoint }) .WithDescription("Gets project beneficiaries") .RequireAuthorization(Constants.MainScope) - .WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName()) + .WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName("Retrieval")) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) -- 2.47.3