Files
monorepo/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanService.cs
T
2026-03-17 08:54:34 -05:00

449 lines
19 KiB
C#

using System.Linq.Dynamic.Core;
using ReserveManagementSystemApi.Enums;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Helpers;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class LoanService : ILoanService
{
private readonly ILoanRepository _loanRepository;
private readonly ILoanTypeRepository _loanTypeRepository;
private readonly IAuditHistoryService _auditHistoryService;
private ILoanCalculationService _loanCalculationService;
private const double MonthInDays = 30.4375;
private readonly ICoreApiServiceClient _coreApiServiceClient;
public LoanService(ILoanRepository loanRepository, ILoanTypeRepository loanTypeRepository, IAuditHistoryService auditHistoryService, ICoreApiServiceClient coreApiServiceClient)
{
_loanRepository = loanRepository;
_loanTypeRepository = loanTypeRepository;
_auditHistoryService = auditHistoryService;
_coreApiServiceClient = coreApiServiceClient;
_loanCalculationService = new LoanCalculationService();
}
public async Task<IEnumerable<Loan>> GetAllAsync()
{
try
{
return await _loanRepository.GetAll().OrderByDescending(x => x.ImportId).ThenBy(y => y.MemberNumber).ThenBy(z => z.LoanNumber).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving loans.");
throw;
}
}
public async Task<Loan> GetByIdAsync(int id)
{
Loan loan;
try
{
loan = await _loanRepository.FindByIdAsync(id);
if (loan != null)
{
loan.ValidationErrors = ValidationHelper.SetValidationErrors(loan);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving loan with id {id}.");
throw;
}
if (loan == null)
{
throw new Exception($"Loan with id {id} not found.");
}
return loan;
}
public async Task<PaginatedListResponse> GetFilteredAsync(LoanFilter filter)
{
try
{
IQueryable<Loan> loans;
PaginatedListResponse pagedListResponse = new PaginatedListResponse();
var query = _loanRepository.GetAll();
var filteredQuery = filter.ApplyFilter(query);
List<string> listOfFieldNames = typeof(Loan).GetProperties().Select(f => f.Name).ToList();
listOfFieldNames.AddRange(typeof(Dealer).GetProperties().Select(f => $"Dealer.{f.Name}").ToList());
listOfFieldNames.AddRange(typeof(Vehicle).GetProperties().Select(f => $"Vehicle.{f.Name}").ToList());
var result = listOfFieldNames.Where(n => n == filter.SortField).FirstOrDefault();
if (!string.IsNullOrEmpty(filter.SortField) && result != null)
{
loans = filteredQuery.OrderBy($"{filter.SortField} {filter.SortDirection}, ImportId desc, MemberNumber, LoanNumber");
}
else
{
loans = filteredQuery.OrderBy($"ImportId desc, MemberNumber, LoanNumber");
}
pagedListResponse = await PaginatedListResponse.CreateListAsync(loans, filter.PageIndex, filter.MaxRecordsPerPage, filter.MaxPagesPerRange);
return pagedListResponse;
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving loans with filter.");
throw;
}
}
public async Task<LoanReserve> GetLoanReserveAsync(LoanReserveInput input)
{
var result = new LoanReserve();
var loan = new Loan
{
LoanDate = input.LoanDate,
FirstPmtDate = input.FirstPmtDate,
LoanAmount = input.LoanAmount,
LoanTypeId = input.LoanType,
BuyRate = input.BuyRate,
ContractRate = input.ContractRate,
Term = (int)Math.Round(Convert.ToDecimal(input.OriginalTermInDays) / (decimal)MonthInDays, 0),
AccountType = input.AccountType
};
loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId);
try
{
_loanCalculationService = new LoanCalculationService();
result = _loanCalculationService.CalculateSingleReserve(loan);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public async Task<LoanChargeback> GetLoanChargebackAsync(LoanChargebackInput input)
{
var result = new LoanChargeback();
var loan = new Loan
{
LoanDate = input.LoanDate,
FirstPmtDate = input.FirstPmtDate,
FinalPmtDate = input.FinalPmtDate,
LoanTypeId = input.LoanType,
ReserveAmount = input.ReserveAmount,
PaidLoanDate = input.PaidLoanDate,
Term = input.Term,
AccountType = input.AccountType
};
loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId);
try
{
result = _loanCalculationService.CalculateSingleChargeback(loan);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public async Task<List<Loan>> SaveLoansAsync(List<Loan> loans)
{
var loansToAddOrUpdate = new List<Loan>();
var historyRecords = new List<AuditHistory>();
var loanTypes = await _loanTypeRepository.GetLoanTypesAsync();
var numberOfLoansToUpdateReserve = 0;
var numberOfLoansToAdd = 0;
var numberOfLoansToUpdate = 0;
try
{
Log.Information($"Have {loans.Count} loans to add or update");
loans.ForEach(loan =>
{
var validationErrors = ValidationHelper.SetValidationErrors(loan);
loan.ActionRequired = validationErrors.Count > 0;
loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId);
var originalLoan = _loanRepository.GetAll().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan != null) //have an updated loan
{
if (!loan.Equals(originalLoan) || (loan.Vehicle != null && !loan.Vehicle.Equals(originalLoan.Vehicle)))
{
numberOfLoansToUpdate++;
loan.LoanId = originalLoan.LoanId;
var needToUpdateReserve = !loan.HasSameReserve(originalLoan);
if (needToUpdateReserve)
{
numberOfLoansToUpdateReserve++;
loan = _loanCalculationService.CalculateLoanReserve(loan, originalLoan.ReserveAmount, originalLoan.ExportedReserveDate);
}
historyRecords.Add(_auditHistoryService.GetExistingLoanUpdateHistoryAsync(loan, originalLoan));
CopyFromDnaLoanToOriginalLoan(loan, originalLoan, needToUpdateReserve);
originalLoan.LoanStatus = LoanStatus.Updated.ToString();
loansToAddOrUpdate.Add(originalLoan);
}
}
else //have a new loan
{
numberOfLoansToAdd++;
loan = _loanCalculationService.CalculateLoanReserve(loan);
loan.Dealer = null;
loan.LoanStatus = LoanStatus.New.ToString();
loansToAddOrUpdate.Add(loan);
}
});
Log.Information($"{numberOfLoansToUpdate} loans were updated");
Log.Information($"{numberOfLoansToUpdateReserve} loans had reserve recalculated");
Log.Information($"{numberOfLoansToAdd} loans were added");
if (loansToAddOrUpdate.Any())
{
await _loanRepository.UpdateAsync(loansToAddOrUpdate);
}
if (historyRecords.Any())
{
await _auditHistoryService.AddHistoriesAsync(historyRecords);
}
}
catch (Exception e)
{
Log.Error("Failure to Save Loans: {0} {1}", e.Message, e.StackTrace);
throw;
}
return loansToAddOrUpdate;
}
public async Task<List<Loan>> SaveWithChargebackAsync(List<Loan> loans)
{
var historiesForClosedLoans = new List<AuditHistory>();
var historiesForLoansNotFound = new List<AuditHistory>();
var loansToClose = new List<Loan>();
var description = "Importing closed loan changed loan status";
loans.ForEach(loan =>
{
var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan != null) //have an updated loan
{
if (!originalLoan.PaidLoanDate.HasValue || originalLoan.PaidLoanDate != loan.PaidLoanDate)
{
var oldStatus = originalLoan.LoanStatus;
originalLoan.PaidLoanDate = loan.PaidLoanDate;
originalLoan.ImportId = loan.ImportId;
var chargebackResult = _loanCalculationService.CalculateSingleChargeback(originalLoan);
AssignChargebackResult(originalLoan, chargebackResult);
historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(originalLoan, oldStatus, description));
loansToClose.Add(originalLoan);
}
}
else //active loan not found to close
{
//set this temporary status to flag selecting original loan with details in order to calculate both reserve and chargeback
loan.LoanStatus = LoanStatus.MissingActiveLoan.ToString();
loansToClose.Add(loan);
historiesForLoansNotFound.Add(_auditHistoryService.GetAuditHistoryForNonexistingClosedLoan(loan));
}
});
await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans);
await _auditHistoryService.AddHistoriesAsync(historiesForLoansNotFound);
await _loanRepository.UpdateAsync(loansToClose.Where(l => l.LoanStatus != "MissingActiveLoan"));
return loansToClose;
}
/// <summary>This method was used for one time import, we want to save the original reserve info for an already closed loan pre Go-Live. </summary>
public async Task<List<Loan>> SaveWithReserveAndChargebackAsync(List<Loan> loans)
{
try
{
var loanTypes = await _loanTypeRepository.GetLoanTypesAsync();
var historiesForClosedLoans = new List<AuditHistory>();
var loansToClose = new List<Loan>();
var description = "Importing closed loan changed loan status";
loans.ForEach(loan =>
{
var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan == null)
{
loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId);
loan = _loanCalculationService.CalculateLoanReserve(loan, null, null);
var chargebackResult = _loanCalculationService.CalculateSingleChargeback(loan);
AssignChargebackResult(loan, chargebackResult);
loansToClose.Add(loan);
historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(loan, "", description));
}
});
await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans);
await _loanRepository.UpdateAsync(loansToClose);
return loansToClose;
}
catch (Exception ex)
{
Log.Error(ex.Message);
throw;
}
}
public async Task<List<string>> SearchMembers(string searchText)
{
var memberNumbers = new List<string>();
try
{
memberNumbers = await _loanRepository.GetAll().Where(x => x.MemberNumber.StartsWith(searchText))
.Select(y => y.MemberNumber)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of members.");
throw;
}
return memberNumbers.OrderBy(x => long.Parse(x)).ToList();
}
public async Task<List<string>> SearchLoanNumbers(string searchText)
{
var loanNumbers = new List<string>();
try
{
loanNumbers = await _loanRepository.GetAll().Where(x => x.LoanNumber.StartsWith(searchText))
.Select(x => x.LoanNumber)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of loan numbers.");
throw;
}
return loanNumbers.OrderBy(x => long.Parse(x)).ToList();
}
public async Task<List<string>> SearchApplicationIds(string searchText)
{
var applicationIds = new List<string>();
try
{
applicationIds = await _loanRepository.GetAll().Where(x => x.ApplicationId.StartsWith(searchText))
.Select(x => x.ApplicationId)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of application ids.");
throw;
}
return applicationIds.OrderBy(x => long.Parse(x)).ToList();
}
public async Task UpdateDealerOriginalBalanceAsync(List<string> accountNumbers)
{
foreach(var account in accountNumbers)
{
var exportResult = await _coreApiServiceClient.PutDealerOriginalBalance(account);
if (exportResult != null)
{
foreach (var result in exportResult)
if (!string.IsNullOrEmpty(result.ErrorMessage))
{
Log.Error($"Failure to update dealer original balance loan: {result.AccountNumber} {result.ErrorMessage} ");
}
}
else //if nothing is returned all updates succeeded
{
Log.Information($"Update to dealers original balances completed successfully");
}
}
}
private static void AssignChargebackResult(Loan loan, LoanChargeback chargebackResult, bool skipExport = false)
{
loan.ChargeBack = Math.Round(chargebackResult.ChargebackAmount, 4);
loan.ChargebackMessage = chargebackResult.ChargebackTypeMessage;
loan.RemainingMonthsAtClose = chargebackResult.RemainingMonthsAtClose;
loan.ActionRequired = false;
//if skipping export then loan will already have ExportedChargebackDate set
loan.ExportedChargebackDate = skipExport ? loan.ExportedChargebackDate : null;
loan.LoanStatus = LoanStatus.Closed.ToString();
loan.Dealer = null;
}
private static void CopyFromDnaLoanToOriginalLoan(Loan loan, Loan originalLoan, bool needToUpdateReserve)
{
originalLoan.ActionRequired = loan.ActionRequired;
originalLoan.ImportId = loan.ImportId;
originalLoan.MemberNumber = loan.MemberNumber;
originalLoan.MemberFirstName = loan.MemberFirstName;
originalLoan.MemberInitial = loan.MemberInitial;
originalLoan.MemberLastName = loan.MemberLastName;
originalLoan.DealerOrganizationNumber = loan.DealerOrganizationNumber;
originalLoan.LoanDate = loan.LoanDate;
originalLoan.PostedDate = loan.PostedDate;
originalLoan.FirstPmtDate = loan.FirstPmtDate;
originalLoan.FinalPmtDate = loan.FinalPmtDate;
originalLoan.LoanAmount = loan.LoanAmount;
originalLoan.BuyRate = loan.BuyRate;
originalLoan.ContractRate = loan.ContractRate;
originalLoan.MonthlyPmt = loan.MonthlyPmt;
originalLoan.LoanTypeId = loan.LoanTypeId;
originalLoan.ApplicationId = loan.ApplicationId;
//suppressing these insurance updates until DNA has correct value
//originalLoan.Warranty = loan.Warranty;
//originalLoan.Gap = loan.Gap;
originalLoan.CreditLife = loan.CreditLife;
originalLoan.CreditDisability = loan.CreditDisability;
//originalLoan.Mbp = loan.Mbp;
originalLoan.AccountType = loan.AccountType;
if (originalLoan.Vehicle != null)
{
originalLoan.Vehicle.VINNumber = loan.Vehicle?.VINNumber;
originalLoan.Vehicle.Make = loan.Vehicle?.Make;
originalLoan.Vehicle.Model = loan.Vehicle?.Model;
originalLoan.Vehicle.Mileage = loan.Vehicle?.Mileage ?? 0;
originalLoan.Vehicle.Year = loan.Vehicle?.Year;
originalLoan.Vehicle.Color = loan.Vehicle?.Color;
}
if (needToUpdateReserve)
{
originalLoan.ReserveAmount = loan.ReserveAmount;
originalLoan.ReserveMessage = loan.ReserveMessage;
originalLoan.ExportedReserveDate = loan.ExportedReserveDate;
originalLoan.Term = loan.Term;
originalLoan.FlatFeePaid = loan.FlatFeePaid;
originalLoan.Adjustment = loan.Adjustment;
}
}
}
}