Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v2010.10.17.1 -> Main #282

Merged
merged 12 commits into from
Oct 15, 2024
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![.NET Core Unit Tests](https://github.com/ministryofjustice/CFO-CaseAssessmentTrackingSystem/actions/workflows/unittest.yml/badge.svg?branch=develop)](https://github.com/ministryofjustice/CFO-CaseAssessmentTrackingSystem/actions/workflows/unittest.yml)

# Overview

HMPPS Creating Future Opportunities (CFO) utilise the Case Assessment and Tracking System (CATS) to support delivery of https://www.CreatingFutureOpportunities.gov.uk (CFO Evolution). The programme utilises external funding to perform rehabilitative services with offenders in custody and the community. Approx. 600 users from non-government organisations use CATS to record work performed with offenders creating an evidence base that supports performance management, payments to providers, ongoing research and audits from external bodies.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Cfo.Cats.Application.Features.Identity.Notifications.IdentityEvents;

public class IdentityAuditNotificationHandler(IUnitOfWork unitOfWork) : INotificationHandler<IdentityAuditNotification>
{
public async Task Handle(IdentityAuditNotification notification, CancellationToken cancellationToken)
{
IdentityAuditTrail audit = IdentityAuditTrail.Create(notification.UserName, notification.PerformedBy, notification.ActionType, notification.IpAddress);
unitOfWork.DbContext.IdentityAuditTrails.Add(audit);
await unitOfWork.DbContext.IdentityAuditTrails.AddAsync(audit);
await unitOfWork.SaveChangesAsync(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Cfo.Cats.Application.Features.Identity.Notifications.IdentityEvents;

public class UpdateLastUpdatedStampNotificationHandler(IUnitOfWork unitOfWork) : INotificationHandler<IdentityAuditNotification>
{
public async Task Handle(IdentityAuditNotification notification, CancellationToken cancellationToken)
{
IdentityActionType[] types = [ IdentityActionType.LoginPasswordOnly, IdentityActionType.LoginWithTwoFactorCode ];

if(types.Contains(notification.ActionType))
{
await unitOfWork.DbContext.Users
.Where(u => u.UserName == notification.UserName)
.ExecuteUpdateAsync(e => e.SetProperty(u => u.LastLogin, DateTime.UtcNow));
await unitOfWork.SaveChangesAsync(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.SecurityConstants;

namespace Cfo.Cats.Application.Features.Participants.Commands;

public static class ChangeEnrolmentLocation
{
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
public class Command : IRequest<Result>
{
public string? ParticipantId { get; set; }
public int? NewLocationId { get; set; }

public string? JustificationReason { get; set; }

public UserProfile? CurrentUser { get; set; }
}

public class Handler(IUnitOfWork unitOfWork) : IRequestHandler<Command, Result>
{
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
var participant = await unitOfWork.DbContext
.Participants
.FirstAsync(x => x.Id == request.ParticipantId, cancellationToken);

participant.SetEnrolmentLocation(request.NewLocationId.GetValueOrDefault(), request.JustificationReason!);

return Result.Success();

}
}

public class Validator : AbstractValidator<Command>
{
private readonly IUnitOfWork _unitOfWork;

public Validator(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;

RuleFor(c => c.ParticipantId)
.NotNull()
.MaximumLength(9)
.MinimumLength(9)
.Matches(ValidationConstants.AlphaNumeric)
.WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id"));

RuleFor(c => c.NewLocationId)
.NotNull()
.WithMessage("New location must be provided")
.GreaterThan(0);

RuleFor(c => c.CurrentUser)
.NotNull();

RuleFor(c => c.ParticipantId!)
.MustAsync(Exist);

RuleFor(c => c.ParticipantId!)
.MustAsync(BeChangeable);

RuleFor(c => c.JustificationReason)
.NotNull()
.NotEmpty()
.MaximumLength(ValidationConstants.NotesLength)
.Matches(ValidationConstants.Notes)
.WithMessage(string.Format(ValidationConstants.NotesMessage, "Justification Reason"));
}

private async Task<bool> Exist(string participantId, CancellationToken cancellationToken)
{
var participant = await _unitOfWork.DbContext
.Participants
.IgnoreAutoIncludes()
.FirstOrDefaultAsync(x => x.Id == participantId, cancellationToken);

return participant != null;
}

private async Task<bool> BeChangeable(string participantId, CancellationToken cancellationToken)
{
var participant = await _unitOfWork.DbContext
.Participants
.IgnoreAutoIncludes()
.FirstAsync(x => x.Id == participantId, cancellationToken);

return participant.EnrolmentStatus!.AllowEnrolmentLocationChange();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ public Validator(IUnitOfWork unitOfWork)
.WithMessage("Comments are mandatory with this referral source")
.Matches(ValidationConstants.Notes).WithMessage(string.Format(ValidationConstants.NotesMessage, "Referral source comments"));
});


RuleFor(x => x.ReferralComments)
.MaximumLength(1000)
.WithMessage("Referral Comments must be less than 1000 characters");
}

private async Task<bool> NotAlreadyExist(string identifier, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.Features.Locations.DTOs;
using Cfo.Cats.Application.Features.Participants.Caching;
using Cfo.Cats.Application.SecurityConstants;
using Cfo.Cats.Domain.Entities.Participants;

namespace Cfo.Cats.Application.Features.Participants.Commands;

Expand Down Expand Up @@ -84,9 +84,13 @@ public Validator()
.When(x => x.AlternativeLocation is not null)
.WithMessage("You must provide a different alternative location to the current location");

RuleFor(x => x.JustificationReason)
RuleFor(c => c.JustificationReason)
.NotNull()
.NotEmpty()
.WithMessage("Justification reason is mandatory when enrolling in a different location");
.WithMessage("Justification reason is mandatory when enrolling in a different location")
.MaximumLength(ValidationConstants.NotesLength)
.Matches(ValidationConstants.Notes)
.WithMessage(string.Format(ValidationConstants.NotesMessage, "Justification Reason"));
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class ParticipantSummaryDto
public DateTime? RiskDue { get; set; }
public int? RiskDueInDays { get; set; }
public string? Nationality { get; set; }

public string? EnrolmentLocationJustification { get; set; }

/// <summary>
/// The current enrolment status of the participant
/// </summary>
Expand Down Expand Up @@ -73,7 +76,8 @@ public Mapping()
.ForMember(target => target.ParticipantName, options => options.MapFrom(source => source.FirstName + ' ' + source.LastName))
.ForMember(dest => dest.RiskDue, opt => opt.MapFrom(src => src.RiskDue))
.ForMember(dest => dest.RiskDueInDays, opt => opt.MapFrom(src => src.RiskDueInDays()))
.ForMember(dest => dest.Nationality, opt => opt.MapFrom(src => src.Nationality));
.ForMember(dest => dest.Nationality, opt => opt.MapFrom(src => src.Nationality))
.ForMember(dest => dest.EnrolmentLocationJustification, opt => opt.MapFrom(src => src.EnrolmentLocationJustification));

CreateMap<ParticipantAssessment, AssessmentSummaryDto>()
.ForMember(target => target.AssessmentId, options => options.MapFrom(source => source.Id))
Expand Down
127 changes: 57 additions & 70 deletions src/Domain/Common/Enums/EnrolmentStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace Cfo.Cats.Domain.Common.Enums;

public abstract class EnrolmentStatus : SmartEnum<EnrolmentStatus>
{

public static readonly EnrolmentStatus IdentifiedStatus = new Identified();
public static readonly EnrolmentStatus EnrollingStatus = new Enrolling();
public static readonly EnrolmentStatus SubmittedToProviderStatus = new SubmittedToProvider();
Expand All @@ -15,106 +14,94 @@ public abstract class EnrolmentStatus : SmartEnum<EnrolmentStatus>


private EnrolmentStatus(string name, int value)
: base(name, value) { }
: base(name, value)
{
}

/// <summary>
/// Statuses that we can transition to.
/// Statuses that we can transition to.
/// </summary>
protected abstract EnrolmentStatus[] GetAllowedTransitions();

/// <summary>
/// Indicates the status of the enrolment is at a QA stage.
/// Indicates this status allows archiving.
/// </summary>
/// <returns></returns>
public virtual bool IsQaStage()
{
return false;
}
public bool AllowArchive() =>
CanTransitionTo(ArchivedStatus);

public virtual bool CanTransitionTo(EnrolmentStatus next)
{
return GetAllowedTransitions()
/// <summary>
/// Indicates the case can be made dormant
/// </summary>
public bool AllowSuspend() =>
CanTransitionTo(DormantStatus);

public bool AllowSubmitToPqa() =>
CanTransitionTo(SubmittedToProviderStatus);

public virtual bool AllowEnrolmentLocationChange()
=> false;

public bool CanTransitionTo(EnrolmentStatus next) =>
GetAllowedTransitions()
.Any(e => next == e);
}

private sealed class Identified : EnrolmentStatus
{
/// <summary>
/// Indicates that a participant at this enrolment stage is allowed to have a new assessment created
/// </summary>
/// <returns>True if the current status allows reassessment</returns>
public virtual bool SupportsReassessment() => true;

public Identified()
: base("Identified", 0) { }
/// <summary>
/// Indicates we can add right to work when the status is at this stage
/// </summary>
/// <returns>True if we allow addition of Right to Work documentation</returns>
public virtual bool AllowRightToWorkAddition() => false;

protected override EnrolmentStatus[] GetAllowedTransitions()
=> [ ArchivedStatus, EnrollingStatus];
private sealed class Identified() : EnrolmentStatus("Identified", 0)
{
protected override EnrolmentStatus[] GetAllowedTransitions() => [ArchivedStatus, EnrollingStatus];
}

private sealed class SubmittedToProvider : EnrolmentStatus
private sealed class SubmittedToProvider() : EnrolmentStatus("Submitted to Provider", 1)
{
public SubmittedToProvider()
: base("Submitted to Provider", 1) { }

protected override EnrolmentStatus[] GetAllowedTransitions()
=> [ArchivedStatus, EnrollingStatus, SubmittedToAuthorityStatus];

public override bool StatusSupportsReassessment() => false;
protected override EnrolmentStatus[] GetAllowedTransitions() => [ArchivedStatus, EnrollingStatus, SubmittedToAuthorityStatus];

public override bool IsQaStage() => true;
public override bool SupportsReassessment() => false;

public override bool AllowEnrolmentLocationChange() => true;
}

private sealed class SubmittedToAuthority : EnrolmentStatus
{
public SubmittedToAuthority()
: base("Submitted to Authority", 2) { }

public override bool StatusSupportsReassessment() => false;

public override bool IsQaStage() => true;
private sealed class SubmittedToAuthority() : EnrolmentStatus("Submitted to Authority", 2)
{
public override bool SupportsReassessment() => false;

protected override EnrolmentStatus[] GetAllowedTransitions() =>
[ SubmittedToProviderStatus, ApprovedStatus ];
protected override EnrolmentStatus[] GetAllowedTransitions() => [SubmittedToProviderStatus, ApprovedStatus];
}

private sealed class Approved : EnrolmentStatus
{
public Approved()
: base("Approved", 3) { }

protected override EnrolmentStatus[] GetAllowedTransitions() =>
[ArchivedStatus, DormantStatus];
private sealed class Approved() : EnrolmentStatus("Approved", 3)
{
protected override EnrolmentStatus[] GetAllowedTransitions() => [ArchivedStatus, DormantStatus];

public override bool AllowRightToWorkAddition() => true;
}

private sealed class Archived : EnrolmentStatus
{
public Archived()
: base("Archived", 4) { }

protected override EnrolmentStatus[] GetAllowedTransitions() =>
[IdentifiedStatus];

private sealed class Archived() : EnrolmentStatus("Archived", 4)
{
protected override EnrolmentStatus[] GetAllowedTransitions() => [IdentifiedStatus];
}

private sealed class Dormant : EnrolmentStatus
private sealed class Dormant() : EnrolmentStatus("Dormant", 5)
{
public Dormant()
: base("Dormant", 5) { }

protected override EnrolmentStatus[] GetAllowedTransitions() =>
[ ArchivedStatus, ApprovedStatus ];
protected override EnrolmentStatus[] GetAllowedTransitions() => [ArchivedStatus, ApprovedStatus];
}

private sealed class Enrolling : EnrolmentStatus
private sealed class Enrolling() : EnrolmentStatus("Enrolling", 6)
{
public Enrolling()
: base("Enrolling", 6) { }
protected override EnrolmentStatus[] GetAllowedTransitions() => [ArchivedStatus, SubmittedToProviderStatus];

protected override EnrolmentStatus[] GetAllowedTransitions()
=> [ArchivedStatus, SubmittedToProviderStatus];
public override bool AllowRightToWorkAddition() => true;
public override bool AllowEnrolmentLocationChange() => true;
}

/// <summary>
/// Indicates that a participant at this enrolment stage is allowed to have a new assessment created
/// </summary>
public virtual bool StatusSupportsReassessment() => true;
}


}
Loading
Loading