Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/src/Feature.Form.Submissions/GetById/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ WITH submissions AS
(SELECT "Languages"
FROM "PollingStationInformationForms"
WHERE "ElectionRoundId" = @electionRoundId) AS "Languages",
(SELECT "Id"
FROM "PollingStationInformationForms"
WHERE "ElectionRoundId" = @electionRoundId) AS "FormId",
psi."FollowUpStatus" as "FollowUpStatus",
'[]'::jsonb AS "Attachments",
'[]'::jsonb AS "Notes",
Expand All @@ -67,6 +70,7 @@ UNION ALL
f."Questions",
f."DefaultLanguage",
f."Languages",
f."Id" AS "FormId",
fs."FollowUpStatus",
COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt"))
FROM "Attachments" a
Expand Down Expand Up @@ -95,6 +99,7 @@ UNION ALL
INNER JOIN "Forms" f ON f."Id" = fs."FormId"
WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId)
SELECT s."SubmissionId",
s."FormId",
s."TimeSubmitted",
s."FormCode",
s."FormType",
Expand Down
154 changes: 154 additions & 0 deletions api/src/Feature.Form.Submissions/GetByIdV2/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using Module.Answers.Models;
using Vote.Monitor.Core.Services.FileStorage.Contracts;

namespace Feature.Form.Submissions.GetByIdV2;

public class Endpoint(
IAuthorizationService authorizationService,
INpgsqlConnectionFactory dbConnectionFactory,
IFileStorageService fileStorageService) : Endpoint<Request, Results<Ok<FormSubmissionView>, NotFound>>
{
public override void Configure()
{
Get("/api/election-rounds/{electionRoundId}/form-submissions/{submissionId}:v2");
DontAutoTag();
Options(x => x.WithTags("form-submissions"));
Summary(s => { s.Summary = "Gets submission by id"; });

Policies(PolicyNames.NgoAdminsOnly);
}

public override async Task<Results<Ok<FormSubmissionView>, NotFound>> ExecuteAsync(Request req,
CancellationToken ct)
{
var authorizationResult =
await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId));
if (!authorizationResult.Succeeded)
{
return TypedResults.NotFound();
}

var sql = """
WITH submissions AS
(SELECT psi."Id" AS "SubmissionId",
'PSI' AS "FormType",
'PSI' AS "FormCode",
psi."PollingStationId",
psi."MonitoringObserverId",
psi."Answers",
(SELECT "Id"
FROM "PollingStationInformationForms"
WHERE "ElectionRoundId" = @electionRoundId) AS "FormId",
psi."FollowUpStatus" as "FollowUpStatus",
'[]'::jsonb AS "Attachments",
'[]'::jsonb AS "Notes",
"LastUpdatedAt" AS "TimeSubmitted",
psi."ArrivalTime",
psi."DepartureTime",
psi."Breaks",
psi."IsCompleted"
FROM "PollingStationInformation" psi
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = psi."MonitoringObserverId"
WHERE psi."Id" = @submissionId and psi."ElectionRoundId" = @electionRoundId
UNION ALL
SELECT
fs."Id" AS "SubmissionId",
f."FormType" AS "FormType",
f."Code" AS "FormCode",
fs."PollingStationId",
fs."MonitoringObserverId",
fs."Answers",
f."Id" AS "FormId",
fs."FollowUpStatus",
COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt"))
FROM "Attachments" a
WHERE
a."ElectionRoundId" = @electionRoundId
AND a."FormId" = fs."FormId"
AND a."MonitoringObserverId" = fs."MonitoringObserverId"
AND a."IsDeleted" = false AND a."IsCompleted" = true
AND fs."PollingStationId" = a."PollingStationId"),'[]'::JSONB) AS "Attachments",

COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'Text', "Text", 'TimeSubmitted', "LastUpdatedAt"))
FROM "Notes" n
WHERE
n."ElectionRoundId" = @electionRoundId
AND n."FormId" = fs."FormId"
AND n."MonitoringObserverId" = fs."MonitoringObserverId"
AND fs."PollingStationId" = n."PollingStationId"), '[]'::JSONB) AS "Notes",

"LastUpdatedAt" AS "TimeSubmitted",
NULL AS "ArrivalTime",
NULL AS "DepartureTime",
'[]'::jsonb AS "Breaks",
fs."IsCompleted"
FROM "FormSubmissions" fs
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = FS."MonitoringObserverId"
INNER JOIN "Forms" f ON f."Id" = fs."FormId"
WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId)
SELECT s."SubmissionId",
s."FormId",
s."TimeSubmitted",
s."FormCode",
s."FormType",
ps."Id" AS "PollingStationId",
ps."Level1",
ps."Level2",
ps."Level3",
ps."Level4",
ps."Level5",
ps."Number",
s."MonitoringObserverId",
AMO."DisplayName" "ObserverName",
AMO."Email",
AMO."PhoneNumber",
AMO."Tags",
AMO."NgoName",
AMO."IsOwnObserver",
s."Attachments",
s."Notes",
s."Answers",
s."FollowUpStatus",
s."ArrivalTime",
s."DepartureTime",
s."Breaks",
s."IsCompleted"
FROM submissions s
INNER JOIN "PollingStations" ps ON ps."Id" = s."PollingStationId"
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = s."MonitoringObserverId"
""";

