449 lines
19 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|