Skip to content

Commit

Permalink
My Opportunity Verification Improvements (#1209)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianwium authored Jan 23, 2025
1 parent 5f9d327 commit 06ec273
Show file tree
Hide file tree
Showing 15 changed files with 3,206 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ public static long ConvertToMinutes(string intervalName, int count)
return minutes;
}

public static (TimeIntervalOption Interval, short Count) ConvertToCommitmentInterval(DateTimeOffset dateStart, DateTimeOffset dateEnd)
{
if (dateEnd < dateStart)
throw new ArgumentOutOfRangeException(nameof(dateEnd), "End date is earlier than the start date");

var totalMinutes = (dateEnd - dateStart).TotalMinutes;

return totalMinutes switch
{
< 60 => (TimeIntervalOption.Minute, (short)Math.Round(totalMinutes)),
< 60 * 24 => (TimeIntervalOption.Hour, (short)Math.Round(totalMinutes / 60)),
< 60 * 24 * 7 => (TimeIntervalOption.Day, (short)Math.Round(totalMinutes / (60 * 24))),
< 60 * 24 * 30 => (TimeIntervalOption.Week, (short)Math.Round(totalMinutes / (60 * 24 * 7))),
_ => (TimeIntervalOption.Month, (short)Math.Round(totalMinutes / (60 * 24 * 30))),
};
}