var queryArgs = new
{
electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, submissionId = req.SubmissionId
};

FormSubmissionView submission = null;

using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
{
submission = await dbConnection.QueryFirstOrDefaultAsync<FormSubmissionView>(sql, queryArgs);
}

if (submission is null)
{
return TypedResults.NotFound();
}

submission = submission with
{
Attachments = await Task.WhenAll(
submission.Attachments.Select(async attachment =>
{
var result =
await fileStorageService.GetPresignedUrlAsync(attachment.FilePath, attachment.UploadedFileName);
return result is GetPresignedUrlResult.Ok(var url, _, var urlValidityInSeconds)
? attachment with { PresignedUrl = url, UrlValidityInSeconds = urlValidityInSeconds }
: attachment;
})
)
};

return TypedResults.Ok(submission);
}
}
12 changes: 12 additions & 0 deletions api/src/Feature.Form.Submissions/GetByIdV2/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Vote.Monitor.Core.Security;

namespace Feature.Form.Submissions.GetByIdV2;

public class Request
{
public Guid ElectionRoundId { get; set; }

[FromClaim(ApplicationClaimTypes.NgoId)]
public Guid NgoId { get; set; }
public Guid SubmissionId { get; set; }
}
11 changes: 11 additions & 0 deletions api/src/Feature.Form.Submissions/GetByIdV2/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Feature.Form.Submissions.GetByIdV2;

public class Validator : Validator<Request>
{
public Validator()
{
RuleFor(x => x.ElectionRoundId).NotEmpty();
RuleFor(x => x.NgoId).NotEmpty();
RuleFor(x => x.SubmissionId).NotEmpty();
}
}
3 changes: 3 additions & 0 deletions api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ OR mo."PhoneNumber" ILIKE @searchText
WITH polling_station_submissions AS (SELECT psi."Id" AS "SubmissionId",
'PSI' AS "FormType",
'PSI' AS "FormCode",
psif."Id" "FormId",
psi."PollingStationId",
psi."MonitoringObserverId",
psi."NumberOfQuestionsAnswered",
Expand Down Expand Up @@ -166,6 +167,7 @@ OR mo."PhoneNumber" ILIKE @searchText
form_submissions AS (SELECT fs."Id" AS "SubmissionId",
f."FormType",
f."Code" AS "FormCode",
f."Id" AS "FormId",
fs."PollingStationId",
fs."MonitoringObserverId",
fs."NumberOfQuestionsAnswered",
Expand Down Expand Up @@ -212,6 +214,7 @@ OR mo."PhoneNumber" ILIKE @searchText
OR (@questionsAnswered = 'None' AND fs."NumberOfQuestionsAnswered" = 0)))
SELECT s."SubmissionId",
s."TimeSubmitted",
s."FormId",
s."FormCode",
s."FormType",
s."DefaultLanguage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record FormSubmissionEntry
public Guid SubmissionId { get; init; }
public DateTime TimeSubmitted { get; init; }

public Guid FormId { get; init; }
public string FormCode { get; init; } = null!;
public TranslatedString FormName { get; init; } = null!;

Expand Down
14 changes: 12 additions & 2 deletions api/src/Feature.Forms/Get/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
using Feature.Forms.Specifications;
using Microsoft.AspNetCore.Authorization;
using Vote.Monitor.Domain.Entities.CoalitionAggregate;
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;
using GetCoalitionFormSpecification = Feature.Forms.Specifications.GetCoalitionFormSpecification;

namespace Feature.Forms.Get;

