using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using LCU.API.Collections.RepoApi.DataAccess; using LCU.API.Collections.RepoApi.Domain.Extensions; using LCU.API.Collections.RepoApi.Domain.Processing; using LCU.API.Collections.RepoApi.Exceptions; using LCU.API.Collections.RepoApi.Models; using LCU.Shared.Collections.RepoApiLib.Models; using LCU.Shared.Collections.RepoApiLib.Models.Producer; using Serilog; namespace LCU.API.Collections.RepoApi.Services.Producer { public class RepoAccountProcessor : IRepoAccountProcessor { private readonly IRepoAccountProcessorService _repoAccountProcessorService; private readonly IAccountAlertCheckService _accountAlertCheckService; private readonly IEmailer _emailer; private readonly ISettingsService _settingsService; private readonly ICollectionsDao _collectionsDao; private readonly ICoreDao _coreDao; private readonly IRepoAccountService _repoAccountService; private readonly IRepoLetterService _repoLetterService; private readonly IAuctionAssignmentService _auctionAssignmentService; public const string UserStatusFieldOld = "User Status Code 1"; public const int CaseTypeRepo = 7; // Source: collections API configuration call private const string O4RStatus = "O4R"; private const string NoStatus = ""; private const string RepoStatus = "REPO"; private const string RecaStatus = "RECA"; private const string RehoStatus = "REHO"; private const string RedeemedStatus = "REDEEMED"; private const string TemenosQueuedBy = "Temenos"; public RepoAccountProcessor( IRepoAccountProcessorService repoAccountProcessorService, IAccountAlertCheckService accountAlertCheckService, ICollectionsDao collectionsDao, IEmailer emailer, ISettingsService settingsService, IRepoAccountService repoAccountService, IRepoLetterService repoLetterService, ICoreDao coreDao, IAuctionAssignmentService auctionAssignmentService) { _repoAccountProcessorService = repoAccountProcessorService; _accountAlertCheckService = accountAlertCheckService; _collectionsDao = collectionsDao; _emailer = emailer; _settingsService = settingsService; _repoAccountService = repoAccountService; _repoLetterService = repoLetterService; _coreDao = coreDao; _auctionAssignmentService = auctionAssignmentService; } public async Task HandleAccount(ProcessingItem item) { var processingSlip = await CreateProcessingSlip(item); if (IsAutoCloseAccount(processingSlip)) { Log.Debug($"Auto-Closing account {processingSlip.AccountNo} due to no Temenos Account."); await AutoCloseAccount(processingSlip); return RepoProcessingResult.Autoclosed; } processingSlip.AssertReadyForPreChecks(); // 9640 Attempt to pre-check for a matching status when case is missing. // If no found case, and the status hasn't changed; instead of failing just skip the unconverted account. if (IsUnconvertedIntellectAccount(processingSlip)) { Log.Debug($"Repo Processor: {item.AccountNo} is not updated for Temenos; skipping."); return RepoProcessingResult.Unconverted; } processingSlip.RecoveryCase = processingSlip.RecoveryAccount.FindRecoveryCaseOrDefault(processingSlip.CaseId); if (!IsPossiblyRepossessionCase(processingSlip)) { return RepoProcessingResult.Ineligible; } processingSlip.AssertReadyForFullProcessing(); if (ShouldCreateNewAccount(processingSlip)) { Log.Debug($"Creating new repo account for {item.AccountNo} : {item.CaseId} "); processingSlip.UpdatedRepoAccount = await CreateNewRepoAccount(processingSlip); await _accountAlertCheckService.MaybeAlertOnInvalidStatus(processingSlip); processingSlip = await AddAlertDataToProcessingSlip(processingSlip); processingSlip.AssertReadyForAlertChecks(); await _accountAlertCheckService.AlertChecksPrimary(processingSlip); return RepoProcessingResult.Created; } if (!processingSlip.ExistingRepoAccount.HasAnyCollateral()) { await _emailer.MaybeSendO4rNoCollateralEmail(processingSlip.ExistingRepoAccount); throw new MissingRequirementException("Existing Account Collateral", $"{processingSlip.AccountNo} did not have collateral on existing account."); } Log.Debug($"Updating Repo account for {item.AccountNo} : {item.CaseId}"); processingSlip.UpdatedRepoAccount = await UpdateExistingRepoAccount(processingSlip); //Add check for recent account repo closing when in RECA status to prevent reprocessing and new repo account creation with a DateClosed date = null //The DateClosed date = null will ensure the account gets reprocessed, we don't want alert checks with these accounts if (processingSlip.ExistingRepoAccount.DateClosed == null && processingSlip.UpdatedRepoAccount.DateClosed != null) { return RepoProcessingResult.Updated; } processingSlip = await AddAlertDataToProcessingSlip(processingSlip); processingSlip.AssertReadyForAlertChecks(); await _accountAlertCheckService.AlertChecksPrimary(processingSlip); return RepoProcessingResult.Updated; } // New method to give a chance to filter out non-repo cases and mark ineligible private bool IsPossiblyRepossessionCase(RepoAccountProcessingSlip slip) { if (slip.RecoveryCase == null) return true; try { if (slip.RecoveryCase.CaseTypeId != CaseTypeRepo) { return false; } StatusService.ConvertStatus(slip.RecoveryCase.RepoStatus); } catch (IneligibleAccountException) { return false; } return true; } private async Task AutoCloseAccount(RepoAccountProcessingSlip processingSlip) { AppUser appUser = new AppUser() { Email = processingSlip.RequestedBy.QueuedBy, FirstName = "System", LastName = "System" }; processingSlip.ExistingRepoAccount.DateClosed = DateTime.Now; processingSlip.ExistingRepoAccount.AccountLastProcessed = DateTime.Now; await _repoAccountService.UpdateAsync(processingSlip.ExistingRepoAccount, appUser); } private bool IsAutoCloseAccount(RepoAccountProcessingSlip processingSlip) { // If recovery account couldn't be fetched, that would have been an exception. // If recovery account is null, it means "not there", so close the RepoAccount. // This means close the RepoAccount because the account // was likely not brought over from Intellect. If something DOES happen, just create // a new account later.... return (processingSlip.RecoveryAccount == null && processingSlip.ExistingRepoAccount != null && processingSlip.ExistingRepoAccount.DateClosed == null); } public async Task CreateProcessingSlip(ProcessingItem item) { var accountNo = VerifyAccountNo(item); var slip = new RepoAccountProcessingSlip { AccountNo = accountNo, CurrentSettings = await _settingsService.GetCachedRepoSettingsAsync(), RecoveryAccount = await _collectionsDao.RetrieveAccountAsync(accountNo), ExistingRepoAccount = await _repoAccountService.GetNewestOpenAccount(accountNo), Loan = await _coreDao.RetrieveLoanAsync(accountNo), CaseId = VerifyCaseId(item.CaseId), RequestedBy = item }; if (slip.RecoveryAccount != null) { slip.Letters = await _repoLetterService.ListByAccountIdAsync(slip.RecoveryAccount.AccountId); } return slip; } public async Task AddAlertDataToProcessingSlip(RepoAccountProcessingSlip slip) { slip.TransactionHistory = await _coreDao.RetrieveTransactionsAsync(slip.AccountNo); slip.Member = await _coreDao.RetrieveMemberAsync(slip.AccountNo); var uniqueIds = slip.UpdatedRepoAccount.GetUniqueAuctionAssignmentIds(); slip.AuctionAssignments = await _auctionAssignmentService.GetListByIdsAsync(uniqueIds); return slip; } public bool IsUnconvertedIntellectAccount(RepoAccountProcessingSlip repoAccountSlip) { var recoveryAccount = repoAccountSlip.RecoveryAccount; var caseId = repoAccountSlip.CaseId; var originalRepoAccount = repoAccountSlip.ExistingRepoAccount; if (!recoveryAccount.HasRecoveryCase(caseId) && recoveryAccount.HasOldIntellectStatus()) { Log.Information( $"Repo Processor: {repoAccountSlip.AccountNo} has no case, but user status code. Running pre-check."); return (originalRepoAccount.Status == recoveryAccount.CustomFieldMap[UserStatusFieldOld]); } return false; } public int VerifyCaseId(string itemCaseId) { try { return string.IsNullOrWhiteSpace(itemCaseId) ? 0 : Int32.Parse(itemCaseId); } catch (Exception) { throw new IneligibleAccountException( $"Bad CaseId {itemCaseId}", $"The CaseId is present, but in a non-integer format."); } } public string VerifyAccountNo(ProcessingItem item) { if (item == null) { throw new IneligibleAccountException("Processing Item", "The processing item was null."); } if (item.AccountNo == null) { throw new IneligibleAccountException( "Null AccountNo", "There should never be null accounts on the queue."); } // if (Regex.IsMatch(item.AccountNo, "^[a-zA-Z0-9]*$")) // { // throw new IneligibleAccountException( // "AccountNo", // "AccountNo contains alphabetical or symbol characters"); // } return item.AccountNo; } public async Task UpdateExistingRepoAccount(RepoAccountProcessingSlip slip) { AppUser appUser = new AppUser() { Email = slip.RequestedBy.QueuedBy, FirstName = "System", LastName = "System" }; if (ShouldRunCompleteUpdate(slip)) { RepoAccount updatedRepoAccount = _repoAccountProcessorService.UpdateRepoAccountFromParts(slip); await _accountAlertCheckService.AlertChecksPreStatusReject(slip); _repoAccountProcessorService.CheckForRejectedStatusChange(slip.ExistingRepoAccount, updatedRepoAccount, slip.CurrentSettings); _repoAccountProcessorService.TryUpdateOrCreateRepoCompanyAssignment(updatedRepoAccount, slip.RecoveryCase, slip.CurrentSettings); _repoAccountProcessorService.TryUpdateCollateralRetitleDate(updatedRepoAccount, slip.RecoveryCase); await _repoAccountService.UpdateAsync(updatedRepoAccount, appUser); return updatedRepoAccount; } _repoAccountProcessorService.TryUpdateOrCreateRepoCompanyAssignment(slip.ExistingRepoAccount, slip.RecoveryCase, slip.CurrentSettings); _repoAccountProcessorService.AddAccountUpdateRecord(slip); _repoAccountProcessorService.TryUpdateCollateralRetitleDate(slip.ExistingRepoAccount, slip.RecoveryCase); _repoAccountProcessorService.UpdateRepoAccountDateClosed(slip.ExistingRepoAccount); await _repoAccountService.UpdateAsync(slip.ExistingRepoAccount, appUser); return slip.ExistingRepoAccount; } public bool ShouldRunCompleteUpdate(RepoAccountProcessingSlip slip) { if (slip.ExistingRepoAccount == null) { throw new MissingRequirementException("Existing Repo Account", $"Existing Repo Account required for update check, {slip.AccountNo}"); } var newStatus = StatusService.ConvertStatus(slip.RecoveryCase.RepoStatus); var dateLastPaid = slip.RecoveryAccount.DateLastPaid != null ? DateTime.Parse(slip.RecoveryAccount.DateLastPaid) : slip.ExistingRepoAccount.LastPaymentDate; var refStatusChanged = slip.RecoveryAccount.AccountStatus != slip.ExistingRepoAccount.ReferenceStatus; // This is a special case, it picked up the written off flag, before there was a written off date. if (string.Equals(slip.ExistingRepoAccount.ReferenceStatus, RepoAccountProcessorService.WrittenOff, StringComparison.CurrentCultureIgnoreCase) && slip.ExistingRepoAccount.WrittenOffDate == null) { return true; } var needsRedemptionExpired = slip.ExistingRepoAccount.RedemptionExpiredDate == null && (slip.RecoveryCase.RedemptionExpiredDateIl != null || slip.RecoveryCase.RedemptionExpiredDateWi != null); return !string.Equals(newStatus, slip.ExistingRepoAccount.Status, StringComparison.CurrentCultureIgnoreCase) || slip.ExistingRepoAccount.LastPaymentDate != dateLastPaid || slip.ExistingRepoAccount.Balance != slip.RecoveryAccount.Balance || needsRedemptionExpired || refStatusChanged; } public async Task CreateNewRepoAccount(RepoAccountProcessingSlip slip) { var repoAccount = _repoAccountProcessorService.CreateRepoAccount(slip.RecoveryAccount, slip.RecoveryCase, slip.Loan); var newRepoAccount = await _repoAccountService.SaveAsync(repoAccount); return newRepoAccount; } public bool ShouldCreateNewAccount(RepoAccountProcessingSlip slip) { return slip != null && (slip.ExistingRepoAccount == null || slip.ExistingRepoAccount.DateClosed != null); } } }