331 lines
15 KiB
C#
331 lines
15 KiB
C#
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<RepoProcessingResult> 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<RepoAccountProcessingSlip> 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<RepoAccountProcessingSlip> 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<RepoAccount> 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<RepoAccount> 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);
|
|
}
|
|
}
|
|
} |