public class Endpoint(
IAuthorizationService authorizationService,
IReadRepository<FormAggregate> formRepository,
IReadRepository<Coalition> coalitionRepository) : Endpoint<Request, Results<Ok<FormFullModel>, NotFound>>
IReadRepository<Coalition> coalitionRepository,
IReadRepository<PollingStationInformationForm> psiFormRepository) : Endpoint<Request, Results<Ok<FormFullModel>, NotFound>>
{
public override void Configure()
{
Expand All @@ -27,12 +29,20 @@ public override async Task<Results<Ok<FormFullModel>, NotFound>> ExecuteAsync(Re
{
return TypedResults.NotFound();
}

var psiFormSpecification =
new GetPsiFormById(req.ElectionRoundId, req.Id);
var coalitionFormSpecification =
new GetCoalitionFormSpecification(req.ElectionRoundId, req.NgoId, req.Id);
var ngoFormSpecification =
new GetFormByIdSpecification(req.ElectionRoundId, req.NgoId, req.Id);

var psiForm = await psiFormRepository.FirstOrDefaultAsync(psiFormSpecification, ct);

if (psiForm is not null)
{
return TypedResults.Ok(FormFullModel.FromEntity(psiForm));
}

var form = (await coalitionRepository.FirstOrDefaultAsync(coalitionFormSpecification, ct)) ??
(await formRepository.FirstOrDefaultAsync(ngoFormSpecification, ct));

Expand Down
19 changes: 19 additions & 0 deletions api/src/Feature.Forms/Models/FormFullModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Vote.Monitor.Domain.Entities.FormBase;
using Module.Forms.Mappers;
using Module.Forms.Models;
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;

namespace Feature.Forms.Models;

Expand Down Expand Up @@ -48,4 +49,22 @@ public static FormFullModel FromEntity(FormAggregate form) => form == null
Icon = form.Icon,
DisplayOrder = form.DisplayOrder
};

public static FormFullModel FromEntity(PollingStationInformationForm form) => form == null
? null
: new FormFullModel
{
Id = form.Id,
Code = form.Code,
FormType = form.FormType,
Status = form.Status,
DefaultLanguage = form.DefaultLanguage,
Languages = form.Languages,
Name = form.Name,
Questions = form.Questions.Select(QuestionsMapper.ToModel).ToList(),
NumberOfQuestions = form.NumberOfQuestions,
Description = form.Description,
LanguagesTranslationStatus = form.LanguagesTranslationStatus,
Icon = form.Icon,
};
}
11 changes: 11 additions & 0 deletions api/src/Feature.Forms/Specifications/GetPsiFormById.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;

namespace Feature.Forms.Specifications;

public sealed class GetPsiFormById : SingleResultSpecification<PollingStationInformationForm>
{
public GetPsiFormById(Guid electionRoundId, Guid id)
{
Query.Where(x => x.ElectionRoundId == electionRoundId && x.Id == id);
}
}
1 change: 1 addition & 0 deletions api/src/Module.Answers/Models/FormSubmissionView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public record FormSubmissionView
{
public Guid SubmissionId { get; init; }
public DateTime TimeSubmitted { get; init; }
public Guid FormId { get; init; }
public string FormCode { get; init; }
public string DefaultLanguage { get; init; }
public string[] Languages { get; init; } = [];
Expand Down
26 changes: 25 additions & 1 deletion api/tests/Feature.Forms.UnitTests/Endpoints/GetEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using NSubstitute.ReturnsExtensions;
using Vote.Monitor.Domain.Entities.CoalitionAggregate;
using Vote.Monitor.Domain.Entities.FormAggregate;
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;

namespace Feature.Forms.UnitTests.Endpoints;

Expand All @@ -12,11 +13,12 @@ public class GetEndpointTests
private readonly IAuthorizationService _authorizationService = Substitute.For<IAuthorizationService>();
private readonly IReadRepository<Form> _repository = Substitute.For<IReadRepository<Form>>();
private readonly IReadRepository<Coalition> _coalitionRepository = Substitute.For<IReadRepository<Coalition>>();
private readonly IReadRepository<PollingStationInformationForm> _psiFormRepository = Substitute.For<IReadRepository<PollingStationInformationForm>>();
private readonly Get.Endpoint _endpoint;

public GetEndpointTests()
{
_endpoint = Factory.Create<Get.Endpoint>(_authorizationService, _repository, _coalitionRepository);
_endpoint = Factory.Create<Get.Endpoint>(_authorizationService, _repository, _coalitionRepository, _psiFormRepository);
_authorizationService
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<object>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());
Expand All @@ -42,6 +44,28 @@ public async Task ShouldReturnNotFound_WhenUserIsNotAuthorized()
.Which
.Result.Should().BeOfType<NotFound>();
}

[Fact]
public async Task Should_ReturnPSIForm_WhenFormExists()
{
// Arrange
var psiForm = new PollingStationInformationFormFaker().Generate();

_psiFormRepository
.FirstOrDefaultAsync(Arg.Any<GetPsiFormById>())
.Returns(psiForm);

// Act
var request = new Get.Request { Id = psiForm.Id };
var result = await _endpoint.ExecuteAsync(request, CancellationToken.None);

// Assert
result
.Should().BeOfType<Results<Ok<FormFullModel>, NotFound>>()
.Which
.Result.Should().BeOfType<Ok<FormFullModel>>()
.Which.Value.Should().BeEquivalentTo(psiForm, options => options.ExcludingMissingMembers());
}

[Fact]
public async Task Should_ReturnForm_WhenFormExists()
Expand Down
Loading