276 lines
13 KiB
C#
276 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) : 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
|
|
};
|
|
|
|
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."));
|
|
}));
|
|
}
|