public static int GetOrder(string intervalAsString)
{
if (Enum.TryParse<TimeIntervalOption>(intervalAsString, out var interval))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ public static MyOpportunityInfo ToInfo(this Models.MyOpportunity value)
VerificationStatusId = value.VerificationStatusId,
VerificationStatus = value.VerificationStatus,
CommentVerification = value.CommentVerification,
CommitmentInterval = value.CommitmentInterval,
CommitmentIntervalCount = value.CommitmentIntervalCount,
DateStart = value.DateStart,
DateEnd = value.DateEnd,
DateCompleted = value.DateCompleted,
ZltoReward = value.ZltoReward,
YomaReward = value.YomaReward,
Recommendable = value.Recommendable,
StarRating = value.StarRating,
Feedback = value.Feedback,
DateModified = value.DateModified,
Verifications = value.Verifications?.Select(o =>
new MyOpportunityInfoVerification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ public class MyOpportunity

public string? CommentVerification { get; set; }

public Guid? CommitmentIntervalId { get; set; }

public Core.TimeIntervalOption? CommitmentInterval { get; set; }

public short? CommitmentIntervalCount { get; set; }

public DateTimeOffset? DateStart { get; set; }

public DateTimeOffset? DateEnd { get; set; }
Expand All @@ -110,6 +116,12 @@ public class MyOpportunity

public decimal? YomaReward { get; set; }

public bool? Recommendable { get; set; }

public byte? StarRating { get; set; }

public string? Feedback { get; set; }

public DateTimeOffset DateCreated { get; set; }

public DateTimeOffset DateModified { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ public class MyOpportunityInfo
[Name("Comment")]
public string? CommentVerification { get; set; }

[Name("Completion Interval")]
public Core.TimeIntervalOption? CommitmentInterval { get; set; }

[Name("Completion Interval Count")]
public short? CommitmentIntervalCount { get; set; }

[Name("Date Start")]
public DateTimeOffset? DateStart { get; set; }

Expand All @@ -108,6 +114,15 @@ public class MyOpportunityInfo
[Ignore] //reserved for future use
public decimal? YomaReward { get; set; }

[Ignore]
public bool? Recommendable { get; set; }

[Ignore]
public byte? StarRating { get; set; }

[Name("Feedback")]
public string? Feedback { get; set; }

[Name("Date Connected")]
public DateTimeOffset DateModified { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public class MyOpportunityRequestVerify

public DateTimeOffset? DateEnd { get; set; }

public MyOpportunityRequestVerifyCommitmentInterval? CommitmentInterval { get; set; }

public bool? Recommendable { get; set; }

public byte? StarRating { get; set; }

public string? Feedback { get; set; }

[JsonIgnore]
internal bool OverridePending { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Newtonsoft.Json;
using Yoma.Core.Domain.Core;

namespace Yoma.Core.Domain.MyOpportunity.Models
{
public class MyOpportunityRequestVerifyCommitmentInterval
{
public Guid Id { get; set; }

public short Count { get; set; }

[JsonIgnore]
internal TimeIntervalOption? Option { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
using System.Globalization;
using System.Reflection;
using Yoma.Core.Domain.Lookups.Interfaces;
using Yoma.Core.Domain.Lookups.Helpers;
using Yoma.Core.Domain.Lookups.Models;

namespace Yoma.Core.Domain.MyOpportunity.Services
{
Expand All @@ -60,6 +62,7 @@ public class MyOpportunityService : IMyOpportunityService
private readonly INotificationDeliveryService _notificationDeliveryService;
private readonly ICountryService _countryService;
private readonly IGenderService _genderService;
private readonly ITimeIntervalService _timeIntervalService;
private readonly MyOpportunitySearchFilterValidator _myOpportunitySearchFilterValidator;
private readonly MyOpportunityRequestValidatorVerify _myOpportunityRequestValidatorVerify;
private readonly MyOpportunityRequestValidatorVerifyFinalize _myOpportunityRequestValidatorVerifyFinalize;
Expand Down Expand Up @@ -94,6 +97,7 @@ public MyOpportunityService(ILogger<MyOpportunityService> logger,
INotificationDeliveryService notificationDeliveryService,
ICountryService countryService,
IGenderService genderService,
ITimeIntervalService timeIntervalService,
MyOpportunitySearchFilterValidator myOpportunitySearchFilterValidator,
MyOpportunityRequestValidatorVerify myOpportunityRequestValidatorVerify,
MyOpportunityRequestValidatorVerifyFinalize myOpportunityRequestValidatorVerifyFinalize,
Expand Down Expand Up @@ -121,6 +125,7 @@ public MyOpportunityService(ILogger<MyOpportunityService> logger,
_notificationDeliveryService = notificationDeliveryService;
_countryService = countryService;
_genderService = genderService;
_timeIntervalService = timeIntervalService;
_myOpportunitySearchFilterValidator = myOpportunitySearchFilterValidator;
_myOpportunityRequestValidatorVerify = myOpportunityRequestValidatorVerify;
_myOpportunityRequestValidatorVerifyFinalize = myOpportunityRequestValidatorVerifyFinalize;
Expand Down Expand Up @@ -1261,22 +1266,70 @@ private static string PerformActionNotPossibleValidationMessage(Opportunity.Mode
return $"Opportunity '{opportunity.Title}' {description}, because {reasonText}. Please check these conditions and try again";
}

private void PerformActionSendForVerificationParseCommitment(MyOpportunityRequestVerify request, Opportunity.Models.Opportunity opportunity)
{
if (request.DateStart.HasValue) request.DateStart = request.DateStart.RemoveTime();
if (request.DateEnd.HasValue) request.DateEnd = request.DateEnd.ToEndOfDay();

if (request.DateStart.HasValue && request.DateStart.Value < opportunity.DateStart)
throw new ValidationException($"Start date can not be earlier than the opportunity start date of '{opportunity.DateStart:yyyy-MM-dd}'");

if (request.DateEnd.HasValue)
{
if (opportunity.DateEnd.HasValue && request.DateEnd.Value > opportunity.DateEnd.Value)
throw new ValidationException($"End date cannot be later than the opportunity end date of '{opportunity.DateEnd.Value:yyyy-MM-dd}'");

if (request.DateEnd.Value > DateTimeOffset.UtcNow.ToEndOfDay())
throw new ValidationException($"End date cannot be in the future. Please select today's date or earlier");
}

TimeInterval? timeInterval;

//validation should ensure either the start date or commitment interval is provided; failsafe ensuring commitement takes preference
if (request.CommitmentInterval != null)
{
if (!request.DateEnd.HasValue) return;

timeInterval = _timeIntervalService.GetById(request.CommitmentInterval.Id);

request.CommitmentInterval.Option = Enum.Parse<TimeIntervalOption>(timeInterval.Name, true);

var totalMinutes = TimeIntervalHelper.ConvertToMinutes(timeInterval.Name, request.CommitmentInterval.Count);
request.DateStart = request.DateEnd.Value.AddMinutes(-totalMinutes).RemoveTime();

return;
}

//validation should ensure both start (provided no commitment was specified) and end dates are provided; failsafe by not setting commitement if not provided
if (!request.DateStart.HasValue || !request.DateEnd.HasValue) return;

var (Interval, Count) = TimeIntervalHelper.ConvertToCommitmentInterval(request.DateStart.Value, request.DateEnd.Value);

timeInterval = _timeIntervalService.GetByName(Interval.ToString());

request.CommitmentInterval = new MyOpportunityRequestVerifyCommitmentInterval
{
Id = timeInterval.Id,
Count = Count,
Option = Enum.Parse<TimeIntervalOption>(timeInterval.Name, true)
};
}

private async Task PerformActionSendForVerification(User user, Guid opportunityId, MyOpportunityRequestVerify request, VerificationMethod? requiredVerificationMethod)
{
ArgumentNullException.ThrowIfNull(request, nameof(request));

await _myOpportunityRequestValidatorVerify.ValidateAndThrowAsync(request);

if (request.DateStart.HasValue) request.DateStart = request.DateStart.RemoveTime();
if (request.DateEnd.HasValue) request.DateEnd = request.DateEnd.ToEndOfDay();

//provided opportunity is published (and started) or expired
var opportunity = _opportunityService.GetById(opportunityId, true, true, false);
var canSendForVerification = opportunity.Status == Status.Expired;
if (!canSendForVerification) canSendForVerification = opportunity.Published && opportunity.DateStart <= DateTimeOffset.UtcNow;
if (!canSendForVerification)
throw new ValidationException(PerformActionNotPossibleValidationMessage(opportunity, "cannot be sent for verification"));

PerformActionSendForVerificationParseCommitment(request, opportunity);

if (!opportunity.VerificationEnabled)
throw new ValidationException($"Opportunity '{opportunity.Title}' can not be completed / verification is not enabled");

Expand All @@ -1289,18 +1342,6 @@ private async Task PerformActionSendForVerification(User user, Guid opportunityI
if (requiredVerificationMethod.HasValue && opportunity.VerificationMethod != requiredVerificationMethod.Value)
throw new ValidationException($"Opportunity '{opportunity.Title}' cannot be completed / requires verification method {requiredVerificationMethod}");

if (request.DateStart.HasValue && request.DateStart.Value < opportunity.DateStart)
throw new ValidationException($"Start date can not be earlier than the opportunity start date of '{opportunity.DateStart:yyyy-MM-dd}'");

if (request.DateEnd.HasValue)
{
if (opportunity.DateEnd.HasValue && request.DateEnd.Value > opportunity.DateEnd.Value)
throw new ValidationException($"End date cannot be later than the opportunity end date of '{opportunity.DateEnd.Value:yyyy-MM-dd}'");

if (request.DateEnd.Value > DateTimeOffset.UtcNow.ToEndOfDay())
throw new ValidationException($"End date cannot be in the future. Please select today's date or earlier");
}

var actionVerificationId = _myOpportunityActionService.GetByName(Action.Verification.ToString()).Id;
var verificationStatusPendingId = _myOpportunityVerificationStatusService.GetByName(VerificationStatus.Pending.ToString()).Id;

Expand Down Expand Up @@ -1337,8 +1378,14 @@ private async Task PerformActionSendForVerification(User user, Guid opportunityI
}

myOpportunity.VerificationStatusId = verificationStatusPendingId;
myOpportunity.CommitmentIntervalId = request.CommitmentInterval?.Id;
myOpportunity.CommitmentIntervalCount = request.CommitmentInterval?.Count;
myOpportunity.CommitmentInterval = request.CommitmentInterval?.Option;
myOpportunity.DateStart = request.DateStart;
myOpportunity.DateEnd = request.DateEnd;
myOpportunity.Recommendable = request.Recommendable;
myOpportunity.StarRating = request.StarRating;
myOpportunity.Feedback = request.Feedback;

await PerformActionSendForVerificationProcessVerificationTypes(request, opportunity, myOpportunity, isNew);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
using FluentValidation;
using Yoma.Core.Domain.Lookups.Interfaces;
using Yoma.Core.Domain.MyOpportunity.Models;

namespace Yoma.Core.Domain.MyOpportunity.Validators
{
public class MyOpportunityRequestValidatorVerify : AbstractValidator<MyOpportunityRequestVerify>
{
#region Class Variables
private readonly ITimeIntervalService _timeIntervalService;
#endregion

#region Constructor
public MyOpportunityRequestValidatorVerify()
public MyOpportunityRequestValidatorVerify(ITimeIntervalService timeIntervalService)
{
_timeIntervalService = timeIntervalService;

RuleFor(x => x.Certificate).Must(file => file == null || file.Length > 0).WithMessage("{PropertyName} is optional, but if specified, cannot be empty.");
RuleFor(x => x.VoiceNote).Must(file => file == null || file.Length > 0).WithMessage("{PropertyName} is optional, but if specified, cannot be empty.");
RuleFor(x => x.Picture).Must(file => file == null || file.Length > 0).WithMessage("{PropertyName} is optional, but if specified, cannot be empty.");
Expand All @@ -19,12 +26,56 @@ public MyOpportunityRequestValidatorVerify()
.Must(x => x == null || (x.Coordinates != null && x.Coordinates.All(coordinate => coordinate.Length >= 3)))
.WithMessage("3 or more coordinate points expected per coordinate set i.e. Point: X-coordinate (longitude -180 to +180), Y-coordinate (latitude -90 to +90), Z-elevation.")
.When(x => x.Geometry != null && x.Geometry.Type != Core.SpatialType.None);

//with instant-verifications start or end date not captured
RuleFor(x => x.DateStart).NotEmpty().When(x => !x.InstantOrImportedVerification).WithMessage("{PropertyName} is required.");
RuleFor(model => model.DateEnd)
.GreaterThanOrEqualTo(model => model.DateStart)
.When(model => model.DateEnd.HasValue)
.WithMessage("{PropertyName} is earlier than the Start Date.");
RuleFor(x => x)
.Must(x => x.InstantOrImportedVerification || (!x.DateStart.HasValue && x.CommitmentInterval != null) || (x.DateStart.HasValue && x.CommitmentInterval == null))
.WithMessage("Either Start date or commitment interval (time to complete) must be specified, but not both.");

RuleFor(x => x.DateStart)
.NotEmpty()
.When(x => x.CommitmentInterval == null && !x.InstantOrImportedVerification)
.WithMessage("Start date is required when the commitment interval (time to comnplete) is not specified.");

RuleFor(x => x.CommitmentInterval)
.NotNull()
.When(x => !x.DateStart.HasValue && !x.InstantOrImportedVerification)
.WithMessage("Commitment interval (time to complete) is required when start date is not specified.")
.DependentRules(() =>
{
RuleFor(x => x.CommitmentInterval!.Id)
.Must(id => id != Guid.Empty && CommitmentIntervalExists(id))
.WithMessage("Commitment interval is empty or does not exist.")
.When(x => x.CommitmentInterval != null);

RuleFor(x => x.CommitmentInterval!.Count)
.GreaterThanOrEqualTo((short)1)
.WithMessage("Commitment interval count must be greater than or equal to 1.")
.When(x => x.CommitmentInterval != null);
});

RuleFor(x => x.DateEnd)
.NotEmpty()
.When(x => !x.InstantOrImportedVerification)
.WithMessage("End date (when did you finish) is required")
.GreaterThanOrEqualTo(x => x.DateStart)
.When(x => x.DateStart.HasValue)
.WithMessage("End date (when did you finish) is earlier than the start date");

RuleFor(x => x.StarRating)
.InclusiveBetween((byte)1, (byte)5)
.When(x => x.StarRating.HasValue)
.WithMessage("Star rating must be between 1 and 5 if specified.");

RuleFor(x => x.Feedback).Length(1, 500).When(x => !string.IsNullOrEmpty(x.Feedback)).WithMessage("Feedback must be between 1 and 500 characters.");
}
#endregion

#region Private Members
private bool CommitmentIntervalExists(Guid id)
{
if (id == Guid.Empty) return false;
return _timeIntervalService.GetByIdOrNull(id) != null;
}
#endregion
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1891,7 +1891,7 @@ await _executionStrategyService.ExecuteInExecutionStrategyAsync(async () =>
request = new OpportunityRequestUpdate
{
Id = existingByExternalId.Id,
VerificationMethod = !existingByExternalId.VerificationMethod.HasValue ? VerificationMethod.Automatic : existingByExternalId.VerificationMethod.Value, //preserve existing method if set
VerificationMethod = existingByExternalId.VerificationMethod ?? VerificationMethod.Automatic, //preserve existing method if set
SSISchemaName = string.IsNullOrEmpty(existingByExternalId.SSISchemaName) ? SSISSchemaHelper.ToFullName(SchemaType.Opportunity, $"Default") : existingByExternalId.SSISchemaName //preserve existing schema if set
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public OpportunityRequestValidatorBase(IOpportunityTypeService opportunityTypeSe
RuleFor(x => x.Description).NotEmpty();
RuleFor(x => x.TypeId).NotEmpty().Must(TypeExists).WithMessage($"Specified type is invalid / does not exist.");
RuleFor(x => x.OrganizationId).NotEmpty().Must(OrganizationActive).WithMessage("The selected organization is either invalid or inactive.");
RuleFor(x => x.Summary).NotEmpty().Length(1, 150).WithMessage("'{PropertyName}' must be between 1 and 150 characters.");
RuleFor(x => x.Summary).NotEmpty().Length(1, 150).WithMessage("'{PropertyName}' is required and must be between 1 and 150 characters.");
//instructions (varchar(max); auto trimmed
RuleFor(x => x.URL).Length(1, 2048).Must(ValidURL).When(x => !string.IsNullOrEmpty(x.URL)).WithMessage("'{PropertyName}' must be between 1 and 2048 characters long and be a valid URL if specified.");

Expand Down
Loading

0 comments on commit 06ec273

Please sign in to comment.