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,11 @@
namespace ReserveManagementSystemApi.Configurations
{
public class ApiConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public string[] CorsAllowedOrigins { get; set; }
public FeatureFlags FeatureFlags { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace ReserveManagementSystemApi.Configurations
{
public class FeatureFlags
{
public bool RunRmsActiveLoanConsumer { get; set; }
public bool EnableImportExportActive { get; set; }
public bool EnableImportExportClosed { get; set; }
}
}
@@ -0,0 +1,9 @@
using LCU.Library.Internal.LcuMassTransitModels;
namespace ReserveManagementSystemApi.Configurations
{
public class ReceiveEndpointConfigurations
{
public ReceiveEndpointConfiguration RmsActiveLoanReceiveEndpoint { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace ReserveManagementSystemApi.Configurations
{
public class SqlConnectionConfiguration
{
public string ConnectionString { get; set; }
public int MaxRetryCount { get; set; }
public TimeSpan MaxRetryDelay { get; set; }
}
}
@@ -0,0 +1,40 @@
using MassTransit;
using LCU.Api.Internal.MaidBotLib.Models;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.Consumers;
public class RmsActiveImportConsumer : IConsumer<RmsActiveImportRequest>
{
private readonly IServiceScopeFactory _scopeFactory;
public RmsActiveImportConsumer(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task Consume(ConsumeContext<RmsActiveImportRequest> context)
{
try
{
Log.Debug("Starting RmsActiveImportConsumer.");
using var scope = _scopeFactory.CreateScope();
await Task.Run(async () => await scope.ServiceProvider.GetRequiredService<IRmsActiveLoanService>().HandleRmsActiveImportRequest(context.Message));
Log.Debug("Ending RmsActiveImportConsumer.");
}
catch (ServiceException)
{
throw;
}
catch (Exception exception)
{
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Fatal(exception, $"Unexpected RmsActiveImportConsumer Failure. " +
$"The RmsActiveImportRequest is placed on ReserveManagementSystemApi.ActiveImport_error.");
throw;
}
}
}
@@ -0,0 +1,34 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuditHistoryController : ControllerBase
{
private readonly IAuditHistoryService _auditHistoryService;
public AuditHistoryController(IAuditHistoryService auditHistoryService)
{
_auditHistoryService = auditHistoryService;
}
[HttpGet(Name = "GetAllHistory")]
[ProducesResponseType(typeof(IEnumerable<AuditHistory>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _auditHistoryService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Audit Histories");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,51 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DealerController : ControllerBase
{
private readonly IDealerService _dealerService;
public DealerController(IDealerService dealerService)
{
_dealerService = dealerService;
}
[HttpGet("Search/{dealerSearchText}", Name = "Search")]
[ProducesResponseType(typeof(IEnumerable<Dealer>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Search(string dealerSearchText)
{
try
{
var result = await _dealerService.Search(dealerSearchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning dealers");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetByDealerCode/{dealerCode}", Name = "GetDealerByCode")]
[ProducesResponseType(typeof(Dealer), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetByDealerCode(string dealerCode)
{
try
{
var result = await _dealerService.GetDealerByCodeAsync(dealerCode);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning dealer");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,183 @@
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemApi.Services.QueueServices;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ImportController : ControllerBase
{
private readonly IImportReadService _importReadService;
private readonly IImportService _importService;
private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService;
public ImportController(IImportReadService importReadService, IImportService importService, IQueueRmsActiveLoanService queueRmsActiveLoanService)
{
_importReadService = importReadService;
_importService = importService;
_queueRmsActiveLoanService = queueRmsActiveLoanService;
}
[HttpGet(Name = "GetAllImports")]
[ProducesResponseType(typeof(IEnumerable<Import>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _importService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Imports");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetRecentImports/{numberOfImports}", Name = "GetRecentImports")]
[ProducesResponseType(typeof(IEnumerable<Import>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetRecent(int numberOfImports)
{
try
{
var result = await _importService.GetRecentAsync(numberOfImports);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Imports");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetById/{importId}", Name = "GetImportById")]
[ProducesResponseType(typeof(Loan), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetById(int importId)
{
try
{
var result = await _importService.GetByIdAsync(importId);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan with id {importId}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportNewLoanFile", Name ="ImportNewLoanFile")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportNewLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadNewLoanImportFile(stream, file.FileName, DateTime.Now);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import new loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportBoatLoanFile", Name = "ImportBoatLoanFile")]
[ProducesResponseType(typeof(List<ImportBoatLoan>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportBoatLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadBoatLoanImportFile(stream);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import boat loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportClosedLoanFile", Name = "ImportClosedLoanFile")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadClosedLoanImportFile(stream, file.FileName, DateTime.Now);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import closed loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportActiveLoans", Name = "ImportActiveLoans")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportActiveLoans(string? newerThanDateTime = "", bool skipExport = false)
{
try
{
var request = new RmsActiveImportRequest()
{
ImportDateTime = newerThanDateTime,
SkipExport = skipExport
};
await _queueRmsActiveLoanService.QueueRmsActiveLoanServiceRequest(request);
return Accepted();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Active Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportClosedLoans", Name = "ImportClosedLoans")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoans(string? newerThanDateTime)
{
try
{
var import = await _importReadService.ImportClosedLoansFromDNA(newerThanDateTime);
return Ok(import);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Closed Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportClosedLoansWithUpdates", Name = "ImportClosedLoansWithUpdates")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoansWithUpdates(string? newerThanDateTime, bool skipExport = false)
{
try
{
var import = await _importReadService.ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, skipExport);
return Ok(import);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,146 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoanController : ControllerBase
{
private readonly ILoanService _loanService;
public LoanController(ILoanService loanService)
{
_loanService = loanService;
}
[HttpGet(Name = "GetAllLoans")]
[ProducesResponseType(typeof(IEnumerable<Loan>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _loanService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("SearchLoans", Name = "SearchLoans")]
[ProducesResponseType(typeof(PaginatedListResponse), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Post([FromBody]LoanFilter filter)
{
try
{
var result = await _loanService.GetFilteredAsync(filter);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Filtered Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchMembers/{searchText}", Name = "SearchMembers")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchMembers(string searchText)
{
try
{
var result = await _loanService.SearchMembers(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning members");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchLoanNumbers/{searchText}", Name = "SearchLoanNumbers")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchLoanNumbers(string searchText)
{
try
{
var result = await _loanService.SearchLoanNumbers(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan numbers");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchApplicationIds/{searchText}", Name = "SearchApplicationIds")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchApplicationIds(string searchText)
{
try
{
var result = await _loanService.SearchApplicationIds(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning application ids");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetById/{loanId}", Name = "GetLoanById")]
[ProducesResponseType(typeof(Loan), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetById(int loanId)
{
try
{
var result = await _loanService.GetByIdAsync(loanId);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan with id {loanId}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("CalculateReserve", Name = "CalculateReserve")]
public async Task<LoanReserve> Post([FromBody] LoanReserveInput input)
{
try
{
return await _loanService.GetLoanReserveAsync(input);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace);
return null;
}
}
[HttpPost("CalculateChargeback", Name = "CalculateChargeback")]
public async Task<LoanChargeback> Post([FromBody] LoanChargebackInput input)
{
try
{
return await _loanService.GetLoanChargebackAsync(input);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace);
return null;
}
}
}
}
@@ -0,0 +1,33 @@
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class StatusController : ControllerBase
{
private readonly IStatusService _statusService;
public StatusController(IStatusService statusService)
{
_statusService = statusService;
}
[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
return Ok(_statusService.GetEnviromentView());
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,10 @@
namespace ReserveManagementSystemApi.Enums
{
public enum ActivityType
{
Warning,
Error,
LoanStatusChange,
NewLoanUpdate
}
}
@@ -0,0 +1,10 @@
namespace ReserveManagementSystemApi.Enums
{
public enum LoanStatus
{
New,
Updated,
Closed,
MissingActiveLoan
}
}
@@ -0,0 +1,27 @@
namespace ReserveManagementSystemApi.Helpers
{
public static class BatchHelper
{
public static IEnumerable<IEnumerable<TSource>> CreateBatch <TSource>(
List<TSource> source, int batchSize)
{
var items = new TSource[batchSize];
var count = 0;
foreach (var item in source)
{
items[count++] = item;
if (count == batchSize)
{
yield return items;
items = new TSource[batchSize];
count = 0;
}
}
if (count > 0)
{
yield return items.Take(count);
}
}
}
}
@@ -0,0 +1,56 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class BoatLoanCalculationHelper
{
private LoanReserve loanReserve = new LoanReserve();
public BoatLoanCalculationHelper()
{
}
public LoanReserve CalculateStandardMarkup(CalculationConfig cc)
{
var markup = cc.LoanData().ContractRate - cc.LoanData().BuyRate;
loanReserve.ReserveTypeMessage = "Standard Reserve";
switch (markup)
{
case decimal d when (d >= (decimal)0.00 && d <= (decimal)0.49):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).01;
break;
case decimal d when (d >= (decimal)0.50 && d <= (decimal)0.74):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).02;
break;
case decimal d when (d >= (decimal)0.75 && d <= (decimal)0.99):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).03;
break;
case decimal d when (d >= (decimal)1.00 && d <= (decimal)1.49):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).04;
break;
case decimal d when (d >= (decimal)1.50 && d <= (decimal)1.99):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).05;
break;
case decimal d when (d == (decimal)2.00):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06;
break;
case decimal d when (d > (decimal)2.00):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06;
loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup";
break;
default:
loanReserve.ReserveAmount = 0;
loanReserve.ReserveTypeMessage = "No Reserve";
break;
}
return loanReserve;
}
internal DateTime GetLastChargebackDate (DateTime firstPmtDate, int numOfChargebackPayments)
{
return firstPmtDate.AddMonths(numOfChargebackPayments - 1);
}
}
}
@@ -0,0 +1,210 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class ImportHelper
{
private string _firstName;
private string _middleInitial;
private const double MonthInDays = 30.4375;
public ImportHelper()
{
}
public List<Loan> TransformNewLoan(List<Models.ImportNewLoan> iImportLoans, int iImportId)
{
var result = new List<Loan>();
foreach (var iLoan in iImportLoans)
{
Loan loan = new Loan();
loan.ImportId = iImportId;
loan.MemberNumber = iLoan.MemberNumber;
loan.LoanNumber = iLoan.LoanNumber;
SplitFirstMiddleName(iLoan.FirstAndMiddleName);
loan.MemberFirstName = _firstName?.ToUpper();
loan.MemberInitial = _middleInitial?.ToUpper();
loan.MemberLastName = iLoan.LastName?.ToUpper();
loan.DealerOrganizationNumber = iLoan.DealerOrganizationNumber;
loan.LoanDate = iLoan.LoanDate;
loan.PostedDate = iLoan.PostedDate;
loan.FirstPmtDate = iLoan.FirstPmtDate;
loan.FinalPmtDate = iLoan.FinalPmtDate;
loan.LoanAmount = iLoan.LoanAmount;
loan.BuyRate = Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero);
loan.ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero);
loan.MonthlyPmt = iLoan.MonthlyPmt;
loan.LoanTypeId = iLoan.LoanType;
loan.ChargeBack = null;
loan.PaidLoanDate = null;
loan.ApplicationId = iLoan.ApplicationId;
loan.AccountType = iLoan.AccountType;
loan.Gap = !string.IsNullOrEmpty(iLoan.Gap) && Convert.ToBoolean(iLoan.Gap);
loan.Warranty = !string.IsNullOrEmpty(iLoan.Warranty) && Convert.ToBoolean(iLoan.Warranty);
loan.CreditLife = !string.IsNullOrEmpty(iLoan.CreditLife) && Convert.ToBoolean(iLoan.CreditLife);
loan.CreditDisability = !string.IsNullOrEmpty(iLoan.CreditDisability) && Convert.ToBoolean(iLoan.CreditDisability);
loan.Mbp = !string.IsNullOrEmpty(iLoan.Mbp) && Convert.ToBoolean(iLoan.Mbp);
loan.Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0);
Vehicle vehicle = new Vehicle();
vehicle.VINNumber = !string.IsNullOrEmpty(iLoan.VINNumber) ? iLoan.VINNumber : null;
vehicle.Make = iLoan.Make;
vehicle.Model = iLoan.Model;
vehicle.Year = iLoan.Year;
vehicle.Mileage = ValidateMileage(iLoan.Mileage);
vehicle.Color = iLoan.Color;
loan.Vehicle = vehicle;
result.Add(loan);
}
return result;
}
public List<Loan> TransformLoanFromDNA(List<IndirectLoanDetail> importLoans, int iImportId)
{
var result = new List<Loan>();
if (!importLoans.Any())
{
return result;
}
foreach (var iLoan in importLoans)
{
Loan loan = new Loan
{
ImportId = iImportId,
MemberNumber = iLoan.MemberNumber,
LoanNumber = iLoan.LoanNumber,
MemberFirstName = iLoan.MemberFirstName?.ToUpper(),
MemberInitial = iLoan.MemberInitial?.ToUpper(),
MemberLastName = iLoan.MemberLastName?.ToUpper(),
DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty,
LoanDate = iLoan.LoanDate,
FirstPmtDate = iLoan.FirstPmtDate,
FinalPmtDate = iLoan.FinalPmtDate,
LoanAmount = iLoan.LoanAmount,
BuyRate = (iLoan.BuyRate != 0) ? Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero) : 0,
ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero),
MonthlyPmt = iLoan.MonthlyPmt,
LoanTypeId = iLoan.LoanType,
ApplicationId = iLoan.ApplicationId,
Gap = iLoan.GAP,
Warranty = iLoan.Warranty,
CreditLife = iLoan.CreditLife,
CreditDisability = iLoan.CreditDisability,
Mbp = iLoan.Mbp,
AccountType = iLoan.AccountType,
PaidLoanDate = iLoan.PaidLoanDate,
Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0)
};
if (iLoan.PostedDate == DateTime.MinValue)
{
loan.PostedDate = iLoan.LoanDate;
}
else
{
loan.PostedDate = iLoan.PostedDate;
}
loan.Vehicle = new Vehicle
{
VINNumber = !string.IsNullOrEmpty(iLoan.Vin) ? iLoan.Vin : null,
Make = iLoan.Make,
Model = iLoan.Model,
Year = iLoan.Year.ToString(),
Mileage = ValidateMileage(iLoan.Mileage.ToString()),
Color = iLoan.Color
};
loan.Dealer = new Dealer
{
DealerCode = (!String.IsNullOrWhiteSpace(iLoan.DealerCode)) && (iLoan.DealerCode.Length <= 4) ? iLoan.DealerCode : string.Empty,
Name = (!String.IsNullOrWhiteSpace(iLoan.DealerName)) ? iLoan.DealerName : string.Empty,
DealerAccountNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerAccountNumber)) ? iLoan.DealerAccountNumber : string.Empty,
DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty
};
result.Add(loan);
}
return result;
}
public List<Loan> TransformBoatLoan(List<ImportBoatLoan> importLoans)
{
var result = new List<Loan>();
if (!importLoans.Any())
{
return result;
}
foreach (var iLoan in importLoans)
{
Loan loan = new Loan
{
LoanNumber = iLoan.LoanNumber,
ReserveAmount = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "FALSE") ? iLoan.Amount : 0,
ChargeBack = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "TRUE") ? iLoan.Amount : 0,
Adjustment = (!String.IsNullOrWhiteSpace(iLoan.IsAdjustment) && iLoan.IsAdjustment.ToUpper() == "TRUE") ? iLoan.Amount : 0,
AccountType = iLoan.AccountType
};
loan.Dealer = new Dealer
{
DealerAccountNumber = iLoan.DealerAccountNumber
};
result.Add(loan);
}
return result;
}
private double ValidateMileage(string mileage)
{
double convertedMileage;
if (double.TryParse(mileage, out convertedMileage))
{
return convertedMileage;
}
return convertedMileage;
}
public IEnumerable<Loan> TransformClosedLoans(List<ClosedIndirectLoan> importLoans, int importId)
{
return importLoans.Select(x => new Loan
{
ImportId = importId,
LoanNumber = x.LoanNumber,
PaidLoanDate = x.PaidLoanDate
//will update LoanStatus when saving
});
}
private void SplitFirstMiddleName(string name)
{
//initial first name last name rules with first name character max length set in DB as 20, and middle initial max as 1
_middleInitial = String.Empty;
_firstName = name;
string[] names = name.Trim().Split(" ");
if (names != null && names.Length > 1) //when true there is a middle initial
{
_firstName = _firstName.Substring(0, name.Length - names.Last().Length).Trim();
_middleInitial = names.Last().Substring(0, 1);
}
else
{
_firstName = name; //no middle initial
}
}
}
}
@@ -0,0 +1,120 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class LoanCalculationHelper
{
private decimal _daysInterest;
private LoanReserve loanReserve = new LoanReserve();
public LoanCalculationHelper()
{
}
public LoanReserve CalculateBuyDown(CalculationConfig cc)
{
// Case - Buy down Below Threshold where difference in rates is less than or equal to 0.25 percent
if (cc.BuydownRateDifference() <= cc.NoReserveThreshold()) //we already know it's positive
{
loanReserve.ReserveAmount = 0;
loanReserve.ReserveTypeMessage = "No Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
// Case - This is a regular Buy Down
_daysInterest = cc.BuydownRateDifference() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = -(cc.BuyFinanceCharge - cc.ContractFinanceCharge + _daysInterest);
loanReserve.ReserveTypeMessage = "Buy Down Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
public LoanReserve CalculateNoMarkup(CalculationConfig cc)
{
// Case - Contract rate is equal to the Buy rate
_daysInterest = cc.StdRate() * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = (cc.ContractFinanceCharge - cc.BuyFinanceCharge + _daysInterest);
loanReserve.ReserveTypeMessage = "No Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
public LoanReserve CalculateStandardMarkup(CalculationConfig cc)
{
// Case - Contract rate is greater than Buy rate
_daysInterest = (1 - cc.ReserveDiscount()/100) * cc.StdRate() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = ((1 - cc.ReserveDiscount() / 100) * (cc.ContractFinanceCharge - cc.BuyFinanceCharge) + _daysInterest);
if ((double)cc.StdRate() <= 2.5)
{
loanReserve.ReserveTypeMessage = "Standard Reserve";
}
else
{
loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup";
}
return loanReserve;
}
public LoanReserve AssignFlatFee(LoanReserve loanReserve, CalculationConfig cc)
{
loanReserve.FlatFeePaid = false;
if (loanReserve.ReserveAmount < (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100)))
{
loanReserve.ReserveAmount = (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100));
loanReserve.ReserveTypeMessage = "Flat Fee Reserve";
loanReserve.FlatFeePaid = true;
}
return loanReserve;
}
public decimal FinanceCharge(int term, decimal loanAmount, decimal rate)
{
if (rate == 0) { return 0; }
double paymentRate = (double)rate / (double)1200;
decimal rateFactor = (decimal)Math.Pow((1 + paymentRate), (double)term);
return (loanAmount * term * rate / 1200 * rateFactor) / (rateFactor - 1) - loanAmount;
}
internal DateTime GetLastChargebackDate(DateTime loanDate, DateTime firstPmtDate, int numOfChargebackPayments, Int16? numOfChargebackDays)
{
// in rare cases that date from loan date is most recent, check table values and compare between loan date vs. first payment date
int result = DateTime.Compare(loanDate.AddDays((Double)numOfChargebackDays), firstPmtDate.AddMonths(numOfChargebackPayments - 1));
if (result > 0)
{
return loanDate.AddDays((Double)numOfChargebackDays); //Loan Date is most recent
}
return firstPmtDate.AddMonths(numOfChargebackPayments - 1);
}
internal decimal CalculateChargeback(decimal reserve, int term, int remainingMonths)
{
return reserve * MonthFactor(remainingMonths) / MonthFactor(term);
}
private decimal MonthFactor(int numberOfMonths)
{
return numberOfMonths * (numberOfMonths + 1) / 2;
}
public int GetRemainingMonths(Loan loanData)
{
if (loanData.PaidLoanDate >= loanData.FinalPmtDate)
return 0;
var monthsOccurred = (loanData.PaidLoanDate.Value - loanData.FirstPmtDate).TotalDays / (365) * 12 + 1;
var remainingMonths = (int)(loanData.Term - Math.Round((decimal)monthsOccurred, 0));
return remainingMonths;
}
}
}
@@ -0,0 +1,72 @@
using NCrontab;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.HostedService
{
public class HostedImportExportActive : BackgroundService
{
private readonly IConfiguration _config;
private readonly IServiceScopeFactory _scopeFactory;
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public const string DefaultCronPattern = "0 19 * * 0-6";
private CancellationToken _cancellationToken;
public HostedImportExportActive(IConfiguration config, IServiceScopeFactory scopeFactory)
{
_config = config;
_scopeFactory = scopeFactory;
try
{
_schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportActiveCronSchedule").Value);
}
catch (Exception ex)
{
Log.Error(ex, "Failure parsing crontab in appsettings.");
_schedule = CrontabSchedule.Parse(DefaultCronPattern);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Log.Information("Hosted ImportExportActive is starting.");
Log.Information($"The import export active job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}");
_cancellationToken = cancellationToken;
try
{
await ImportExportScheduler(cancellationToken);
}
catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested)
{
Log.Information(ex, "Hosted ImportExportActive shut down normally.");
}
catch (Exception ex)
{
Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportActive");
}
Log.Information("Hosted ImportExportActive is stopping.");
}
private async Task ImportExportScheduler(CancellationToken cancellationToken)
{
do
{
if (DateTime.Now > _nextRun)
{
using (var scope = _scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<IScheduleService>().RunImportExportActive(_cancellationToken);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, cancellationToken); //5 seconds delay
} while (!cancellationToken.IsCancellationRequested);
}
}
}
@@ -0,0 +1,72 @@
using NCrontab;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.HostedService
{
public class HostedImportExportClosed : BackgroundService
{
private readonly IConfiguration _config;
private readonly IServiceScopeFactory _scopeFactory;
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public const string DefaultCronPattern = "15 19 * * 0-6";
private CancellationToken _cancellationToken;
public HostedImportExportClosed(IConfiguration config, IServiceScopeFactory scopeFactory)
{
_config = config;
_scopeFactory = scopeFactory;
try
{
_schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportClosedCronSchedule").Value);
}
catch (Exception ex)
{
Log.Error(ex, "Failure parsing crontab in appsettings.");
_schedule = CrontabSchedule.Parse(DefaultCronPattern);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Log.Information("Hosted ImportExportClosed is starting.");
Log.Information($"The import export closed job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}");
_cancellationToken = cancellationToken;
try
{
await ImportExportScheduler(cancellationToken);
}
catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested)
{
Log.Information(ex, "Hosted ImportExportClosed shut down normally.");
}
catch (Exception ex)
{
Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportClosed");
}
Log.Information("Hosted ImportExportClosed is stopping.");
}
private async Task ImportExportScheduler(CancellationToken cancellationToken)
{
do
{
if (DateTime.Now > _nextRun)
{
using (var scope = _scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<IScheduleService>().RunImportExportClosed(_cancellationToken);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, cancellationToken); //5 seconds delay
} while (!cancellationToken.IsCancellationRequested);
}
}
}
@@ -0,0 +1,180 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Mappers
{
public static class ExportMapper
{
//used for Chargeback transactions
private const string GeneralLedgerReceipt = "GLR";
private const string DealerInterestRebate = "DRIR";
private const string ChargeBkDesc = "ChrgBk";
private const string ChargeBkAdjDesc = "ChrgBk Adj";
private const string GeneralLedgerCategory = "NOTE";
private const string GeneralLedgerType = "BAL";
//used for Reserve transactions
private const string DealerExpense = "DLRE";
private const string DealerIncome = "DLRI";
private const string ReserveAdjDesc = "Adjustment";
//used for all transaction types
private const string UpfrontCategory = "UPFR";
private const string DealerType = "DLR";
private const string ElectronicFundType = "EL";
private const string NoHoldClearCategory = "IMED";
private const string FundTypeDetail = "INTR";
public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForReserve(Loan loan)
{
return new GroupedMonetaryTransactionRequest
{
MonetaryTransactionRequests = BuildReserveTransactions(loan),
ParentRequestNumber = loan.LoanNumber
};
}
private static List<MonetaryTransactionRequest> BuildReserveTransactions(Loan loan)
{
var listTransaction = new List<MonetaryTransactionRequest>();
var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0;
var reserveRounded = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(reserveRounded, 2, MidpointRounding.AwayFromZero);
var transactionDescription = isAdjustment ? ReserveAdjDesc : null;
var loanAmount = (double)amount * -1;
listTransaction.Add(BuildParentTransaction(loanAmount, loan.LoanNumber, DealerExpense, transactionDescription));
var dealerAmount = (double)amount;
listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerIncome, transactionDescription));
return listTransaction;
}
public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForChargeback(Loan loan, MinorAccountType accountType)
{
var request = new GroupedMonetaryTransactionRequest();
if (accountType != null) {
request.MonetaryTransactionRequests = BuildChargebackTransactions(loan, accountType.GeneralLedgerAccountNumber);
request.ParentRequestNumber = loan.LoanNumber;
}
return request;
}
private static List<MonetaryTransactionRequest> BuildChargebackTransactions(Loan loan, string accountNumber)
{
var listTransaction = new List<MonetaryTransactionRequest>();
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0;
var chargeBackRounded = Math.Round((decimal)loan.ChargeBack, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(chargeBackRounded, 2, MidpointRounding.AwayFromZero);
var transactionDescription = isAdjustment ? $"{ChargeBkAdjDesc}-{loan.LoanNumber}" : $"{ChargeBkDesc}-{loan.LoanNumber}";
var loanAmount = (double)amount;
listTransaction.Add(BuildParentTransaction(loanAmount, accountNumber, GeneralLedgerReceipt, transactionDescription));
var dealerAmount = (double)amount * -1;
listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerInterestRebate, transactionDescription));
return listTransaction;
}
private static MonetaryTransactionRequest BuildParentTransaction(double amount, string accountNumber, string transactionType, string transactionDescription)
{
//if a reserve the parent transaction contains the loan number, if chargeback the account is the ledger account for minor account type
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
_balanceTypeList.Add(BuildBalanceType(amount, transactionType));
_fundList.Add(BuildFund(amount));
_fundBalanceTypeList.Add(BuildFundBalanceType(amount));
var transaction = new MonetaryTransactionRequest()
{
AccountNumber = (long)System.Convert.ToUInt64(accountNumber),
TransactionAmount = (transactionType != GeneralLedgerReceipt) ? amount : (double?)null,
RequestNumber = "1",
ParentRequestNumber = null,
TransactionTypeCode = transactionType,
Description = transactionDescription,
BalanceTypes = _balanceTypeList,
Funds = _fundList,
FundBalanceTypes = (transactionType == GeneralLedgerReceipt) ? _fundBalanceTypeList : null,
};
return transaction;
}
private static MonetaryTransactionRequest BuildDealerTransaction(double amount, string dealerAccount, string transactionType, string transactionDescription)
{
//for second request take the offset amount value of parent request amount
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
_balanceTypeList.Add(BuildBalanceType(amount, transactionType));
_fundList.Add(BuildFund(amount));
_fundBalanceTypeList.Add(BuildFundBalanceType(amount));
var transaction = new MonetaryTransactionRequest()
{
AccountNumber = (string.IsNullOrEmpty(dealerAccount) == true) ? 0 : (long)System.Convert.ToUInt64(dealerAccount),
TransactionAmount = amount,
RequestNumber = "2",
ParentRequestNumber = "1",
TransactionTypeCode = transactionType,
Description = transactionDescription,
BalanceTypes = _balanceTypeList,
Funds = _fundList,
};
return transaction;
}
private static List<FundBalanceType> _fundBalanceTypeList;
private static FundBalanceType BuildFundBalanceType(double amount)
{
return new FundBalanceType
{
FundBalanceAmount = amount,
FundTypeCode = ElectronicFundType,
FundTypeDetailCode = NoHoldClearCategory
};
}
private static List<BalanceType1> _balanceTypeList;
private static BalanceType1 BuildBalanceType(double amount, string transactionType)
{
return new BalanceType1
{
BalanceTypeAmount = amount,
BalanceCategoryCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerCategory : UpfrontCategory,
BalanceTypeCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerType : DealerType,
};
}
private static List<Fund> _fundList;
private static Fund BuildFund(double amount)
{
return new Fund
{
ClearingCategoryCode = NoHoldClearCategory,
FundAmount = amount,
FundTypeCode = ElectronicFundType,
FundTypeDetailCode = FundTypeDetail,
NumberOfItems = 1,
};
}
}
}
@@ -0,0 +1,43 @@
using CsvHelper.Configuration.Attributes;
using System;
using System.Text.Json.Serialization;
namespace ReserveManagementSystemApi.Models
{
public class ImportBoatLoan
{
[Index(0)]
public string LoanNumber { get; set; }
[JsonIgnore]
[Index(1)]
public string DealerAccountNumber { get; set; }
[JsonIgnore]
[Index(2)]
public decimal Amount { get; set; }
[JsonIgnore]
[Index(3)]
public string IsChargeback { get; set; }
[JsonIgnore]
[Index(4)]
public string IsAdjustment { get; set; }
[JsonIgnore]
[Index(5)]
[Optional]
public string AccountType { get; set; }
[Index(6)]
[Optional]
public string ErrorMessage { get; set; }
[Index(7)]
[Optional]
public bool? WasSuccessful { get; set; }
}
}
@@ -0,0 +1,113 @@
using CsvHelper.Configuration.Attributes;
using System;
namespace ReserveManagementSystemApi.Models
{
public class ImportNewLoan
{
[Index(0)]
public string DealerOrganizationNumber { get; set; }
[Index(1)]
public string MemberNumber { get; set; }
[Index(2)]
public string LoanNumber { get; set; }
[Index(3)]
public string LastName { get; set; }
[Index(4)]
public string FirstAndMiddleName { get; set; }
[Index(5)]
public DateTime LoanDate { get; set; }
[Index(6)]
public DateTime PostedDate { get; set; }
[Index(7)]
public DateTime FirstPmtDate { get; set; }
[Index(8)]
public DateTime FinalPmtDate { get; set; }
[Index(9)]
public decimal LoanAmount { get; set; }
[Index(10)]
public decimal BuyRate { get; set; }
[Index(11)]
public decimal ContractRate { get; set; }
[Index(12)]
public decimal MonthlyPmt { get; set; }
[Index(13)]
public string LoanType { get; set; }
[Index(14)]
public string InsuranceIndicator { get; set; }
[Index(15)]
public string NewMember { get; set; }
[Index(16)]
[Optional]
public string OriginalTermInDays { get; set; }
[Index(17)]
[Optional]
public string VINNumber { get; set; }
[Index(18)]
[Optional]
public string Make { get; set; }
[Index(19)]
[Optional]
public string Model { get; set; }
[Index(20)]
[Optional]
public string Year { get; set; }
[Index(21)]
[Optional]
public string Mileage { get; set; }
[Index(22)]
[Optional]
public string Color { get; set; }
[Index(23)]
[Optional]
public string ApplicationId { get; set; }
[Index(24)]
[Optional]
public string Warranty { get; set; }
[Index(25)]
[Optional]
public string Gap { get; set; }
[Index(26)]
[Optional]
public string CreditLife { get; set; }
[Index(27)]
[Optional]
public string CreditDisability { get; set; }
[Index(28)]
[Optional]
public string Mbp { get; set; }
[Index(29)]
[Optional]
public string AccountType { get; set; }
}
}
@@ -0,0 +1,249 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemApi.HostedService;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Configurations;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using MassTransit;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json;
using LCU.Library.Internal.LcuHttpClient;
using LCU.Library.Internal.ApiSecurity;
using LCU.Library.Internal.Logging.Models;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.Configuration;
using LCU.Library.Internal.LcuMassTransit;
using LCU.Library.Internal.LcuMassTransitModels;
using ReserveManagementSystemApi.Consumers;
using ReserveManagementSystemApi.Services.QueueServices;
using LCU.Library.Internal.Status;
namespace ReserveManagementSystemApi
{
public class Program
{
public static string MyAllowSpecificOrigins { get; private set; }
static void Main(string[] args)
{
MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
WebApplicationBuilder builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
}
public static WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("/app/config/environment-appsettings/appsettings.json", true, true);
builder.Configuration.AddJsonFile("/app/config/workload-appsettings/appsettings.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-seq-api-key.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-rms-database-connection.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-username.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-password.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-api-security-configuration-internal-api-key-set.json", true, true);
builder.Host.UseSerilog();
return builder;
}
public static void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddLCUHttpServices();
var apiConfiguration = configuration.Bind<ApiConfiguration>(services, "Primary");
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins(apiConfiguration.CorsAllowedOrigins).AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(configuration.GetSection("SqlConnectionConfiguration").GetSection("ConnectionString").Value);
});
var connectionConfiguration = MassTransitConfigurationSetUp.AddConnectionConfiguration(ref services, configuration);
Log.Debug(JsonConvert.SerializeObject(connectionConfiguration));
var receiveEndpointConfigurations = new ReceiveEndpointConfigurations()
{
RmsActiveLoanReceiveEndpoint = MassTransitConfigurationSetUp.GetReceiveEndpointConfiguration(configuration, "RmsActiveLoanReceiveEndpoint")
};
services.AddSingleton(receiveEndpointConfigurations);
Log.Debug(JsonConvert.SerializeObject(receiveEndpointConfigurations));
AddMassTransitServices(ref services, connectionConfiguration, receiveEndpointConfigurations, apiConfiguration);
AddQueueServices(services);
var loggerConfiguration = new LoggingConfiguration(apiConfiguration.AppName,
apiConfiguration.SeqApiKey,
apiConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
var rmsApiConfiguration = configuration.Bind<RmsApiConfiguration>(services, "RmsApiConfiguration");
var coreApiConfiguration = configuration.Bind<CoreApiConfiguration>(services, "CoreApiConfiguration");
services.AddApiKeySecurity(configuration);
services.AddLCUStatusServices();
//Services
services.AddScoped<IStatusService, StatusService>();
services.AddScoped<ILoanCalculationService, LoanCalculationService>();
services.AddScoped<ILoanService, LoanService>();
services.AddScoped<IImportService, ImportService>();
services.AddScoped<IImportReadService, ImportReadService>();
services.AddScoped<IAuditHistoryService, AuditHistoryService>();
services.AddScoped<IDealerService, DealerService>();
services.AddScoped<IExportService, ExportService>();
services.AddScoped<IVehicleService, VehicleService>();
services.AddScoped<IScheduleService, ScheduleService>();
services.AddScoped<IRmsActiveLoanService, RmsActiveLoanService>();
//Service Clients
services.AddScoped<ICoreApiServiceClient, CoreApiServiceClient>();
//Repositories
services.AddScoped<ILoanRepository, LoanRepository>();
services.AddScoped<ILoanTypeRepository, LoanTypeRepository>();
services.AddScoped<IImportRepository, ImportRepository>();
services.AddScoped<IAuditHistoryRepository, AuditHistoryRepository>();
services.AddScoped<IDealerRepository, DealerRepository>();
services.AddScoped<IVehicleRepository, VehicleRepository>();
services.AddScoped<IMinorAccountTypeRepository, MinorAccountTypeRepository>();
//HostedServices
if (apiConfiguration.FeatureFlags.EnableImportExportActive)
{
services.AddHostedService<HostedImportExportActive>();
}
if (apiConfiguration.FeatureFlags.EnableImportExportClosed)
{
services.AddHostedService<HostedImportExportClosed>();
}
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = $"{apiConfiguration.AppName}",
Version = "v1",
Contact = new OpenApiContact { Name = "SpecFlow Tests", Url = new Uri("/Specs/index.html", UriKind.Relative) }
});
});
}
public static void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
app.UseSwagger(c =>
{
c.PreSerializeFilters.Add((swaggerDoc, request) =>
{
const string prefixHeader = "X-Forwarded-Prefix";
if (!request.Headers.ContainsKey(prefixHeader)) return;
var serverUrl = request.Headers[prefixHeader];
swaggerDoc.Servers = new List<OpenApiServer>()
{
new() { Description = serverUrl, Url = serverUrl }
};
});
});
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger";
c.SwaggerEndpoint("v1/swagger.json", "ReserveManagementSystemApi v1");
});
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var specsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Specs");
if (Directory.Exists(specsFolder))
{
app.UseFileServer(new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Specs")),
RequestPath = new PathString("/Specs"),
EnableDefaultFiles = true
});
}
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
}
public static void AddMassTransitServices(ref IServiceCollection services,
ConnectionConfiguration connectionConfiguration,
ReceiveEndpointConfigurations receiveEndpointConfigurations,
ApiConfiguration apiConfiguration)
{
services.AddMassTransit(x =>
{
x.AddConsumer<RmsActiveImportConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(connectionConfiguration.HostUrl, "/", host =>
{
host.Username(connectionConfiguration.UserName);
host.Password(connectionConfiguration.Password);
});
cfg.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
if (apiConfiguration.FeatureFlags.RunRmsActiveLoanConsumer)
{
cfg.ReceiveEndpoint(receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.ReceiveEndpoint, e =>
{
e.ConfigureConsumer<RmsActiveImportConsumer>(context);
e.SetQuorumQueue();
e.PrefetchCount = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.PrefetchLimit;
e.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
});
}
});
});
services.AddMassTransitHostedService();
}
public static void AddQueueServices(IServiceCollection services)
{
services.AddScoped<IQueueRmsActiveLoanService, QueueRmsActiveLoanService>();
}
}
}
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.1.1</Version>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<Description></Description>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LCU.Api.Internal.MaidBotLib" Version="1.0.2" />
<PackageReference Include="LCU.Library.Internal.ApiSecurity" Version="1.0.2" />
<PackageReference Include="LCU.Library.Internal.Configuration" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.CoreApiWrapperModel" Version="2.3.0" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuHttpClient" Version="1.9.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransit" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransitModels" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.Status" Version="1.0.1" />
<PackageReference Include="LCU.Library.Internal.StatusModels" Version="1.0.0" />
<PackageReference Include="LCU.Shared.Common.NotificationApiLib" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.18" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.NETCore.App" Version="2.2.8" />
<PackageReference Include="NCrontab.Signed" Version="3.3.2" />
<PackageReference Include="Pickles.CommandLine" Version="2.21.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Specs\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,51 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31025.194
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi", "ReserveManagementSystemApi.csproj", "{954C8B67-1471-4FB2-81BD-713BEE5E679E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemLib", "..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj", "{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}"
EndProject
Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "ReserveManagementSystemApi.Sql", "..\ReserveManagementSystemApi.Sql\ReserveManagementSystemApi.Sql.sqlproj", "{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi.Tests", "..\ReserveManagementSystemApi.Tests\ReserveManagementSystemApi.Tests.csproj", "{CFE504B7-58D7-444B-A4B8-8A219400BA42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveManagementSystemApi.Test", "..\ReserveManagementSystemApi.Test\ReserveManagementSystemApi.Test.csproj", "{5BC5FBBC-2B72-4055-95E0-41192F581784}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.Build.0 = Release|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.Build.0 = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Build.0 = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Deploy.0 = Release|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.Build.0 = Release|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {27D5180A-D1E5-4786-9F4D-99C4652488F4}
EndGlobalSection
EndGlobal
@@ -0,0 +1,120 @@
using ReserveManagementSystemLib.Configuration;
using LCU.Library.Internal.ApiSecurity.Models;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.ApiSecurity.Enums;
using LCU.Library.Internal.LcuHttpClient.Utilities;
namespace ReserveManagementSystemApi.ServiceClients
{
public class CoreApiServiceClient : ICoreApiServiceClient
{
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
private readonly CoreApiConfiguration _coreApiWrapperConfiguration;
private readonly IPostRequest _postRequest;
private readonly IGetRequest _getRequest;
private readonly IPutRequest _putRequest;
private const string ClientKeyValue = "ReserveManagementSystem";
public CoreApiServiceClient(
ApiSecurityConfiguration apiSecurityConfiguration,
CoreApiConfiguration coreApiWrapperConfiguration,
IGetRequest getRequest,
IPostRequest postRequest,
IPutRequest putRequest
)
{
_apiSecurityConfiguration = apiSecurityConfiguration;
_coreApiWrapperConfiguration = coreApiWrapperConfiguration;
_getRequest = getRequest;
_postRequest = postRequest;
_putRequest = putRequest;
}
public async Task<List<MonetaryTransactionResponse>> PostMonetaryTransactions(List<GroupedMonetaryTransactionRequest> loanReserves)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PostMonetaryTransactions}";
var uri = new Uri(path);
var response = await _postRequest.Async<List<GroupedMonetaryTransactionRequest>, List<MonetaryTransactionResponse>>(uri, loanReserves,
HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)),
0, Convert.ToInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error in posting MonetaryTransactions.");
throw;
}
}
public async Task<List<UpdateDealerOriginalBalanceResponse>> PutDealerOriginalBalance(string accountNumber)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PutDealerOriginalBalance}";
var uri = new Uri(path);
var response = await _putRequest.Async<string, List<UpdateDealerOriginalBalanceResponse>>(uri, accountNumber,
HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)),
0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error updating Dealer Original Balance");
throw;
}
}
public async Task<List<IndirectLoanDetail>> GetIndirectLoansByPostedDate(string afterPostDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetIndirectLoansByPostedDate}{afterPostDate}";
var response = await _getRequest.AsyncWithNewton<List<IndirectLoanDetail>>(path, null, 0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Indirect Loans By Posted Date");
return null;
}
}
public async Task<List<ClosedIndirectLoan>> GetClosedLoansByPostedDate(string afterPostDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedLoansByPostedDate}{afterPostDate}";
var response = await _getRequest.AsyncWithNewton<List<ClosedIndirectLoan>>(path);
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Closed Loans By Posted Date");
}
return null;
}
public async Task<List<IndirectLoanDetail>> GetClosedLoansWithDetailByClosedDate(string afterClosedDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedIndirectLoansWithDetailByPostedDate}{afterClosedDate}";
var response = await _getRequest.AsyncWithNewton<List<IndirectLoanDetail>>(path);
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Closed Loans With Detail");
}
return null;
}
}
}
@@ -0,0 +1,19 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
namespace ReserveManagementSystemApi.ServiceClients
{
public interface ICoreApiServiceClient
{
Task<List<MonetaryTransactionResponse>> PostMonetaryTransactions(List<GroupedMonetaryTransactionRequest> loans);
Task<List<IndirectLoanDetail>> GetIndirectLoansByPostedDate(string afterPostDate);
Task<List<ClosedIndirectLoan>> GetClosedLoansByPostedDate(string afterPostDate);
Task<List<IndirectLoanDetail>> GetClosedLoansWithDetailByClosedDate(string afterClosedDate);
Task<List<UpdateDealerOriginalBalanceResponse>> PutDealerOriginalBalance(string accountNumber);
}
}
@@ -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;
}
}
}
}
@@ -0,0 +1,8 @@
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.EntityFrameworkCore;
global using Serilog;
global using System;
global using System.Linq;
global using System.Threading.Tasks;
global using LCU.Library.Internal.Exceptions;
@@ -0,0 +1,59 @@
{
"Primary": {
"AppName": "Reserve Management System",
"SeqUrl": "http://localhost",
"SeqApiKey": "VALUE-IN-KEYVAULT",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
],
"FeatureFlags": {
"RunRmsActiveLoanConsumer": true,
"EnableImportExportActive": false,
"EnableImportExportClosed": false
}
},
"SqlConnectionConfiguration": {
"ConnectionString": "data source=localhost;initial catalog=ReserveManagementSystem;Integrated Security=SSPI;MultipleActiveResultSets=True;App=EntityFramework",
"MaxRetryCount": 3,
"MaxRetryDelay": "00:00:30"
},
"ConnectionConfiguration": {
"HostUrl": "localhost",
"UserName": "guest",
"Password": "guest"
},
"RmsActiveLoanReceiveEndpoint": {
"ReceiveEndpoint": "LCU.ReserveManagementSystem.ActiveImport",
"ReceiveEndpointType": "exchange",
"PrefetchLimit": 1
},
"ApiSecurityConfiguration": {
"ApiKeyAuthenticationFailureBehavior": "Error",
"InternalApiKeySet": {
"ReserveManagementSystem": "VALUE-IN-KEYVAULT"
},
"ExternalApiKeySet": {}
},
"CoreApiConfiguration": {
"ApiTimeOutInSeconds": 420,
"BaseUrl": "https://localhost:59303",
"GetIndirectLoansByPostedDate": "/api/core/IndirectLoan/",
"GetClosedLoansByPostedDate": "/api/core/IndirectLoan/Closed/",
"GetClosedIndirectLoansWithDetailByPostedDate": "/api/core/IndirectLoan/ClosedWithDetail/",
"PostMonetaryTransactions": "/api/core/Transaction/PostMonetaryTransactions",
"PutDealerOriginalBalance": "/api/core/IndirectLoan/PutDealerOriginalBalance"
},
"AllowedHosts": "*",
"HostedServices": {
"ImportExportActiveCronSchedule": "0 19 * * 0-6",
"ImportExportClosedCronSchedule": "15 19 * * 0-6"
},
"RmsApiConfiguration": {
"DefaultImportStartDate": "2021-07-01 00:00:00",
"DefaultBatchSize": 100,
"DefaultConvertedLoanPostDate": "2021-10-31"
}
}