using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Projects; public sealed class ProjectService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : IProjectService { public async Task UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); var changedTotal = await context.Projects .Where(f => f.Id == request.ProjectId) .ExecuteUpdateAsync(f => f .SetProperty(p => p.Status, request.Status) .SetProperty(p => p.UpdatedBy, userId) .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); })); public async Task> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var projects = await context.Projects.AsNoTracking().OrderByDescending(p => p.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(projects.Select(p => p.Map()).ToArray()); })); public async Task> GetProjectAsync(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 project = await context.Projects.AsNoTracking() .FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken); return Result.FailIf(project is null, NotFoundError.New) .ToResult(project!.Map()); })); public async Task> GetActivitiesAsync(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 activities = await context.Activities.AsNoTracking() .Where(a => a.ProjectId == decodeResult.Value) .OrderByDescending(a => a.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(activities.Select(a => a.Map()).ToArray()); })); public async Task CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var name = request.Name.Humanize(LetterCasing.Title).Trim(); if (await context.Projects.AnyAsync(p => p.Name == name && p.Sector == request.Sector && p.ProjectType == request.ProjectType, cancellationToken)) return Result.Fail(ValidationError.Create("A similarly named project exists of the same project type and sector")); if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken)) return Result.Fail(NotFoundError.New); var description = request.Description.Humanize(LetterCasing.Sentence).Trim(); context.Projects.Add(new Domain.Entities.Project { ProgrammeId = request.ProgrammeId, Name = name, Description = description, Sector = request.Sector, ProjectType = request.ProjectType, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow, TenantId = tenantContext.TenantId }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("No changes detected, update cancelled.")); })); public async Task CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); if (request.MilestoneId.HasValue) if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken)) return Result.Fail(NotFoundError.New); context.Activities.Add(new Domain.Entities.Activity { ProjectId = request.ProjectId, MilestoneId = request.MilestoneId, Category = request.Category.Humanize(LetterCasing.Title).Trim(), Amount = request.Amount, Title = request.Title.Humanize(LetterCasing.Title).Trim(), Description = request.Description, PerformedAt = request.PerformedAt, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("No record was created.")); })); public async Task> GetProjectMilestonesAsync(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 milestones = await context.Milestones.AsNoTracking() .Where(m => m.ProjectId == decodeResult.Value) .OrderBy(m => m.DueAt) .ToListAsync(cancellationToken); return Result.Ok(milestones.Select(i => i.Map()).ToArray()); })); public async Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) => 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 evidences = await context.Indicators.AsNoTracking() .Where(e => e.ProjectId == decodeResult.Value) .OrderByDescending(o => o.Id) .ToListAsync(cancellationToken); return Result.Ok(evidences.Select(e => e.Map()).ToArray()); })); public async Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create().CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestone = await context.Milestones.AsNoTracking() .FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); return Result.FailIf(milestone is null, NotFoundError.New) .ToResult(milestone!.Map()); })); public async Task> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .OrderByDescending(m => m.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(milestones.Select(i => i.Map()).ToArray()); })); public async Task UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.DueAt, request.DueAt) .SetProperty(u => u.Priority, request.Priority) .SetProperty(u => u.Status, request.Status) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); })); public async Task UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.Description, request.Description.Humanize(LetterCasing.Sentence).Trim()) .SetProperty(u => u.StartedAt, request.StartedAt) .SetProperty(u => u.EndedAt, request.EndedAt) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) .SetProperty(u => u.Sector, request.Sector) .SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); })); public async Task> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(activityIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var activity = await context.Activities.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken); return Result.FailIf(activity is null, NotFoundError.New) .ToResult(activity!.Map()); })); public async Task> GetProjectRisksAsync(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 risks = await context.Risks.AsNoTracking() .Where(r => r.ProjectId == decodeResult.Value) .OrderByDescending(r => r.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(risks.Select(i => i.Map()).ToArray()); })); public async Task CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var name = request.Name.Humanize(LetterCasing.Title).Trim(); if (!await context.Portfolios.AnyAsync(p => p.Id == request.PortfolioId, cancellationToken)) return Result.Fail(ValidationError.Create("Portfolio does not exist.")); if (await context.Programmes.AnyAsync(p => p.Name == name, cancellationToken)) return Result.Fail(ValidationError.Create("Programme of the same name exists")); context.Programmes.Add(new Domain.Entities.Programme { PortfolioId = request.PortfolioId, OwnedBy = request.OwnerId, Name = name, CreatedBy = userId, CreatedAt = DateTime.UtcNow, Status = ApprovalStatus.Pending, TenantId = tenantContext.TenantId }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); })); public async Task UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); var changedTotal = await context.Programmes.Where(p => p.Id == request.ProgrammeId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.OwnedBy, request.OwnerId) .SetProperty(u => u.Status, request.Status) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); })); public async Task> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var programme = await context.Programmes.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken); return Result.FailIf(programme is null, NotFoundError.New) .ToResult(programme!.Map()); })); public async Task> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var programmes = await context.Programmes.AsNoTracking() .OrderByDescending(m => m.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(programmes.Select(i => i.Map()).ToArray()); })); public async Task> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Programmes.AnyAsync(p => p.Id == decodeResult.Value, cancellationToken)) return Result.Fail(NotFoundError.New); var projects = await context.Projects.AsNoTracking() .Where(p => p.ProgrammeId == decodeResult.Value) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(projects.Select(i => i.Map()).ToArray()); })); public async Task CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(ValidationError.Create("Project does not exist")); 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, }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create change request")); })); public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); 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); return Result.OkIf(changedTotal > 0, UpdateError.Create("No records were changed")); })); public async Task> GetChangeRequestsAsync(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 changeRequests = await context.ChangeRequests.AsNoTracking() .Where(c => c.ProjectId == decodeResult.Value) .ToListAsync(cancellationToken); return Result.Ok(changeRequests?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(changeRequestIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changeRequest = await context.ChangeRequests.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken); return Result.FailIf(changeRequest is null, NotFoundError.New) .ToResult(changeRequest!.Map()); })); public async Task CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(ValidationError.Create("Project does not exist")); 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() }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create compliance item")); })); public async Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); 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); 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()); })); }