Files
2026-04-20 14:47:24 -05:00

585 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.DataAccess;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models.Results;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Consumer;
using LCU.Shared.Collections.RepoApiLib.Models.Settings;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public class RepoAccountProcessorService : IRepoAccountProcessorService
{
private const string RedeemedStatus = "REDEEMED";
private const string PaidSkipStatus = "PSKP";
private const string RepoCancelledStatus = "RECA";
public const string WrittenOff = "Written Off";
public const string SixtyPLusUserAssignedLabel = "SixtyPlus User Assigned";
private readonly ICoreDao _coreDao;
private readonly IRedemptionExpiredDateCalculator _redemptionExpiredDateCalculator;
private readonly IAccountAlertCheckService _accountAlertCheckService;
private readonly IEventAlertService _eventAlertService;
private readonly IRepoCompanySelectionService _repoCompanySelectionService;
public RepoAccountProcessorService(ICoreDao coreDao,
IRedemptionExpiredDateCalculator redemptionExpiredDateCalculator,
IAccountAlertCheckService accountAlertCheckService,
IEventAlertService eventAlertService, IRepoCompanySelectionService repoCompanySelectionService)
{
_coreDao = coreDao;
_redemptionExpiredDateCalculator = redemptionExpiredDateCalculator;
_accountAlertCheckService = accountAlertCheckService;
_eventAlertService = eventAlertService;
_repoCompanySelectionService = repoCompanySelectionService;
}
// This process determined from the original batch code. (LCU.CollectionRepossessionTracking::IntellecRepoImport)
// -> A) Get XP/Core/Apex Account
// -> B) Get XP/Core/Apex Loan
// -> B.1) Fields needed: Branch, Next due date
// ---> If it can't find a loan, it does nothing... what do we WANT do?
// -> C) Get XP/Core/Apex Loan Collateral
// ---> If there is no collateral, it does nothing... what do we WANT do?
// -> D) Get XP/Core/Apex Borrowers (Individuals)
// ---> No borrowers throws an exception? ... what do we WANT do?
// -> E) Create Repo Account from Loan, Collateral, Borrowers and Temenos Account
public RepoAccount CreateRepoAccount(TemenosAccount account, TemenosCase recoveryCase, LoanDetail loanDetail)
{
var collateralDetailRequest = _coreDao.RetrieveCollateralAsync(account.AccountIdentifier);
var collateralDetail = collateralDetailRequest.GetAwaiter().GetResult();
if (collateralDetail == null || !collateralDetail.Any())
{
throw new AccountCreationFailed(
$"Repo-Account Not Created using Account with AccountNo: {account.AccountIdentifier}. No Collateral.");
}
var borrowers = ConstructRepoAccountBorrowers(loanDetail);
if (borrowers == null || !borrowers.Any())
{
throw new AccountCreationFailed(
$"Repo-Account Not Created using Account with AccountNo: {account.AccountIdentifier}. No Borrowers.");
}
var repoAccount = ConstructRepoAccountFromParts(loanDetail, account, recoveryCase);
repoAccount.Borrowers = borrowers;
repoAccount.Collaterals = ConstructCollateralFromDetails(collateralDetail);
TryUpdateCollateralRetitleDate(repoAccount, recoveryCase);
Log.Information($"Created Repo-Account for Not-Found Account. AccountNo: {account.AccountIdentifier}");
return repoAccount;
}
public List<Borrower> ConstructRepoAccountBorrowers(LoanDetail coreAccount)
{
List<Borrower> borrowers = new List<Borrower>();
try
{
foreach (var coreAccountBorrower in coreAccount.Borrowers)
{
var borrower = new Borrower()
{
IndividualName = coreAccountBorrower.Name,
IndividualId = coreAccountBorrower.IndividualId.ToString(),
OwnershipType = (int)ConvertBorrowerType(coreAccountBorrower.Type)
};
borrowers.Add(borrower);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error in ConstructRepoAccountBorrowers");
}
return borrowers;
}
public List<Collateral> ConstructCollateralFromDetails(IReadOnlyCollection<CollateralDetail> collateralDetail)
{
List<Collateral> collateralList = new List<Collateral>();
foreach (var detail in collateralDetail)
{
var collateral = new Collateral()
{
BodyStyle = null,
CollateralTypeId = null,
Description = detail.Description,
Inoperable = null,
RefCollateralId = detail.CollateralId.ToString()
};
if (detail.Other != null)
{
collateral.Vin = detail.Other.MiscellaneousInformation;
}
if (detail.Vehicle != null)
{
collateral.Make = detail.Vehicle.Make;
collateral.Model = detail.Vehicle.Model;
collateral.Vin = detail.Vehicle.VIN;
collateral.Year = detail.Vehicle.Year;
collateral.BodyStyle = detail.Vehicle.BodyStyle;
}
collateralList.Add(collateral);
}
return collateralList;
}
public RepoAccount ConstructRepoAccountFromParts(LoanDetail coreAccount, TemenosAccount account,
TemenosCase recoveryCase)
{
DateTime? lastPaymentDate = null;
DateTime? dateFirstDelinquent = null;
var lastPaymentResult = GetDateTimeFromString(account.DateLastPaid);
if (lastPaymentResult.IsSuccess)
{
lastPaymentDate = lastPaymentResult.Value;
}
else
{
Log.Warning(
$"Construct Repo Account from temenos accountNo: {account.AccountNumber}. Could not parse last payment result : {account.DateLastPaid}.",
lastPaymentResult, account);
}
var dateFirstDelinquentResult = GetDateTimeFromString(account.DateFirstDelinquent);
if (dateFirstDelinquentResult.IsSuccess)
{
dateFirstDelinquent = dateFirstDelinquentResult.Value;
}
else
{
Log.Warning(
$"Construct Repo Account from temenos accountNo: {account.AccountNumber}. failed parsing dateFirstDelinquent {account.DateFirstDelinquent}",
dateFirstDelinquentResult, account);
// This is no longer a "MissingRequirementException", per Bill Johns.
}
var sixtyPlusUserAssignedNumber = GetSixtyPlusUserNumber(account);
var repoAccount = new RepoAccount()
{
DueToPayDate = coreAccount.NextDueDate,
Balance = account.Balance,
AccountTypeCode = account.AccountType.Code,
OrigAccountTypeCode = account.AccountType.Code,
LoanNo = long.Parse(account.AccountIdentifier),
MemberNo = MemberIdFromAccountId(account.AccountIdentifier),
Branch = account.Branch != null ? int.Parse(account.Branch) : 0,
AccountNo = account.AccountIdentifier,
Status = StatusService.ConvertStatus(recoveryCase.RepoStatus),
O4RDate = recoveryCase.CreatedDate,
Collector = account.LoanOfficer,
DateFirstDelinquent = dateFirstDelinquent,
DelinquentAmount = account.DQAmount,
CurrentDelinquentAmount = account.DQAmount,
SubStatus = "",
DaysDelinquent = account.DaysDq,
LastPaymentDate = lastPaymentDate,
LstPmtAmt = account.AmountLastPaid,
OrigAmount = account.OriginalLoanAmount,
RequiredPaymentAmount = account.DQAmount,
PrimaryIndividualId = coreAccount.IndividualId[0].ToString(),
Bankruptcy = $"{account.BankruptcyFlag}",
DateFirstIntellect = recoveryCase.CreatedDate,
ReferenceId = account.AccountId,
ReferenceCaseId = recoveryCase.CaseId,
AccountLastProcessed = DateTime.Now,
ReferenceStatus = account.AccountStatus,
WrittenOffDate = coreAccount.Status == WrittenOff ? coreAccount.WrittenOffDate : (DateTime?)null,
SixtyPlusCollectorId = sixtyPlusUserAssignedNumber
};
return repoAccount;
}
public long MemberIdFromAccountId(string accountId)
{
return _coreDao.RetrieveMemberAsync(accountId).GetAwaiter().GetResult().MemberId;
}
public RepoAccount UpdateRepoAccountFromParts(RepoAccountProcessingSlip slip)
{
RepoAccount originalRepoAccount = slip.ExistingRepoAccount;
TemenosAccount account = slip.RecoveryAccount;
TemenosCase recoveryCase = slip.RecoveryCase;
IReadOnlyCollection<RepoLetter> letters = slip.Letters;
var redemptionExpiredDate = _redemptionExpiredDateCalculator.CalculateAsync(recoveryCase, letters);
DateTime? lastPaymentDate = null;
DateTime? dateFirstDelinquent = originalRepoAccount.DateFirstDelinquent;
var lastPaymentResult = GetDateTimeFromString(account.DateLastPaid);
if (lastPaymentResult.IsSuccess)
{
lastPaymentDate = lastPaymentResult.Value;
}
else
{
Log.Warning(
$"Repo account {originalRepoAccount.RepoId} ({originalRepoAccount.AccountNo}). Could not parse last payment result : {account.DateLastPaid}.",
lastPaymentResult);
}
var dateFirstDelinquentResult = GetDateTimeFromString(account.DateFirstDelinquent);
if (dateFirstDelinquentResult.IsSuccess)
{
dateFirstDelinquent = dateFirstDelinquentResult.Value;
}
else
{
Log.Error(
$"Repo account {originalRepoAccount.RepoId} ({originalRepoAccount.AccountNo}) failed parsing dateFirstDelinquent {account.DateFirstDelinquent}",
dateFirstDelinquentResult);
// This is no longer a "MissingRequirementException", per Bill Johns.
}
var sixtyPlusUserAssignedNumber = GetSixtyPlusUserNumber(account);
var repoAccount = new RepoAccount()
{
RepoId = originalRepoAccount.RepoId,
DueToPayDate = account.DueDate,
Balance = account.Balance,
AccountTypeCode = account.AccountType.Code,
OrigAccountTypeCode = originalRepoAccount.OrigAccountTypeCode,
LoanNo = originalRepoAccount.LoanNo,
MemberNo = originalRepoAccount.MemberNo,
Branch = originalRepoAccount.Branch,
AccountNo = originalRepoAccount.AccountNo,
Status = StatusService.ConvertStatus(recoveryCase.RepoStatus),
O4RDate = originalRepoAccount.O4RDate,
Collector = account.LoanOfficer,
DateFirstDelinquent = dateFirstDelinquent,
DelinquentAmount = account.DQAmount,
CurrentDelinquentAmount = account.DQAmount,
SubStatus = "",
DaysDelinquent = originalRepoAccount.DaysDelinquent, //leave this set to days delinquent when added.
LastPaymentDate = lastPaymentDate,
LstPmtAmt = account.AmountLastPaid ?? 0,
OrigAmount = account.OriginalLoanAmount ?? 0,
RedemptionExpiredDate = redemptionExpiredDate,
RequiredPaymentAmount = account.DQAmount,
PrimaryIndividualId = originalRepoAccount.PrimaryIndividualId,
Bankruptcy = $"{account.BankruptcyFlag}",
Collaterals = originalRepoAccount.Collaterals,
RepoStatusChanges = originalRepoAccount.RepoStatusChanges,
ManualRedemptionDate = originalRepoAccount.ManualRedemptionDate,
DateFirstIntellect = originalRepoAccount.DateFirstIntellect,
Borrowers = originalRepoAccount.Borrowers,
PaidSkipAmount = originalRepoAccount.PaidSkipAmount,
RefundStatus = originalRepoAccount.RefundStatus,
ReferenceId = account.AccountId,
ReferenceCaseId = recoveryCase.CaseId,
AccountLastProcessed = DateTime.Now,
ReferenceStatus = account.AccountStatus,
WrittenOffDate = slip.Loan.Status == WrittenOff ? slip.Loan.WrittenOffDate : (DateTime?)null,
SixtyPlusCollectorId = sixtyPlusUserAssignedNumber
};
return repoAccount;
}
/**
* High Risk function because of string comparison and unknown mapping.
* The old system got the integer from the database.
* The new system gets a string from the web service.
* This maps the new string to the old integer, if borrower types start
* showing up wrong, look here...
*/
public OwnershipType ConvertBorrowerType(string type)
{
if (type != null && string.Equals(type.Trim(), "Borrower", StringComparison.OrdinalIgnoreCase))
{
return OwnershipType.Borrower;
}
if (type != null && string.Equals(type.Trim(), "Co-Borrower", StringComparison.OrdinalIgnoreCase))
{
return OwnershipType.Coborrower;
}
return OwnershipType.Other;
}
public void CheckForRejectedStatusChange(RepoAccount originalRepoAccount, RepoAccount updatedRepoAccount,
Settings settings)
{
if (!IsStatusMatch(originalRepoAccount, updatedRepoAccount.Status) &&
!IsStatusChangeAllowed(originalRepoAccount.Status, updatedRepoAccount.Status,
settings.AllowedStatusChanges))
{
Log.Warning(
$"Status Change Reverted for Account {updatedRepoAccount.AccountNo} from {originalRepoAccount.Status} to {updatedRepoAccount.Status}");
_accountAlertCheckService.ManualAlert(new EventAlert()
{
RepoId = updatedRepoAccount.RepoId,
AccountNo = updatedRepoAccount.AccountNo,
CreatedBy = "Importer",
DateAdded = DateTime.Now,
Message = "Collections System Status Does Not Match Repo Management",
SubMessage =
$"Invalid Status Change from {originalRepoAccount.Status} to {updatedRepoAccount.Status}"
});
updatedRepoAccount.Status = originalRepoAccount.Status;
}
}
public bool IsStatusChangeAllowed(string oldStatus, string newStatus,
IEnumerable<AllowedStatusChange> settingsAllowedStatusChanges)
{
foreach (var allowedStatusChange in settingsAllowedStatusChanges)
{
if (IsStringMatch(allowedStatusChange.CurrentStatus, oldStatus) &&
IsStringMatch(allowedStatusChange.AllowedChangeStatus, newStatus))
{
return true;
}
}
return false;
}
public bool IsStatusMatch(RepoAccount repo, string status)
{
if (repo?.Status == null || status == null)
{
return false;
}
return string.Equals(repo.Status.Trim(), status.Trim(), StringComparison.OrdinalIgnoreCase);
}
public bool IsStringMatch(string x, string y)
{
if (x == null || y == null)
{
return false;
}
return string.Equals(x.Trim(), y.Trim(), StringComparison.OrdinalIgnoreCase);
}
public void UpdateRepoAccountDateClosed(RepoAccount repoAccount)
{
// Close the account after four days in certain statuses
if ((repoAccount.Status == PaidSkipStatus || repoAccount.Status == RedeemedStatus || repoAccount.Status == RepoCancelledStatus)
&& repoAccount.RepoStatusChanges.LastOrDefault(x => x.StatusTo == repoAccount.Status)?.ChangedDate <
DateTime.Now.AddDays(-3).Date)
{
repoAccount.DateClosed = DateTime.Now;
repoAccount.AccountLastProcessed = DateTime.Now;
// TODO calling this still has a side effect.
_eventAlertService.MarkAllAsRead(repoAccount.AccountNo).GetAwaiter().GetResult();
}
}
public void TryUpdateCollateralRetitleDate(RepoAccount repo, TemenosCase temenosCase)
{
DateTime? retitledDate = (temenosCase.TitleOrdered) ? temenosCase.TitleOrderedDate : null;
foreach (Collateral collateral in repo.Collaterals)
{
collateral.RetitledInLandmarkName = retitledDate;
}
}
// This exists to update accounts not originally created from Temenos
public void AddAccountUpdateRecord(RepoAccountProcessingSlip slip)
{
slip.ExistingRepoAccount.ReferenceId = slip.RecoveryAccount.AccountId;
slip.ExistingRepoAccount.ReferenceCaseId = slip.RecoveryCase.CaseId;
slip.ExistingRepoAccount.AccountLastProcessed = DateTime.Now;
}
public void TryUpdateOrCreateRepoCompanyAssignment(RepoAccount repoAccount, TemenosCase temenosCase,
Settings settings)
{
//Temenos may send blank string instead of null
var repoCompanyIdFromCase = string.IsNullOrEmpty(temenosCase.RepoAgentId)
? null
: MapRepoAgentIdToRepoCompanyId(temenosCase.RepoAgentId, settings.RepoCompanies);
foreach (var collateral in repoAccount.Collaterals)
{
var mostRecent = collateral.RepoCompanySelections.OrderByDescending(x => x.DateTimeCreated)
.FirstOrDefault();
// TODO, move to Slip, save elsewhere.
var updated =
UpdateOrCreateRepoCompanyAssignment(collateral, mostRecent, temenosCase, repoCompanyIdFromCase);
// This can no longer happen, but watch for it anyway.
if (updated.Any(repoCompanySelection =>
repoCompanySelection.DateRepoed != null &&
repoCompanySelection.RepoCompanyAssignmentDate == null))
{
Log.Error(
$"Tried to set DateRepoed before RepoCompanyAssignment for accountNo {repoAccount.AccountNo}. Please Investigate.");
throw new MissingRequirementException(
$"RepoCompanyAssignment for repo account {repoAccount.AccountNo} Missing RepoCompanyAssignmentDate",
$"Cannot assign a DateRepoed before a RepoCompanyAssignment Date.");
}
if (updated.Any())
{
_repoCompanySelectionService.SaveChangesAsync(updated).GetAwaiter().GetResult();
}
}
}
public List<RepoCompanySelection> UpdateOrCreateRepoCompanyAssignment(Collateral collateral, RepoCompanySelection mostRecentSelection,
TemenosCase temenosCase, int? repoCompanyIdFromCase)
{
var selectionUpdates = new List<RepoCompanySelection>();
//If this is the first repoCompanySelection create when temenos has data
if (mostRecentSelection == null)
{
if (temenosCase.RepoAgentId != null && temenosCase.RepoType != null &&
temenosCase.DateRepoAssigned != null)
{
var newRepoCompanyEntry = CreateNewRepoCompanyAssignment(collateral, temenosCase, repoCompanyIdFromCase);
collateral.RepoCompanySelections = collateral.RepoCompanySelections.Append(newRepoCompanyEntry).ToList();
selectionUpdates.Add(newRepoCompanyEntry);
}
return selectionUpdates;
}
//If the repoCompany has changed update with temenos data
if (mostRecentSelection.RepoCompanyId != repoCompanyIdFromCase ||
mostRecentSelection.RepoType != temenosCase.RepoType ||
mostRecentSelection.RepoCompanyAssignmentDate != temenosCase.DateRepoAssigned)
{
if (mostRecentSelection.DateRepoed == null)
{
mostRecentSelection.DateRepoed = DateTime.Now;
if (mostRecentSelection.RepoCompanyAssignmentDate == null)
{
mostRecentSelection.RepoCompanyAssignmentDate = mostRecentSelection.DateTimeCreated ?? DateTime.Now;
}
selectionUpdates.Add(mostRecentSelection);
}
var newRepoCompanyEntry = CreateNewRepoCompanyAssignment(collateral, temenosCase, repoCompanyIdFromCase);
collateral.RepoCompanySelections = collateral.RepoCompanySelections.Append(newRepoCompanyEntry).ToList();
selectionUpdates.Add(newRepoCompanyEntry);
return selectionUpdates;
}
//If no need to update or create, sync Repoed Date
mostRecentSelection.DateRepoed = temenosCase.DateRepossessed;
selectionUpdates.Add(mostRecentSelection);
return selectionUpdates;
}
public RepoCompanySelection CreateNewRepoCompanyAssignment(Collateral collateral, TemenosCase temenosCase, int? newRepoCompanyId)
{
// DateRepoAssigned is defaulted to Today. The business (Bill) doesn't want it to throw, if they start having trouble
// with the dates; change this to throw a MissingRequirementException on the field.
var newRepoCompanySelection = new RepoCompanySelection()
{
RepoCompanyId = newRepoCompanyId,
RepoType = temenosCase.RepoType ?? RepoType.Unknown,
RepoCompanyAssignmentDate = temenosCase.DateRepoAssigned ?? DateTime.Now,
DateRepoed = temenosCase.DateRepossessed,
CollateralId = collateral.CollateralId,
DateTimeCreated = DateTime.Now,
Id = null
};
return newRepoCompanySelection;
}
public int? MapRepoAgentIdToRepoCompanyId(string repoAgentId, IReadOnlyCollection<RepoCompany> repoCompanies)
{
try
{
var repoCompany =
repoCompanies.FirstOrDefault(company => company.ReferenceId == int.Parse(repoAgentId));
if (repoCompany != null)
{
return repoCompany.ID;
}
}
catch (Exception ex)
{
Log.Error(ex, $"Error when trying to map RepoAgentID: {repoAgentId} to a RepoCompany ReferenceID", repoCompanies);
}
// If you get this error, ensure there is a matching RepoCompany and that it has a RepoAgentID that matches the config call in Collections API.
throw new MissingRequirementException("RepoCompanyAssignment - RepoCompanyID", $"Ensure a RepoCompany with RepoAgentID: {repoAgentId} is present");
}
public IResult<DateTime?> GetDateTimeFromString(string dtString)
{
if (string.IsNullOrEmpty(dtString))
{
Log.Error("Cannot parse null or empty date string");
return new DateTimeResult()
{
IsSuccess = false,
ErrorMessage = "Cannot Parse Empty Date String",
};
}
try
{
return new DateTimeResult()
{
IsSuccess = true,
Value = DateTime.Parse(dtString)
};
}
catch (Exception ex)
{
Log.Error(ex, $"Cannot Parse Date String {dtString}");
return new DateTimeResult()
{
IsSuccess = false,
ErrorMessage = $"There was an Exception when processing the date string: {dtString}"
};
}
}
public int? GetSixtyPlusUserNumber(TemenosAccount account)
{
try
{
account.CustomFieldMap.TryGetValue(SixtyPLusUserAssignedLabel, out string sixtyPlusUser);
if (String.IsNullOrWhiteSpace(sixtyPlusUser))
{
return null;
}
Int32.TryParse(sixtyPlusUser, out int sixtyPlusUserNumber);
return sixtyPlusUserNumber;
}
catch(NullReferenceException nullEx)
{
Log.Warning($"60+ Collector key not found in Temenos Account for Account Number: {account.AccountNumber}. Error: {nullEx}");
return null;
}
catch(Exception ex)
{
Log.Error($"Error while attempting to Parse 60+ Collector ID for Account Number: {account.AccountNumber}. Error: {ex}");
return null;
}
}
}
}