using System.Globalization; using System.IO; using LCU.API.Internal.Operations.IncentiveApi.Exceptions; using LCU.API.Internal.Operations.IncentiveApi.Services.AutoDecision; using CsvHelper; using CsvHelper.Configuration; using LCU.API.Internal.Operations.IncentiveApiLib.Extensions; using LCU.API.Internal.Operations.IncentiveApiLib.Repositories; using LCU.API.Internal.Operations.IncentiveApi.Models; using LCU.API.Internal.Operations.IncentiveApi.Models.Allied; using LCU.Library.Internal.IncentiveApiModels.Enums; using LCU.Library.Internal.IncentiveApiModels.Entities; using LCU.Library.Internal.IncentiveApiModels.Requests; using LCU.API.Internal.Operations.IncentiveApi.Services.MicroApiServices; using LCU.API.Internal.Operations.IncentiveApi.Services.Transformations; using LCU.Library.Internal.ApiResponse.Services; using LCU.Library.Internal.IncentiveApiModels.Results; namespace LCU.API.Internal.Operations.IncentiveApi.Services.Allied; public class AlliedGapMbpTransactionImportService : IAlliedGapMbpTransactionImportService { private readonly IUserRepository _userRepository; private readonly IActivityService _activityService; private readonly IActivityDecisionService _activityDecisionService; private readonly IActivityDetailService _activityDetailService; private readonly IDeadActivityService _deadActivityService; private readonly IFileOperationsApiService _fileOperationsApiService; private readonly IGapInsuranceActivityService _gapInsuranceActivityService; private readonly ProcessResult _alliedGapMbpProcessResult; public AlliedGapMbpTransactionImportService( IUserRepository userRepository, IGapInsuranceActivityService gapInsuranceActivityService, IFileOperationsApiService fileOperationsApiService, IActivityService activityService, IActivityDecisionService activityDecisionService, IActivityDetailService activityDetailService, IDeadActivityService deadActivityService ) { _userRepository = userRepository; _gapInsuranceActivityService = gapInsuranceActivityService; _fileOperationsApiService = fileOperationsApiService; _activityService = activityService; _activityDecisionService = activityDecisionService; _activityDetailService = activityDetailService; _deadActivityService = deadActivityService; _alliedGapMbpProcessResult = new ProcessResult(); } public async Task RunAlliedGapMbpProcess(AlliedGapMbpRequest request) { try { var importedAlliedSales = await ImportTransactions(request); Log.Information($"Imported {importedAlliedSales.Count} Allied Gap/Mbp sales."); var matchedRemoved = await _gapInsuranceActivityService.MatchOrRemoveGapAccessories(); Log.Information($"Matched/Removed {matchedRemoved} Allied Gap/Mbp records."); _alliedGapMbpProcessResult.MessageStack.Add($"Imported {importedAlliedSales.Count()} sales, Matched/Removed {matchedRemoved} records for Allied Gap/Mbp Sales processing."); await QueueReturnResponse.QueueResponse(request, _alliedGapMbpProcessResult.Success, _alliedGapMbpProcessResult.MessageStack); } catch (Exception exception) { var message = $"There was an issue with handling Import Allied Gap/Mbp Sales Exception Message: {exception.Message}."; _alliedGapMbpProcessResult.MessageStack.Add(message); _alliedGapMbpProcessResult.Success = false; await QueueReturnResponse.QueueResponse(request, _alliedGapMbpProcessResult.Success, _alliedGapMbpProcessResult.MessageStack); } } public async Task> ImportTransactions(AlliedGapMbpRequest request) where T : AlliedSaleRow { var dynamicFileListRequest = request.ToDynamicAlliedGapMbpFileListRequest(); var files = await _fileOperationsApiService.BuildDynamicFileList(dynamicFileListRequest); var activity = new List(); foreach (var fileName in files) { var stringContent = await _fileOperationsApiService.GetFileContent($"{request.FinalBasePath}{fileName}"); using var stringReader = new StringReader(stringContent); var config = new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = ",", HasHeaderRecord = true, HeaderValidated = null, MissingFieldFound = null, IgnoreBlankLines = true }; using var csv = new CsvReader(stringReader, config); var records = csv.GetRecords().ToList(); var allActivity = await CreateActivity(records); activity.AddRange(allActivity); } return activity; } public async Task> CreateActivity(IEnumerable transactions) { var activities = new List(); var deadActivities = new List(); try { foreach (var alliedSaleRow in transactions) { var activity = new Activity() { Accepted = DateTime.Parse(alliedSaleRow.SaleDate).Date.AddHours(12), DepartmentId = -1, Detail = new ActivityDetail() { AccountNumber = alliedSaleRow.DetermineAccountNumber(), AccountStatus = alliedSaleRow.Status, MemberName = alliedSaleRow.Buyer, SubAccountNumber = alliedSaleRow.SalesContractNumber }, EmployeeEmail = null, EmployeeId = null, Product = alliedSaleRow.DetermineAlliedProduct(), SourceCreated = DateTime.Parse(alliedSaleRow.SaleDate).Date.AddHours(12), SourceId = alliedSaleRow.DetermineAccountNumber(), SourceSystem = SystemType.Allied, State = ActivityState.Created, Type = ActivityType.Sale }; var employee = await FindEmployee(alliedSaleRow.Issuer.Trim()); if (employee == null) { Log.Warning($"Allied Import, No user for {alliedSaleRow.Issuer}. Saving as Dead Activity."); deadActivities.Add(new DeadActivity(activity, DeadActivityReason.MissingRequiredField, $"Allied Import, No user for {alliedSaleRow.Issuer}")); continue; } activity.AppUserId = employee.Id; activity.EmployeeEmail = employee.AdEmail; activity.EmployeeId = employee.EmployeeId; var homeDept = employee.FindHomeDepartment(); if (homeDept == null) { Log.Warning($"User has no reliably determinable home department, {employee.EmployeeId}. Saving as Dead Activity."); deadActivities.Add(new DeadActivity(activity, DeadActivityReason.MissingRequiredField, $"User has no reliably determinable home department, {employee.EmployeeId}")); continue; } activity.DepartmentId = homeDept.DepartmentId; activities.Add(activity); } foreach (var activity in activities) { var activityStateUpdate = await _activityDecisionService.TryDecisionActivity(activity); activity.State = activityStateUpdate.State; } var activitySets = activities.GroupBy(x => new { x.DepartmentId, x.EmployeeEmail, x.EmployeeId, x.Product, x.SourceSystem, x.Type, x.Detail.AccountNumber, x.Detail.SubAccountNumber }); var updateOrders = new List(); foreach (var activitySet in activitySets) { var existingActivity = await _activityService.GetActivityForDetailUpdate(activitySet.FirstOrDefault()); if (existingActivity?.State != ActivityState.Transferred && existingActivity?.State != ActivityState.PendingTransfer && existingActivity?.State != ActivityState.Rejected) { updateOrders.Add( new ActivityProcessingOrder { ExistingActivity = existingActivity, ActivityUpdateItems = activitySet.ToList() } ); } } try { await _deadActivityService.SaveDeadActivityRange(deadActivities); } catch (Exception ex) { Log.Error(ex, "Failed to save dead activities from AlliedTransactionImportService. Dead Activities will be lost"); } return TryProcessUpdates(updateOrders).GetAwaiter().GetResult().ToList(); } catch (Exception ex) { Log.Error(ex, ex.Message); } return new List(); } private async Task> TryProcessUpdates(IEnumerable activityDetailUpdateOrders) { var returnItems = new List(); foreach (var activityDetailUpdateOrder in activityDetailUpdateOrders) { try { var activityProcessingResult = await HandleActivityProcessingOrder(activityDetailUpdateOrder); if (activityProcessingResult.UpdateCount <= 0) { continue; } Log.Debug($"{activityProcessingResult.UpdateCount} updates performed on Allied SourceId: {activityProcessingResult.ExistingActivity.SourceId}"); returnItems.Add(activityDetailUpdateOrder.ExistingActivity); } catch (Exception ex) { Log.Error(ex, ex.Message); } } return returnItems; } public ActivityProcessingOrder FilterAndOrderUpdates(ActivityProcessingOrder activityProcessingOrder) { activityProcessingOrder.ActivityUpdateItems = activityProcessingOrder.ActivityUpdateItems .OrderBy(x => x.SourceCreated) .ThenBy(x => x.Detail.AccountStatus.ToString().Equals("Reinstated", StringComparison.InvariantCultureIgnoreCase)) .ThenBy(x => x.Detail.AccountStatus.ToString().Equals("Cancelled", StringComparison.InvariantCultureIgnoreCase)) .ThenBy(x => x.Detail.AccountStatus.ToString().Equals("Cancelled/Re-Quoted", StringComparison.InvariantCultureIgnoreCase)) .ThenBy(x => x.Detail.AccountStatus.ToString().Equals("Sold", StringComparison.InvariantCultureIgnoreCase)) .ThenBy(x => x.Detail.AccountStatus.ToString().Equals("Flagged", StringComparison.InvariantCultureIgnoreCase)) .ToList(); if (activityProcessingOrder.ExistingActivity == null) { return activityProcessingOrder; } var startIndex = activityProcessingOrder.ActivityUpdateItems .FindIndex(x => x.Detail.AccountStatus.Equals(activityProcessingOrder.ExistingActivity.Detail.AccountStatus, StringComparison.InvariantCultureIgnoreCase) ); if (startIndex > 0) { activityProcessingOrder.ActivityUpdateItems.RemoveRange(0, startIndex); } return activityProcessingOrder; } public async Task HandleActivityProcessingOrder(ActivityProcessingOrder activityProcessingOrder) { activityProcessingOrder = FilterAndOrderUpdates(activityProcessingOrder); while (!activityProcessingOrder.ProcessingComplete) { foreach (var updateActivity in activityProcessingOrder.ActivityUpdateItems) { try { var updateActivityUpdateCount = 0; if (activityProcessingOrder.ExistingActivity == null) { try { activityProcessingOrder.ExistingActivity = await _activityService.CreateAndRetrieveActivity(updateActivity); updateActivityUpdateCount++; } catch (NotFoundException nex) { Log.Error(nex, $"Failed to create new Allied Activity SourceId:{updateActivity.SourceId}"); await _deadActivityService.SaveDeadActivity( new DeadActivity(updateActivity, DeadActivityReason.MissingRequiredField, "Failed to create activity due to missing data") ); } catch (Exception ex) { Log.Error(ex, $"Failed to create new Allied Activity SourceId:{updateActivity.SourceId}"); await _deadActivityService.SaveDeadActivity(new DeadActivity(updateActivity, DeadActivityReason.Unknown, "Failed to create activity")); } finally { if (activityProcessingOrder.ExistingActivity == null) { activityProcessingOrder.ProcessingComplete = true; Log.Debug($"No existing Allied Activity SourceId: {updateActivity.SourceId} "); } } } else { var activityDetailsUpdates = activityProcessingOrder.ExistingActivity.Detail.GetPropertyValueVariances(updateActivity.Detail); foreach (var update in activityDetailsUpdates) { await _activityDetailService.SaveActivityDetailUpdate(new ActivityDetailUpdate { ActivityDetailId = activityProcessingOrder.ExistingActivity.Detail.Id, Note = $"{update.ComparedProperty} has changed from {update.PropertyValueOne} to {update.PropertyValueTwo}" }); activityProcessingOrder.ExistingActivity.Detail.GetType() .GetProperty(update.ComparedProperty) ? .SetValue(activityProcessingOrder.ExistingActivity.Detail, updateActivity.Detail.GetType().GetProperty(update.ComparedProperty)?.GetValue(updateActivity.Detail), null); updateActivityUpdateCount++; } } var existingActivityStateUpdate = await _activityDecisionService.TryDecisionActivity(activityProcessingOrder.ExistingActivity); if (activityProcessingOrder.ExistingActivity.State != existingActivityStateUpdate.State) { updateActivityUpdateCount++; await _activityService.ApplyStateChange(existingActivityStateUpdate); } activityProcessingOrder.UpdateCount += updateActivityUpdateCount; } catch (Exception ex) { Log.Error(ex, $"Failed to update activity details for activity ID:{activityProcessingOrder.ExistingActivity.Id}"); } } activityProcessingOrder.ProcessingComplete = true; } return activityProcessingOrder; } private async Task FindEmployee(string query) { try { var users = await _userRepository.SearchUsersAsync(new UserSearchRequest() { Query = query }); if (users?.FirstOrDefault() == null || users.Count() > 1) { if (query.IndexOf(",", StringComparison.Ordinal) > 0) { users = await _userRepository.SearchUsersAsync(new UserSearchRequest() { Query = query.Split(",").First() }); if (users?.FirstOrDefault() == null || users.Count() > 1) { return null; } } return null; } return users?.FirstOrDefault(); } catch (Exception ex) { Log.Error(ex, "Error when finding user by email in AlliedTransactionImportService"); return null; } } }