using LCU.API.Internal.Operations.IncentiveApi.Exceptions; using LCU.API.Internal.Operations.IncentiveApi.Services.DuplicateDetection; using LCU.API.Internal.Operations.IncentiveApi.Models.DataTransfer; using LCU.API.Internal.Operations.IncentiveApiLib.Extensions; using LCU.API.Internal.Operations.IncentiveApi.Models; using LCU.API.Internal.Operations.IncentiveApi.Models.ExternalActivities; using LCU.API.Internal.Operations.IncentiveApiLib.Services; using LCU.API.Internal.Operations.IncentiveApiLib.Repositories; using LCU.Library.Internal.IncentiveApiModels.Entities; using LCU.Library.Internal.IncentiveApiModels.Enums; using LCU.Library.Internal.IncentiveApiModels.Common; using LCU.API.Internal.Operations.IncentiveApi.Services.MicroApiServices; using LCU.Library.Internal.IncentiveApiModels.Requests; using LCU.Library.Internal.IncentiveApiModels.Results; namespace LCU.API.Internal.Operations.IncentiveApi.Services; public class ActivityService : IActivityService { private const string TransferredFromString = "Activity has been transferred from Activity ID: {0}"; private const string TransferredToString = "Activity has been transferred to Activity ID: {0}"; private readonly IActivityRepository _activityRepository; private readonly IActivityChangeRepository _activityChangeRepository; private readonly IActivityTransferActionRepository _activityTransferActionRepository; private readonly IUserService _userService; private readonly INotificationApiService _notificationService; private readonly IUnitOfWork _unitOfWork; private readonly IDuplicateDetectionService _duplicateDetectionService; private readonly IAuthorizationService _authorizationService; private readonly ProcessResult _incentiveProcessResult; public ActivityService( IActivityRepository activityRepository, IActivityChangeRepository activityChangeRepository, IActivityTransferActionRepository activityTransferActionRepository, IUserService userService, INotificationApiService notificationService, IDuplicateDetectionService duplicateDetectionService, IAuthorizationService authorizationService, IUnitOfWork unitOfWork ) { _activityRepository = activityRepository; _activityChangeRepository = activityChangeRepository; _activityTransferActionRepository = activityTransferActionRepository; _userService = userService; _notificationService = notificationService; _unitOfWork = unitOfWork; _authorizationService = authorizationService; _duplicateDetectionService = duplicateDetectionService; _incentiveProcessResult = new ProcessResult(); } public async Task SaveActivityAsync(Activity activity) { if (LogGapInformation(activity)) { Log.Information($"Activity {activity.Id} - {activity.Detail.MemberName} " + $"GapEligible saved as : {activity.Detail.IsGapInsuranceEligible} {DateTime.UtcNow} "); } if (string.IsNullOrWhiteSpace(activity.EmployeeId)) { throw new DeadActivityException( $"Attempted to save activity missing EmployeeId."); } if (activity.DepartmentId < 0) { throw new DeadActivityException( $"Attempted to save activity with invalid DepartmentId of {activity.DepartmentId}"); } var appUser = await _userService.GetActiveUserByEmployeeIdAsync(activity.EmployeeId) ?? await _userService.GetUserByEmployeeIdAsync(activity.EmployeeId); if (appUser == null) { throw new NotFoundException($"Employee Not Found For EmployeeId: {activity.EmployeeId}"); } activity.AppUserId = appUser.Id; activity.Accepted = activity.Accepted.Date.AddHours(12); await _activityRepository.SaveActivityAsync(activity); await _unitOfWork.CompleteAsync(); await MarkDuplicates(new List { activity }); return activity.Id; } public static bool LogGapInformation(Activity activity) { return activity.Product == Product.Vehicle && activity.SourceSystem == SystemType.Dna && activity.Detail != null && activity.Detail.IsGapInsuranceEligible == false && activity.Detail.AccountBalance.HasValue && activity.Detail.AccountBalance.Value <= 100000 && (!string.IsNullOrEmpty(activity.Detail?.MemberName)) && (!activity.Detail.MemberName.ToLower().Contains(" inc") && !activity.Detail.MemberName.Contains(" llc")); } public async Task CreateAndRetrieveActivity(Activity activity) { var newActivityId = await SaveActivityAsync(activity); return await GetActivity(newActivityId); } public async Task MarkDuplicates(IReadOnlyCollection activities) { foreach (var activity in activities) { var duplicates = await _duplicateDetectionService.TryFindDuplicates(activity); if (duplicates == null || !duplicates.Any()) continue; var change = new ActivityStateChange { ActivityId = activity.Id, State = ActivityState.Rejected, ActivityMatchId = duplicates.First().Id, Note = $"Activity rejected, duplicate of {duplicates.First().Id}." }; await ApplyStateChange(change); } } public async Task SaveActivityRangeAsync(IReadOnlyCollection activities) { await _activityRepository.SaveActivityRangeAsync(activities.ToList()); await _unitOfWork.CompleteAsync(); await MarkDuplicates(activities); } public async Task RunReferralStatusUpdates() { try { var activityUpdates = await MatchOrExpireReferrals(); var message = $"There were {activityUpdates.Count()} Referral Activity State Updates"; Log.Information(message); _incentiveProcessResult.MessageStack.Add(message); } catch (Exception exception) { var message = $"There was an issue with Activity State Updates Process. Exception Message: {exception.Message}."; _incentiveProcessResult.MessageStack.Add(message); _incentiveProcessResult.Success = false; } return _incentiveProcessResult; } public async Task> MatchOrExpireReferrals() { var activeReferrals = await _activityRepository.FindUnmatchedReferrals(); var changes = new List(); foreach (var referral in activeReferrals) { var matchedSale = await _activityRepository.FindUnmatchedSaleForReferral(referral); if (matchedSale != null) { var acceptedStateChange = await ApplyAcceptedStateChange(referral, matchedSale); changes.Add(acceptedStateChange); if (referral.EmployeeId == matchedSale.EmployeeId && referral.Product == Product.NewChecking && matchedSale.Product == Product.NewChecking) { ActivityStateChange rejectedStateChange = await ApplyRejectedStateChange(referral, matchedSale); changes.Add(rejectedStateChange); } continue; } var expireRange = referral.Product != Product.FirstMortgageReferral ? DateTime.Today.Subtract(TimeSpan.FromDays(60)) : DateTime.Today.Subtract(TimeSpan.FromDays(120)); if (referral.SourceCreated < expireRange) { var change = new ActivityStateChange { ActivityId = referral.Id, State = ActivityState.Rejected, Note = "Referral has expired." }; await ApplyStateChange(change); changes.Add(change); } } return changes; } private async Task ApplyRejectedStateChange(Activity referral, Activity matchedSale) { var creditedEmployee = _userService.GetUserByEmployeeIdAsync(matchedSale.EmployeeId); var rejectedStateChange = new ActivityStateChange { ActivityId = referral.Id, State = ActivityState.Rejected, ActivityMatchId = matchedSale.Id, Note = $"Referral rejected as {creditedEmployee.Result.FullName} is also employee on {matchedSale.Id}." }; await ApplyStateChange(rejectedStateChange); return rejectedStateChange; } private async Task ApplyAcceptedStateChange(Activity referral, Activity matchedSale) { var acceptedStateChange = new ActivityStateChange { ActivityId = referral.Id, State = ActivityState.Accepted, ActivityMatchId = matchedSale.Id, Note = $"Referral was matched to {matchedSale.Id}." }; await ApplyStateChange(acceptedStateChange); return acceptedStateChange; } public async Task ApplyStateChange(ActivityStateChange activityStateChange, bool commitTransaction = true) { var existingActivity = await _activityRepository.GetActivityById(activityStateChange.ActivityId); if (existingActivity == null) { throw new NotFoundException($"No activity with Id {activityStateChange.ActivityId} could be found"); } if (activityStateChange.ActivityMatchId == activityStateChange.ActivityId) { throw new Exception("Activity cannot change a state against itself."); } if (existingActivity.State == activityStateChange.State && (activityStateChange.AcceptedDate == null || existingActivity.Accepted == activityStateChange.AcceptedDate)) { await _activityChangeRepository.SaveActivityStateChange(activityStateChange); await _unitOfWork.CompleteAsync(); return existingActivity; } existingActivity.State = activityStateChange.State; if (activityStateChange.AcceptedDate != null) { existingActivity.Accepted = (DateTime)activityStateChange.AcceptedDate?.Date.AddHours(12); } if (activityStateChange.State == ActivityState.Accepted || activityStateChange.State == ActivityState.Rejected) { if (activityStateChange.AcceptedDate == null) { existingActivity.Accepted = DateTime.UtcNow.Date.AddHours(12); } if (activityStateChange.ActivityMatchId > 0) { var matchedActivity = await _activityRepository.GetActivityById(activityStateChange.ActivityMatchId); if (matchedActivity == null) { throw new NotFoundException($"No matched activity with Id {activityStateChange.ActivityMatchId} could be found"); } existingActivity.MatchingActivityId = activityStateChange.ActivityMatchId; if (activityStateChange.State == ActivityState.Accepted) { matchedActivity.MatchingActivityId = activityStateChange.ActivityId; } } } await _activityChangeRepository.SaveActivityStateChange(activityStateChange); if (commitTransaction) { await _unitOfWork.CompleteAsync(); } return existingActivity; } public async Task> GetActivitiesAsync() { var activities = await _activityRepository.GetActivitiesAsync(); return activities ?? new List(); } public async Task> ListByFilterAsync(ActivityFilter filter) { try { return await _activityRepository.GetPagedFilteredActivities(filter); } catch (Exception ex) { Log.Error(ex, $"Failed to retrieve filtered Actions limit: {filter.Limit}, page: {filter.Page}"); throw new ServerErrorException(); } } public async Task> GetActivityStateChanges(int activityId) { return await _activityChangeRepository.GetActivityChangesByActivityId(activityId); } public async Task> GetDepartmentActivityForMonthRangeAsync( List userDepartments, MonthRange monthRange ) { var deptProgress = new List(); foreach (var dept in userDepartments) { var isAssignedInMonthRange = dept.StartDate.Date < monthRange.EndTime.Date && (dept.EndDate == null || monthRange.StartTime.Date < dept.EndDate.Value.Date); if (isAssignedInMonthRange) { var startDate = dept.StartDate.Date < monthRange.StartTime.Date ? monthRange.StartTime.Date : dept.StartDate.Date; var endDate = dept.EndDate == null ? monthRange.EndTime.Date : dept.EndDate.Value.Date > monthRange.EndTime.Date ? monthRange.EndTime.Date : dept.EndDate.Value.Date; var monthRangeOfAssignment = new MonthRange ( startDate.Year, startDate.Month, endDate.Year, endDate.Month ); var progress = new DepartmentProgress(dept.DepartmentId, monthRangeOfAssignment); var departmentActivity = await _activityRepository.FindDepartmentActivityByMonthRange( progress.DepartmentId, progress.MonthRange); progress.ApplyActivity(departmentActivity); deptProgress.Add(progress); } } return deptProgress; } public async Task GetActivity(int activityId) { var activity = await _activityRepository.GetActivityIncludingPayouts(activityId); if (activity == null) { throw new NotFoundException($"No activity with Id {activityId} could be found"); } return activity; } public async Task GetActivityForStateUpdate(Activity queryActivity) { var activity = await _activityRepository.FindActivityForStateUpdate(queryActivity); if (activity == null) { throw new NotFoundException( $"No activity with matching query activity could be found. AccountNumber: {queryActivity.Detail.AccountNumber} MemberName:{queryActivity.Detail.MemberName}"); } return activity; } public async Task GetActivityForDetailUpdate(Activity queryActivity) { return await _activityRepository.FindActivityForDetailUpdate(queryActivity); } public async Task ValidateCanApplyActivityStateChange(AppUser appUser, ActivityStateChange activityStateChange) { var activity = await _activityRepository.GetActivityIncludingPayouts(activityStateChange.ActivityId); if (activity == null) { throw new NotFoundException($"No activity with Id {activityStateChange.ActivityId} could be found"); } if (activity.ActivityPayouts != null && activity.ActivityPayouts.Any()) { throw new BadActivityStateChangeException("Cannot change the state of an activity that has a payout."); } if (activityStateChange.AcceptedDate != null) { var lastDateNotPaidOut = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1).AddMonths(-1); if (activityStateChange.AcceptedDate < lastDateNotPaidOut || activityStateChange.AcceptedDate > DateTime.Today.AddMonths(1)) { throw new BadActivityStateChangeException( "Cannot change the accepted date of an activity to a date that's already paid out, or is greater than 1 month from now."); } } var required = activity.RequiredToChangeState(activityStateChange.State); switch (required) { case StateChangeRequirement.NotAllowed: case StateChangeRequirement.SystemOnly: throw new BadActivityStateChangeException("Activity can not be changed to target state."); case StateChangeRequirement.AdminOnly when appUser.HasRoleAnywhere(Role.Admin): case StateChangeRequirement.AdminOrPermission when appUser.HasRoleAnywhere(Role.Admin) || await _authorizationService.AppUserCanMakeStateChange(appUser, activity, activityStateChange.State): case StateChangeRequirement.Hierarchical when await _authorizationService.ValidateHierarchicalDepartmentPermission(appUser, Role.DepartmentManagerRoles, activity.DepartmentId) || await _authorizationService.AppUserCanMakeStateChange(appUser, activity, activityStateChange.State): return; default: throw new NotAuthorizedException("User does not have the permission to change the activity state."); } } public async Task RequestTransfer(ActivityTransferRequest activityTransferRequest) { var activity = await _activityRepository.GetActivityIncludingPayouts(activityTransferRequest.RequestedActivityId); if (activity == null) { throw new NotFoundException( $"No activity with Id {activityTransferRequest.RequestedActivityId} could be found"); } if (activity.ActivityPayouts.Any()) { throw new BadActivityTransferActionException( "Cannot request a transfer of an activity that has a payout."); } if (!ActivityTransferAction.IsEligibleForTransfer(activity.State)) { throw new BadActivityTransferActionException( "Cannot request a transfer that isn't pending or approved."); } var newAppUser = await _userService.GetActiveUserByEmployeeIdAsync(activityTransferRequest.NewEmployeeId); var activityTransferAction = new ActivityTransferAction { Id = 0, ActivityStateBeforeTransfer = activity.State, RequestedActivityId = activity.Id, State = ActivityTransferState.Requested, NewAppUserId = newAppUser.Id, NewActivityDepartmentId = activityTransferRequest.NewDepartmentId, NewActivityEmployeeId = activityTransferRequest.NewEmployeeId }; await _activityTransferActionRepository.SaveTransferAsync(activityTransferAction); var stateChange = new ActivityStateChange { ActivityId = activityTransferRequest.RequestedActivityId, Note = "", State = ActivityState.PendingTransfer }; await ApplyStateChange(stateChange); var initiatingManager = await _userService .FindDepartmentManagers(activityTransferAction.RequestedActivity.DepartmentId); var destinationManager = await _userService .FindDepartmentManagers(activityTransferAction.NewActivityDepartmentId); var recipients = initiatingManager .Concat(destinationManager) .Select(manager => manager.AdEmail) .Distinct() .ToList(); await _notificationService.SendTransferRequestInitiatedNotification(activityTransferAction, recipients); return activityTransferAction; } public async Task TransferToOwnDepartment( ActivityTransferRequest activityTransferRequest, AppUser employee ) { var existingActivity = await _activityRepository.GetActivityIncludingPayouts(activityTransferRequest.RequestedActivityId); if (existingActivity == null) { throw new NotFoundException( $"No activity with Id {activityTransferRequest.RequestedActivityId} could be found"); } if (!string.Equals(existingActivity.EmployeeId, employee.EmployeeId) || !string.Equals(activityTransferRequest.NewEmployeeId, employee.EmployeeId)) { throw new NotAuthorizedException( "This activity does not belong to the requesting user and cannot be transferred."); } if (!employee.HasDepartment(activityTransferRequest.NewDepartmentId)) { throw new BadActivityTransferActionException( "Cannot request a transfer of an activity to a department that the user is not assigned to."); } if (existingActivity.ActivityPayouts.Any()) { throw new BadActivityTransferActionException( "Cannot request a transfer of an activity that has a payout."); } if (existingActivity.State != ActivityState.Accepted) { throw new BadActivityTransferActionException( "Cannot request a transfer to another assigned department for an activity that isn't accepted."); } var newAppUser = await _userService.GetActiveUserByEmployeeIdAsync(activityTransferRequest.NewEmployeeId); var activityTransferAction = new ActivityTransferAction { Id = 0, ActivityStateBeforeTransfer = existingActivity.State, RequestedActivityId = existingActivity.Id, State = ActivityTransferState.Accepted, NewAppUserId = newAppUser.Id, NewActivityDepartmentId = activityTransferRequest.NewDepartmentId, NewActivityEmployeeId = activityTransferRequest.NewEmployeeId }; var newActivity = Activity.CopyActivityForTransfer(existingActivity, activityTransferAction, employee.AdEmail); activityTransferAction.NewActivity = newActivity; await _activityTransferActionRepository.SaveTransferAsync(activityTransferAction); await _unitOfWork.CompleteAsync(); var newStateChange = new ActivityStateChange { ActivityId = newActivity.Id, ActivityMatchId = existingActivity.Id, Note = string.Format(TransferredFromString, existingActivity.Id), State = activityTransferAction.ActivityStateBeforeTransfer }; await ApplyStateChange(newStateChange); var stateChange = new ActivityStateChange { ActivityId = existingActivity.Id, ActivityMatchId = newActivity.Id, Note = string.Format(TransferredToString, newActivity.Id), State = ActivityState.Transferred }; await ApplyStateChange(stateChange); return activityTransferAction; } public async Task ApproveTransfer( AppUser appUser, ActivityTransferApprovalRequest transferApproval ) { var activityTransferAction = await GetActivityTransferAction(transferApproval); var activity = await _activityRepository.GetActivityIncludingPayouts(activityTransferAction.RequestedActivityId); var targetStateChange = transferApproval.ApprovalState == ActivityTransferState.Accepted ? ActivityState.Accepted : activityTransferAction.ActivityStateBeforeTransfer; var canApproveTransferFrom = await _authorizationService.AppUserCanMakeStateChange(appUser, activity, targetStateChange); var canApproveTransferTo = await _authorizationService.AppUserCanMakeStateChange(appUser, activity, targetStateChange, activityTransferAction.NewActivityDepartmentId); if (!canApproveTransferFrom && !canApproveTransferTo) { throw new NotAuthorizedException("Employee is not allowed to approve for either department"); } if (transferApproval.ApprovalState == ActivityTransferState.Rejected) { return await RejectTransfer(activityTransferAction); } if (transferApproval.ApprovalState == ActivityTransferState.Accepted) { var isApprovedByRequestedDepartment = !string.IsNullOrEmpty(activityTransferAction.RequestedDepartmentApprover); var isApprovedByNewDepartment = !string.IsNullOrEmpty(activityTransferAction.NewDepartmentApprover); if (canApproveTransferFrom && !isApprovedByRequestedDepartment) { activityTransferAction.RequestedDepartmentApprover = appUser.FullName; activityTransferAction.RequestedDepartmentApproved = DateTime.Now; isApprovedByRequestedDepartment = true; } if (canApproveTransferTo && !isApprovedByNewDepartment) { activityTransferAction.NewDepartmentApprover = appUser.FullName; activityTransferAction.NewDepartmentApproved = DateTime.Now; isApprovedByNewDepartment = true; } if (isApprovedByRequestedDepartment && isApprovedByNewDepartment) { var existingActivity = await _activityRepository .GetActivityIncludingPayouts(activityTransferAction.RequestedActivityId); var newActivityEmployee = await _userService .GetUserByEmployeeIdAsync(activityTransferAction.NewActivityEmployeeId); var newActivity = Activity.CopyActivityForTransfer(existingActivity, activityTransferAction, newActivityEmployee.AdEmail); activityTransferAction.NewActivity = newActivity; activityTransferAction.State = ActivityTransferState.Accepted; await _unitOfWork.CompleteAsync(); var newStateChange = new ActivityStateChange { ActivityId = newActivity.Id, ActivityMatchId = existingActivity.Id, Note = string.Format(TransferredFromString, existingActivity.Id), State = activityTransferAction.ActivityStateBeforeTransfer }; await ApplyStateChange(newStateChange); var stateChange = new ActivityStateChange { ActivityId = existingActivity.Id, ActivityMatchId = newActivity.Id, Note = string.Format(TransferredToString, newActivity.Id), State = ActivityState.Transferred }; await ApplyStateChange(stateChange); } else { await _unitOfWork.CompleteAsync(); } } return activityTransferAction; } public async Task GetRequestedTransfer(int activityId) { return await _activityTransferActionRepository.GetRequestedTransferActionByActivityId(activityId); } private async Task GetActivityTransferAction( ActivityTransferApprovalRequest transferApproval ) { var activityTransferAction = await _activityTransferActionRepository .GetTransferActionById(transferApproval.Id); if (activityTransferAction == null) { throw new NotFoundException( $"No activity transfer action with Id {transferApproval.Id} could be found"); } if (activityTransferAction.State != ActivityTransferState.Requested) { throw new BadActivityTransferActionException("Approval has already been decided for this activity"); } return activityTransferAction; } private async Task RejectTransfer(ActivityTransferAction activityTransferAction) { activityTransferAction.State = ActivityTransferState.Rejected; var stateChange = new ActivityStateChange { ActivityId = activityTransferAction.RequestedActivityId, Note = "Activity transfer has been rejected.", State = activityTransferAction.ActivityStateBeforeTransfer }; await ApplyStateChange(stateChange); return activityTransferAction; } public async Task CancelPendingTransfersBetween(MonthRange range) { var transfers = await _activityTransferActionRepository.GetActiveTransfersForStateAsync(ActivityState.Accepted); foreach (var transfer in transfers) { if (transfer.RequestedActivity.Accepted.Date >= range.StartTime.Date && transfer.RequestedActivity.Accepted.Date <= range.EndTime.Date) { await RejectTransfer(transfer); } } } public async Task> FilterExternalActivitiesByExistingSourceId( List incomingEternalActivities ) { var existingActivities = await _activityRepository.GetActivitiesBySourceId(incomingEternalActivities.Select(x => x.GetSourceId()) .ToList()); return incomingEternalActivities .Where(newActivity => !existingActivities.Any(existingActivity => existingActivity.Product == newActivity.GetProductType() && existingActivity.Type == newActivity.GetActivityType() && existingActivity.SourceId == newActivity.GetSourceId())) .ToList(); } public async Task GetActivityAuthorizedCommands(AppUser appUser, int activityId) { var activity = await _activityRepository.GetActivityIncludingPayouts(activityId); if (activity == null) { throw new NotFoundException($"No activity with Id {activityId} could be found"); } var allowedChanges = new List(); var canApproveTransferFrom = false; var canApproveTransferTo = false; var activityTransferAction = await GetRequestedTransfer(activityId); var targetDepartmentId = activityTransferAction == null || activity.State != ActivityState.PendingTransfer ? 0 : activityTransferAction.NewActivityDepartmentId; var hasHierarchicalPermission = await _authorizationService.ValidateHierarchicalDepartmentPermission(appUser, Role.DepartmentManagerRoles, activity.DepartmentId); foreach (var stateDetail in ActivityStateDetail.ActivityStateDetails) { var changeRequirement = activity.RequiredToChangeState(stateDetail.State); switch (changeRequirement) { case StateChangeRequirement.AdminOnly when appUser.HasRoleAnywhere(Role.Admin): case StateChangeRequirement.AdminOrPermission when appUser.HasRoleAnywhere(Role.Admin) || await _authorizationService.AppUserCanMakeStateChange(appUser, activity, stateDetail.State, targetDepartmentId): case StateChangeRequirement.Hierarchical when hasHierarchicalPermission || await _authorizationService.AppUserCanMakeStateChange(appUser, activity, stateDetail.State, targetDepartmentId): allowedChanges.Add(stateDetail.State); break; } } if (activityTransferAction != null && allowedChanges.Contains(ActivityState.Transferred)) { canApproveTransferFrom = await _authorizationService.AppUserCanMakeStateChange(appUser, activity, ActivityState.Transferred); canApproveTransferTo = await _authorizationService.AppUserCanMakeStateChange(appUser, activity, ActivityState.Transferred, activityTransferAction.NewActivityDepartmentId); } allowedChanges = allowedChanges.Distinct().ToList(); return new AuthorizedActivityCommands(allowedChanges.ToArray(), canApproveTransferFrom, canApproveTransferTo); } }