Files
monorepo/LCU.API.Collections.RepoApi-old/LCU.API.Collections.RepoApi/Services/Producer/RepoAccountProcessor.cs
T
2026-04-20 14:47:24 -05:00

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);
}
}
}