Files
pfm/PostFundManagement.Application/Services/Grants/GrantsService.cs
T

277 lines
13 KiB
C#

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.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service, ITenantContextService tenantContext) : IGrantsService
{
public async Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contracts = await context.Contracts.AsNoTracking()
.OrderByDescending(c => c.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(contracts.Select(c => c.Map()).ToArray());
}));
public async Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Contract>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(contract is null, NotFoundError.New)
.ToResult(contract!.Map());
}));
public async Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budgets = await context.Budgets.AsNoTracking()
.OrderByDescending(b => b.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(budgets.Select(b => b.Map()).ToArray());
}));
public async Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Budget>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budget = await context.Budgets.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(budget is null, NotFoundError.New)
.ToResult(budget!.Map());
}));
public async Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var awards = await context.Awards.AsNoTracking()
.OrderByDescending(a => a.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(awards.Select(a => a.Map()).ToArray());
}));
public async Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Award>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(award is null, NotFoundError.New)
.ToResult(award!.Map());
}));
public async Task<Result> ExecuteContractAsync(ExecuteContractRequest 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.Contracts
.Where(c => c.Id == request.ContractId && c.Status != ContractStatus.Active)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.ExternalSignatureId, request.ExternalSignatureId)
.SetProperty(u => u.SignedDocumentUrl, request.SignedDocumentUrl)
.SetProperty(u => u.CreatedBy, userId)
.SetProperty(u => u.Status, ContractStatus.Active), cancellationToken);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, UpdateError.New);
}));
public async Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
if (award == null)
return Result.Fail(NotFoundError.New);
var contract = new Domain.Entities.Contract
{
AwardId = request.AwardId,
TotalValue = request.TotalValue,
EffectiveAt = request.EffectiveAt,
ExpiresAt = request.ExpiresAt,
Status = ContractStatus.Draft,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Contracts.Add(contract);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Budget amount must be greater than zero."));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail(NotFoundError.New);
var budget = new Domain.Entities.Budget
{
ProjectId = request.ProjectId,
Name = request.Name,
Amount = request.Amount,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Budgets.Add(budget);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Approved amount must be greater than zero."));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
return Result.Fail(NotFoundError.New);
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Result.Fail(NotFoundError.New);
var award = new Domain.Entities.Award
{
OrganisationId = request.OrganisationId,
ProgrammeId = request.ProgrammeId,
Amount = request.Amount,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
};
context.Awards.Add(award);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
if (contract is null)
return Result.Fail<string>(NotFoundError.New);
if (string.IsNullOrWhiteSpace(contract.SignedDocumentUrl))
return Result.Fail<string>(ValidationError.Create("Ecidence is invalid, has no saved documentUrl"));
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(contract.SignedDocumentUrl,
TimeSpan.FromMinutes(15), null, cancellationToken);
return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
NotFoundError.Create("Failed to generate secure file url"))
.ToResult(secureDownloadUrlResult.Value);
}));
public async Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var stream = file.OpenReadStream();
var uploadResult = await s3Service.UploadFileAsync(file.FileName, "contracts", stream,
file.ContentType, cancellationToken);
if (uploadResult.IsFailed)
return Result.Fail(uploadResult.Errors);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Contracts.Where(c => c.Id == decodeResult.Value)
.ExecuteUpdateAsync(f => f
.SetProperty(c => c.SignedDocumentUrl, uploadResult.Value), cancellationToken);
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(uploadResult.Value,
TimeSpan.FromMinutes(15), null, cancellationToken);
return Result.OkIf(changedTotal > 0 && secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
CreateError.Create("Failed to upload contract file"));
}));
public async Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Contracts.Where(c => c.Id == request.ContractId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update the contract."));
}));
}