diff --git a/src/Application/Features/Assessments/Commands/BeginAssessment.cs b/src/Application/Features/Assessments/Commands/BeginAssessment.cs index 8fbb8213..670359da 100644 --- a/src/Application/Features/Assessments/Commands/BeginAssessment.cs +++ b/src/Application/Features/Assessments/Commands/BeginAssessment.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Cfo.Cats.Application.Common.Security; +using Cfo.Cats.Application.Common.Validators; using Cfo.Cats.Application.Features.Assessments.Caching; using Cfo.Cats.Application.Features.Assessments.DTOs; using Cfo.Cats.Application.Features.Assessments.DTOs.V1.Pathways.Education; @@ -78,12 +79,30 @@ public async Task> Handle(Command request, CancellationToken cancel public class Validator : AbstractValidator { - public Validator() + private readonly IUnitOfWork _unitOfWork; + + public Validator(IUnitOfWork unitOfWork) { + _unitOfWork = unitOfWork; + RuleFor(c => c.ParticipantId) .MinimumLength(9) - .MaximumLength(9); + .MaximumLength(9) + .Matches(ValidationConstants.AlphaNumeric) + .WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id")); + + RuleFor(c => c.ParticipantId) + .MustAsync(Exist) + .WithMessage("Participant not found") + .MustAsync(HaveEnrolmentLocation) + .WithMessage("Participant must have an enrolment location"); } + + private async Task Exist(string participantId, CancellationToken cancellationToken) + => await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == participantId, cancellationToken); + + private async Task HaveEnrolmentLocation(string participantId, CancellationToken cancellationToken) + => await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == participantId && e.EnrolmentLocation != null, cancellationToken); } } diff --git a/src/Application/Features/Assessments/Commands/SaveAssessment.cs b/src/Application/Features/Assessments/Commands/SaveAssessment.cs index 95fc78ba..b347e28f 100644 --- a/src/Application/Features/Assessments/Commands/SaveAssessment.cs +++ b/src/Application/Features/Assessments/Commands/SaveAssessment.cs @@ -1,4 +1,5 @@ using Cfo.Cats.Application.Common.Security; +using Cfo.Cats.Application.Common.Validators; using Cfo.Cats.Application.Features.Assessments.Caching; using Cfo.Cats.Application.Features.Assessments.DTOs; using Cfo.Cats.Application.SecurityConstants; @@ -75,4 +76,33 @@ public async Task Handle(Command request, CancellationToken cancellation } } + public class Validator : AbstractValidator + { + private readonly IUnitOfWork _unitOfWork; + + public Validator(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + + RuleFor(c => c.Assessment.Id) + .MustAsync(Exist) + .WithMessage("Assessment not found"); + + RuleFor(c => c.Assessment.ParticipantId) + .MustAsync(Exist) + .WithMessage("Participant not found") + .MustAsync(HaveEnrolmentLocation) + .WithMessage("Participant must have an enrolment location"); + } + + private async Task Exist(Guid assessmentId, CancellationToken cancellationToken) + => await _unitOfWork.DbContext.ParticipantAssessments.AnyAsync(e => e.Id == assessmentId, cancellationToken); + + private async Task Exist(string participantId, CancellationToken cancellationToken) + => await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == participantId, cancellationToken); + + private async Task HaveEnrolmentLocation(string participantId, CancellationToken cancellationToken) + => await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == participantId && e.EnrolmentLocation != null, cancellationToken); + + } } diff --git a/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryValidator.cs b/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryValidator.cs index b37e9d96..618acd33 100644 --- a/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryValidator.cs +++ b/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryValidator.cs @@ -28,10 +28,11 @@ public CandidateSearchQueryValidator() RuleFor(q => q.ExternalIdentifier) .NotEmpty() - .NotNull() .WithMessage("External identifier is required") .Matches(ValidationConstants.AlphaNumeric) - .WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "External Identifier")); + .WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "External Identifier")) + .Length(7) // CRN and NOMIS Number (both 7 chars) + .WithMessage("Unrecognised format for External Identifier"); } } \ No newline at end of file diff --git a/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs b/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs index 44d86597..b8d009cd 100644 --- a/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs +++ b/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs @@ -78,6 +78,7 @@ public Mapping() .ForMember(x => x.SuperiorName, s => s.MapFrom(y => y.Superior!.UserName)) .ForMember(x => x.TenantName, s => s.MapFrom(y => y.Tenant!.Name)) .ForMember(x => x.AssignedRoles, s => s.MapFrom(y => y.UserRoles.Select(r => r.Role.Name))) + .ForMember(x => x.PhoneNumber, s => s.MapFrom(x => x.PhoneNumber)) .ReverseMap() .ForMember(x => x.UserName, s => s.MapFrom(y => y.Email)) .ForMember(x => x.Notes, s => s.Ignore()) diff --git a/src/Application/Features/QualityAssurance/Commands/SubmitPqaResponse.cs b/src/Application/Features/QualityAssurance/Commands/SubmitPqaResponse.cs index fe3d3bc4..14f606a8 100644 --- a/src/Application/Features/QualityAssurance/Commands/SubmitPqaResponse.cs +++ b/src/Application/Features/QualityAssurance/Commands/SubmitPqaResponse.cs @@ -44,16 +44,14 @@ public A_IsValidRequest() .NotNull() .WithMessage("You must accept or return the request"); - When(x => x.Accept == false, () => { + When(x => x.Accept is false, () => + { RuleFor(x => x.Message) - .NotNull() - .WithMessage("A message is required when returning") .NotEmpty() .WithMessage("A message is required when returning") .Matches(ValidationConstants.Notes) .WithMessage(string.Format(ValidationConstants.NotesMessage, "Message")); }); - } } diff --git a/src/Application/Features/QualityAssurance/Commands/SubmitQa1Response.cs b/src/Application/Features/QualityAssurance/Commands/SubmitQa1Response.cs index 93f1a31b..00e68891 100644 --- a/src/Application/Features/QualityAssurance/Commands/SubmitQa1Response.cs +++ b/src/Application/Features/QualityAssurance/Commands/SubmitQa1Response.cs @@ -44,10 +44,8 @@ public A_IsValidRequest() .NotNull() .WithMessage("You must accept or return the request"); - When(x => x.Accept == false, () => { + When(x => x.Accept is false, () => { RuleFor(x => x.Message) - .NotNull() - .WithMessage("A message is required when returning") .NotEmpty() .WithMessage("A message is required when returning") .Matches(ValidationConstants.Notes) diff --git a/src/Application/Features/QualityAssurance/Commands/SubmitQa2Response.cs b/src/Application/Features/QualityAssurance/Commands/SubmitQa2Response.cs index fdc408e8..1994f087 100644 --- a/src/Application/Features/QualityAssurance/Commands/SubmitQa2Response.cs +++ b/src/Application/Features/QualityAssurance/Commands/SubmitQa2Response.cs @@ -44,10 +44,8 @@ public A_IsValidRequest() .NotNull() .WithMessage("You must accept or return the request"); - When(x => x.Accept == false, () => { + When(x => x.Accept is false, () => { RuleFor(x => x.Message) - .NotNull() - .WithMessage("A message is required when returning") .NotEmpty() .WithMessage("A message is required when returning") .Matches(ValidationConstants.Notes) diff --git a/src/Application/Features/QualityAssurance/Commands/SubmitToProviderQa.cs b/src/Application/Features/QualityAssurance/Commands/SubmitToProviderQa.cs index 64b464dc..4315b677 100644 --- a/src/Application/Features/QualityAssurance/Commands/SubmitToProviderQa.cs +++ b/src/Application/Features/QualityAssurance/Commands/SubmitToProviderQa.cs @@ -10,7 +10,6 @@ public static class SubmitToProviderQa public class Command : IRequest { public required string ParticipantId { get; set; } - } public class Handler(IUnitOfWork unitOfWork) : IRequestHandler @@ -36,9 +35,9 @@ public A_ParticipantMustExistValidator(IUnitOfWork unitOfWork) .MinimumLength(9) .MaximumLength(9) .WithMessage("Invalid Participant Id") + .Matches(ValidationConstants.AlphaNumeric).WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id")) .MustAsync(MustExist) - .WithMessage("Participant does not exist") - .Matches(ValidationConstants.AlphaNumeric).WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id")); + .WithMessage("Participant does not exist"); } private async Task MustExist(string identifier, CancellationToken cancellationToken) => await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == identifier, cancellationToken); @@ -80,6 +79,7 @@ private async Task MustBeScored(string identifier, CancellationToken cance { var assessments = await _unitOfWork.DbContext.ParticipantAssessments .Include(pa => pa.Scores) + .Where(pa => pa.ParticipantId == identifier) .ToArrayAsync(cancellationToken); var latest = assessments.MaxBy(a => a.Created); @@ -106,13 +106,13 @@ private async Task MustHaveTwoReds(string identifier, CancellationToken ca { var assessments = await _unitOfWork.DbContext.ParticipantAssessments .Include(pa => pa.Scores) + .Where(pa=>pa.ParticipantId == identifier) .ToArrayAsync(cancellationToken); var latest = assessments.MaxBy(a => a.Created); return latest is not null - && latest.Scores.Count(s => s.Score is > 0 and < 10) > 1; + && latest.Scores.Count(s => s.Score is >= 0 and < 10) > 1; } } - } diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor index c065fa34..b4646df8 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor @@ -36,11 +36,14 @@ Back to Search - Enrol Candidate - + diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs index 878abbfd..831c9e96 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs @@ -11,6 +11,8 @@ public partial class MatchFound [Inject] private IPicklistService PicklistService { get; set; } = default!; + private bool loading; + private MudForm? _form; private CreateParticipant.Command? Model; @@ -65,14 +67,25 @@ private Task BackToSearch() private async Task EnrolCandidate() { - await _form!.Validate().ConfigureAwait(false); - if (_form!.IsValid) + try { - var result = await GetNewMediator().Send(Model!); - if (result.Succeeded) + loading = true; + + await _form!.Validate().ConfigureAwait(false); + + if (_form!.IsValid) { - await OnParticipantEnrolled.InvokeAsync(); + var result = await GetNewMediator().Send(Model!); + if (result.Succeeded) + { + await OnParticipantEnrolled.InvokeAsync(); + } } + + } + finally + { + loading = false; } } diff --git a/src/Server.UI/Pages/Identity/Authentication/Login.razor b/src/Server.UI/Pages/Identity/Authentication/Login.razor index 4b32e9c7..44feb233 100644 --- a/src/Server.UI/Pages/Identity/Authentication/Login.razor +++ b/src/Server.UI/Pages/Identity/Authentication/Login.razor @@ -77,7 +77,7 @@
- +
diff --git a/src/Server.UI/Pages/Identity/Users/Users.razor b/src/Server.UI/Pages/Identity/Users/Users.razor index 2b4a8c14..edc0d51a 100644 --- a/src/Server.UI/Pages/Identity/Users/Users.razor +++ b/src/Server.UI/Pages/Identity/Users/Users.razor @@ -389,8 +389,8 @@ policies ??= new() { { SecurityPolicies.SystemFunctionsWrite, (await AuthService.AuthorizeAsync(state.User, SecurityPolicies.SystemFunctionsWrite)).Succeeded }, - { SecurityPolicies.Import, (await AuthService.AuthorizeAsync(state.User, SecurityPolicies.Import)).Succeeded }, - { SecurityPolicies.Export, (await AuthService.AuthorizeAsync(state.User, SecurityPolicies.Export)).Succeeded } + { SecurityPolicies.Import, false /*(await AuthService.AuthorizeAsync(state.User, SecurityPolicies.Import)).Succeeded */ }, + { SecurityPolicies.Export, false /* (await AuthService.AuthorizeAsync(state.User, SecurityPolicies.Export)).Succeeded */ } }; _canCreate = policies.GetValueOrDefault(SecurityPolicies.SystemFunctionsWrite); @@ -533,6 +533,8 @@ var applicationUser = Mapper.Map(model); applicationUser.EmailConfirmed = true; applicationUser.IsActive = true; + applicationUser.TwoFactorEnabled = true; + applicationUser.PhoneNumberConfirmed = string.IsNullOrWhiteSpace(applicationUser.PhoneNumber) == false; var identityResult = await UserManager.CreateAsync(applicationUser); if (!identityResult.Succeeded) @@ -556,6 +558,8 @@ Mapper.Map(model, user); + user.PhoneNumberConfirmed = string.IsNullOrWhiteSpace(user.PhoneNumber) == false; + var identityResult = await UserManager.UpdateAsync(user); if (identityResult.Succeeded) { diff --git a/src/Server.UI/Pages/Participants/Components/CaseSummary.razor.cs b/src/Server.UI/Pages/Participants/Components/CaseSummary.razor.cs index c717d0d7..af88d8eb 100644 --- a/src/Server.UI/Pages/Participants/Components/CaseSummary.razor.cs +++ b/src/Server.UI/Pages/Participants/Components/CaseSummary.razor.cs @@ -36,12 +36,17 @@ public async Task BeginAssessment() { ParticipantId = ParticipantSummaryDto.Id }; + var result = await GetNewMediator().Send(command); if (result.Succeeded) { Navigation.NavigateTo($"/pages/participants/{ParticipantSummaryDto.Id}/assessment/{result.Data}"); } + else + { + Snackbar.Add(result.ErrorMessage, Severity.Error); + } } public void ContinueAssessment() diff --git a/src/Server.UI/Pages/QA/Enrolments/PQA.razor b/src/Server.UI/Pages/QA/Enrolments/PQA.razor index 8092cdf9..8eab774b 100644 --- a/src/Server.UI/Pages/QA/Enrolments/PQA.razor +++ b/src/Server.UI/Pages/QA/Enrolments/PQA.razor @@ -87,7 +87,7 @@ { - + Accept @@ -97,7 +97,7 @@ - + Submit diff --git a/src/Server.UI/Pages/QA/Enrolments/QA1.razor b/src/Server.UI/Pages/QA/Enrolments/QA1.razor index 19f3effe..42314285 100644 --- a/src/Server.UI/Pages/QA/Enrolments/QA1.razor +++ b/src/Server.UI/Pages/QA/Enrolments/QA1.razor @@ -87,7 +87,7 @@ { - + Accept @@ -97,7 +97,7 @@ - + Submit diff --git a/src/Server.UI/Pages/QA/Enrolments/QA2.razor b/src/Server.UI/Pages/QA/Enrolments/QA2.razor index f885f502..0b2b43e7 100644 --- a/src/Server.UI/Pages/QA/Enrolments/QA2.razor +++ b/src/Server.UI/Pages/QA/Enrolments/QA2.razor @@ -96,8 +96,8 @@ - - + + Submit