Implemented remaining business functionality

This commit is contained in:
2026-09-25 19:00:50 +02:00
parent 1efbf9d99f
commit 86e2bad4fe
42 changed files with 1755 additions and 172 deletions
@@ -383,123 +383,443 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(projects.Select(i => i.Map()).ToArray());
}));
public Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result> 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<Result<ChangeRequest[]>> 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<Result<ChangeRequest>> 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<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result> UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result> 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<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records were changed"));
}));
public Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
public Task<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
if (decodeResult.IsFailed)
return Result.Fail<ChangeRequest[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
public Task<Result> UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result<Beneficiary[]>> 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<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
return Result.Ok(changeRequests?.Select(i => i.Map()).ToArray() ?? []);
}));
public Task<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(changeRequestIdHash);
public Task<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
if (decodeResult.IsFailed)
return Result.Fail<ChangeRequest>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
public Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
var changeRequest = await context.ChangeRequests.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
public Task<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
return Result.FailIf(changeRequest is null, NotFoundError.New)
.ToResult(changeRequest!.Map());
}));
public Task<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result<Outcome[]>> 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<Result<Outcome>> 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<Result> 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<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public async Task<Result> UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public Task<Result<SiteVisit[]>> 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<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
public async Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ComplianceItem[]>(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<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(complianceItemIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ComplianceItem>(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<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result> UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Beneficiary[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Beneficiary[]>(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<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Beneficiary>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Beneficiary>(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<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Issue[]>(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<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(issueIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Issue>(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<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Outcome[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Outcome[]>(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<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Outcome>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(outcomeIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Outcome>(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<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(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<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<SiteVisit[]>(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<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(siteVisitIdHash);
if (decodeResult.IsFailed)
return Result.Fail<SiteVisit>(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());
}));
}