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> 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 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 GetFilteredAsync(LoanFilter filter) { try { IQueryable loans; PaginatedListResponse pagedListResponse = new PaginatedListResponse(); var query = _loanRepository.GetAll(); var filteredQuery = filter.ApplyFilter(query); List 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 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 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> SaveLoansAsync(List loans) { var loansToAddOrUpdate = new List(); var historyRecords = new List(); 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> SaveWithChargebackAsync(List loans) { var historiesForClosedLoans = new List(); var historiesForLoansNotFound = new List(); var loansToClose = new List(); 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; } /// This method was used for one time import, we want to save the original reserve info for an already closed loan pre Go-Live. public async Task> SaveWithReserveAndChargebackAsync(List loans) { try { var loanTypes = await _loanTypeRepository.GetLoanTypesAsync(); var historiesForClosedLoans = new List(); var loansToClose = new List(); 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> SearchMembers(string searchText) { var memberNumbers = new List(); 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> SearchLoanNumbers(string searchText) { var loanNumbers = new List(); 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> SearchApplicationIds(string searchText) { var applicationIds = new List(); 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 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; } } } }