This commit is contained in:
Zeeshaun
2026-03-17 08:54:34 -05:00
parent 64856c640e
commit cdcba54aa4
330 changed files with 54110 additions and 0 deletions
@@ -0,0 +1,146 @@
using Newtonsoft.Json;
using ReserveManagementSystemApi.Enums;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class AuditHistoryService : IAuditHistoryService
{
private readonly IAuditHistoryRepository _auditHistoryRepository;
const string SYSTEM_MODIFIED_BY = "RMS";
public AuditHistoryService(IAuditHistoryRepository auditHistoryRepository)
{
_auditHistoryRepository = auditHistoryRepository;
}
public async Task<IEnumerable<AuditHistory>> GetAllAsync()
{
try
{
return await _auditHistoryRepository.GetAll().OrderByDescending(o => o.ModifiedDate).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving audit histories.");
throw;
}
}
public async Task<AuditHistory> FindByIdAsync(int id)
{
AuditHistory history;
try
{
history = await _auditHistoryRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving history with id {id}.");
throw;
}
if (history == null)
{
throw new Exception($"Audit history with id {id} not found.");
}
return history;
}
public async Task<AuditHistory> AddHistoryAsync(AuditHistory history)
{
return await _auditHistoryRepository.AddHistoryAsync(history);
}
public async Task<int> AddAuditHistoriesForNonexistingLoansAsync(List<Loan> loans)
{
var currentDate = DateTime.Now;
var histories = loans.Select(loan => new AuditHistory
{
LoanId = null,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.Error.ToString(),
AuditDescription = "Loan in closed file not found",
OldValue = null,
NewValue = null
}).ToList();
return await _auditHistoryRepository.AddHistoriesAsync(histories);
}
public AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan)
{
var currentDate = DateTime.Now;
return new AuditHistory
{
LoanId = null,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.Error.ToString(),
AuditDescription = "Loan in closed import not found",
OldValue = null,
NewValue = null
};
}
public AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan)
{
var currentDate = DateTime.Now;
return new AuditHistory
{
LoanId = loan.LoanId,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.NewLoanUpdate.ToString(),
AuditDescription = "Loan found in table, record updated",
OldValue = JsonConvert.SerializeObject(originalLoan),
NewValue = JsonConvert.SerializeObject(loan)
};
}
public AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description)
{
return new AuditHistory
{
LoanId = loan.LoanId,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = DateTime.Now,
ActivityType = ActivityType.LoanStatusChange.ToString(),
OldValue = oldStatus,
NewValue = loan.LoanStatus,
AuditDescription = description
};
}
public async Task<int> AddHistoriesAsync(List<AuditHistory> histories)
{
return await _auditHistoryRepository.AddHistoriesAsync(histories);
}
public async Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description)
{
var auditHist = new AuditHistory
{
MemberNumber = memberNumber,
LoanNumber = loanNumber,
ActivityType = ActivityType.Error.ToString(),
ModifiedDate = DateTime.Now,
AuditDescription = description,
ModifiedBy = SYSTEM_MODIFIED_BY
};
await _auditHistoryRepository.AddHistoryAsync(auditHist);
}
}
}
@@ -0,0 +1,100 @@
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class DealerService: IDealerService
{
private readonly IDealerRepository _dealerRepository;
public DealerService(IDealerRepository dealerRepository)
{
_dealerRepository = dealerRepository;
}
public async Task<IEnumerable<Dealer>> Search(string dealerSearchText)
{
try
{
return await _dealerRepository.GetAll().Where(d => d.DealerCode.Contains(dealerSearchText) || d.Name.Contains(dealerSearchText)).OrderBy(x => x.DealerCode).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving dealers.");
throw;
}
}
public async Task<Dealer> GetDealerByCodeAsync(string dealerCode)
{
try
{
return await _dealerRepository.FindByCodeAsync(dealerCode);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving dealer by code.");
throw;
}
}
public async Task<IEnumerable<Dealer>> GetAllDealersAsync()
{
return await _dealerRepository.GetAll().ToListAsync();
}
public async Task UpdateDealersAsync(List<Dealer> dealers)
{
try
{
var allDealers = await GetAllDealersAsync();
var dealersToAdd = new List<Dealer>();
var dealersToUpdate = new List<Dealer>();
dealers.ForEach(d =>
{
var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == d.DealerOrganizationNumber);
if (currentDealer != null && !currentDealer.Equals(d))
{
currentDealer.Name = d.Name ?? currentDealer.Name;
currentDealer.DealerCode = d.DealerCode?.ToUpper() ?? currentDealer.DealerCode;
currentDealer.DealerAccountNumber = d.DealerAccountNumber ?? currentDealer.DealerAccountNumber;
currentDealer.DealerOrganizationNumber = d.DealerOrganizationNumber ?? currentDealer.DealerOrganizationNumber;
dealersToUpdate.Add(currentDealer);
}
else if (currentDealer == null)
{
//write the new dealer record to set Action required to true if invalid
dealersToAdd.Add(d);
}
});
if (dealersToUpdate.Any())
{
await _dealerRepository.UpdateDealersAsync(dealersToUpdate);
}
if (dealersToAdd.Any())
{
await _dealerRepository.AddDealersAsync(dealersToAdd);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure updating dealers.");
throw;
}
}
public async Task UpdateDealersAsync(List<Loan> loans)
{
var dealers = loans.Select(l => new Dealer
{
DealerCode = l.Dealer.DealerCode,
Name = l.Dealer.Name,
DealerOrganizationNumber = l.DealerOrganizationNumber,
DealerAccountNumber = l.Dealer.DealerAccountNumber
})
.GroupBy(d => d.DealerOrganizationNumber).Select(g => g.First()).ToList();
await UpdateDealersAsync(dealers);
}
}
}
@@ -0,0 +1,275 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using ReserveManagementSystemApi.Mappers;
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
using ReserveManagementSystemLib.Configuration;
namespace ReserveManagementSystemApi.Services
{
public class ExportService: IExportService
{
private readonly ILoanRepository _loanRepository;
private readonly ICoreApiServiceClient _coreApiServiceClient;
private readonly IMinorAccountTypeRepository _minorAccountTypeRepository;
private readonly RmsApiConfiguration _rmsApiConfig;
public ExportService(ILoanRepository loanRepository, ICoreApiServiceClient coreApiServiceClient, IMinorAccountTypeRepository minorAccountTypeRepository, RmsApiConfiguration rmsApiConfig)
{
_loanRepository = loanRepository;
_coreApiServiceClient = coreApiServiceClient;
_minorAccountTypeRepository = minorAccountTypeRepository;
_rmsApiConfig = rmsApiConfig;
}
public async Task<int> ExportReservesAsync(DateTime currentDate)
{
var numberOfLoansUpdated = 0;
try
{
var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedReserveDate == null).ToListAsync();
var groupedLoanRequests = loansToUpdate.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList();
var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests);
if (exportResult != null || exportResult.Any())
{
var loansExported = new List<Loan>();
loansToUpdate.ForEach(loan =>
{
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == loan.LoanNumber).FirstOrDefault();
if (validLoan != null && validLoan.WasSuccessful)
{
loan.ExportedReserveDate = currentDate;
numberOfLoansUpdated += 1;
loan.Adjustment = 0;
loansExported.Add(loan); //update successful loan export with exported date
}
else
{
Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}");
}
});
await _loanRepository.UpdateAsync(loansExported);
Log.Information($"Exported {numberOfLoansUpdated} reserves");
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting loan reserves.");
throw;
}
return numberOfLoansUpdated;
}
public async Task<int> ExportChargebacksAsync(DateTime currentDate)
{
var numberOfLoansUpdated = 0;
try
{
var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedChargebackDate == null && l.PaidLoanDate != null).ToListAsync();
var groupedLoanRequests = new List<GroupedMonetaryTransactionRequest>();
loansToUpdate.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType));
}
);
var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests);
if (exportResult != null || exportResult.Any())
{
var loansExported = new List<Loan>();
loansToUpdate.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == accountType.GeneralLedgerAccountNumber).FirstOrDefault();
if (validLoan != null && validLoan.WasSuccessful)
{
loan.ExportedChargebackDate = currentDate;
numberOfLoansUpdated += 1;
loan.Adjustment = 0;
loansExported.Add(loan); //update successful loan export with exported date
}
else
{
Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}");
}
});
await _loanRepository.UpdateAsync(loansExported);
Log.Information($"Exported {numberOfLoansUpdated} chargebacks");
}
else
{
Log.Error("Failure exporting loan chargebacks.");
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting loan chargebacks.");
throw;
}
return numberOfLoansUpdated;
}
public async Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans)
{
var loansExported = new List<ImportBoatLoan>();
try
{
var reserveLoans = loans.Where(l => l.ReserveAmount != 0);
if (reserveLoans != null || reserveLoans.Any())
{
var groupedReserveRequests = reserveLoans.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList();
var exportReserveResult = await SendBatchListMonetaryTransactionRequests(groupedReserveRequests);
loansExported.AddRange(MapBoatExportResults(exportReserveResult, reserveLoans.ToList()));
}
var chargebackLoans = loans.Where(l => l.ChargeBack != 0).ToList();
if (chargebackLoans != null || chargebackLoans.Any())
{
var groupedLoanRequests = new List<GroupedMonetaryTransactionRequest>();
chargebackLoans.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType));
}
);
var exportResult = await _coreApiServiceClient.PostMonetaryTransactions(groupedLoanRequests);
loansExported.AddRange(MapBoatExportResults(exportResult, chargebackLoans.ToList()));
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting boat loans.");
throw;
}
return loansExported;
}
public async Task<int> MarkReservesAsExportedAsync(List<Loan> loans, DateTime currentDate)
{
try
{
loans.ForEach(l =>
{
l.ExportedReserveDate = currentDate;
});
await _loanRepository.UpdateAsync(loans);
Log.Information($"Marked {loans.Count} loan reserves as exported (without exporting)");
}
catch (Exception ex)
{
Log.Error(ex, "Failure marking loan reserves as exported.");
throw;
}
return loans.Count;
}
public async Task<int> MarkReservesAndChargebacksAsExportedAsync(List<Loan> loans, DateTime currentDate)
{
try
{
loans.ForEach(l =>
{
l.ExportedReserveDate = currentDate;
l.ExportedChargebackDate = currentDate;
});
await _loanRepository.UpdateAsync(loans);
Log.Information($"Marked {loans.Count} loan reserves and chargebacks as exported (without exporting)");
}
catch (Exception ex)
{
Log.Error(ex, "Failure marking loan reserves and chargebacks as exported.");
throw;
}
return loans.Count;
}
private List<ImportBoatLoan> MapBoatExportResults(List<MonetaryTransactionResponse> exportResult, List<Loan> loansToUpdate)
{
var numberOfLoansUpdated = 0;
var loansExported = new List<ImportBoatLoan>();
if (exportResult != null || exportResult.Any())
{
loansToUpdate.ForEach(loan =>
{
var boatLoan = new ImportBoatLoan();
string acccountNumber;
if (loan.ReserveAmount != 0)
{
acccountNumber = loan.LoanNumber;
}
else
{
var accountType = GetMinorAccountType(loan.AccountType);
acccountNumber = accountType.GeneralLedgerAccountNumber;
}
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == acccountNumber).Any();
var validDealer = exportResult.Where(e => e.AccountNumber.ToString() == loan.Dealer.DealerAccountNumber).Any();
if (validLoan && validDealer)
{
numberOfLoansUpdated += 1;
boatLoan.LoanNumber = loan.LoanNumber;
boatLoan.WasSuccessful = true;
}
else
{
var error = $"Failure to export boat loan account number: {loan.LoanNumber}";
boatLoan.LoanNumber = loan.LoanNumber;
boatLoan.WasSuccessful = false;
boatLoan.ErrorMessage = error;
Log.Error(error);
}
loansExported.Add(boatLoan);
});
Log.Information($"Exported {numberOfLoansUpdated} boat loans");
}
return loansExported;
}
private MinorAccountType GetMinorAccountType(string loanAccountType)
{
var result = _minorAccountTypeRepository.GetMinorAccountType(loanAccountType);
return result;
}
public async Task<List<MonetaryTransactionResponse>> SendBatchListMonetaryTransactionRequests(List<GroupedMonetaryTransactionRequest> groupedLoanRequests)
{
var result = new List<MonetaryTransactionResponse>();
var batchSize = _rmsApiConfig.DefaultBatchSize;
foreach (var batch in BatchHelper.CreateBatch<GroupedMonetaryTransactionRequest>(groupedLoanRequests, batchSize))
{
var response = await _coreApiServiceClient.PostMonetaryTransactions(batch.ToList());
result.AddRange(response);
};
return result;
}
}
}
@@ -0,0 +1,25 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IAuditHistoryService
{
Task<IEnumerable<AuditHistory>> GetAllAsync();
Task<AuditHistory> FindByIdAsync(int id);
Task<AuditHistory> AddHistoryAsync(AuditHistory history);
Task<int> AddAuditHistoriesForNonexistingLoansAsync(List<Loan> loans);
AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan);
AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan);
AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description);
Task<int> AddHistoriesAsync(List<AuditHistory> histories);
Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description);
}
}
@@ -0,0 +1,17 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IDealerService
{
Task<IEnumerable<Dealer>> Search(string dealerSearch);
Task<Dealer> GetDealerByCodeAsync(string dealerCode);
Task UpdateDealersAsync(List<Dealer> dealers);
Task UpdateDealersAsync(List<Loan> loans);
Task<IEnumerable<Dealer>> GetAllDealersAsync();
}
}
@@ -0,0 +1,18 @@
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IExportService
{
Task<int> ExportReservesAsync(DateTime currentDate);
Task<int> ExportChargebacksAsync(DateTime currentDate);
Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans);
Task<int> MarkReservesAsExportedAsync(List<Loan> loans, DateTime currentDate);
Task<int> MarkReservesAndChargebacksAsExportedAsync(List<Loan> loans, DateTime currentDate);
}
}
@@ -0,0 +1,28 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IImportReadService
{
Task<Import> ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false);
Task<Import> ImportClosedLoansFromDNA(string newerThanDateTime);
Task<Import> ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport);
Task<Import> ProcessActiveLoanImport(List<IndirectLoanDetail> loanDetails, DateTime currentTime, bool skipExport);
Task<Import> ProcessClosedLoanImport(List<ClosedIndirectLoan> closedLoans, DateTime currentTime, string newerThanDateTime);
Task<Import> ProcessClosedLoansWithUpdates(List<IndirectLoanDetail> dnaLoans, DateTime currentTime, bool skipExport);
Task<Import> ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime);
Task<List<ImportBoatLoan>> ReadBoatLoanImportFile(StreamReader importFile);
Task<Import> ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime);
}
}
@@ -0,0 +1,20 @@
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IImportService
{
Task<IEnumerable<Import>> GetAllAsync();
Task<IEnumerable<Import>> GetRecentAsync(int numberOfImports);
Task<Import> GetByIdAsync(int id);
Task<Import> AddImportAsync(Import import);
Task<Import> UpdateImportAsync(Import import);
Task<Import> GetLastLoanImportByType(ImportType importType);
}
}
@@ -0,0 +1,13 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface ILoanCalculationService
{
LoanReserve CalculateSingleReserve(Loan loan);
LoanChargeback CalculateSingleChargeback(Loan loan);
Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportedReserveDate = null);
}
}
@@ -0,0 +1,31 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface ILoanService
{
Task<IEnumerable<Loan>> GetAllAsync();
Task<PaginatedListResponse> GetFilteredAsync(LoanFilter filter);
Task<Loan> GetByIdAsync(int id);
Task<LoanReserve> GetLoanReserveAsync(LoanReserveInput input);
Task<LoanChargeback> GetLoanChargebackAsync(LoanChargebackInput input);
Task<List<Loan>> SaveLoansAsync(List<Loan> loans);
Task<List<Loan>> SaveWithChargebackAsync(List<Loan> loans);
Task<List<Loan>> SaveWithReserveAndChargebackAsync(List<Loan> loans);
Task<List<string>> SearchMembers(string searchText);
Task<List<string>> SearchLoanNumbers(string searchText);
Task<List<string>> SearchApplicationIds(string searchText);
Task UpdateDealerOriginalBalanceAsync(List<string> accountNumbers);
}
}
@@ -0,0 +1,9 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IRmsActiveLoanService
{
Task HandleRmsActiveImportRequest(RmsActiveImportRequest request);
}
}
@@ -0,0 +1,8 @@
namespace ReserveManagementSystemApi.Services
{
interface IScheduleService
{
Task RunImportExportActive(CancellationToken cancellationToken);
Task RunImportExportClosed(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,7 @@
using LCU.Library.Internal.StatusModels.Models;
namespace ReserveManagementSystemApi.Services;
public interface IStatusService
{
EnvironmentView GetEnviromentView();
}
@@ -0,0 +1,10 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IVehicleService
{
Task<Vehicle> GetVehicleByLoanIdAsync(int? loanId);
}
}
@@ -0,0 +1,402 @@
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Helpers;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemApi.Enums;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
namespace ReserveManagementSystemApi.Services
{
public class ImportReadService : IImportReadService
{
private readonly ILoanService _loanService;
private readonly IImportService _importService;
private readonly IDealerService _dealerService;
private readonly IExportService _exportService;
private readonly ICoreApiServiceClient _coreApiServiceClient;
private readonly RmsApiConfiguration _rmsApiConfig;
private ImportHelper _importHelper = new ImportHelper();
public ImportReadService(ILoanService loanService,
IImportService importService,
IDealerService dealerService,
IExportService exportService,
ICoreApiServiceClient coreApiServiceClient,
RmsApiConfiguration rmsApiConfig)
{
_loanService = loanService;
_importService = importService;
_dealerService = dealerService;
_exportService = exportService;
_coreApiServiceClient = coreApiServiceClient;
_rmsApiConfig = rmsApiConfig;
}
public async Task<Import> ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false)
{
if (string.IsNullOrEmpty(newerThanDateTime))
{
Import lastImport = await _importService.GetLastLoanImportByType(ImportType.Active);
newerThanDateTime = GetStartDateForNextImport(lastImport);
}
Log.Information("Get Active Loans By Posted Date is starting.");
var dnaLoans = await _coreApiServiceClient.GetIndirectLoansByPostedDate(newerThanDateTime);
Log.Information($"Get Active Loans By Posted Date has ended. Found {dnaLoans.Count()} active loan records.");
Log.Information("Starting ProcessActiveLoanImport");
var import = await ProcessActiveLoanImport(dnaLoans, DateTime.Now, skipExport);
Log.Information("ProcessActiveLoanImport has ended");
return import;
}
public async Task<Import> ImportClosedLoansFromDNA(string newerThanDateTime = "")
{
if (string.IsNullOrEmpty(newerThanDateTime))
{
var lastImport = await _importService.GetLastLoanImportByType(ImportType.Closed);
newerThanDateTime = GetStartDateForNextImport(lastImport);
}
var dnaLoans = await _coreApiServiceClient.GetClosedLoansByPostedDate(newerThanDateTime);
var import = await ProcessClosedLoanImport(dnaLoans, DateTime.Now, newerThanDateTime);
return import;
}
public async Task<Import> ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport)
{
var dnaLoans = await _coreApiServiceClient.GetClosedLoansWithDetailByClosedDate(newerThanDateTime);
var newImport = new Import();
try
{
newImport = await ProcessClosedLoansWithUpdates(dnaLoans, DateTime.Now, skipExport);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import closed loans from DNA.");
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending))
.Error(ex, $"Reserve Management System Failure. " +
$"Error while importing Closed Loans from DNA.");
throw;
}
return newImport;
}
public async Task<Import> ProcessActiveLoanImport(List<IndirectLoanDetail> loanDetails, DateTime currentTime, bool skipExport = false)
{
var newImport = new Import
{
ImportType = ImportType.Active.ToString(),
StartDate = currentTime
};
newImport = await _importService.AddImportAsync(newImport);
try
{
var loans = _importHelper.TransformLoanFromDNA(loanDetails, newImport.ImportId); //Transform import to loan object
await _dealerService.UpdateDealersAsync(loans);
var allDealers = await _dealerService.GetAllDealersAsync();
loans.ForEach(loan =>
{
var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == loan.DealerOrganizationNumber);
loan.Dealer = currentDealer; // assign the loan the updated dealer then check validation in save method
});
await SaveLoansExportAndImportAsync(newImport, loans, currentTime, skipExport);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import active loans from DNA.");
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending))
.Error(ex, $"Reserve Management System Failure. " +
$"Error while importing Active Loans from DNA.");
throw;
}
return newImport;
}
public async Task<Import> ProcessClosedLoansWithUpdates(List<IndirectLoanDetail> dnaLoans, DateTime currentTime, bool skipExport)
{
var closedImport = new Import
{
ImportType = ImportType.Closed.ToString(),
StartDate = currentTime
};
closedImport = await _importService.AddImportAsync(closedImport);
var loans = _importHelper.TransformLoanFromDNA(dnaLoans, closedImport.ImportId);
await _dealerService.UpdateDealersAsync(loans);
var loansClosed = await _loanService.SaveWithReserveAndChargebackAsync(loans);
if (!skipExport)
{
//prevent converted loans from exporting
DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate);
var convertedLoansMarkExported = loansClosed.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "Closed").ToList();
await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime);
//update dealer original balance with only non-converted loans
await _exportService.ExportReservesAsync(DateTime.Now);
var nonConvertedLoans = loansClosed.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList();
var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers);
await _exportService.ExportChargebacksAsync(DateTime.Now);
}
else
{
await _exportService.MarkReservesAndChargebacksAsExportedAsync(loansClosed, DateTime.Now);
}
closedImport.EndDate = DateTime.Now;
closedImport.RecordCount = loansClosed.Count;
closedImport.RecordsSaved = loansClosed.Count;
closedImport.Success = true;
await _importService.UpdateImportAsync(closedImport);
return closedImport;
}
public async Task<Import> ProcessClosedLoanImport(List<ClosedIndirectLoan> closedLoans, DateTime currentTime, string newerThanDateTime)
{
List<Loan> loansUpdated;
var closedImport = new Import
{
ImportType = ImportType.Closed.ToString(),
StartDate = currentTime
};
closedImport = await _importService.AddImportAsync(closedImport);
try
{
loansUpdated = await CloseLoans(closedLoans, closedImport);
var needDetails = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList();
if (needDetails.Count > 0)
{
await ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, false);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import closed loans from DNA.");
throw;
}
return closedImport;
}
public async Task<Import> ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime)
{
var loansFromImport = new List<ImportNewLoan>();
var newImport = new Import
{
FileName = fileName.Truncate(50),
StartDate = currentTime,
ImportType = ImportType.Active.ToString()
};
newImport = await _importService.AddImportAsync(newImport);
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
TrimOptions = TrimOptions.Trim,
HeaderValidated = null,
MissingFieldFound = null
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ImportNewLoan>().ToList();
}
var dealers = loansFromImport.Select(l => new Dealer
{
DealerOrganizationNumber = l.DealerOrganizationNumber
})
.GroupBy(d => d.DealerOrganizationNumber).First().ToList();
await _dealerService.UpdateDealersAsync(dealers);
var loans = _importHelper.TransformNewLoan(loansFromImport, newImport.ImportId); //Transform import to loan object
await SaveLoansExportAndImportAsync(newImport, loans, currentTime);
}
catch (Exception ex)
{
Log.Error("Failure to read new loan file, message: {0} {1}", ex.Message, ex.StackTrace);
throw;
}
return newImport;
}
public async Task<List<ImportBoatLoan>> ReadBoatLoanImportFile(StreamReader importFile)
{
var loansFromImport = new List<ImportBoatLoan>();
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
TrimOptions = TrimOptions.Trim,
HeaderValidated = null,
MissingFieldFound = null,
BadDataFound = null
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ImportBoatLoan>().ToList();
}
var loans = _importHelper.TransformBoatLoan(loansFromImport);
loansFromImport = await ExportBoatLoansAsync(loans);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to read boat loan file");
throw;
}
return loansFromImport;
}
public async Task<Import> ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime)
{
var loansFromImport = new List<ClosedIndirectLoan>();
List<Loan> loansUpdated;
var closedImport = new Import
{
FileName = fileName.Truncate(50),
StartDate = currentTime,
ImportType = ImportType.Closed.ToString()
};
closedImport = await _importService.AddImportAsync(closedImport);
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
TrimOptions = TrimOptions.Trim,
PrepareHeaderForMatch = args => args.Header.ToLower()
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ClosedIndirectLoan>().ToList();
}
loansUpdated = await CloseLoans(loansFromImport, closedImport);
Log.Information($"Closed {loansUpdated.Count} loans");
await _exportService.ExportChargebacksAsync(DateTime.Now);
}
catch (Exception ex)
{
Log.Error("Failure to read closed loan file, message: {0} {1}", ex.Message, ex.StackTrace);
throw;
}
return closedImport;
}
private async Task<List<Loan>> CloseLoans(List<ClosedIndirectLoan> loansFromImport, Import closedImport)
{
var loans = _importHelper.TransformClosedLoans(loansFromImport, closedImport.ImportId).ToList();
var loansUpdated = await _loanService.SaveWithChargebackAsync(loans);
var missingActiveLoans = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList();
await _exportService.ExportChargebacksAsync(DateTime.Now);
closedImport.EndDate = DateTime.Now;
closedImport.RecordCount = loansFromImport.Count;
closedImport.RecordsSaved = loansUpdated.Count - missingActiveLoans.Count;
closedImport.Success = true;
await _importService.UpdateImportAsync(closedImport);
Log.Information($"Closed {loansUpdated.Count} loans");
return loansUpdated;
}
private async Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans, bool skipExport = false)
{
var boatResults = new List<ImportBoatLoan>();
if (!skipExport)
{
boatResults = await _exportService.ExportBoatLoansAsync(loans);
var reserveLoans = loans.Where(l => l.ReserveAmount != 0);
var listAccounts = reserveLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccounts);
}
Log.Information($"Imported {loans.Count} boat loans");
return boatResults;
}
private string GetStartDateForNextImport(Import lastImport)
{
if (lastImport?.StartDate != null)
{
return lastImport.StartDate.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
}
return _rmsApiConfig.DefaultImportStartDate;
}
private async Task SaveLoansExportAndImportAsync(Import import, List<Loan> loans, DateTime currentTime, bool skipExport = false)
{
var loansImported = await _loanService.SaveLoansAsync(loans);
if (!skipExport)
{
//prevent converted loans from exporting
DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate);
var convertedLoansMarkExported = loansImported.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "New").ToList();
await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime);
//update dealer original balance with only non-converted loans
await _exportService.ExportReservesAsync(currentTime);
var nonConvertedLoans = loansImported.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList();
var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers);
}
else
{
await _exportService.MarkReservesAsExportedAsync(loansImported, currentTime);
}
Log.Information($"Imported {loansImported.Count} active loans");
import.EndDate = DateTime.Now;
import.RecordCount = loansImported.Count;
import.RecordsSaved = loansImported.Count;
import.Success = true;
await _importService.UpdateImportAsync(import);
}
}
}
@@ -0,0 +1,89 @@
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class ImportService: IImportService
{
private readonly IImportRepository _importRepository;
public ImportService(IImportRepository importRepository)
{
_importRepository = importRepository;
}
public async Task<IEnumerable<Import>> GetAllAsync()
{
try
{
return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving imports.");
throw;
}
}
public async Task<Import> GetLastLoanImportByType(ImportType importType)
{
Import import;
try
{
import = await _importRepository.GetLastLoanDnaImportByType(importType).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving last import.");
throw;
}
return import;
}
public async Task<IEnumerable<Import>> GetRecentAsync(int numberOfImports)
{
try
{
return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).Take(numberOfImports).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving imports.");
throw;
}
}
public async Task<Import> GetByIdAsync(int id)
{
Import import;
try
{
import = await _importRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving import with id {id}.");
throw;
}
if (import == null)
{
throw new Exception($"Import with id {id} not found.");
}
return import;
}
public async Task<Import> AddImportAsync(Import newImport)
{
return await _importRepository.AddImportAsync(newImport);
}
public async Task<Import> UpdateImportAsync(Import import)
{
return await _importRepository.UpdateImportAsync(import);
}
}
}
@@ -0,0 +1,314 @@
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public class LoanCalculationService : ILoanCalculationService
{
private readonly LoanCalculationHelper _loanCalculationHelper;
private readonly BoatLoanCalculationHelper _boatLoanCalculationHelper;
private CalculationConfig _calculationConfig;
public LoanCalculationService()
{
_loanCalculationHelper = new LoanCalculationHelper();
_boatLoanCalculationHelper = new BoatLoanCalculationHelper();
}
public LoanReserve CalculateSingleReserve(Loan loan)
{
var result = new LoanReserve();
try
{
_calculationConfig = new CalculationConfig(loan);
if (!loan.IsBoatLoan(loan))
{
result = ReserveCalculation(loan);
}
else
{
result = BoatReserveCalculation(loan);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan: {0} {1}", e.Message, e.StackTrace);
result.ReserveTypeMessage = "Exception while calculating reserve";
}
return result;
}
public LoanChargeback CalculateSingleChargeback(Loan loan)
{
var result = new LoanChargeback();
try
{
_calculationConfig = new CalculationConfig(loan);
if (!loan.IsBoatLoan(loan))
{
result = ChargebackCalculation(loan);
}
else
{
result = BoatChargebackCalculation(loan);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Chargeback: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportReserveDate = null)
{
try
{
_calculationConfig = new CalculationConfig(loan);
var loanReserve = new LoanReserve();
if (!loan.IsBoatLoan(loan))
{
loanReserve = ReserveCalculation(loan);
}
else
{
loanReserve = BoatReserveCalculation(loan);
}
loan = AssignReserveValues(loan, loanReserve);
if (originalReserve.HasValue)
{
CalculateReserveAdjustment(loan, originalReserve, originalExportReserveDate);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Reserve: {0} {1}", e.Message, e.StackTrace);
loan.ReserveMessage = "Exception while calculating reserve";
}
return loan;
}
private static void CalculateReserveAdjustment(Loan loan, decimal? originalReserve, DateTime? originalExportReserveDate)
{
var updatedReserveSaved = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var updatedReserveRounded = Math.Round(updatedReserveSaved, 2, MidpointRounding.AwayFromZero);
var originalReserveRounded = Math.Round(originalReserve.Value, 2, MidpointRounding.AwayFromZero);
if (updatedReserveRounded != originalReserveRounded)
{
loan.Adjustment = (updatedReserveRounded - originalReserveRounded);
loan.ExportedReserveDate = null; //we only want to remove last export date if reserve amount changed
}
else
{
loan.ExportedReserveDate = originalExportReserveDate;
}
}
private LoanReserve ReserveCalculation(Loan loanData)
{
var loanReserve = new LoanReserve();
_calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate%
_calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate%
if (loanData.ContractRate < loanData.BuyRate)
{
//Calculate Buy Down.
return _loanCalculationHelper.CalculateBuyDown(_calculationConfig);
}
else
{
// Calculate Reserve Amount for all other loans.
if ((double)loanData.ContractRate == (double)loanData.BuyRate)
{
loanReserve = _loanCalculationHelper.CalculateNoMarkup(_calculationConfig);
}
else
{
loanReserve = _loanCalculationHelper.CalculateStandardMarkup(_calculationConfig);
}
//Assign any Flat Fees if applicable
return _loanCalculationHelper.AssignFlatFee(loanReserve, _calculationConfig);
}
}
private LoanReserve BoatReserveCalculation(Loan loanData)
{
if (loanData.ContractRate < loanData.BuyRate)
{
_calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate%
_calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate%
return _loanCalculationHelper.CalculateBuyDown(_calculationConfig);
}
else
{
// Calculate Reserve Amount for all other loans.
return _boatLoanCalculationHelper.CalculateStandardMarkup(_calculationConfig);
}
}
private Loan AssignReserveValues(Loan loan, LoanReserve loanReserve)
{
loan.ReserveAmount = loanReserve.ReserveAmount;
loan.ReserveMessage = loanReserve.ReserveTypeMessage;
loan.Term = _calculationConfig.LoanData().Term;
loan.FlatFeePaid = loanReserve.FlatFeePaid;
return loan;
}
private LoanChargeback ChargebackCalculation(Loan loanData)
{
if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN"))
{
return SetBuyDownChargeback(loanData);
}
if (loanData.LoanType?.ChargebackMethod == "S")
{
return CalculateBBLoans(loanData);
}
else
{
return CalculateLoanWithChargebackDate(loanData);
}
}
private LoanChargeback BoatChargebackCalculation(Loan loanData)
{
if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN"))
{
return SetBuyDownChargeback(loanData);
}
else
{
return CalculateBoatLoanWithChargebackDate(loanData);
}
}
private LoanChargeback CalculateBoatLoanWithChargebackDate(Loan loanData)
{
var numOfChargebackPayments = loanData.Term <= 63 ? 6 : 12; //term is 63 or less chargeback date is 6 monthly payments, otherwise 12
var lastChargebackDate = _boatLoanCalculationHelper.GetLastChargebackDate(loanData.FirstPmtDate, numOfChargebackPayments);
if (loanData.PaidLoanDate > lastChargebackDate)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
return ChargebackWithRemainingMonths(loanData);
}
private LoanChargeback CalculateLoanWithChargebackDate(Loan loanData)
{
var numOfChargebackPayments = loanData.LoanType?.NoChargebackPayments ?? 3;
var numOfChargebackDays = loanData.LoanType?.NoChargebackDays ?? 90;
var lastChargebackDate = _loanCalculationHelper.GetLastChargebackDate(loanData.LoanDate, loanData.FirstPmtDate, numOfChargebackPayments, numOfChargebackDays);
if (loanData.PaidLoanDate > lastChargebackDate)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
return ChargebackWithRemainingMonths(loanData);
}
private LoanChargeback ChargebackWithRemainingMonths(Loan loanData)
{
int remainingMonths = 0;
string message = "Paid On or Before Chargeback Date";
remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData);
return new LoanChargeback
{
//this needs to be the original term formula
ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths),
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : message,
RemainingMonthsAtClose = (byte)remainingMonths
};
}
private LoanChargeback ChargebackWithRemainingMonthsBB(Loan loanData)
{
int remainingMonths = 0;
string message = "Paid After Chargeback Date";
//add chargeback date for BB Loans
var chargeBackDate = loanData.FirstPmtDate.AddMonths(loanData.Term - 1);
if (loanData.PaidLoanDate <= chargeBackDate)
{
remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData);
if (remainingMonths <= 0)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = message,
RemainingMonthsAtClose = 0
};
}
else
{
message = "Paid On or Before Chargeback Date";
}
}
return new LoanChargeback
{
ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths),
ChargebackTypeMessage = message,
RemainingMonthsAtClose = (byte)remainingMonths
};
}
private LoanChargeback CalculateBBLoans(Loan loanData)
{
if (loanData.ReserveAmount != 0)
{
return ChargebackWithRemainingMonthsBB(loanData);
}
else
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = "No Reserve - No Chargeback",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
}
private LoanChargeback SetBuyDownChargeback(Loan loanData)
{
//Buydown scenarios will not calculate a chargeback and return a message indicating this.
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = "Buy Down Reserve - No Chargeback",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
}
}
@@ -0,0 +1,448 @@
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;
}
}
}
}
@@ -0,0 +1,9 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services.QueueServices
{
public interface IQueueRmsActiveLoanService
{
Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request);
}
}
@@ -0,0 +1,36 @@
using LCU.Library.Internal.LcuMassTransitModels;
using MassTransit;
using Newtonsoft.Json;
using ReserveManagementSystemApi.Configurations;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services.QueueServices
{
public class QueueRmsActiveLoanService : IQueueRmsActiveLoanService
{
private readonly ISendEndpoint _sendEndpoint;
private readonly ReceiveEndpointConfiguration _rmsActiveLoanImportReceiveEndpoint;
public QueueRmsActiveLoanService(IBusControl busControl, ReceiveEndpointConfigurations receiveEndpointConfigurations)
{
_rmsActiveLoanImportReceiveEndpoint = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint;
_sendEndpoint = busControl.GetSendEndpoint(_rmsActiveLoanImportReceiveEndpoint.ReceiveEndpointUri).Result;
}
public async Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request)
{
try
{
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await _sendEndpoint.Send(request, cancellationTokenSource.Token);
}
catch (Exception exception)
{
var message = "Failed to Queue RmsActiveLoanImport Request. " +
$"RmsActiveLoanImport: {JsonConvert.SerializeObject(request)}";
Log.Error(exception, message);
throw new ServiceException($"{message}, Exception Message: {exception.Message}");
}
}
}
}
@@ -0,0 +1,31 @@
using ReserveManagementSystemApi.Services.QueueServices;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public class RmsActiveLoanService : IRmsActiveLoanService
{
private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService;
private readonly IImportReadService _importReadService;
public RmsActiveLoanService(
IQueueRmsActiveLoanService queueRmsActiveLoanService, IImportReadService importReadService
)
{
_queueRmsActiveLoanService = queueRmsActiveLoanService;
_importReadService = importReadService;
}
public async Task HandleRmsActiveImportRequest(RmsActiveImportRequest request)
{
try
{
await _importReadService.ImportActiveLoansFromDNA(request.ImportDateTime, request.SkipExport);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Active Loans from DNA");
throw new ServiceException($"Exception Message: {ex.Message} - Failure importing Active Loans from DNA");
}
}
}
}
@@ -0,0 +1,42 @@
namespace ReserveManagementSystemApi.Services
{
public class ScheduleService : IScheduleService
{
private readonly IImportReadService _importReadService;
private readonly IExportService _exportService;
public ScheduleService(IImportReadService importReadService, IExportService exportService)
{
_importReadService = importReadService;
_exportService = exportService;
}
public async Task RunImportExportActive(CancellationToken cancellationToken)
{
try
{
Log.Information("Importing active loans from DNA");
var activeImport = await _importReadService.ImportActiveLoansFromDNA(string.Empty);
Log.Information($"{activeImport.RecordsSaved} active loans were saved");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to perform scheduled active loan import.");
}
}
public async Task RunImportExportClosed(CancellationToken cancellationToken)
{
try
{
Log.Information("Importing closed loans from DNA");
var closedImport = await _importReadService.ImportClosedLoansFromDNA(string.Empty);
Log.Information($"{closedImport.RecordsSaved} loans were closed");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to perform scheduled closed loan import.");
}
}
}
}
@@ -0,0 +1,28 @@
using LCU.Library.Internal.Status.Services;
using LCU.Library.Internal.StatusModels.Models;
namespace ReserveManagementSystemApi.Services;
public class StatusService : IStatusService
{
private readonly IEnvironmentViewService _environmentViewService;
public StatusService(IEnvironmentViewService environmentViewService)
{
_environmentViewService = environmentViewService;
}
public EnvironmentView GetEnviromentView()
{
try
{
var environmentView = _environmentViewService.BuildEnvironmentView();
Log.Verbose("Status Environment View Heartbeat. EnvironmentView: {@EnvironmentView}", environmentView);
return environmentView;
}
catch (Exception exception)
{
Log.Error(exception, $"Failure Getting Enviroment View Information.");
throw new ServiceException($"Failure Getting Enviroment View Information.");
}
}
}
@@ -0,0 +1,29 @@
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class VehicleService: IVehicleService
{
private readonly IVehicleRepository _vehicleRepository;
public VehicleService(IVehicleRepository vehicleRepository)
{
_vehicleRepository = vehicleRepository;
}
public async Task<Vehicle> GetVehicleByLoanIdAsync(int? loanId)
{
try
{
return await _vehicleRepository.FindByLoanIdAsync(loanId);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving vehicle by loan id.");
throw;
}
}
}
}