Improved compliance monitoring

This commit is contained in:
2026-10-22 15:42:36 +02:00
parent 9835121fd9
commit 299d6ff4dc
27 changed files with 4245 additions and 149 deletions
@@ -94,6 +94,16 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var disbursement = await context.Disbursements.AsNoTracking()
.FirstOrDefaultAsync(d => d.Id == decodeResult.Value, cancellationToken);
if (disbursement is null)
return Result.Fail(NotFoundError.Create("Disbursement not found."));
var isAwardApproved = await context.Awards
.AnyAsync(a => a.ProjectId == disbursement.ProjectId && a.Status == ApprovalStatus.Approved, cancellationToken);
if (!isAwardApproved)
return Result.Fail(ValidationError.Create("Cannot approve disbursement: The project's linked award is not approved or does not exist."));
var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value)
.ExecuteUpdateAsync(f => f
.SetProperty(f => f.Status, ApprovalStatus.Approved)
@@ -161,6 +161,11 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail(NotFoundError.New);
var hasApprovedAward = await context.Awards
.AnyAsync(a => a.ProjectId == request.ProjectId && a.Status == ApprovalStatus.Approved, cancellationToken);
if (!hasApprovedAward)
return Result.Fail(ValidationError.Create("Cannot create budget: The project's linked award is not approved or does not exist."));
var budget = new Domain.Entities.Budget
{
ProjectId = request.ProjectId,
@@ -20,6 +20,27 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
if (request.Status == ApprovalStatus.Approved)
{
var hasUnmetCompliance = await context.ComplianceItems
.AnyAsync(c => c.ProjectId == request.ProjectId && c.Status == ComplianceStatus.NotMet, cancellationToken);
if (hasUnmetCompliance)
return Result.Fail(ValidationError.Create("Project cannot be approved because some compliance items are not met."));
var projectBeneficiaryIds = await context.Beneficiaries
.Where(b => b.ProjectId == request.ProjectId)
.Select(b => b.Id)
.ToListAsync(cancellationToken);
if (projectBeneficiaryIds.Count > 0)
{
var hasUnmetOutcomes = await context.Outcomes
.AnyAsync(o => projectBeneficiaryIds.Contains(o.BeneficiaryId) && o.Status == ComplianceStatus.NotMet, cancellationToken);
if (hasUnmetOutcomes)
return Result.Fail(ValidationError.Create("Project cannot be approved because some beneficiary outcomes are not met."));
}
}
var changedTotal = await context.Projects
.Where(f => f.Id == request.ProjectId)
.ExecuteUpdateAsync(f => f
@@ -544,8 +565,9 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
ProjectId = request.ProjectId,
BeneficiaryId = request.BeneficiaryId,
Requirements = request.Requirements,
Status = ApprovalStatus.Pending,
Status = request.Status,
TenantId = tenantContext.TenantId,
Title = request.Title.Humanize(LetterCasing.Title).Trim()
});
@@ -563,7 +585,10 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.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);
.SetProperty(u => u.BeneficiaryId, request.BeneficiaryId)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow)
.SetProperty(u => u.VerifiedBy, userId), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
@@ -768,6 +793,7 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedBy = userId,
Description = request.Description,
Name = request.Name.Humanize(LetterCasing.Title).Trim(),
Status = request.Status,
TenantId = tenantContext.TenantId
});
@@ -783,7 +809,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.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);
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
@@ -15,11 +15,13 @@ public sealed record CreateSiteVisitRequest([Required] long ProjectId,
public sealed record UpdateOutcomeRequest([Required] long OutcomeId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
[Required][MaxLength(TextLength)] string Description,
[Required] ComplianceStatus Status = ComplianceStatus.NotMet);
public sealed record CreateOutcomeRequest([Required] long BeneficiaryId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
[Required][MaxLength(TextLength)] string Description,
ComplianceStatus Status = ComplianceStatus.NotMet);
public sealed record UpdateIssueRequest([Required] long IssueId,
[Required][MaxLength(LabelLength)] string Title,
@@ -53,11 +55,14 @@ public sealed record CreateBeneficiaryRequest([Required] long ProjectId,
public sealed record UpdateComplianceItemRequest([Required] long ComplianceItemId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Requirements,
[Required] ApprovalStatus Status);
[Required] ComplianceStatus Status,
long? BeneficiaryId = null);
public sealed record CreateComplianceItemRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Requirements);
[Required][MaxLength(TextLength)] string Requirements,
ComplianceStatus Status = ComplianceStatus.NotMet,
long? BeneficiaryId = null);
public sealed record UpdateChangeRequest([Required] long ChangeRequestId,
[Required][MaxLength(LabelLength)] string Title,