This commit is contained in:
Zeeshaun
2026-04-20 14:47:24 -05:00
parent 89057eceea
commit 8b3ea92501
485 changed files with 71042 additions and 0 deletions
@@ -0,0 +1,461 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.API.Collections.RepoApi.Models.Requests;
using LCU.Shared.Collections.RepoApiLib.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using LCU.Shared.Collections.RepoApiLib.WebServices.AutoIMS;
using Serilog;
using AuctionAssignment = LCU.Shared.Collections.RepoApiLib.Models.AuctionAssignment;
namespace LCU.API.Collections.RepoApi.Services
{
public class AuctionAssignmentService : IAuctionAssignmentService
{
private readonly IAuctionAssignmentRepository _auctionAssignmentRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IEmailer _emailer;
private readonly IAutoImsWebService _autoImsWebService;
private readonly IRepoAccountService _repoAccountService;
public AuctionAssignmentService(
IAutoImsWebService autoImsWebService,
IRepoAccountService repoAccountService,
IAuctionAssignmentRepository auctionAssignmentRepository,
IUnitOfWork unitOfWork,
IEmailer emailer)
{
_autoImsWebService = autoImsWebService;
_repoAccountService = repoAccountService;
_auctionAssignmentRepository = auctionAssignmentRepository;
_unitOfWork = unitOfWork;
_emailer = emailer;
}
public async Task<PagedResult<AuctionAssignment>> ListByFilterAsync(AuctionAssignmentFilter filter)
{
try
{
if (filter.ReadyForFloorPrice)
{
return await _auctionAssignmentRepository.ListReadyForFloorPriceAsync();
}
return await _auctionAssignmentRepository.ListAllAsync(filter.Page, filter.Limit);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving AuctionAssignments");
throw new ServerErrorException();
}
}
public async Task<IReadOnlyCollection<AuctionAssignment>> GetListByIdsAsync(IReadOnlyCollection<int> idSet)
{
try
{
return await _auctionAssignmentRepository.FindListByIdsAsync(idSet);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving AuctionAssignments for ids: {@idSet}");
throw new ServerErrorException();
}
}
public async Task<AuctionAssignment> GetByIdAsync(int id)
{
AuctionAssignment auctionAssignment;
try
{
auctionAssignment = await _auctionAssignmentRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving AuctionAssignment with id: {id}");
throw new ServerErrorException();
}
if (auctionAssignment == null)
{
throw new NotFoundException($" AuctionAssignment with {id} not found.");
}
return auctionAssignment;
}
public async Task<IReadOnlyCollection<AuctionAssignment>> GetByRepoIdAsync(int repoId)
{
// Ensure the repo exists. If it doesn't, a NFE should be thrown here
await _repoAccountService.Get(repoId);
try
{
return await _auctionAssignmentRepository.FindByRepoIdAsync(repoId);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving AuctionAssignments by RepoId with RepoID: {repoId}");
throw new ServerErrorException();
}
}
public async Task<AuctionAssignmentPostRequest> SaveAsync(AuctionAssignmentPostRequest auctionAssignmentPostRequest)
{
Collateral primaryCollateral = auctionAssignmentPostRequest.Collaterals
.First(collateral => collateral.CollateralId == auctionAssignmentPostRequest.PrimaryCollateralId);
string auctionCustomerNo;
try
{
auctionCustomerNo = await GetAuctionCustomerNoAsync(auctionAssignmentPostRequest.AuctionAssignment.RepoId.Value, primaryCollateral.RefCollateralId);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Auction Customer Number with auctionAssignmentPostRequest {auctionAssignmentPostRequest}", auctionAssignmentPostRequest);
throw new ServerErrorException();
}
// If active auction already exists, do not create
var conflictingAuctionAssignments = await _findActiveAuctionsWithMatchingCollateral(primaryCollateral.RepoId, auctionAssignmentPostRequest.Collaterals);
if (conflictingAuctionAssignments!= null && conflictingAuctionAssignments.Any())
{
throw new DuplicateResourceException("AuctionAssignment");
}
try
{
var auctionAssignmentsToCollaterals = CreateJoiningAuctionAssignmentToCollateralsList(
auctionAssignmentPostRequest.AuctionAssignment, auctionAssignmentPostRequest.Collaterals);
auctionAssignmentPostRequest.AuctionAssignment.AuctionCustomerNo = auctionCustomerNo;
auctionAssignmentPostRequest.AuctionAssignment.DateSubmitted = DateTime.Now;
auctionAssignmentPostRequest.AuctionAssignment.CollateralVIN = primaryCollateral.Vin;
auctionAssignmentPostRequest.AuctionAssignment.AuctionAssignmentToCollaterals = auctionAssignmentsToCollaterals;
auctionAssignmentPostRequest.AuctionAssignment = await _auctionAssignmentRepository.AddAsync(auctionAssignmentPostRequest.AuctionAssignment);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure creating AuctionAssignment object: {auctionAssignmentPostRequest.AuctionAssignment}.", auctionAssignmentPostRequest.AuctionAssignment);
throw new ServerErrorException();
}
try
{
await CreateAutoIMSAuction(primaryCollateral, auctionAssignmentPostRequest.VehicleLocation, auctionAssignmentPostRequest.AuctionAssignment);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure creating AutoIMS Auction with Primary Collateral: {primaryCollateral} Removing AuctionAssignment...", primaryCollateral);
try { _auctionAssignmentRepository.Remove(auctionAssignmentPostRequest.AuctionAssignment); }
catch (Exception ex2) { Log.Error(ex2, $"Failure removing AuctionAssignment : {auctionAssignmentPostRequest.AuctionAssignment} after failed AutoIMS creation.", auctionAssignmentPostRequest.AuctionAssignment); }
throw new WebException("Failed to add auction assignment to AutoIMS.");
}
return auctionAssignmentPostRequest;
}
public async Task<AuctionAssignment> UpdateAsync(AuctionAssignment auctionAssignment)
{
var existingAuctionAssignment = await _auctionAssignmentRepository.FindByIdAsync(auctionAssignment.ID);
if (existingAuctionAssignment == null)
{
throw new NotFoundException($" Auction Assignment with id {auctionAssignment.ID} not found.");
}
try
{
if (existingAuctionAssignment.FloorPrice != auctionAssignment.FloorPrice)
{
await UpdateAuctionFloor(auctionAssignment, existingAuctionAssignment);
}
if (existingAuctionAssignment.SentTitleDocsDate != auctionAssignment.SentTitleDocsDate)
{
await UpdateTitleSentDate(auctionAssignment, existingAuctionAssignment);
}
if (existingAuctionAssignment.RedeemedDate != auctionAssignment.RedeemedDate)
{
await UpdateRedeemedDate(auctionAssignment, existingAuctionAssignment);
}
if (existingAuctionAssignment.HoldStatus != auctionAssignment.HoldStatus || existingAuctionAssignment.HoldUntil != auctionAssignment.HoldUntil)
{
await UpdateHoldData(auctionAssignment, existingAuctionAssignment);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to fully update auction assignment with id {auctionAssignment.ID} in AutoIMS");
throw new WebException("Failed to fully update auction assignment in AutoIMS.");
}
return auctionAssignment;
}
public async Task<string> GetAuctionCustomerNoAsync(int repoId, string refCollateralId)
{
RepoAccount repoAccount = await _repoAccountService.Get(repoId);
Borrower primaryBorrower = repoAccount.Borrowers.Where((b) => b.OwnershipType == 6).FirstOrDefault();
if (primaryBorrower == null)
{
throw new NotFoundException($"Primary borrower for Repo Account {repoAccount.AccountNo} not found.");
}
return primaryBorrower.IndividualId + '-' + refCollateralId;
}
/**
* This function will attempt to create an Auction through AutoIMS.
*
* It does not catch and will throw if anything goes wrong.
* Assume lack of exception equals success.
*/
public async Task<WebServiceResponse> CreateAutoIMSAuction(
Collateral collateral,
VehicleLocation vehicleLocation,
AuctionAssignment auctionAssignment)
{
Vehicle vehicle = new Vehicle()
{
ClientAccountNumber = auctionAssignment.AuctionCustomerNo,
VIN = collateral.Vin,
Make = collateral.Make,
Model = collateral.Model,
ModelYear = collateral.Year.GetValueOrDefault(),
BodyStyle = collateral.BodyStyle,
Classification = collateral.VehicleType.GetValueOrDefault(),
ClientField1 = auctionAssignment.ID + "",
AuctionAssignment = new Shared.Collections.RepoApiLib.WebServices.AutoIMS.AuctionAssignment()
{
AuctionID = auctionAssignment.AuctionId,
PickupDate = auctionAssignment.AvailablePickupDate.GetValueOrDefault().ToString("yyyy-MM-dd"),
LegalSaleDate = auctionAssignment.LegalSaleDate.GetValueOrDefault().ToString("yyyy-MM-dd"),
PickupOrDropOffNote = auctionAssignment.AuctionNotes,
CleaningLevelRequested = auctionAssignment.CleaningLevel,
},
VehicleLocation = vehicleLocation,
};
return await _autoImsWebService.PostVehicleAsync<Vehicle>(vehicle);
}
/**
* This function will attempt to remove an Auction through AutoIMS.
*
* It does not catch and will throw if anything goes wrong.
* Assume lack of exception equals success.
*/
public async Task<WebServiceResponse> RemoveAuctionFromAutoIMS(AuctionAssignment auctionAssignment)
{
return await _autoImsWebService.RemoveFromAuction(auctionAssignment.AuctionCustomerNo, auctionAssignment.CollateralVIN);
}
public IEnumerable<AuctionAssignmentToCollateral> CreateJoiningAuctionAssignmentToCollateralsList(AuctionAssignment auctionAssignment, IEnumerable<Collateral> collaterals)
{
return (from collateral in collaterals
select new AuctionAssignmentToCollateral
{
AuctionAssignment = auctionAssignment,
Collateral = collateral
}).ToList();
}
/**
* This function will attempt to call AutoIMS with an auction floor, and will not update the
* floor related values unless it succeeds
*/
public async Task UpdateAuctionFloor(AuctionAssignment auctionAssignmentInput,
AuctionAssignment existingAssignment)
{
FloorPriceVehicle vehicle = new FloorPriceVehicle();
vehicle.ClientAccountNumber = auctionAssignmentInput.AuctionCustomerNo;
vehicle.VIN = auctionAssignmentInput.CollateralVIN;
vehicle.AuctionAssignment.FloorPrice = auctionAssignmentInput.FloorPrice ?? 0;
WebServiceResponse response = await _autoImsWebService.PostVehicleAsync<FloorPriceVehicle>(vehicle);
existingAssignment.FloorPriceResponseCode = response?.Status.Code;
existingAssignment.FloorPriceResponseDescription = response?.Status.Description;
if (response?.Status.ValidationError?.Any() ?? false)
{
foreach (var item in response.Status.ValidationError)
{
existingAssignment.FloorPriceSubmissionErrors +=
"Code: " + item.Code + " Message: " + item.Message + "|";
}
}
else if (response?.Status.Code == 1)
{
// e-mail management on successfully accepted floor price change
var isManualFloorSet = existingAssignment.FloorPriceValuesUsed == auctionAssignmentInput.FloorPriceValuesUsed;
var repoAccount = await _repoAccountService.FindByAuctionAssignmentId(existingAssignment.ID);
await _emailer.SendFloorPriceAcceptedEmail(isManualFloorSet, repoAccount?.AccountNo, existingAssignment.FloorPrice, auctionAssignmentInput.FloorPrice);
existingAssignment.FloorPrice = auctionAssignmentInput.FloorPrice;
existingAssignment.FloorPriceValuesUsed = auctionAssignmentInput.FloorPriceValuesUsed;
}
await _completeAuctionAssignmentUpdateTransaction(existingAssignment);
}
/**
* This function will attempt to call AutoIMS and set the title sent date, and will not update the
* title sent related values unless it succeeds
*/
public async Task UpdateTitleSentDate(AuctionAssignment auctionAssignmentInput,
AuctionAssignment existingAssignment)
{
WebServiceResponse response = await _autoImsWebService.TitleSent(auctionAssignmentInput.CollateralVIN);
existingAssignment.TitleSentResponseCode = response?.Status.Code;
existingAssignment.TitleSentResponseDescription = response?.Status.Description;
if (response?.Status.ValidationError?.Any() ?? false)
{
foreach (var item in response.Status.ValidationError)
{
existingAssignment.TitleSentSubmissionErrors +=
"Code: " + item.Code + " Message: " + item.Message + "|";
}
}
else if (response?.Status.Code == 1)
{
existingAssignment.SentTitleDocsDate = auctionAssignmentInput.SentTitleDocsDate;
}
await _completeAuctionAssignmentUpdateTransaction(existingAssignment);
}
/**
* This function will attempt to call AutoIMS and set the redeemed date, and will not update the
* redeemed date unless it succeeds
*/
public async Task UpdateRedeemedDate(AuctionAssignment auctionAssignmentInput,
AuctionAssignment existingAssignment)
{
WebServiceResponse response = await _autoImsWebService.Redeem(auctionAssignmentInput.CollateralVIN);
if (response?.Status.ValidationError?.Any() ?? false)
{
foreach (var item in response.Status.ValidationError)
{
// There are no database columns for redemption submission errors so log the error
Log.Error($"Redemption submission error. Code: {item.Code} Message: {item.Message} | { auctionAssignmentInput}");
}
}
else if (response?.Status.Code == 1)
{
existingAssignment.RedeemedDate = auctionAssignmentInput.RedeemedDate;
}
await _completeAuctionAssignmentUpdateTransaction(existingAssignment);
}
/**
* This function will attempt to call AutoIMS to place a hold, and will not update the
* hold related values unless it succeeds
*/
public async Task UpdateHoldData(AuctionAssignment auctionAssignmentInput,
AuctionAssignment existingAssignment)
{
var holdStatus = auctionAssignmentInput.HoldStatus ?? 0;
var holdUntil = auctionAssignmentInput.HoldUntil ?? DateTime.MinValue;
WebServiceResponse response = await _autoImsWebService.PlaceHold(auctionAssignmentInput.CollateralVIN, holdStatus, holdUntil);
if (response?.Status.ValidationError?.Any() ?? false)
{
foreach (var item in response.Status.ValidationError)
{
existingAssignment.SubmissionErrors +=
"Code: " + item.Code + " Message: " + item.Message + "|";
}
}
else if (response?.Status.Code == 1)
{
existingAssignment.HoldStatus = auctionAssignmentInput.HoldStatus;
existingAssignment.HoldUntil = auctionAssignmentInput.HoldUntil;
}
await _completeAuctionAssignmentUpdateTransaction(existingAssignment);
}
public async Task<AuctionAssignment> RemoveAuction(int id)
{
var auctionAssignment = await _auctionAssignmentRepository.FindByIdAsync(id);
if (auctionAssignment == null)
{
throw new NotFoundException($" Auction Assignment with id {id} not found.");
}
try
{
await RemoveAuctionFromAutoIMS(auctionAssignment);
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to remove auction assignment from AutoIMS {auctionAssignment.ID}");
throw new WebException("Failed to remove auction assignment from AutoIMS.");
}
auctionAssignment.IsActive = false;
await _completeAuctionAssignmentUpdateTransaction(auctionAssignment);
return auctionAssignment;
}
private async Task _completeAuctionAssignmentUpdateTransaction(AuctionAssignment auctionAssignment)
{
try
{
_auctionAssignmentRepository.Update(auctionAssignment);
await _unitOfWork.CompleteAsync();
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Auction Assignment: {auctionAssignment}", auctionAssignment);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Auction Assignment: {auctionAssignment}", auctionAssignment);
throw new ServerErrorException();
}
}
private async Task<IEnumerable<AuctionAssignment>> _findActiveAuctionsWithMatchingCollateral(int repoId, IEnumerable<Collateral> collaterals)
{
var assignments = await _auctionAssignmentRepository.FindByRepoIdAsync(repoId);
if (assignments != null && assignments.Any())
{
return assignments.Where(auctionAssignment => auctionAssignment.IsActive == true &&
auctionAssignment.AuctionAssignmentToCollaterals.Any(aa2c => collaterals
.Select(collateral => collateral.CollateralId)
.Contains(aa2c.CollateralId)));
}
return null;
}
}
}
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class AuctionHouseService : IAuctionHouseService
{
private readonly IAuctionHouseRepository _auctionHouseRepository;
private readonly IUnitOfWork _unitOfWork;
public AuctionHouseService(
IAuctionHouseRepository auctionHouseRepository,
IUnitOfWork unitOfWork)
{
_auctionHouseRepository = auctionHouseRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<AuctionHouse>> ListAllAsync()
{
try
{
return await _auctionHouseRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Auctions");
throw new ServerErrorException();
}
}
public async Task<AuctionHouse> GetAsync(string id)
{
AuctionHouse auction;
try
{
auction = await _auctionHouseRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Auction with id: {id}");
throw new ServerErrorException();
}
if (auction == null)
{
throw new NotFoundException($"Auction with id {id} not found.");
}
return auction;
}
public async Task<AuctionHouse> SaveAsync(AuctionHouse auction)
{
try
{
await _auctionHouseRepository.AddAsync(auction);
await _unitOfWork.CompleteAsync();
return auction;
}
catch (Exception ex)
{
Log.Error(ex, "Failure saving Auction", auction);
throw new ServerErrorException();
}
}
public async Task<AuctionHouse> UpdateAsync(AuctionHouse auction)
{
AuctionHouse existing;
try
{
existing = await _auctionHouseRepository.FindByIdAsync(auction.AuctionID);
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to find existing Auction House to update with id: {auction.AuctionID}", auction);
throw new ServerErrorException();
}
if (existing == null)
{
throw new NotFoundException($"Auction with id {auction.AuctionID} not found.");
}
existing.Name = auction.Name;
existing.Active = auction.Active;
existing.Address_Line_1 = auction.Address_Line_1;
existing.Address_Line_2 = auction.Address_Line_2;
existing.City = auction.City;
existing.State = auction.State;
existing.ZIP = auction.ZIP;
existing.Country = auction.Country;
existing.Phone = auction.Phone;
existing.Fax = auction.Fax;
existing.Latitude = auction.Latitude;
existing.Longitude = auction.Longitude;
try
{
_auctionHouseRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return (existing);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Auction: {auction}", auction);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Auction {auction}", auction);
throw new ServerErrorException();
}
}
public async Task<AuctionHouse> DeleteAsync(string id)
{
AuctionHouse existingAuction;
try
{
existingAuction = await _auctionHouseRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to find existing Auction House to delete with id: {id}");
throw new ServerErrorException();
}
if (existingAuction == null)
{
throw new NotFoundException($"Auction with id {id} not found.");
}
try
{
_auctionHouseRepository.Remove(existingAuction);
await _unitOfWork.CompleteAsync();
return existingAuction;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting Auction with id: {id}");
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting Auction with id : {id}");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,151 @@
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class AuditRecordService : IAuditRecordService
{
private readonly IAuditRecordRepository _repoAccountRepository;
private readonly IUnitOfWork _unitOfWork;
public AuditRecordService(
IAuditRecordRepository repoAccountRepository,
IUnitOfWork unitOfWork)
{
_repoAccountRepository = repoAccountRepository;
_unitOfWork = unitOfWork;
}
public async Task<PagedResult<AuditRecord>> ListAll(PageFilter filter)
{
try
{
return await _repoAccountRepository.ListAllAsync(filter.Page, filter.Limit);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving AuditRecords with page filter {filter}",filter);
throw new ServerErrorException();
}
}
public async Task<AuditRecord> SaveAsync(AuditRecord repoAccount)
{
try
{
await _repoAccountRepository.AddAsync(repoAccount);
await _unitOfWork.CompleteAsync();
return repoAccount;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving AuditRecord for repoAccount: {repoAccount}", repoAccount);
throw new ServerErrorException();
}
}
public async Task<AuditRecord> Get(int id)
{
AuditRecord repoAccount;
try
{
repoAccount = await _repoAccountRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving AuditRecord wtih id: {id}");
throw new ServerErrorException();
}
if (repoAccount == null)
{
throw new NotFoundException($" AuditRecords with id {id} not found.");
}
return repoAccount;
}
public async Task<AuditRecord> UpdateAsync(AuditRecord auditRecord)
{
AuditRecord existingAuditRecord = null;
try
{
existingAuditRecord = await _repoAccountRepository.FindByIdAsync(auditRecord.ID);
}
catch(Exception ex)
{
Log.Error(ex, $"Failure retreving existing auditRecord wtih auditrecordId: AuditRecordID: {auditRecord.ID}", auditRecord);
throw new ServerErrorException();
}
if (existingAuditRecord == null)
{
throw new NotFoundException($" AuditRecord with id {auditRecord.ID} not found.");
}
try
{
_repoAccountRepository.Update(existingAuditRecord);
await _unitOfWork.CompleteAsync();
return (existingAuditRecord);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating AuditRecord: {auditRecord}", auditRecord);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating AuditRecord: {auditRecord}", auditRecord);
throw new ServerErrorException();
}
}
public async Task<AuditRecord> DeleteAsync(int id)
{
AuditRecord existingAuditRecord = null;
try
{
existingAuditRecord = await _repoAccountRepository.FindByIdAsync(id);
}
catch(Exception ex)
{
Log.Error(ex, $"Failure retreving existing auditRecord wtih auditrecordId: AuditRecordID: {id}");
throw new ServerErrorException();
}
if (existingAuditRecord == null)
{
throw new NotFoundException($" AuditRecord with id {id} not found.");
}
try
{
_repoAccountRepository.Remove(existingAuditRecord);
await _unitOfWork.CompleteAsync();
return existingAuditRecord;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting AuditRecord with id: {id}", id);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting AuditRecord with id: {id}", id);
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using LCU.API.Collections.RepoApi.Configuration;
using Serilog;
using WinSCP;
using SessionOptions = WinSCP.SessionOptions;
using TransferMode = WinSCP.TransferMode;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AuctionFtpClientService : IAuctionFtpClientService
{
private readonly SessionOptions _sessionOptions;
private readonly RepoApiConfiguration _configuration;
public AuctionFtpClientService(RepoApiConfiguration configuration)
{
_configuration = configuration;
_sessionOptions = new SessionOptions()
{
Protocol = Protocol.Sftp,
HostName = configuration.FtpConfiguration.HostName,
UserName = configuration.FtpConfiguration.UserName,
PortNumber = configuration.FtpConfiguration.Port,
SshHostKeyFingerprint = configuration.FtpConfiguration.SshHostKeyFingerprint,
SshPrivateKeyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
configuration.FtpConfiguration.SshPrivateKeyPath)
};
}
public Dictionary<FileInfo, string> Download()
{
var fileResults = new Dictionary<FileInfo, string>();
using (Session session = new Session())
{
TransferOperationResult transferResult = null;
try
{
session.ExecutablePath = _configuration.FtpConfiguration.ExecutablePath;
session.Open(_sessionOptions);
}
catch (WinSCP.SessionLocalException wsle)
{
Log.Error(wsle, "Failed to open FTP session, WINSCP likely missing");
return fileResults;
}
catch (Exception ex)
{
Log.Error(ex,"Failed to open FTP session");
return fileResults;
}
TransferOptions transferOptions = new TransferOptions();
transferOptions.TransferMode = TransferMode.Automatic;
transferOptions.PreserveTimestamp = true;
RemoteDirectoryInfo directory = session.ListDirectory(_configuration.RemoteFilePath);
foreach (RemoteFileInfo fileInfo in directory.Files)
{
string localFileName = Path.Combine(_configuration.LocalDirectoryPath, fileInfo.Name);
var remoteFileName = Path.Combine(_configuration.RemoteFilePath, fileInfo.Name);
FileInfo thisFileInfo = null;
try
{
if (!fileInfo.IsDirectory && !File.Exists(localFileName))
{
thisFileInfo = new FileInfo(localFileName);
fileResults.Add(thisFileInfo, "success");
transferResult = session.GetFiles(remoteFileName, localFileName, true, transferOptions);
// Throw on any error
transferResult.Check();
}
}
catch (Exception ex)
{
Log.Error(ex, "Exception while FTP'ing files ");
if (thisFileInfo != null)
{
fileResults[thisFileInfo] = ex.Message;
}
}
}
}
return fileResults;
}
}
}
@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Extensions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Persistence;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using LCU.Shared.Collections.RepoApiLib.WebServices.AutoIMS;
using Microsoft.EntityFrameworkCore;
using Serilog;
using AuctionAssignment = LCU.Shared.Collections.RepoApiLib.Models.AuctionAssignment;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AuctionProcessorDao : IAuctionProcessorDao
{
private readonly IImsAutoFileDataRepository _imsAutoFileDataRepository;
private readonly IAutoImsWebService _autoImsWebService;
private readonly IUnitOfWork _unitOfWork;
private readonly AppDbContext _context;
private readonly string statusTypeRepo = "REPO";
private readonly string statusTypeSold = "SOLD";
private readonly string statusTypeRedeemed = "REDEEMED";
public AuctionProcessorDao(
IImsAutoFileDataRepository imsAutoFileDataRepository,
IAutoImsWebService autoImsWebService,
AppDbContext context,
IUnitOfWork unitOfWork
)
{
_context = context;
_unitOfWork = unitOfWork;
_imsAutoFileDataRepository = imsAutoFileDataRepository;
_autoImsWebService = autoImsWebService;
}
public string UpdateAutoImsFromAuctionData(ImsAutoFileData item)
{
try
{
WebServiceResponse response = null;
Task.Run(async () =>
{
response = await _autoImsWebService.UpdateAuctionIdentifyingValuesAsync(
item.AuctionCustomerNo,
item.VIN,
item.AuctionAssignmentId.ToString());
}).Wait();
return "Updated AutoIMS with Landmark identifiers.";
}
catch (Exception ex)
{
Log.Error(ex, "UpdateAutoIMS From Auction Failure");
return "Failure Updating AutoIMS with Landmark identifiers.";
}
}
public static string GetFormattedVin(string vin)
{
return !string.IsNullOrWhiteSpace(vin)
? vin.Trim().PadLeft(17, '0').ToUpper()
: null;
}
public RepoAccount FindByAuctionCustomerNoAndVinAsync(string auctionCustomerNo, string unformattedVin)
{
List<RepoAccount> repos = FindRepoByStatus(auctionCustomerNo, unformattedVin, statusTypeRepo);
if (!repos.Any())
{
repos = FindRepoByStatus(auctionCustomerNo, unformattedVin, statusTypeSold);
}
if (!repos.Any())
{
repos = FindRepoByStatus(auctionCustomerNo, unformattedVin, statusTypeRedeemed);
}
/*
Currently we cannot error on multiple rows returned because we can have duplicate repos in the database
We will need to perform extensive data cleanup before this can be enabled
*/
// if (repos.Count > 1)
// {
// throw new Exception($"Multiple Auction Assignments Returned For: AuctionCustomerNo:{auctionCustomerNo} | VIN:{vin}");
// }
return repos.FirstOrDefault();
}
public ImsAutoFileData UpdateExistingFile(ImsAutoFileData existingAuctionFile, ImsAutoFileData imsAutoFileData)
{
_context.Entry(existingAuctionFile).CurrentValues.SetValues(imsAutoFileData);
imsAutoFileData.DateReceivedFromAutoIMS = existingAuctionFile.DateReceivedFromAutoIMS;
imsAutoFileData.CollateralImagePath =
_autoImsWebService.GetCollateralImage(imsAutoFileData.ConditionReportLink);
_imsAutoFileDataRepository.Update(existingAuctionFile);
_unitOfWork.CompleteAsync().GetAwaiter().GetResult();
return imsAutoFileData;
}
public ImsAutoFileData CreateImsAuctionFileData(ImsAutoFileData imsAutoFileData)
{
imsAutoFileData.DateReceivedFromAutoIMS = DateTime.Now;
imsAutoFileData.CollateralImagePath =
_autoImsWebService.GetCollateralImage(imsAutoFileData.ConditionReportLink);
Add(imsAutoFileData);
_unitOfWork.CompleteAsync().GetAwaiter().GetResult();
return imsAutoFileData;
}
public void Add(ImsAutoFileData assignment)
{
_context.ImsAutoFileData.Add(assignment);
}
public ImsAutoFileData FindByAuctionAssignmentIdAsync(int auctionAssignmentId)
{
return _context.ImsAutoFileData.AsNoTracking()
.FirstOrDefault(x => x.AuctionAssignmentId == auctionAssignmentId);
}
public ImsAutoFileData GetNonConformingImsFileRecord(string vin, string auctionCustomerNo)
{
ImsAutoFileData imsAutoFile = null;
// Allow for vehicle brought back to auction after sale. it happens!
DateTime withinThirtyDays = DateTime.Now.AddDays(-30);
imsAutoFile = _context.ImsAutoFileData.AsNoTracking().FirstOrDefault(x =>
(x.VIN.Trim() == vin && x.AuctionCustomerNo == auctionCustomerNo
&& (x.SoldDate == null || x.SoldDate >= withinThirtyDays)));
return imsAutoFile;
}
public ImsAutoFileData FindImsAuctionFileData(int auctionAssignmentId, string customerNo, string VIN)
{
var existingAuctionFile = FindByAuctionAssignmentIdAsync(auctionAssignmentId) ?? GetNonConformingImsFileRecord(VIN, customerNo);
return existingAuctionFile;
}
public AuctionAssignment FindByIdAsync(int id)
{
return _context.AuctionAssignments.Where(c => c.ID == id).IncludeAll().FirstOrDefault();
}
public AuctionAssignment FindByAuctionCustomerNoAndVin(string auctionCustomerNo, string vin)
{
List<AuctionAssignment> auctionAssignments = FindAuctionByStatus(auctionCustomerNo, vin, statusTypeRepo);
if(!auctionAssignments.Any())
{
auctionAssignments = FindAuctionByStatus(auctionCustomerNo, vin, statusTypeSold);
}
if (!auctionAssignments.Any())
{
auctionAssignments = FindAuctionByStatus(auctionCustomerNo, vin, statusTypeRedeemed);
}
return auctionAssignments.FirstOrDefault();
}
public AuctionAssignment FindAuctionAssignment(ImsAutoFileData item)
{
var auctionAssignment
= /* FindByIdAsync(item.AuctionAssignmentId) this can't be used because old numbers will exist at first. ??*/
FindByAuctionCustomerNoAndVin(item.AuctionCustomerNo, item.VIN);
if (auctionAssignment == null)
{
throw new Exception("Auction assignment not found for Individual/VIN " + item.AuctionCustomerNo);
}
return auctionAssignment;
}
public RepoAccount FindRepoForImsAuctionItem(ImsAutoFileData item)
{
RepoAccount repo = null;
// GJF, this can't be used because old numbers will exist at first....
// if (item.AuctionAssignmentId > 0) {
// repo = await _repoAccountRepository.FindByAuctionAssignmentIdAsync(item.AuctionAssignmentId);
// }
repo = FindByAuctionCustomerNoAndVinAsync(item.AuctionCustomerNo, item.VIN);
if (repo == null)
{
throw new Exception("Repo Account not found for Individual/VIN " + item.AuctionCustomerNo);
}
return repo;
}
public IReadOnlyCollection<AuctionAssignment> ListReadyForFloorPrice()
{
var temp = (from x in _context.AuctionAssignments
where (x.FloorPrice ?? 0) == 0 && x.AutoIMS != null && x.AutoIMS.SoldDate == null &&
(x.AutoIMS.StatedMileage ?? -1) > 1 && x.AutoIMS.ScheduledSaleDate != null &&
x.AutoIMS.ScheduledSaleDate > DateTime.Now.Date &&
x.RedeemedDate == null &&
((x.AutoIMS.ApprovedVehicleGrade ?? 0) > 0 || (x.AutoIMS.VehicleGrade ?? 0) > 0)
select x).OrderByDescending(x => x.ID);
return temp.IncludeAll().ToList();
}
private List<RepoAccount> FindRepoByStatus(string auctionCustomerNo, string unformattedVin, string statusType)
{
return (from account in _context.RepoAccounts
join collateral in _context.Collaterals on account.RepoId equals collateral.RepoId
join auctionAssignment in _context.AuctionAssignments on account.RepoId equals auctionAssignment.RepoId
where GetFormattedVin(collateral.Vin) == GetFormattedVin(unformattedVin)
&& (auctionAssignment.AuctionCustomerNo == auctionCustomerNo || string.IsNullOrWhiteSpace(auctionCustomerNo))
&& account.Status == statusType
select account).ToList();
}
private List<AuctionAssignment> FindAuctionByStatus(string auctionCustomerNo, string vin, string statusType)
{
return (from auctionAssignment in _context.AuctionAssignments
join account in _context.RepoAccounts on auctionAssignment.RepoId equals account.RepoId
join collateral in _context.Collaterals on account.RepoId equals collateral.RepoId
where GetFormattedVin(collateral.Vin) == GetFormattedVin(vin)
&& (auctionAssignment.AuctionCustomerNo == auctionCustomerNo || string.IsNullOrWhiteSpace(auctionCustomerNo))
&& account.Status == statusType
select auctionAssignment).ToList();
}
}
}
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AutoImsAuctionProcessorService : IAutoImsAuctionProcessorService
{
private readonly IAuctionProcessorDao _auctionProcessorDao;
public AutoImsAuctionProcessorService(IAuctionProcessorDao auctionProcessorDao)
{
_auctionProcessorDao = auctionProcessorDao;
}
public ImportResultRow ImportSingleAutoImsAuction(ImsAutoFileData incomingAutoImsData)
{
AuctionAssignment auctionAssignment;
var result = new ImportResultRow()
{
Success = true,
Description = incomingAutoImsData.Description,
Messages = new List<string>()
};
incomingAutoImsData.VIN = incomingAutoImsData.VIN.Trim();
if (incomingAutoImsData.VIN.Length != 17) // must be 17. Auction may add as less than 17 characters.
{
Log.Error($"Vin Length Not Correct, {incomingAutoImsData.AuctionCustomerNo}, {incomingAutoImsData.VIN}, Skipping.");
result.Success = false;
result.Messages.Add("Invalid VIN, this row has been skipped.");
return result;
}
bool requiresAutoImsUpdate =
string.IsNullOrWhiteSpace(incomingAutoImsData.AuctionCustomerNo) ||
incomingAutoImsData.AuctionAssignmentId == 0;
try
{
result.RepoAccount = _auctionProcessorDao.FindRepoForImsAuctionItem(incomingAutoImsData);
auctionAssignment = _auctionProcessorDao.FindAuctionAssignment(incomingAutoImsData);
}
catch (Exception ex)
{
result.Success = false;
result.Messages.Add("Failed to load required data => " + ex.Message);
return result;
}
RepoAccount repo = result.RepoAccount;
// Always re-assign these fields, carried from previous system.
incomingAutoImsData.RepoId = auctionAssignment.RepoId ?? 0;
incomingAutoImsData.AuctionCustomerNo = auctionAssignment.AuctionCustomerNo;
incomingAutoImsData.AuctionAssignmentId = auctionAssignment.ID;
ImsAutoFileData existingAuctionFile = auctionAssignment.AutoIMS;
if (existingAuctionFile == null)
{
existingAuctionFile = _auctionProcessorDao.FindImsAuctionFileData(auctionAssignment.ID,
incomingAutoImsData.AuctionCustomerNo,
incomingAutoImsData.VIN);
}
if (existingAuctionFile == null)
{
result.Messages.Add("New Entry Created.");
existingAuctionFile = _auctionProcessorDao.CreateImsAuctionFileData(incomingAutoImsData);
if (existingAuctionFile.AssignmentAccepted != null)
{
result.Messages.Add("Auction accepted by Auto IMS on " + existingAuctionFile.AssignmentAccepted);
}
}
else
{
result.Messages.Add("Updated existing IMS Auction Entry.");
existingAuctionFile = _auctionProcessorDao.UpdateExistingFile(existingAuctionFile, incomingAutoImsData);
}
result = CheckAuctionGates(existingAuctionFile, result);
// update autoims with customer number and auction assignment id when not added through our interface.
if (requiresAutoImsUpdate)
{
result.Messages.Add(
_auctionProcessorDao.UpdateAutoImsFromAuctionData(existingAuctionFile));
}
result.Data = existingAuctionFile;
return result;
}
public ImportResultRow CheckAuctionGates(ImsAutoFileData existingAuctionFile, ImportResultRow result)
{
if (existingAuctionFile.VehicleGrade > 0)
{
result.Messages.Add("Grade present: " + existingAuctionFile.VehicleGrade + ".");
}
if (existingAuctionFile.FloorPriceAccepted > 0)
{
result.Messages.Add("Accepted Floor Price present: $" +
existingAuctionFile.FloorPriceAccepted + ".");
}
if (existingAuctionFile.SoldDate != null)
{
result.Messages.Add("Auction Sold:" + existingAuctionFile.SoldDate + ".");
}
if (existingAuctionFile.FundsSent != null)
{
result.Messages.Add("Funds Send Date received: " + existingAuctionFile.FundsSent + ".");
}
return result;
}
}
}
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LCU.API.Collections.RepoApi.Configuration;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AutoImsDownloaderService : IAutoImsDownloaderService
{
private readonly RepoApiConfiguration _configuration;
private readonly IAuctionFtpClientService _auctionFtpClientService;
public AutoImsDownloaderService(IAuctionFtpClientService auctionFtpClientService,
RepoApiConfiguration configuration)
{
_configuration = configuration;
_auctionFtpClientService = auctionFtpClientService;
}
public Dictionary<FileInfo, string> DownLoadAuctionFiles()
{
var fileResults = _auctionFtpClientService.Download();
return fileResults;
}
public Dictionary<FileInfo, string> CombineWithLocalFiles(Dictionary<FileInfo, string> fileResults)
{
// check if there are any files in the folder already that are not in our downloaded list.
string[] fileEntries = Directory.GetFiles(_configuration.LocalDirectoryPath);
foreach (string file in fileEntries)
{
var newFile = new FileInfo(file);
Log.Information($"Auto IMS Job Added file: {file}");
if (fileResults.Keys.All(x => x.Name != newFile.Name))
{
fileResults.Add(newFile, "success");
}
}
return fileResults;
}
public IEnumerable<FileInfo> ConvertToFileList(Dictionary<FileInfo, string> fileResults)
{
if (fileResults.Any(x => x.Value != "success"))
{
foreach (var file in fileResults.Where(x => x.Value != "success"))
{
Log.Information($"File Downloads failed: {file.Key} - {file.Value}");
}
}
if (fileResults.Any(x => x.Value == "success"))
{
var ascendingOrderOfFiles = fileResults.Where(x => x.Value == "success")
.OrderBy(x => x.Key.LastWriteTime).Select(x => x.Key).ToList();
return ascendingOrderOfFiles;
}
Log.Information("No files found from AutoIMS.");
return new List<FileInfo>();
}
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AutoImsImporterService : IAutoImsImporterService
{
private readonly IAuctionAssignmentRepository _auctionAssignmentRepository;
private readonly IAutoImsNoteProcessorService _autoImsNoteProcessorService;
private readonly IAutoImsAuctionProcessorService _autoImsAuctionProcessorService;
private readonly IFloorPricingService _floorPricingService;
private readonly IAuctionAssignmentService _auctionAssignmentService;
public AutoImsImporterService(
IAuctionAssignmentRepository auctionAssignmentRepository,
IAutoImsNoteProcessorService autoImsNoteProcessorService,
IAutoImsAuctionProcessorService autoImsAuctionProcessorService, IFloorPricingService floorPricingService,
IAuctionAssignmentService auctionAssignmentService)
{
_autoImsAuctionProcessorService = autoImsAuctionProcessorService;
_floorPricingService = floorPricingService;
_auctionAssignmentService = auctionAssignmentService;
_autoImsNoteProcessorService = autoImsNoteProcessorService;
_auctionAssignmentRepository = auctionAssignmentRepository;
}
/**
* A set of notes comes from AutoIMS and must be inserted (But not updated).
* Duplicates/Existing are checked for based on writer, time and text.
*/
public IReadOnlyCollection<AuctionAssignmentNote> ImportNotesToDatabase(IEnumerable<AuctionAssignmentNote> data)
{
List<AuctionAssignmentNote> savedData = new List<AuctionAssignmentNote>();
foreach (var item in data)
{
try
{
var result = _autoImsNoteProcessorService.ImportSingleNoteRow(item);
if (result != null)
{
savedData.Add(result);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Error processing notes for auction accountNo: {item.AuctionCustomerNo}");
}
}
return savedData;
}
/**
* Each row in the Data file is an ImsAutoAuctionData row used to track the status of
* a Repo at auction and assist in setting the floor.
*/
public IReadOnlyCollection<ImportResultRow> ImportDataFile(IEnumerable<ImsAutoFileData> models)
{
List<ImportResultRow> savedData = new List<ImportResultRow>();
foreach (var item in models)
{
try
{
var result = _autoImsAuctionProcessorService.ImportSingleAutoImsAuction(item);
if (result != null)
{
savedData.Add(result);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Error processing data for auction account with repoId: {item.RepoId}");
}
}
return savedData;
}
public IReadOnlyCollection<ImportResultRow> SetAllAvailableFloors()
{
var list = new List<ImportResultRow>();
var auctionAssignments =
_auctionAssignmentRepository.ListReadyForFloorPriceAsync()
.ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult();
if (auctionAssignments.Items != null && auctionAssignments.Items.Any())
{
foreach (var auctionAssignment in auctionAssignments.Items)
{
var result = new ImportResultRow() {Messages = new List<string>()};
var floorPriceCalculationResultSet = _floorPricingService.DetermineFloorPrice(auctionAssignment.ID);
if (floorPriceCalculationResultSet?.PrimaryResult != null)
{
result.Messages.Add("Floor Price Results Generated for Account " + auctionAssignment.RepoId);
if (floorPriceCalculationResultSet.PrimaryResult.Success)
{
result.Messages.Add("Floor Price Results SUCCESS for Account " + auctionAssignment.RepoId);
auctionAssignment.FloorPrice = floorPriceCalculationResultSet.PrimaryResult.FloorPrice;
auctionAssignment.FloorPriceValuesUsed =
floorPriceCalculationResultSet.FloorPriceValuesUsed;
var updated = _auctionAssignmentService.UpdateAsync(auctionAssignment).GetAwaiter()
.GetResult();
Log.Information($"Auction Floor Set Id:{updated.ID}, FloorPrice:{updated.FloorPrice}, Skipping.");
result.Messages.Add(
$"Floor price set for {auctionAssignment.RepoId} to {auctionAssignment.FloorPrice}");
result.Success = true;
}
else
{
result.Messages.Add("Floor Price Results FAILURE for Account " + auctionAssignment.RepoId);
result.Messages.Add("Reason: " +
floorPriceCalculationResultSet.PrimaryResult.FailureReason);
}
}
list.Add(result);
}
}
else
{
Log.Information("SetAllAvailableFloors: No floors to set.");
}
return list;
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Linq;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class AutoImsNoteProcessorService : IAutoImsNoteProcessorService
{
private readonly IAuctionAssignmentNoteRepository _auctionAssignmentNoteRepository;
private readonly IUnitOfWork _unitOfWork;
public AutoImsNoteProcessorService(IAuctionAssignmentNoteRepository auctionAssignmentNoteRepository,
IUnitOfWork unitOfWork
)
{
_unitOfWork = unitOfWork;
_auctionAssignmentNoteRepository = auctionAssignmentNoteRepository;
}
public AuctionAssignmentNote ImportSingleNoteRow(AuctionAssignmentNote item)
{
var dateWritten = item.DateWritten ?? DateTime.Now;
var existing =
_auctionAssignmentNoteRepository.FindConflictingNotes(dateWritten, item.Text, item.Writer)
.GetAwaiter().GetResult();
if (!existing.Any())
{
item.DateWritten = dateWritten;
Log.Information("Saving Note.");
_auctionAssignmentNoteRepository.AddAsync(item);
_unitOfWork.CompleteAsync().GetAwaiter().GetResult();
}
else
{
Log.Information($"Note already exists, customerNo: {item.AuctionCustomerNo} on writeDate: {item.DateWritten}");
}
return item;
}
}
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.IO;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAuctionFtpClientService
{
Dictionary<FileInfo, string> Download();
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAuctionProcessorDao
{
string UpdateAutoImsFromAuctionData(ImsAutoFileData item);
RepoAccount FindByAuctionCustomerNoAndVinAsync(string auctionCustomerNo,
string unformattedVin);
ImsAutoFileData UpdateExistingFile(ImsAutoFileData existingAuctionFile, ImsAutoFileData imsAutoFileData);
ImsAutoFileData CreateImsAuctionFileData(ImsAutoFileData imsAutoFileData);
void Add(ImsAutoFileData assignment);
ImsAutoFileData FindByAuctionAssignmentIdAsync(int auctionAssignmentId);
ImsAutoFileData GetNonConformingImsFileRecord(string vin, string auctionCustomerNo);
ImsAutoFileData FindImsAuctionFileData(int auctionAssignmentId, string customerNo, string VIN);
AuctionAssignment FindByIdAsync(int id);
AuctionAssignment FindByAuctionCustomerNoAndVin(string auctionCustomerNo, string vin);
AuctionAssignment FindAuctionAssignment(ImsAutoFileData item);
RepoAccount FindRepoForImsAuctionItem(ImsAutoFileData item);
IReadOnlyCollection<AuctionAssignment> ListReadyForFloorPrice();
}
}
@@ -0,0 +1,10 @@
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAutoImsAuctionProcessorService
{
ImportResultRow ImportSingleAutoImsAuction(ImsAutoFileData item);
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.IO;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAutoImsDownloaderService
{
Dictionary<FileInfo, string> DownLoadAuctionFiles();
Dictionary<FileInfo, string> CombineWithLocalFiles(Dictionary<FileInfo, string> fileResults);
IEnumerable<FileInfo> ConvertToFileList(Dictionary<FileInfo, string> fileResults);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAutoImsImporterService
{
IReadOnlyCollection<AuctionAssignmentNote> ImportNotesToDatabase(IEnumerable<AuctionAssignmentNote> data);
IReadOnlyCollection<ImportResultRow> ImportDataFile(IEnumerable<ImsAutoFileData> models);
IReadOnlyCollection<ImportResultRow> SetAllAvailableFloors();
}
}
@@ -0,0 +1,9 @@
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IAutoImsNoteProcessorService
{
AuctionAssignmentNote ImportSingleNoteRow(AuctionAssignmentNote item);
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.IO;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IImsAutoFileDataConverter
{
ImsAutoFileData SetFieldByName(ImsAutoFileData input, string fieldName, string value);
IReadOnlyCollection<ImsAutoFileData> CsvToImsAutoFileData(Stream file);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.IO;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public interface IImsAutoFileNoteConverter
{
IEnumerable<AuctionAssignmentNote> CsvToImsAutoFileNotes(Stream file);
IEnumerable<AuctionAssignmentNote> CompressIdentifyingRows(IEnumerable<AuctionAssignmentNote> csvNotes);
AuctionAssignmentNote SetFieldByName(AuctionAssignmentNote input, string fieldName, string value);
}
}
@@ -0,0 +1,311 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using LCU.API.Collections.RepoApi.Configuration;
using LCU.Shared.Collections.RepoApiLib.Models;
using NotVisualBasic.FileIO;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class ImsAutoFileDataConverter : IImsAutoFileDataConverter
{
private readonly RepoApiConfiguration _configuration;
public ImsAutoFileDataConverter( RepoApiConfiguration configuration)
{
_configuration = configuration;
}
public IReadOnlyCollection<ImsAutoFileData> CsvToImsAutoFileData(Stream file)
{
List<ImsAutoFileData> listOfObjects = new List<ImsAutoFileData>();
using (CsvTextFieldParser tfp = new CsvTextFieldParser(file))
{
// Get Some Column Names
string[] columns = tfp.ReadFields();
while (!tfp.EndOfData)
{
var fields = tfp.ReadFields();
if (!fields[0].Contains("------") && (fields.Any(x => x.Trim().Length > 0)))
{
var obj = new ImsAutoFileData();
for (var i = 0; i < fields.Length; i++)
{
obj = SetFieldByName(obj, columns[i].Trim(), fields[i]);
}
listOfObjects.Add(obj);
}
}
}
return listOfObjects;
}
public ImsAutoFileData SetFieldByName(ImsAutoFileData input, string fieldName, string value)
{
try
{
switch (fieldName)
{
case "Announced Conditions":
input.AnnouncedConditions = value.Trim();
break;
case "Arbitration Reason":
input.ArbitrationReason = value.Trim();
break;
case "As Is 100K Miles":
input.AsIs100KMiles = (value.Trim() == "Yes");
break;
case "Assignment Accepted":
input.AssignmentAccepted =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Auction Age":
input.AuctionAge = !string.IsNullOrWhiteSpace(value) ? NumbersOnly<int>(value) : (int?) null;
break;
case "Auction Guide Book Price":
input.AuctionGuideBookPrice = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Buyer ID":
input.BuyerId = value.Trim();
break;
case "Buyer Name":
input.BuyerName = value.Trim();
break;
case "Charge Amount":
input.ChargeAmount = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Check Amount":
input.CheckAmount = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Client Age":
input.ClientAge = !string.IsNullOrWhiteSpace(value) ? NumbersOnly<int>(value) : (int?) null;
break;
case "Client Cancelled":
input.ClientCancelled =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Collision Damage":
input.CollisionDamage = value.Trim();
break;
case "Inspected Date":
input.InspectedDate = !string.IsNullOrEmpty(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Date of Loss":
input.DateofLoss = !string.IsNullOrEmpty(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Date Picked Up":
input.DatePickedUp = !string.IsNullOrEmpty(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Date Entered AutoIMS":
input.DateEnteredAutoIMS = !string.IsNullOrEmpty(value)
? DateTime.Parse(value.Substring(0, value.Trim().Length - 4))
: (DateTime?) null;
break;
case "Days Run":
input.DaysRun = !string.IsNullOrWhiteSpace(value) ? NumbersOnly<int>(value) : (int?) null;
break;
case "Days Run/Highest Bid Recorded by Auction":
input.DaysRunHighestBidRecordedbyAuction = value;
break;
case "Days to Delivery":
input.DaystoDelivery =
!string.IsNullOrWhiteSpace(value) ? NumbersOnly<int>(value) : (int?) null;
break;
case "Description":
input.Description = value.Trim();
break;
case "Drivable":
input.IsDrivable = !string.IsNullOrWhiteSpace(value)
? value == "Yes" ? true : false
: (bool?) null;
break;
case "Floor Price (Accepted)":
input.FloorPriceAccepted = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Funds Sent":
input.FundsSent = !string.IsNullOrEmpty(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Gross % Floor":
input.GrossPercentFloor =
!string.IsNullOrWhiteSpace(value) ? NumbersOnly<decimal>(value) : (decimal?) null;
break;
case "Highest Bid":
input.HighestBid = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Mileage":
input.StatedMileage = !string.IsNullOrWhiteSpace(value)
? int.Parse(value.Replace(",", ""))
: (int?) null;
break;
case "Net % Floor":
input.NetPercentFloor = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Other Advance Fees":
input.OtherAdvanceFees = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "OTHER Total":
input.OTHERTotal = value.Trim();
break;
case "OTHER: Advanced Other(72)":
input.OTHERAdvancedOther = value.Trim();
break;
case "OTHER: Advanced Storage(71)":
input.OTHERAdvancedStorage = value.Trim();
break;
case "OTHER: Advanced Tow(70)":
input.OTHERAdvancedTow = value.Trim();
break;
case "OTHER: Miscellaneous(41)":
input.OTHERMiscellaneous = value.Trim();
break;
case "Sale Price":
input.SalePrice = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Scheduled Sale Date":
input.ScheduledSaleDate =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Sold Date":
input.SoldDate = !string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Title #":
input.TitleNumber = value.Trim();
break;
case "Title Received":
input.TitleReceived =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Title Returned":
input.TitleReturned =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Title State":
input.TitleState = value.Trim();
break;
case "Title Status":
input.TitleStatus = value.Trim();
break;
case "Title Status from Auction":
input.TitleStatusfromAuction = value.Trim();
break;
case "Total Charges":
input.TotalCharges = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Vehicle Grade (Approved CR)":
input.ApprovedVehicleGrade = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Operable":
input.IsOperable = !string.IsNullOrWhiteSpace(value)
? value == "Yes" ? true : false
: (bool?) null;
break;
case "Shipment Date":
input.ShipmentDate =
!string.IsNullOrWhiteSpace(value) ? DateTime.Parse(value) : (DateTime?) null;
break;
case "Status":
input.Status = value.Trim();
break;
case "VIN":
input.VIN = value.Trim();
break;
case "Storage Cost":
input.StorageCost = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Storage Rate Per Day":
input.StorageRatePerDay = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Tow Cost":
input.TowCost = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Cn. Acc. #":
input.AuctionCustomerNo = value.Trim();
break;
case "Auction ID":
break;
case "Vehicle Decimal Grade Code":
input.VehicleDecimalGradeCode = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Vehicle Grade":
input.VehicleGrade = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Most Recent National Adjusted MMR Price":
input.MMRNationalAdjustedPrice = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "Most Recent Regional Adjusted MMR Price":
input.MMRRegionalAdjustedPrice = !string.IsNullOrWhiteSpace(value)
? NumbersOnly<decimal>(value)
: (decimal?) null;
break;
case "MMR":
input.MMR = value.Trim();
break;
case "External CR Link":
input.ConditionReportLink = _configuration.ConditionReportUrl + value;
break;
case "Open Text 1":
input.AuctionAssignmentId =
!string.IsNullOrWhiteSpace(value) ? int.Parse(value.Replace(",", "")) : -1;
break;
default:
Log.Error("Got unknown field {0} with value {1} for ImsAutoFileData", fieldName,
value);
break;
}
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to set field {fieldName} with value {value} for ImsAutoFileData");
}
return input;
}
private static T NumbersOnly<T>(string val)
{
var formattedStr = Regex.Replace(val.Trim(), "[^0-9\\.]", "");
if (string.IsNullOrWhiteSpace(formattedStr))
formattedStr = "-1";
return (T) Convert.ChangeType(formattedStr, typeof(T));
}
}
}
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LCU.Shared.Collections.RepoApiLib.Models;
using NotVisualBasic.FileIO;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.AutoIms
{
public class ImsAutoFileNoteConverter : IImsAutoFileNoteConverter
{
public ImsAutoFileNoteConverter()
{
}
public IEnumerable<AuctionAssignmentNote> CsvToImsAutoFileNotes(Stream file)
{
List<AuctionAssignmentNote> listOfObjects = new List<AuctionAssignmentNote>();
using (CsvTextFieldParser tfp = new CsvTextFieldParser(file))
{
// Get Some Column Names
string[] columns = tfp.ReadFields();
while (!tfp.EndOfData)
{
var fields = tfp.ReadFields();
if (!fields[0].Contains("------") && (fields.Any(x => x.Trim().Length > 0)))
{
var obj = new AuctionAssignmentNote();
for (var i = 0; i < fields.Length; i++)
{
obj = SetFieldByName(obj, columns[i].Trim(), fields[i].Trim());
}
listOfObjects.Add(obj);
}
}
}
return listOfObjects;
}
/**
* Take the rows with only account and discard them, using the account no to update the other rows.
* Each row in the CSV does not have CustomerAccountNo, only the first row of each set.
* Further, if a row has a customer account no, it has nothing else.
*/
public IEnumerable<AuctionAssignmentNote> CompressIdentifyingRows(IEnumerable<AuctionAssignmentNote> csvNotes)
{
List<AuctionAssignmentNote> data = new List<AuctionAssignmentNote>();
string accountNo = String.Empty;
foreach (var item in csvNotes)
{
try
{
if (!string.IsNullOrWhiteSpace(item.AuctionCustomerNo) && item.AuctionCustomerNo != accountNo)
{
accountNo = item.AuctionCustomerNo;
// Skip account rows, unless they start showing up with data.
if (string.IsNullOrWhiteSpace(item.Text))
{
continue;
}
}
if (string.IsNullOrWhiteSpace(accountNo))
{
Log.Error($"No Account Number when processing notes row, {item.Text}");
return null;
}
item.AuctionCustomerNo = accountNo;
data.Add(item);
}
catch (Exception ex)
{
Log.Error(ex, $"Error compressing notes for auction accountNo: {accountNo}");
}
}
return data;
}
public AuctionAssignmentNote SetFieldByName(AuctionAssignmentNote input, string fieldName, string value)
{
try
{
switch (fieldName)
{
case "Cn. Acc. #":
input.AuctionCustomerNo = value;
break;
case "Origin":
input.Origin = value;
break;
case "Writer":
input.Writer = value;
break;
case "Type":
input.Type = value;
break;
case "Text":
input.Text = value;
break;
case "Date Written":
input.DateWritten = !string.IsNullOrWhiteSpace(value)
? DateTime.Parse(value.Substring(0, value.Trim().Length - 4))
: (DateTime?) null;
break;
default:
Log.Error("Got unknown field {fieldName} with value {value} for AuctionAssignmentNote");
break;
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to set field {fieldName} with value {value} for AuctionAssignmentNote.");
}
return input;
}
}
}
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.IO;
using LCU.API.Collections.RepoApi.Configuration;
using LCU.API.Collections.RepoApi.Services.AutoIms;
using LCU.Shared.Collections.RepoApiLib.Models;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class AutoImsService
{
private readonly IImsAutoFileDataConverter _imsAutoFileDataConverter;
private readonly IImsAutoFileNoteConverter _imsAutoFileNoteConverter;
private readonly IEmailer _emailer;
private readonly IAutoImsDownloaderService _downloaderService;
private readonly RepoApiConfiguration _config;
private readonly IAutoImsImporterService _autoImsImporterService;
public AutoImsService(
IAutoImsDownloaderService downloaderService,
RepoApiConfiguration config,
IAutoImsImporterService autoImsImporterService,
IImsAutoFileDataConverter imsAutoFileDataConverter,
IImsAutoFileNoteConverter imsAutoFileNoteConverter, IEmailer emailer)
{
_downloaderService = downloaderService;
_config = config;
_emailer = emailer;
_autoImsImporterService = autoImsImporterService;
_imsAutoFileDataConverter = imsAutoFileDataConverter;
_imsAutoFileNoteConverter = imsAutoFileNoteConverter;
}
public IEnumerable<FileInfo> Download()
{
Log.Information("Starting AutoIMS Download Process.");
var files = new Dictionary<FileInfo, string>();
try
{
if (_config.DownloadNewFiles)
{
Log.Debug("Downloading auction files.");
files = _downloaderService.DownLoadAuctionFiles();
}
if (_config.CheckForLocalFiles)
{
Log.Debug("Checking for local files.");
files = _downloaderService.CombineWithLocalFiles(files);
}
}
catch (Exception ex)
{
Log.Error(ex,"AutoIms File Downloader Failure");
}
Log.Information("Finished AutoIMS Download Process.");
return _downloaderService.ConvertToFileList(files);
}
public void ProcessDownloadedFiles(IEnumerable<FileInfo> ascendingOrderOfFiles)
{
Log.Debug("Starting AutoIMS Entry Processing.");
if (ascendingOrderOfFiles == null)
{
throw new Exception("Must pass a valid IEnumerable of FileInfo to ProcessDownloads.");
}
foreach (var file in ascendingOrderOfFiles)
{
ProcessSingleFile(file);
}
}
public void ProcessSingleFile(FileInfo file)
{
Log.Debug("Processing Single Auto IMS File. {0}", file.FullName);
string fileName = file.Name;
FileStream stream = null;
try
{
stream = File.OpenRead(Path.Combine(_config.LocalDirectoryPath, fileName));
}
catch (IOException ioException)
{
Log.Error(ioException, "Failed to open AutoIMS file {0}, got exception.", file.FullName);
return;
}
if (file.Name.ToLower().Contains("notes"))
{
ProcessSingleAutoImsNotesFile(file, stream);
}
else
{
ProcessSingleAutoImsFile(file, stream);
}
if (_config.MoveProcessedFiles)
{
Log.Debug("Moving Files for AutoIMS Process.");
// create current month folder
if (!Directory.Exists(
Path.Combine(_config.LocalDirectoryPath, DateTime.Now.ToString("MMM yyyy"))))
{
var newDir = Path.Combine(_config.LocalDirectoryPath, DateTime.Now.ToString("MMM yyyy"));
Log.Information("Creating folder {0}", newDir);
Directory.CreateDirectory(newDir);
}
var sourceFileName = Path.Combine(_config.LocalDirectoryPath, fileName);
var destFileName =
Path.Combine(_config.LocalDirectoryPath, DateTime.Now.ToString("MMM yyyy"), fileName);
Log.Information("Moving File {0} to Folder {1}.", sourceFileName, destFileName);
File.Move(sourceFileName, destFileName);
}
}
public void ProcessSingleAutoImsNotesFile(FileInfo file, FileStream stream)
{
Log.Debug("Processing notes file {0}.", file);
try
{
IEnumerable<AuctionAssignmentNote> data =
_imsAutoFileNoteConverter.CsvToImsAutoFileNotes(stream);
data = _imsAutoFileNoteConverter.CompressIdentifyingRows(data);
_autoImsImporterService.ImportNotesToDatabase(data);
}
catch (Exception ex)
{
Log.Error(ex,"AutoIMS Note Import Failure");
}
}
public void ProcessSingleAutoImsFile(FileInfo file, FileStream stream)
{
Log.Debug("Processing data file {0}.", file);
try
{
IReadOnlyCollection<ImsAutoFileData> models = _imsAutoFileDataConverter.CsvToImsAutoFileData(stream);
var imported = _autoImsImporterService.ImportDataFile(models);
var floors = _autoImsImporterService.SetAllAvailableFloors();
_emailer.SendRefundProcessStartEmails(imported);
_emailer.SendSummaryEmail(imported, floors);
}
catch (Exception ex)
{
Log.Error(ex,"AutoIMS Data Import Failure");
}
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class CollateralService : ICollateralService
{
private readonly ICollateralRepository _collateralRepository;
private readonly IUnitOfWork _unitOfWork;
public CollateralService(
ICollateralRepository collateralRepository,
IUnitOfWork unitOfWork)
{
_collateralRepository = collateralRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<Collateral>> ListAll()
{
try
{
return await _collateralRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Collaterals");
throw new ServerErrorException();
}
}
public async Task<Collateral> SaveAsync(Collateral collateral)
{
try
{
await _collateralRepository.AddAsync(collateral);
await _unitOfWork.CompleteAsync();
return collateral;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Collateral: {collateral}", collateral);
throw new ServerErrorException();
}
}
public async Task<Collateral> Get(int id)
{
Collateral collateral;
try
{
collateral = await _collateralRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Collateral with id: {id}");
throw new ServerErrorException();
}
if (collateral == null)
{
throw new NotFoundException($"Collateral with id {id} not found.");
}
return collateral;
}
public async Task<Collateral> UpdateAsync(Collateral collateral)
{
Collateral existing = null;
try
{
existing = await _collateralRepository.FindByIdAsync(collateral.CollateralId);
}catch(Exception ex)
{
Log.Error(ex, $"Failure retrieving existing collateral with collateralId: {collateral.CollateralId}",collateral);
throw new ServerErrorException();
}
if (existing == null)
{
throw new NotFoundException($"Collateral with id {collateral.CollateralId} not found.");
}
existing.Vin = collateral.Vin;
existing.Make = collateral.Make;
existing.Model = collateral.Model;
existing.Year = collateral.Year;
existing.Description = collateral.Description;
existing.VehicleType = collateral.VehicleType;
existing.BodyStyle = collateral.BodyStyle;
existing.Inoperable = collateral.Inoperable;
existing.CollateralTypeId = collateral.CollateralTypeId;
existing.RetitledInLandmarkName = collateral.RetitledInLandmarkName;
try
{
_collateralRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return (existing);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Collateral: {collateral}", collateral);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Collateral: {collateral}", collateral);
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Configuration;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
using LCU.Shared.Collections.RepoApiLib.Models.Consumer;
using LCU.Shared.Common.NotificationApiLib.Models;
using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class Emailer : IEmailer
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly RepoApiConfiguration _configuration;
private readonly IEventAlertService _eventAlertService;
private const string O4RStatus = "O4R";
public Emailer(IHttpClientFactory httpClientFactory, RepoApiConfiguration configuration, IEventAlertService eventAlertService)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_eventAlertService = eventAlertService;
}
public async Task<HttpResponseMessage> SendMessageAsync(List<string> recipient, string subject, string body)
{
var emailRequest = new EmailRequest
{
Recipient = recipient,
Subject = subject,
Body = body
};
var encoded = JsonConvert.SerializeObject(emailRequest);
try
{
var client = _httpClientFactory.CreateClient();
var c = new StringContent(encoded, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(_configuration.NotificationsEmailUrl, c);
response.EnsureSuccessStatusCode();
return response;
}
catch (Exception ex)
{
Log.Error(ex, "An exception was caught trying to send an email via Notifications API.");
return null;
}
}
public async Task<HttpResponseMessage> SendFloorPriceAcceptedEmail(bool isManual, string accountNo, decimal? oldFloorPrice, decimal? newFloorPrice)
{
var subject = isManual ? "Manual Floor Price Set" : "Automated Floor Price Set";
var body = string.Format(subject + " for account {0} from {1} to {2}.", accountNo, oldFloorPrice?.ToString("C2", CultureInfo.CurrentCulture), newFloorPrice?.ToString("C2", CultureInfo.CurrentCulture));
return await SendMessageAsync(_configuration.FloorPriceAcceptedRecipients, subject, body);
}
public async Task<HttpResponseMessage> SendSurplusDeficiencyLetterEmailAsync(RepoAccount repoAccount)
{
var message = "Please Send the Surplus Deficiency letter. Please notify the Repo Specialists when the letter has been sent, so that this account can be closed in Repo Manager.<br/><br/>";
var member = repoAccount.Borrowers?.First(b => b.OwnershipType == 6);
var memberName = "Member Name: " + member?.IndividualName + "<br/>";
var accountNo = "Account No: " + repoAccount.AccountNo + "<br/>";
return await SendMessageAsync(_configuration.CollectionsClerkRecipients, "Refunds Recieved", string.Concat(message, memberName, accountNo));
}
public async Task<HttpResponseMessage> MaybeSendO4rNoCollateralEmail(RepoAccount repoAccount)
{
if (repoAccount != null && repoAccount.DateClosed == null && repoAccount.Status == O4RStatus)
{
var subject = "O4R No Collateral";
var body = string.Format("Account {0} set to O4R with no collateral.", repoAccount.AccountNo);
return await SendMessageAsync(_configuration.RepoManagerAdminRecipients, subject, body);
}
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
}
public async Task<HttpResponseMessage> SendAlertSummaryEmailWithEndTime(DateTime endTime)
{
var startTime = endTime.AddHours(-1);
var eventAlerts = _eventAlertService.ListWithinDateRange(startTime, endTime).GetAwaiter().GetResult();
var subject = "Repo Manager Alerts " + endTime.ToString("MM-dd-yyyy HH:mm:ss");
var body = "<h2>" + subject + "</h2><br/>";
foreach (var eventAlert in eventAlerts)
{
var entry = CreateAlertEmailEntry(eventAlert);
if (!string.IsNullOrEmpty(entry))
{
body += CreateAlertEmailEntry(eventAlert) + "<br/><br/>";
}
}
if (!eventAlerts.Any())
{
body += "No new alerts.";
}
return await SendMessageAsync(_configuration.CollectionsRecipients, subject, body);
}
private string CreateAlertEmailEntry(EventAlert eventAlert)
{
var accountNo = "Account No: " + eventAlert.AccountNo + "<br/>";
var alert = "Alert: " + eventAlert.Message + "<br/>";
var url = eventAlert.RepoId != null
? _configuration.RepoManagerAccountUrl + eventAlert.RepoId
: "";
var link = eventAlert.RepoId != null
? string.Format("<a href={0}>{0}</a>", url) + "<br/>"
: "";
return string.Concat(accountNo, alert, link);
}
public void SendSummaryEmail(IReadOnlyCollection<ImportResultRow> imported, IReadOnlyCollection<ImportResultRow> floors)
{
Log.Debug("Sending AutoIMS Import Email");
var subject = "Landmark Auto IMS Import " + DateTime.Now;
var reports = new List<string>();
if (imported == null || floors == null)
{
Log.Error("Null result sets passed to SendSummaryEmail");
return;
}
foreach (var importResultRow in imported)
{
reports.Add(importResultRow.BuildStatusReport(_configuration.RepoManagerAccountUrl));
reports.Add("=====================================================================================================<br/>");
}
foreach (var importResultRow in floors)
{
reports.Add(importResultRow.BuildStatusReport(_configuration.RepoManagerAccountUrl));
}
var result = reports.Join("<br/>");
SendMessageAsync(_configuration.CreditDebitsRecipients, subject, result).GetAwaiter().GetResult();
Log.Debug("AutoIMS Import Email Sent");
}
public void SendRefundProcessStartEmails(IReadOnlyCollection<ImportResultRow> models)
{
if (models == null || !models.Any())
{
Log.Warning("No models to process sold emails for.");
return;
}
var soldResults = models.Where(x => x.Data?.SoldDate?.Date == DateTime.Now.Date).ToList();
foreach (var sale in soldResults)
{
SendMessageAsync(_configuration.CollectionsClerkRecipients, "Auction sold, please start the refund process", CreateRefundProcessEmailBody(sale)).GetAwaiter().GetResult();
}
}
public string CreateRefundProcessEmailBody(ImportResultRow sale)
{
if (sale?.RepoAccount == null || sale.Data == null || sale.RepoAccount.Borrowers == null)
{
Log.Error("Invalid data, can't create email body for account.");
return "";
}
var member = sale.RepoAccount.Borrowers?.First(b => b.OwnershipType == 6);
var individualName = member != null ? member.IndividualName : "No Primary Owner!";
var memberName = "Member Name: " + individualName + "<br/>";
var accountNo = "Account No: " + sale.RepoAccount.AccountNo + "<br/>";
var collateralVin = "VIN: " + sale.Data.VIN + "<br/>";
return string.Concat(memberName, accountNo, collateralVin);
}
public async Task<HttpResponseMessage> SendProccessSummary(RepoRunInfo runInfo)
{
string subject = $"Repo Processor Run Receipt [{runInfo.Receipts.Count}]";
var receiptList = runInfo.Receipts.Select(receipt => receipt.Pretty());
string bodyData = string.Join("<br/>", receiptList);
string body = $"Process Started: {runInfo.StartDateTime} and completed at {runInfo.CompleteDateTime}<br/><br/>";
return await SendMessageAsync(_configuration.RepoManagerAdminRecipients, subject, body + bodyData);
}
}
}
@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class EventAlertService : IEventAlertService
{
private readonly IEventAlertRepository _eventAlertRepository;
private readonly IUnitOfWork _unitOfWork;
private const string RedeemedStatus = "REDEEMED";
private const string CancelledStatus = "RECA";
private readonly IRepoAccountRepository _accountRepository;
public EventAlertService(
IEventAlertRepository eventAlertRepository,
IUnitOfWork unitOfWork,
IRepoAccountRepository accountRepository)
{
_eventAlertRepository = eventAlertRepository;
_unitOfWork = unitOfWork;
_accountRepository = accountRepository;
}
public async Task<IEnumerable<EventAlert>> ListAll()
{
try
{
return await _eventAlertRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving All Event Alerts");
throw new ServerErrorException();
}
}
public async Task<IEnumerable<EventAlert>> ListUnread()
{
try
{
return await _eventAlertRepository.ListUnreadAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Unread Event Alerts");
throw new ServerErrorException();
}
}
public async Task<IEnumerable<EventAlert>> ListWithinDateRange(DateTime start, DateTime end)
{
try
{
return await _eventAlertRepository.ListWithinDateRangeAsync(start, end);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Event Alerts between {start} and {end}");
throw new ServerErrorException();
}
}
public async Task<IEnumerable<EventAlert>> ListActionNeeded()
{
try
{
var repoNoAuction = await _eventAlertRepository.ListRepoNoAuctionAsync();
var repoNoCompany = await _eventAlertRepository.ListRepoNoCompanyAsync();
var o4rNoCompany = await _eventAlertRepository.ListO4rNoCompanyAsync();
var retitle = await _eventAlertRepository.ListReadyForRetitleAsync();
var floorUpdateFail = await _eventAlertRepository.ListFloorUpdateFailedAsync();
var setFloor = await _eventAlertRepository.ListFloorNeedsSettingAsync();
return repoNoAuction.Concat(repoNoCompany).Concat(o4rNoCompany).Concat(retitle).Concat(floorUpdateFail).Concat(setFloor).ToList();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Action Needed Event Alerts");
throw new ServerErrorException();
}
}
public async Task<EventAlert> SaveAsync(EventAlert eventAlert)
{
try
{
if (IsStatusCancelledOrRedeemed(eventAlert.RepoId.Value).Result)
{
throw new UnwantedStatusException(eventAlert.RepoId.Value, eventAlert.AccountNo);
}
await _eventAlertRepository.AddAsync(eventAlert);
await _unitOfWork.CompleteAsync();
return eventAlert;
}
catch (DuplicateResourceException ex)
{
Log.Debug(ex, $"SaveAsync: Valid Alert Already Exists eventAlert:{eventAlert}", eventAlert);
throw ex;
}
catch(UnwantedStatusException ex)
{
Log.Error(ex, $"SaveAsync: Status of Repo is Redeemed or Cancelled. Event Alert: {eventAlert} not created", eventAlert);
throw;
}
catch (Exception ex)
{
Log.Error(ex, $"SaveAsync: Failure saving Event Alert: {eventAlert}", eventAlert);
throw new ServerErrorException();
}
}
public async Task<IEnumerable<EventAlert>> SaveRangeAsync(List<EventAlert> eventAlerts)
{
IEnumerable<EventAlert> addedEventAlerts = new List<EventAlert>();
try
{
addedEventAlerts = await _eventAlertRepository.AddRangeAsync(eventAlerts);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"EventAlertService: Failure Saving Range Of Event Alerts: {eventAlerts}", eventAlerts);
}
return addedEventAlerts;
}
public async Task<EventAlert> Get(int id)
{
EventAlert eventAlert;
try
{
eventAlert = await _eventAlertRepository.FindByIdAsync(id);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Event Alert with id: {id}");
throw new ServerErrorException();
}
if (eventAlert == null)
{
throw new NotFoundException($"EventAlert with id {id} not found.");
}
return eventAlert;
}
public async Task<EventAlert> UpdateAsync(EventAlert eventAlert, AppUser user)
{
EventAlert existingEventAlert = null;
try
{
existingEventAlert = await _eventAlertRepository.FindByIdAsync(eventAlert.Id);
}
catch(Exception ex)
{
Log.Error(ex, $"Failed to find existing event alert with Id: {eventAlert.Id}, user: {user}", eventAlert, user);
throw new ServerErrorException();
}
if (existingEventAlert == null)
{
throw new NotFoundException($"EventAlert with id {eventAlert.Id} not found.");
}
existingEventAlert.Message = eventAlert.Message;
existingEventAlert.SubMessage = eventAlert.SubMessage;
if (existingEventAlert.DateMarkedRead == null && eventAlert.DateMarkedRead != null)
{
existingEventAlert.MarkedReadBy = user.FirstName + " " + user.LastName;
existingEventAlert.DateMarkedRead = eventAlert.DateMarkedRead;
}
try
{
_eventAlertRepository.Update(existingEventAlert);
await _unitOfWork.CompleteAsync();
return (existingEventAlert);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Event Alert: {eventAlert}", eventAlert);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Event Alert {eventAlert}", eventAlert);
throw new ServerErrorException();
}
}
public async Task<EventAlert> DeleteAsync(int id)
{
EventAlert existingEventAlert = null;
try
{
existingEventAlert = await _eventAlertRepository.FindByIdAsync(id);
}
catch(Exception ex)
{
Log.Error(ex, $"Failed to find existing event alert with Id: {id}.");
throw new ServerErrorException();
}
if (existingEventAlert == null)
{
throw new NotFoundException($"EventAlert with id {id} not found.");
}
try
{
_eventAlertRepository.Remove(existingEventAlert);
await _unitOfWork.CompleteAsync();
return existingEventAlert;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting Event Alert with id {id}");
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting Event Alert with id {id}");
throw new ServerErrorException();
}
}
public async Task<IEnumerable<EventAlert>> MarkAllAsRead(string accountNo)
{
IEnumerable<EventAlert> existingEventAlerts = null;
try
{
existingEventAlerts = await _eventAlertRepository.ListUnreadByAccountNoAsync(accountNo);
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to find existing event alerts with accountNo: {accountNo}");
throw new ServerErrorException();
}
if (existingEventAlerts == null)
{
throw new NotFoundException($"EventAlerts for accountNo: {accountNo} not found.");
}
var ret = new List<EventAlert>();
foreach (EventAlert eventAlert in existingEventAlerts)
{
eventAlert.MarkedReadBy = "System";
eventAlert.DateMarkedRead = DateTime.Now;
try
{
_eventAlertRepository.Update(eventAlert);
ret.Add(eventAlert);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Event Alert: {eventAlert} in the list {existingEventAlerts}. Will not commit any changes.", eventAlert);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Event Alert {eventAlert} in the list {existingEventAlerts}. Will not commit any changes", eventAlert);
throw new ServerErrorException();
}
}
await _unitOfWork.CompleteAsync();
return ret;
}
private async Task<bool> IsStatusCancelledOrRedeemed(int repoId)
{
var repo = await _accountRepository.FindByIdAsync(repoId);
if (repo == null)
{
return false;
}
return repo.Status.Equals(CancelledStatus) || repo.Status.Equals(RedeemedStatus);
}
}
}
@@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models.Responses;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class FactoidService : IFactoidService
{
private readonly IReportingRepository _reportingRepository;
private readonly IRepoAccountRepository _repoAccountRepository;
private readonly IImsAutoFileDataRepository _imsAutoFileDataRepository;
private const string O4RStatus = "O4R";
private const string RepoStatus = "REPO";
private const string RedeemedStatus = "REDEEMED";
public FactoidService(
IReportingRepository reportingRepository,
IRepoAccountRepository repoAccountRepository,
IImsAutoFileDataRepository imsAutoFileDataRepository)
{
_reportingRepository = reportingRepository;
_repoAccountRepository = repoAccountRepository;
_imsAutoFileDataRepository = imsAutoFileDataRepository;
}
public int FindAuctionIndex(IReadOnlyCollection<string> auctionHouses, string auctionId)
{
if (auctionId != null)
{
foreach (var item in auctionHouses.Select((value, index) => new {index, value}))
{
if (item.value == auctionId.Trim().ToLower())
{
return item.index;
}
}
}
return auctionHouses.Count - 1;
}
public async Task<FactoidResponse> GetCurrentInventory()
{
var accountsInRepo = await _repoAccountRepository.FindByCurrentStatus(RepoStatus);
var numCollateral = accountsInRepo.Aggregate(0, (total, curAccount) =>
total += curAccount.Collaterals.Count());
return new FactoidResponse
{
CumulativeValue = numCollateral.ToString(),
Name = "Vehicles",
Description = "Current Inventory (REPO)",
FontAwesomeIcon = "fa-car",
Records = accountsInRepo
};
}
public async Task<FactoidResponse> GetAllAccountsO4R()
{
var filteredRepoAccounts = await _reportingRepository.GetAllAccountsInO4R();
return new FactoidResponse
{
CumulativeValue = filteredRepoAccounts.Count().ToString(),
Name = "Accounts",
Description = "Total Accounts in O4R",
FontAwesomeIcon = "fa-car",
Records = filteredRepoAccounts
};
}
public async Task<FactoidResponse> GetPercentFloorPrice(DateTime lowDateTime,
DateTime highDateTime)
{
var soldImsAccounts = await _imsAutoFileDataRepository.GetImsSoldRecordsForPeriod(lowDateTime, highDateTime);
decimal? percentFloor = 0m;
if (soldImsAccounts.Count() != 0)
{
percentFloor = soldImsAccounts.Select(x => x.GrossPercentFloor).Sum() / soldImsAccounts.Count;
}
var records = await _repoAccountRepository.FindByRepoIdAsync(soldImsAccounts.Select(x => x.RepoId.ToString()).ToList());
return new FactoidResponse
{
CumulativeValue = percentFloor?.ToString("#.#") + "%",
Name = "Percentage of Floor",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-money",
Records = records
};
}
public async Task<FactoidResponse> GetRecentlyRedeemed(DateTime lowDate, DateTime highDate)
{
var filteredRepoAccounts = await _repoAccountRepository.FindByStatusChangeInPeriod(RedeemedStatus, lowDate, highDate);
return new FactoidResponse
{
CumulativeValue = filteredRepoAccounts.Count().ToString(),
Name = "Recently Redeemed",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-calendar",
Records = filteredRepoAccounts
};
}
public async Task<FactoidResponse> GetAvgDaysO4RToREPO(DateTime lowDate, DateTime highDate)
{
var recentAccountsInREPO = await _repoAccountRepository.FindByStatusChangeInPeriod(RepoStatus, lowDate, highDate);
var avgDaysO4RtoRepo = GetAverageDaysBetweenStatuses(recentAccountsInREPO, RepoStatus, O4RStatus);
return new FactoidResponse
{
CumulativeValue = avgDaysO4RtoRepo != null ? avgDaysO4RtoRepo.ToString() : "N/A",
Name = "Avg. Days O4R to Repo",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-calendar",
Records = recentAccountsInREPO
};
}
public async Task<FactoidResponse> GetGetAvgDaysDelqToO4R(DateTime lowDate, DateTime highDate)
{
IEnumerable<RepoAccount> recentAccountsInO4R = await _repoAccountRepository.FindByStatusChangeInPeriod(O4RStatus, lowDate, highDate);
var accountsWithoutDateDelinquent = recentAccountsInO4R.Where(account => account.DaysDelinquent == null);
if (accountsWithoutDateDelinquent.Count() > 0)
{
Log.Error($"Accounts found without DaysDelinquent when added: {accountsWithoutDateDelinquent} ", accountsWithoutDateDelinquent);
recentAccountsInO4R = recentAccountsInO4R.Except(accountsWithoutDateDelinquent);
}
var totalAccounts = recentAccountsInO4R.Count();
var avgDaysDelqToO4R = 0;
if (totalAccounts > 0)
{
var delqToO4RSum = recentAccountsInO4R.Aggregate(0, (sum, repoAccount) => sum += repoAccount.DaysDelinquent ?? 0);
avgDaysDelqToO4R = delqToO4RSum / totalAccounts;
}
return new FactoidResponse()
{
CumulativeValue = avgDaysDelqToO4R.ToString(),
Name = "Avg. Days Dlqt To O4R",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-calendar",
Records = recentAccountsInO4R.ToList()
};
}
public async Task<FactoidResponse> GetGetAvgDaysRepoToSold(DateTime lowDate, DateTime highDate)
{
var recentAccountsSold = await _repoAccountRepository.FindByStatusChangeInPeriod("SOLD", lowDate, highDate);
var avgDaysRepoToSold = GetAverageDaysBetweenStatuses(recentAccountsSold, "SOLD", RepoStatus);
return new FactoidResponse
{
CumulativeValue = avgDaysRepoToSold != null ? avgDaysRepoToSold.ToString() : "N/A",
Name = "Avg. Days Repo to Sold",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-calendar",
Records = recentAccountsSold
};
}
public async Task<FactoidResponse> GetCurrentlyOnHold()
{
var accountsOnHold = await _repoAccountRepository.GetCurrentAccountsOnHoldAtAuction();
return new FactoidResponse
{
CumulativeValue = accountsOnHold.Count().ToString(),
Name = "On Hold at Auction",
Description = "Accounts",
FontAwesomeIcon = "fa-car",
Records = accountsOnHold
};
}
public async Task<FactoidResponse> GetRecentlyRepoed(DateTime lowDate, DateTime highDate)
{
var accountsRecentlyRepoed = await _repoAccountRepository.FindByStatusChangeInPeriod(RepoStatus, lowDate, highDate);
return new FactoidResponse
{
CumulativeValue = accountsRecentlyRepoed.Count().ToString(),
Name = "Recently Repoed",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-car",
Records = accountsRecentlyRepoed
};
}
public async Task<FactoidResponse> GetRecentlySold(DateTime lowDate, DateTime highDate)
{
var accountsRecentlySold = await _repoAccountRepository.FindByStatusChangeInPeriod("SOLD", lowDate, highDate);
return new FactoidResponse
{
CumulativeValue = accountsRecentlySold.Count().ToString(),
Name = "Recently Sold",
Description = "Last 30 Days",
FontAwesomeIcon = "fa-car",
Records = accountsRecentlySold
};
}
// Get the average day difference between two status changes.
// If the accounts given don't have valid to and from status, those accounts are ignored from the calulcation.
// However, if calling this array, the accounts SHOULD have valid status changes.
private int? GetAverageDaysBetweenStatuses(IEnumerable<RepoAccount> repoAccount, string statusTo, string statusFrom)
{
IEnumerable<int?> daysBetweenStatuses = repoAccount.Select(
(account) => GetDaysBetweenStatuses(account, statusTo, statusFrom));
var validRepos = daysBetweenStatuses.Aggregate(0, (sum, days) => sum += days != null ? 1 : 0);
var sumOfDays = daysBetweenStatuses.Aggregate(0, (sum, days) => sum += days != null ? days.Value : 0);
if (validRepos == 0)
{
return null;
}
return sumOfDays / validRepos;
}
// If either status cannot be found, return null, indicating an that days could not be calculated.
// If both the from status and to status are found, floor their difference of dates.
private int? GetDaysBetweenStatuses(RepoAccount repoAccount, string newStatus, string oldStatus)
{
var fromStatusDate = GetLatestStatusChange(repoAccount, oldStatus)?.ChangedDate;
var toStatusDate = GetLatestStatusChange(repoAccount, newStatus)?.ChangedDate;
if (toStatusDate != null && fromStatusDate != null)
{
return (int) Math.Floor(toStatusDate.Value.Subtract(fromStatusDate.Value).TotalDays);
}
return null;
}
private RepoStatusChange GetLatestStatusChange(RepoAccount repoAccount, string status)
{
return (
from statusChange in repoAccount.RepoStatusChanges
where string.Equals(statusChange.StatusTo, status, StringComparison.CurrentCultureIgnoreCase)
select statusChange).LastOrDefault();
}
}
}
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LCU.API.Collections.RepoApi.Domain.FloorPricing;
using LCU.API.Collections.RepoApi.Domain.FloorPricing.Strategies;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.WebServices.NADA;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class FloorPricingService : IFloorPricingService
{
private readonly IFloorPricingStrategyFactory _floorFloorPricingStrategyFactory;
private readonly IImsAutoFileDataService _imsAutoFileDataService;
private readonly IVehicleFloorPricingParametersService _vehicleFloorPricingParametersService;
private readonly ICollateralValueService _collateralValueService;
public FloorPricingService(
IFloorPricingStrategyFactory floorFloorPricingStrategyFactory,
IVehicleFloorPricingParametersService vehicleFloorPricingParametersService,
IImsAutoFileDataService imsAutoFileDataService,
ICollateralValueService collateralValueService)
{
_floorFloorPricingStrategyFactory = floorFloorPricingStrategyFactory;
_vehicleFloorPricingParametersService = vehicleFloorPricingParametersService;
_imsAutoFileDataService = imsAutoFileDataService;
_collateralValueService = collateralValueService;
}
public FloorPriceCalculationResultSet DetermineFloorPrice(int auctionAssignmentId)
{
CollateralValuation collateralValuation = new CollateralValuation { };
ImsAutoFileData imsAutoFileData = new ImsAutoFileData { };
var floorPricingParams = _vehicleFloorPricingParametersService.ListAll().GetAwaiter().GetResult();
try
{
imsAutoFileData = _imsAutoFileDataService.GetByAuctionAssignmentId(auctionAssignmentId).GetAwaiter().GetResult();
collateralValuation = GetCollateralValuation(imsAutoFileData.VIN, imsAutoFileData.StatedMileage ?? 0, 0);
}
catch (Exception ex)
{
Log.Error(ex, $"Error when trying to Determine Floor Price for auctionAssignment with id: ${auctionAssignmentId}");
}
FloorPriceCalculationParams fpcParams = new FloorPriceCalculationParams()
{
VehicleFloorPricingParameters = floorPricingParams,
ImsAutoFileData = imsAutoFileData,
CollateralValuation = collateralValuation
};
var prices = GetCalculatedFloorPrices(fpcParams);
var price = SelectBestFloorPrice(prices);
var floorPriceValuesUsed = PopulateFloorPriceValuesUsed(auctionAssignmentId, fpcParams, prices, price);
FloorPriceCalculationResultSet strategicResultSet = new FloorPriceCalculationResultSet { };
strategicResultSet.AllResults = prices;
strategicResultSet.PrimaryResult = price;
strategicResultSet.FloorPriceValuesUsed = floorPriceValuesUsed;
return strategicResultSet;
}
public CollateralValuation GetCollateralValuation(string VIN, int StatedMileage, int vehichleTypeKey)
{
CollateralValuation collateralValuation = new CollateralValuation { };
try
{
collateralValuation = _collateralValueService.GetCollateralValue(VIN, StatedMileage, vehichleTypeKey);
}
catch (Exception ex)
{
if (ex.Message.Contains("license"))
{
Log.Error(ex, "Your collateral value service license may be invalid/expired, please notify the collections department administration.");
}
Log.Error(ex, "NADA ERROR when getting Collateral Value");
}
return collateralValuation;
}
public IReadOnlyCollection<FloorPriceCalculationResult> GetCalculatedFloorPrices(FloorPriceCalculationParams fpcParams)
{
IReadOnlyCollection<IFloorPricingStrategy> strategies =
_floorFloorPricingStrategyFactory.GetFloorPricingStrategies();
List<FloorPriceCalculationResult> calculationResults = new List<FloorPriceCalculationResult>();
foreach (var floorPricingStrategy in strategies)
{
calculationResults.Add(floorPricingStrategy.CalculateFloorPrice(fpcParams));
}
return calculationResults;
}
public FloorPriceCalculationResult SelectBestFloorPrice(IReadOnlyCollection<FloorPriceCalculationResult> results)
{
var successWeight = -1;
FloorPriceCalculationResult successResult = null;
foreach (var result in results)
{
if (result.ResultWeight > successWeight && result.Success)
{
successWeight = result.ResultWeight;
successResult = result;
}
}
if (successResult == null)
{
return new FloorPriceCalculationResult()
{
ResultWeight = -1,
Success = false,
StrategyName = "Manual",
FloorPrice = -1
};
}
return successResult;
}
public FloorPriceValuesUsed PopulateFloorPriceValuesUsed(int auctionAssignmentId, FloorPriceCalculationParams fpcParams, IReadOnlyCollection<FloorPriceCalculationResult> prices,
FloorPriceCalculationResult bestPrice)
{
var result = new FloorPriceValuesUsed
{
AuctionAssignmentId = auctionAssignmentId,
RepoId = fpcParams.ImsAutoFileData.RepoId,
CollateralValuation = fpcParams.CollateralValuation
};
result.AuctionGuideBookPrice = fpcParams.ImsAutoFileData.AuctionGuideBookPrice;
var autoImsResult = prices.FirstOrDefault(x => x.StrategyName.Equals(typeof(AutoImsSuggestedValue).FullName));
result.WasAuctionGuideBookPriceUsed = autoImsResult != null && autoImsResult.Success && autoImsResult.FloorPrice == bestPrice.FloorPrice;
var nadaMmrResult = prices.FirstOrDefault(x => x.StrategyName.Equals(typeof(NadaMmrConditionValue).FullName));
result.MMRNationalAdjustedPrice = fpcParams.ImsAutoFileData.MMRNationalAdjustedPrice;
result.MMRRegionalAdjustedPrice = fpcParams.ImsAutoFileData.MMRRegionalAdjustedPrice;
result.NADALowAdjustedAuctionValue = fpcParams.CollateralValuation.LowAdjustedAuctionValue;
result.NADAAverageAdjustedAuctionValue = fpcParams.CollateralValuation.AverageAdjustedAuctionValue;
result.NADAHighAdjustedAuctionValue = fpcParams.CollateralValuation.HighAdjustedAuctionValue;
result.Condition = fpcParams.ImsAutoFileData.ApprovedVehicleGrade ?? fpcParams.ImsAutoFileData.VehicleGrade;
if (result.Condition == null || result.Condition <= 0)
{
return result;
}
var vfpParams = Enumerable.First(Enumerable.Where(fpcParams.VehicleFloorPricingParameters,
x => result.Condition >= x.LowCondition && result.Condition <= x.HighCondition));
result.LowParam = vfpParams.LowCondition;
result.HighParam = vfpParams.HighCondition;
result.PercentageOf = vfpParams.PercentageOf;
var hasMmrPrice = fpcParams.ImsAutoFileData.MMRNationalAdjustedPrice != null || fpcParams.ImsAutoFileData.MMRRegionalAdjustedPrice != null;
result.WasAuctionGuideBookPriceUsed = autoImsResult != null && autoImsResult.Success && autoImsResult.StrategyName == bestPrice.StrategyName || (!hasMmrPrice && autoImsResult.FloorPrice > 0);
result.WasMMRNationalAdjustedPriceUsed = nadaMmrResult != null && nadaMmrResult.Success && vfpParams.MMRAdjustedValue.ToLower().StartsWith("national") && hasMmrPrice;
result.WasMMRRegionalAdjustedPriceUsed = nadaMmrResult != null && nadaMmrResult.Success && vfpParams.MMRAdjustedValue.ToLower().StartsWith("regional") && hasMmrPrice;
result.WasNADALowAdjustedAuctionValueUsed = nadaMmrResult != null && nadaMmrResult.Success && vfpParams.NADAFirstValue.ToLower().StartsWith("low") || vfpParams.NADASecondValue.ToLower().StartsWith("low");
result.WasNADAAverageAdjustedAuctionValueUsed = nadaMmrResult != null && nadaMmrResult.Success && vfpParams.NADAFirstValue.ToLower().StartsWith("avg") || vfpParams.NADASecondValue.ToLower().StartsWith("avg");
result.WasNADAHighAdjustedAuctionValueUsed = nadaMmrResult != null && nadaMmrResult.Success && vfpParams.NADAFirstValue.ToLower().StartsWith("high") || vfpParams.NADASecondValue.ToLower().StartsWith("high");
return result;
}
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.API.Collections.RepoApi.Models.Requests;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IAuctionAssignmentService
{
Task<PagedResult<AuctionAssignment>> ListByFilterAsync(AuctionAssignmentFilter filter);
Task<AuctionAssignment> GetByIdAsync(int id);
Task<IReadOnlyCollection<AuctionAssignment>> GetByRepoIdAsync(int repoId);
Task<AuctionAssignmentPostRequest> SaveAsync(AuctionAssignmentPostRequest auctionAssignmentPostRequest);
Task<AuctionAssignment> UpdateAsync(AuctionAssignment auctionAssignment);
Task<AuctionAssignment> RemoveAuction(int id);
Task<IReadOnlyCollection<AuctionAssignment>> GetListByIdsAsync(IReadOnlyCollection<int> idSet);
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IAuctionHouseService
{
Task<IEnumerable<AuctionHouse>> ListAllAsync();
Task<AuctionHouse> GetAsync(string id);
Task<AuctionHouse> SaveAsync(AuctionHouse auction);
Task<AuctionHouse> UpdateAsync(AuctionHouse auction);
Task<AuctionHouse> DeleteAsync(string id);
}
}
@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IAuditRecordService
{
Task<PagedResult<AuditRecord>> ListAll(PageFilter filter);
Task<AuditRecord> SaveAsync(AuditRecord repoAccount);
Task<AuditRecord> Get(int id);
Task<AuditRecord> UpdateAsync(AuditRecord auditRecord);
Task<AuditRecord> DeleteAsync(int id);
}
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface ICollateralService
{
Task<IEnumerable<Collateral>> ListAll();
Task<Collateral> Get(int id);
Task<Collateral> SaveAsync(Collateral Collateral);
Task<Collateral> UpdateAsync(Collateral Collateral);
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.AutoIms;
using LCU.Shared.Collections.RepoApiLib.Models.Consumer;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IEmailer
{
Task<HttpResponseMessage> SendFloorPriceAcceptedEmail(bool isManual, string accountNo, decimal? oldFloorPrice, decimal? newFloorPrice);
Task<HttpResponseMessage> SendSurplusDeficiencyLetterEmailAsync(RepoAccount repoAccount);
Task<HttpResponseMessage> MaybeSendO4rNoCollateralEmail(RepoAccount repoAccount);
Task<HttpResponseMessage> SendAlertSummaryEmailWithEndTime(DateTime endTime);
Task<HttpResponseMessage> SendProccessSummary(RepoRunInfo runInfo);
void SendSummaryEmail(IReadOnlyCollection<ImportResultRow> imported, IReadOnlyCollection<ImportResultRow> floors);
void SendRefundProcessStartEmails(IReadOnlyCollection<ImportResultRow> imported);
}
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IEventAlertService
{
Task<IEnumerable<EventAlert>> ListAll();
Task<IEnumerable<EventAlert>> ListUnread();
Task<IEnumerable<EventAlert>> ListActionNeeded();
Task<IEnumerable<EventAlert>> ListWithinDateRange(DateTime start, DateTime end);
Task<EventAlert> Get(int id);
Task<EventAlert> SaveAsync(EventAlert eventAlert);
Task<IEnumerable<EventAlert>> SaveRangeAsync(List<EventAlert> eventAlerts);
Task<EventAlert> UpdateAsync(EventAlert eventAlert, AppUser user);
Task<EventAlert> DeleteAsync(int id);
Task<IEnumerable<EventAlert>> MarkAllAsRead(string eventAlert);
}
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models.Responses;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IFactoidService
{
Task<FactoidResponse> GetAllAccountsO4R();
Task<FactoidResponse> GetCurrentInventory();
int FindAuctionIndex(IReadOnlyCollection<string> auctionHouses, string auctionId);
Task<FactoidResponse> GetPercentFloorPrice(DateTime lowDateTime, DateTime highDateTime);
Task<FactoidResponse> GetRecentlyRedeemed(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetAvgDaysO4RToREPO(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetGetAvgDaysDelqToO4R(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetGetAvgDaysRepoToSold(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetRecentlyRepoed(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetRecentlySold(DateTime lowDate, DateTime highDate);
Task<FactoidResponse> GetCurrentlyOnHold();
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IFloorPricingService
{
FloorPriceCalculationResultSet DetermineFloorPrice(int auctionAssignmentId);
IReadOnlyCollection<FloorPriceCalculationResult> GetCalculatedFloorPrices(FloorPriceCalculationParams fpcParams);
FloorPriceCalculationResult SelectBestFloorPrice(IReadOnlyCollection<FloorPriceCalculationResult> results);
}
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IImsAutoFileDataService
{
Task<IEnumerable<ImsAutoFileData>> ListAll();
Task<ImsAutoFileData> SaveAsync(ImsAutoFileData imsAutoFileData);
Task<ImsAutoFileData> Get(string vin, int repoId);
Task<ImsAutoFileData> GetByAuctionAssignmentId(int auctionAssignmentId);
Task<ImsAutoFileData> UpdateAsync(ImsAutoFileData imsAutoFileData);
Task<ImsAutoFileData> DeleteAsync(string vin, int repoId);
}
}
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IProcessingReceiptService
{
Task<IReadOnlyCollection<ProcessingReceipt>> FindReceiptsAsync(string accountNo,
string accountId = "", string caseId = "");
Task<IReadOnlyCollection<ProcessingReceipt>> GetReceiptsByAccountNoAsync(string accountNo);
Task<IReadOnlyCollection<ProcessingReceipt>> GetRetryListAsync();
Task SaveAsync(ProcessingReceipt receipt);
Task SaveAllAsync(IReadOnlyCollection<ProcessingReceipt> receipts);
}
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRefundStatusService
{
Task<IReadOnlyCollection<RefundStatus>> ListAllAsync();
Task<RefundStatus> GetAsync(int repoId);
Task<RefundStatus> SaveAsync(RefundStatus refundStatus);
Task<RefundStatus> UpdateAsync(RefundStatus refundStatus);
}
}
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.API.Collections.RepoApi.Models.Requests;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Producer;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRepoAccountService
{
Task<PagedResult<RepoAccount>> ListAll(PageFilter pageFilter, RepoAccountPostFilter repoAccountFilter = null);
Task<RepoAccount> FindByAuctionAssignmentId(int auctionAssignmentId);
Task<RepoAccount> SaveAsync(RepoAccount repoAccount);
Task<RepoAccount> Get(int id);
Task<IReadOnlyCollection<RepoAccount>> GetByAccountNo(string accountNo);
Task<RepoAccount> GetNewestOpenAccount(string accountNo);
Task<IReadOnlyCollection<RepoAccount>> SearchRepoAccounts(string searchPhrase, int maxSearchResults);
Task<RepoAccount> UpdateAsync(RepoAccount repoAccount, AppUser currentUser);
Task<RepoAccount> DeleteAsync(int id);
Task<IReadOnlyCollection<ProcessingItem>> GetItemsForProcessing();
RepoAccount SelectFirstOpenAccount(IReadOnlyCollection<RepoAccount> repoAccounts);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRepoCompanySelectionService
{
Task<IEnumerable<RepoCompanySelection>> ListAllAsync();
Task<RepoCompanySelection> GetAsync(int id);
Task SaveChangesAsync(IList<RepoCompanySelection> repoCompanySelections);
}
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRepoCompanyService
{
Task<IEnumerable<RepoCompany>> ListAll();
Task<RepoCompany> Save(RepoCompany repoCompany);
Task<RepoCompany> Get(int id);
Task<RepoCompany> Update(RepoCompany repoCompany, AppUser user);
Task<RepoCompany> Delete(int id);
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRepoLetterService
{
Task<IReadOnlyCollection<RepoLetter>> ListByAccountIdAsync(int accountId);
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IRepoStatusChangeService
{
Task<IEnumerable<RepoStatusChange>> ListByAccount(int repoId);
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface ISavedLocationService
{
Task<IEnumerable<SavedLocation>> ListAllAsync();
Task<SavedLocation> SaveAsync(SavedLocation savedLocation);
Task<SavedLocation> GetAsync(int id);
Task<SavedLocation> UpdateAsync(SavedLocation savedLocation);
Task<SavedLocation> DeleteAsync(int id);
}
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Settings;
namespace LCU.API.Collections.RepoApi.Services
{
public interface ISettingsService
{
Task<Settings> GetCachedRepoSettingsAsync();
Task<Settings> ListAllSettingsAsync();
Task<AccountType> SaveAsync(AccountType accountType);
Task<AllowedStatusChange> SaveAsync(AllowedStatusChange statusChange);
Task<BodyStyle> SaveAsync(BodyStyle bodyStyle);
Task<CleaningLevel> SaveAsync(CleaningLevel cleaningLevel);
Task<CollateralType> SaveAsync(CollateralType collateralType);
Task<CollateralTypeToVehicleType> SaveAsync(CollateralTypeToVehicleType collateralToVehicle);
Task<HoldStatus> SaveAsync(HoldStatus holdStatus);
Task<LoanCategory> SaveAsync(LoanCategory loanCategory);
Task<LocationType> SaveAsync(LocationType locationType);
Task<VehicleType> SaveAsync(VehicleType vehicleType);
Task<AccountType> UpdateAsync(AccountType accountType);
Task<AllowedStatusChange> UpdateAsync(AllowedStatusChange statusChange);
Task<BodyStyle> UpdateAsync(BodyStyle bodyStyle);
Task<CleaningLevel> UpdateAsync(CleaningLevel cleaningLevel);
Task<CollateralType> UpdateAsync(CollateralType collateralType);
Task<CollateralTypeToVehicleType> UpdateAsync(CollateralTypeToVehicleType collateralToVehicle);
Task<HoldStatus> UpdateAsync(HoldStatus holdStatus);
Task<LoanCategory> UpdateAsync(LoanCategory loanCategory);
Task<LocationType> UpdateAsync(LocationType locationType);
Task<VehicleType> UpdateAsync(VehicleType vehicleType);
Task SyncExternalUsersAsync(IReadOnlyCollection<ExternalUser> users);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public interface IVehicleFloorPricingParametersService
{
Task<IEnumerable<VehicleFloorPricingParameters>> ListAll();
Task<VehicleFloorPricingParameters> Get(int id);
Task<VehicleFloorPricingParameters> UpdateAsync(VehicleFloorPricingParameters parameters);
}
}
@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class ImsAutoFileDataService : IImsAutoFileDataService
{
private readonly IImsAutoFileDataRepository _imsAutoFileDataRepository;
private readonly IUnitOfWork _unitOfWork;
public ImsAutoFileDataService(
IImsAutoFileDataRepository imsAutoFileDataRepository,
IUnitOfWork unitOfWork)
{
_imsAutoFileDataRepository = imsAutoFileDataRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<ImsAutoFileData>> ListAll()
{
try
{
return await _imsAutoFileDataRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving ImsAutoFileData Set");
throw new ServerErrorException();
}
}
public async Task<ImsAutoFileData> SaveAsync(ImsAutoFileData imsAutoFileData)
{
try
{
await _imsAutoFileDataRepository.AddAsync(imsAutoFileData);
await _unitOfWork.CompleteAsync();
return imsAutoFileData;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving IMS Auto File Data: {imsAutoFileData}", imsAutoFileData);
throw new ServerErrorException();
}
}
public async Task<ImsAutoFileData> Get(string vin, int repoId)
{
ImsAutoFileData imsAutoFileData;
try
{
imsAutoFileData = await _imsAutoFileDataRepository.FindByIdAsync(vin, repoId);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving IMS Auto File Data with vin: {vin} repoId: {repoId}");
throw new ServerErrorException();
}
if (imsAutoFileData == null)
{
throw new NotFoundException($"IMS Auto File Data id {vin} / {repoId} not found.");
}
return imsAutoFileData;
}
public async Task<ImsAutoFileData> GetByAuctionAssignmentId(int auctionAssignmentId)
{
ImsAutoFileData imsAutoFileData;
try
{
imsAutoFileData = await _imsAutoFileDataRepository.FindByAuctionAssignmentIdAsync(auctionAssignmentId);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving IMS Auto File Data by AuctionAssignmentId: {auctionAssignmentId}", auctionAssignmentId);
throw new ServerErrorException();
}
if (imsAutoFileData == null)
{
throw new NotFoundException($"IMS Auto File Data id {auctionAssignmentId} not found.");
}
return imsAutoFileData;
}
public async Task<ImsAutoFileData> UpdateAsync(ImsAutoFileData imsAutoFileData)
{
ImsAutoFileData existingImsAutoFileData = null;
try
{
existingImsAutoFileData = await _imsAutoFileDataRepository.FindByIdAsync(imsAutoFileData.VIN, imsAutoFileData.RepoId);
}
catch(Exception ex)
{
Log.Error(ex, $"Failed to retreive existingImsAutoFileData with vin: {imsAutoFileData.VIN} and {imsAutoFileData.RepoId}",imsAutoFileData);
throw new ServerErrorException();
}
if (existingImsAutoFileData == null)
{
throw new NotFoundException($"IMS Auto File Data with id {imsAutoFileData.VIN} / {imsAutoFileData.RepoId} not found.");
}
try
{
_imsAutoFileDataRepository.Update(imsAutoFileData);
await _unitOfWork.CompleteAsync();
return (existingImsAutoFileData);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating IMS Auto File Data: {imsAutoFileData}", imsAutoFileData);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating IMS Auto File Data: {imsAutoFileData}", imsAutoFileData);
throw new ServerErrorException();
}
}
public async Task<ImsAutoFileData> DeleteAsync(string vin, int repoId)
{
ImsAutoFileData existingimsAutoFileData = null;
try
{
existingimsAutoFileData = await _imsAutoFileDataRepository.FindByIdAsync(vin, repoId);
}
catch(Exception ex)
{
Log.Error(ex, $"Failed to retreive existingImsAutoFileData with vin: {vin} and {repoId}");
throw new ServerErrorException();
}
if (existingimsAutoFileData == null)
{
throw new NotFoundException($"IMS Auto File Data with id {vin} / {repoId} not found.");
}
try
{
_imsAutoFileDataRepository.Remove(existingimsAutoFileData);
await _unitOfWork.CompleteAsync();
return existingimsAutoFileData;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting IMS Auto File Data with vin: {vin}, repoId: {repoId}");
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting IMS Auto File Data with vin: {vin}, repoId: {repoId}");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class ProcessingReceiptService : IProcessingReceiptService
{
private readonly IProcessingReceiptRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public ProcessingReceiptService(
IProcessingReceiptRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<IReadOnlyCollection<ProcessingReceipt>> FindReceiptsAsync(string accountNo,
string accountId = "", string caseId = "")
{
try
{
return await _repository.FindReceiptsAsync(accountNo, accountId, caseId);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving receipts");
throw new ServerErrorException();
}
}
public async Task<IReadOnlyCollection<ProcessingReceipt>> GetReceiptsByAccountNoAsync(string accountNo)
{
try
{
return await _repository.GetReceiptsByAccountNoAsync(accountNo);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving receipts by accountNo");
throw new ServerErrorException();
}
}
public async Task<IReadOnlyCollection<ProcessingReceipt>> GetRetryListAsync()
{
try
{
return await _repository.GetRetryListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving retry list of receipts");
throw new ServerErrorException();
}
}
public async Task SaveAsync(ProcessingReceipt receipt)
{
try
{
await _repository.AddOrUpdateAsync(receipt);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure saving receipt");
throw new ServerErrorException();
}
}
public async Task SaveAllAsync(IReadOnlyCollection<ProcessingReceipt> receipts)
{
try
{
await _repository.SaveAllAsync(receipts);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure saving receipts");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.Domain.AlertChecker;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.Shared.Collections.RepoApiLib.Models;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public class AccountAlertCheckService : IAccountAlertCheckService
{
private const string O4RStatus = "O4R";
private const string NoStatus = "";
private const string RepoStatus = "REPO";
private const string RecaStatus = "RECA";
private const string RehoStatus = "REHO";
private const string RedeemedStatus = "REDEEMED";
private readonly IEventAlertService _eventAlertService;
private readonly IEnumerable<IAlertChecker> _accountAlertPreStatusCheckers;
private readonly IEnumerable<IAlertChecker> _accountAlertPrimaryCheckers;
private readonly IEnumerable<IAlertChecker> _accountAlertPrimaryIgnoreWrittenOffCheckers;
public AccountAlertCheckService( IEventAlertService eventAlertService)
{
_eventAlertService = eventAlertService;
_accountAlertPreStatusCheckers = new List<IAlertChecker>()
{
new SkipPaidChecker()
};
_accountAlertPrimaryCheckers = new List<IAlertChecker>()
{
new RepoNotMovedChecker( 7),
new RepoNotSoldChecker( 60, 30),
new StatusChangeChecker( RecaStatus),
new StatusChangeChecker( RehoStatus),
new TimeInStatusChecker( RehoStatus, 30, 30),
new StatusChangeChecker( O4RStatus, new List<string>() { NoStatus, RedeemedStatus, RehoStatus, RecaStatus}),
new O4rNoRepoChecker(RepoType.Voluntary),
new O4rNoRepoChecker(RepoType.Involuntary),
new O4rNoRepoChecker(RepoType.PreSkip),
new SkipEndChecker( 91),
new SkipNotPaidChecker( 106, 15),
new DocumentNotFoundChecker("Notice of Plan to Sell", "LTR35", "701", RepoStatus),
new DocumentNotFoundChecker("Notice of Repossession", "LTR34", "701", RepoStatus),
new RtcLetterNotFoundChecker("RTC Letter", new List<string> { "RTC 1", "RTC 3" }),
new SoldNoProceedsChecker(),
new AuctionOnHoldChecker(),
new AuctionVinDifferentChecker(),
new RedemptionEndedNoTitleOrderedChecker()
};
_accountAlertPrimaryIgnoreWrittenOffCheckers = new List<IAlertChecker>()
{
new DebitAdjustmentChecker(),
new PaymentReceivedChecker(),
new CreditAdjustmentChecker(),
new SaleFundsReceivedChecker()
};
}
public Task AlertChecksPreStatusReject(RepoAccountProcessingSlip slip)
{
return CheckForAlerts(_accountAlertPreStatusCheckers, slip);
}
public async Task<EventAlert> ManualAlert(EventAlert alert)
{
return await _eventAlertService.SaveAsync(alert);
}
public async Task MaybeAlertOnInvalidStatus(RepoAccountProcessingSlip slip)
{
var allowedStatuses = slip.CurrentSettings?.AllowedStatusChanges.Select(x => x.CurrentStatus);
if (slip.UpdatedRepoAccount != null && allowedStatuses != null &&
!allowedStatuses.Contains(slip.UpdatedRepoAccount.Status))
{
await _eventAlertService.SaveAsync(new EventAlert()
{
RepoId = slip.UpdatedRepoAccount.RepoId,
AccountNo = slip.UpdatedRepoAccount.AccountNo,
CreatedBy = "Importer",
DateAdded = DateTime.Now,
Message = $"Collections System Status {slip.UpdatedRepoAccount.Status} Not Valid"
});
}
}
public async Task AlertChecksPrimary( RepoAccountProcessingSlip slip)
{
if (slip.TransactionHistory != null && slip.TransactionHistory.Any())
{
await CheckForAlerts(_accountAlertPrimaryIgnoreWrittenOffCheckers, slip);
}
else
{
Log.Error($"No Transactions Available, skipping Transaction Alerts for {slip.AccountNo}");
}
await CheckForAlerts(_accountAlertPrimaryCheckers, slip);
}
private async Task CheckForAlerts(IEnumerable<IAlertChecker> alertCheckers, RepoAccountProcessingSlip slip)
{
var eventAlerts = new List<EventAlert>();
foreach (var alertChecker in alertCheckers)
{
try
{
var createdAlerts = alertChecker.CheckForAlert(slip);
if (createdAlerts != null)
{
eventAlerts.AddRange(createdAlerts);
}
}
catch (Exception ex)
{
var accountNumber = slip.AccountNo ?? "Account Number Not Found";
Log.Error(ex, $"Error Checking Alert {alertChecker.GetType()} For Account: {accountNumber}");
}
}
await _eventAlertService.SaveRangeAsync(eventAlerts);
}
}
}
@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public interface IAccountAlertCheckService
{
/**
* These alerts run before the account is checked to see if it was moved to an un-allowed status.
* What is this moves the status to an un-allowed status?
*/
Task AlertChecksPreStatusReject(RepoAccountProcessingSlip slip);
/**
* These alerts run after the account has been updated or created and is the primary set of alert checks.
*/
Task AlertChecksPrimary(RepoAccountProcessingSlip slip);
Task<EventAlert> ManualAlert(EventAlert alert);
Task MaybeAlertOnInvalidStatus(RepoAccountProcessingSlip processingSlip);
}
}
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.Shared.Collections.RepoApiLib.Models.Producer;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public interface IRepoAccountProcessor
{
Task<RepoProcessingResult> HandleAccount(ProcessingItem item);
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Consumer;
using LCU.Shared.Collections.RepoApiLib.Models.Settings;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public interface IRepoAccountProcessorService
{
RepoAccount CreateRepoAccount(TemenosAccount account, TemenosCase recoveryCase, LoanDetail detail);
void CheckForRejectedStatusChange(RepoAccount originalRepoAccount, RepoAccount updatedRepoAccount, Settings settings);
RepoAccount UpdateRepoAccountFromParts(RepoAccountProcessingSlip slip);
void UpdateRepoAccountDateClosed(RepoAccount repoAccount);
void TryUpdateCollateralRetitleDate(RepoAccount repo, TemenosCase temenosCase);
void AddAccountUpdateRecord(RepoAccountProcessingSlip slip);
RepoCompanySelection CreateNewRepoCompanyAssignment(Collateral collateral, TemenosCase temenosCase, int? newRepoCompanyId);
void TryUpdateOrCreateRepoCompanyAssignment(RepoAccount repoAccount,
TemenosCase temenosCase, Settings settings);
int? MapRepoAgentIdToRepoCompanyId(string repoAgentId, IReadOnlyCollection<RepoCompany> repoCompanies);
}
}
@@ -0,0 +1,331 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.DataAccess;
using LCU.API.Collections.RepoApi.Domain.Extensions;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Producer;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public class RepoAccountProcessor : IRepoAccountProcessor
{
private readonly IRepoAccountProcessorService _repoAccountProcessorService;
private readonly IAccountAlertCheckService _accountAlertCheckService;
private readonly IEmailer _emailer;
private readonly ISettingsService _settingsService;
private readonly ICollectionsDao _collectionsDao;
private readonly ICoreDao _coreDao;
private readonly IRepoAccountService _repoAccountService;
private readonly IRepoLetterService _repoLetterService;
private readonly IAuctionAssignmentService _auctionAssignmentService;
public const string UserStatusFieldOld = "User Status Code 1";
public const int CaseTypeRepo = 7; // Source: collections API configuration call
private const string O4RStatus = "O4R";
private const string NoStatus = "";
private const string RepoStatus = "REPO";
private const string RecaStatus = "RECA";
private const string RehoStatus = "REHO";
private const string RedeemedStatus = "REDEEMED";
private const string TemenosQueuedBy = "Temenos";
public RepoAccountProcessor(
IRepoAccountProcessorService repoAccountProcessorService,
IAccountAlertCheckService accountAlertCheckService,
ICollectionsDao collectionsDao,
IEmailer emailer,
ISettingsService settingsService,
IRepoAccountService repoAccountService,
IRepoLetterService repoLetterService, ICoreDao coreDao,
IAuctionAssignmentService auctionAssignmentService)
{
_repoAccountProcessorService = repoAccountProcessorService;
_accountAlertCheckService = accountAlertCheckService;
_collectionsDao = collectionsDao;
_emailer = emailer;
_settingsService = settingsService;
_repoAccountService = repoAccountService;
_repoLetterService = repoLetterService;
_coreDao = coreDao;
_auctionAssignmentService = auctionAssignmentService;
}
public async Task<RepoProcessingResult> HandleAccount(ProcessingItem item)
{
var processingSlip = await CreateProcessingSlip(item);
if (IsAutoCloseAccount(processingSlip))
{
Log.Debug($"Auto-Closing account {processingSlip.AccountNo} due to no Temenos Account.");
await AutoCloseAccount(processingSlip);
return RepoProcessingResult.Autoclosed;
}
processingSlip.AssertReadyForPreChecks();
// 9640 Attempt to pre-check for a matching status when case is missing.
// If no found case, and the status hasn't changed; instead of failing just skip the unconverted account.
if (IsUnconvertedIntellectAccount(processingSlip))
{
Log.Debug($"Repo Processor: {item.AccountNo} is not updated for Temenos; skipping.");
return RepoProcessingResult.Unconverted;
}
processingSlip.RecoveryCase = processingSlip.RecoveryAccount.FindRecoveryCaseOrDefault(processingSlip.CaseId);
if (!IsPossiblyRepossessionCase(processingSlip))
{
return RepoProcessingResult.Ineligible;
}
processingSlip.AssertReadyForFullProcessing();
if (ShouldCreateNewAccount(processingSlip))
{
Log.Debug($"Creating new repo account for {item.AccountNo} : {item.CaseId} ");
processingSlip.UpdatedRepoAccount = await CreateNewRepoAccount(processingSlip);
await _accountAlertCheckService.MaybeAlertOnInvalidStatus(processingSlip);
processingSlip = await AddAlertDataToProcessingSlip(processingSlip);
processingSlip.AssertReadyForAlertChecks();
await _accountAlertCheckService.AlertChecksPrimary(processingSlip);
return RepoProcessingResult.Created;
}
if (!processingSlip.ExistingRepoAccount.HasAnyCollateral())
{
await _emailer.MaybeSendO4rNoCollateralEmail(processingSlip.ExistingRepoAccount);
throw new MissingRequirementException("Existing Account Collateral",
$"{processingSlip.AccountNo} did not have collateral on existing account.");
}
Log.Debug($"Updating Repo account for {item.AccountNo} : {item.CaseId}");
processingSlip.UpdatedRepoAccount = await UpdateExistingRepoAccount(processingSlip);
//Add check for recent account repo closing when in RECA status to prevent reprocessing and new repo account creation with a DateClosed date = null
//The DateClosed date = null will ensure the account gets reprocessed, we don't want alert checks with these accounts
if (processingSlip.ExistingRepoAccount.DateClosed == null && processingSlip.UpdatedRepoAccount.DateClosed != null)
{
return RepoProcessingResult.Updated;
}
processingSlip = await AddAlertDataToProcessingSlip(processingSlip);
processingSlip.AssertReadyForAlertChecks();
await _accountAlertCheckService.AlertChecksPrimary(processingSlip);
return RepoProcessingResult.Updated;
}
// New method to give a chance to filter out non-repo cases and mark ineligible
private bool IsPossiblyRepossessionCase(RepoAccountProcessingSlip slip)
{
if (slip.RecoveryCase == null) return true;
try
{
if (slip.RecoveryCase.CaseTypeId != CaseTypeRepo)
{
return false;
}
StatusService.ConvertStatus(slip.RecoveryCase.RepoStatus);
}
catch (IneligibleAccountException)
{
return false;
}
return true;
}
private async Task AutoCloseAccount(RepoAccountProcessingSlip processingSlip)
{
AppUser appUser = new AppUser()
{
Email = processingSlip.RequestedBy.QueuedBy,
FirstName = "System",
LastName = "System"
};
processingSlip.ExistingRepoAccount.DateClosed = DateTime.Now;
processingSlip.ExistingRepoAccount.AccountLastProcessed = DateTime.Now;
await _repoAccountService.UpdateAsync(processingSlip.ExistingRepoAccount, appUser);
}
private bool IsAutoCloseAccount(RepoAccountProcessingSlip processingSlip)
{
// If recovery account couldn't be fetched, that would have been an exception.
// If recovery account is null, it means "not there", so close the RepoAccount.
// This means close the RepoAccount because the account
// was likely not brought over from Intellect. If something DOES happen, just create
// a new account later....
return (processingSlip.RecoveryAccount == null
&& processingSlip.ExistingRepoAccount != null
&& processingSlip.ExistingRepoAccount.DateClosed == null);
}
public async Task<RepoAccountProcessingSlip> CreateProcessingSlip(ProcessingItem item)
{
var accountNo = VerifyAccountNo(item);
var slip = new RepoAccountProcessingSlip
{
AccountNo = accountNo,
CurrentSettings = await _settingsService.GetCachedRepoSettingsAsync(),
RecoveryAccount = await _collectionsDao.RetrieveAccountAsync(accountNo),
ExistingRepoAccount = await _repoAccountService.GetNewestOpenAccount(accountNo),
Loan = await _coreDao.RetrieveLoanAsync(accountNo),
CaseId = VerifyCaseId(item.CaseId),
RequestedBy = item
};
if (slip.RecoveryAccount != null)
{
slip.Letters = await _repoLetterService.ListByAccountIdAsync(slip.RecoveryAccount.AccountId);
}
return slip;
}
public async Task<RepoAccountProcessingSlip> AddAlertDataToProcessingSlip(RepoAccountProcessingSlip slip)
{
slip.TransactionHistory = await _coreDao.RetrieveTransactionsAsync(slip.AccountNo);
slip.Member = await _coreDao.RetrieveMemberAsync(slip.AccountNo);
var uniqueIds = slip.UpdatedRepoAccount.GetUniqueAuctionAssignmentIds();
slip.AuctionAssignments = await _auctionAssignmentService.GetListByIdsAsync(uniqueIds);
return slip;
}
public bool IsUnconvertedIntellectAccount(RepoAccountProcessingSlip repoAccountSlip)
{
var recoveryAccount = repoAccountSlip.RecoveryAccount;
var caseId = repoAccountSlip.CaseId;
var originalRepoAccount = repoAccountSlip.ExistingRepoAccount;
if (!recoveryAccount.HasRecoveryCase(caseId) && recoveryAccount.HasOldIntellectStatus())
{
Log.Information(
$"Repo Processor: {repoAccountSlip.AccountNo} has no case, but user status code. Running pre-check.");
return (originalRepoAccount.Status == recoveryAccount.CustomFieldMap[UserStatusFieldOld]);
}
return false;
}
public int VerifyCaseId(string itemCaseId)
{
try
{
return string.IsNullOrWhiteSpace(itemCaseId) ? 0 : Int32.Parse(itemCaseId);
}
catch (Exception)
{
throw new IneligibleAccountException(
$"Bad CaseId {itemCaseId}",
$"The CaseId is present, but in a non-integer format.");
}
}
public string VerifyAccountNo(ProcessingItem item)
{
if (item == null)
{
throw new IneligibleAccountException("Processing Item", "The processing item was null.");
}
if (item.AccountNo == null)
{
throw new IneligibleAccountException(
"Null AccountNo",
"There should never be null accounts on the queue.");
}
// if (Regex.IsMatch(item.AccountNo, "^[a-zA-Z0-9]*$"))
// {
// throw new IneligibleAccountException(
// "AccountNo",
// "AccountNo contains alphabetical or symbol characters");
// }
return item.AccountNo;
}
public async Task<RepoAccount> UpdateExistingRepoAccount(RepoAccountProcessingSlip slip)
{
AppUser appUser = new AppUser()
{
Email = slip.RequestedBy.QueuedBy,
FirstName = "System",
LastName = "System"
};
if (ShouldRunCompleteUpdate(slip))
{
RepoAccount updatedRepoAccount =
_repoAccountProcessorService.UpdateRepoAccountFromParts(slip);
await _accountAlertCheckService.AlertChecksPreStatusReject(slip);
_repoAccountProcessorService.CheckForRejectedStatusChange(slip.ExistingRepoAccount, updatedRepoAccount,
slip.CurrentSettings);
_repoAccountProcessorService.TryUpdateOrCreateRepoCompanyAssignment(updatedRepoAccount,
slip.RecoveryCase, slip.CurrentSettings);
_repoAccountProcessorService.TryUpdateCollateralRetitleDate(updatedRepoAccount, slip.RecoveryCase);
await _repoAccountService.UpdateAsync(updatedRepoAccount, appUser);
return updatedRepoAccount;
}
_repoAccountProcessorService.TryUpdateOrCreateRepoCompanyAssignment(slip.ExistingRepoAccount,
slip.RecoveryCase, slip.CurrentSettings);
_repoAccountProcessorService.AddAccountUpdateRecord(slip);
_repoAccountProcessorService.TryUpdateCollateralRetitleDate(slip.ExistingRepoAccount, slip.RecoveryCase);
_repoAccountProcessorService.UpdateRepoAccountDateClosed(slip.ExistingRepoAccount);
await _repoAccountService.UpdateAsync(slip.ExistingRepoAccount, appUser);
return slip.ExistingRepoAccount;
}
public bool ShouldRunCompleteUpdate(RepoAccountProcessingSlip slip)
{
if (slip.ExistingRepoAccount == null)
{
throw new MissingRequirementException("Existing Repo Account",
$"Existing Repo Account required for update check, {slip.AccountNo}");
}
var newStatus = StatusService.ConvertStatus(slip.RecoveryCase.RepoStatus);
var dateLastPaid = slip.RecoveryAccount.DateLastPaid != null
? DateTime.Parse(slip.RecoveryAccount.DateLastPaid)
: slip.ExistingRepoAccount.LastPaymentDate;
var refStatusChanged = slip.RecoveryAccount.AccountStatus != slip.ExistingRepoAccount.ReferenceStatus;
// This is a special case, it picked up the written off flag, before there was a written off date.
if (string.Equals(slip.ExistingRepoAccount.ReferenceStatus, RepoAccountProcessorService.WrittenOff,
StringComparison.CurrentCultureIgnoreCase)
&& slip.ExistingRepoAccount.WrittenOffDate == null)
{
return true;
}
var needsRedemptionExpired = slip.ExistingRepoAccount.RedemptionExpiredDate == null
&& (slip.RecoveryCase.RedemptionExpiredDateIl != null ||
slip.RecoveryCase.RedemptionExpiredDateWi != null);
return !string.Equals(newStatus, slip.ExistingRepoAccount.Status, StringComparison.CurrentCultureIgnoreCase)
|| slip.ExistingRepoAccount.LastPaymentDate != dateLastPaid ||
slip.ExistingRepoAccount.Balance != slip.RecoveryAccount.Balance
|| needsRedemptionExpired || refStatusChanged;
}
public async Task<RepoAccount> CreateNewRepoAccount(RepoAccountProcessingSlip slip)
{
var repoAccount = _repoAccountProcessorService.CreateRepoAccount(slip.RecoveryAccount, slip.RecoveryCase, slip.Loan);
var newRepoAccount = await _repoAccountService.SaveAsync(repoAccount);
return newRepoAccount;
}
public bool ShouldCreateNewAccount(RepoAccountProcessingSlip slip)
{
return slip != null && (slip.ExistingRepoAccount == null || slip.ExistingRepoAccount.DateClosed != null);
}
}
}
@@ -0,0 +1,585 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.DataAccess;
using LCU.API.Collections.RepoApi.Domain.Processing;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models.Results;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Consumer;
using LCU.Shared.Collections.RepoApiLib.Models.Settings;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Producer
{
public class RepoAccountProcessorService : IRepoAccountProcessorService
{
private const string RedeemedStatus = "REDEEMED";
private const string PaidSkipStatus = "PSKP";
private const string RepoCancelledStatus = "RECA";
public const string WrittenOff = "Written Off";
public const string SixtyPLusUserAssignedLabel = "SixtyPlus User Assigned";
private readonly ICoreDao _coreDao;
private readonly IRedemptionExpiredDateCalculator _redemptionExpiredDateCalculator;
private readonly IAccountAlertCheckService _accountAlertCheckService;
private readonly IEventAlertService _eventAlertService;
private readonly IRepoCompanySelectionService _repoCompanySelectionService;
public RepoAccountProcessorService(ICoreDao coreDao,
IRedemptionExpiredDateCalculator redemptionExpiredDateCalculator,
IAccountAlertCheckService accountAlertCheckService,
IEventAlertService eventAlertService, IRepoCompanySelectionService repoCompanySelectionService)
{
_coreDao = coreDao;
_redemptionExpiredDateCalculator = redemptionExpiredDateCalculator;
_accountAlertCheckService = accountAlertCheckService;
_eventAlertService = eventAlertService;
_repoCompanySelectionService = repoCompanySelectionService;
}
// This process determined from the original batch code. (LCU.CollectionRepossessionTracking::IntellecRepoImport)
// -> A) Get XP/Core/Apex Account
// -> B) Get XP/Core/Apex Loan
// -> B.1) Fields needed: Branch, Next due date
// ---> If it can't find a loan, it does nothing... what do we WANT do?
// -> C) Get XP/Core/Apex Loan Collateral
// ---> If there is no collateral, it does nothing... what do we WANT do?
// -> D) Get XP/Core/Apex Borrowers (Individuals)
// ---> No borrowers throws an exception? ... what do we WANT do?
// -> E) Create Repo Account from Loan, Collateral, Borrowers and Temenos Account
public RepoAccount CreateRepoAccount(TemenosAccount account, TemenosCase recoveryCase, LoanDetail loanDetail)
{
var collateralDetailRequest = _coreDao.RetrieveCollateralAsync(account.AccountIdentifier);
var collateralDetail = collateralDetailRequest.GetAwaiter().GetResult();
if (collateralDetail == null || !collateralDetail.Any())
{
throw new AccountCreationFailed(
$"Repo-Account Not Created using Account with AccountNo: {account.AccountIdentifier}. No Collateral.");
}
var borrowers = ConstructRepoAccountBorrowers(loanDetail);
if (borrowers == null || !borrowers.Any())
{
throw new AccountCreationFailed(
$"Repo-Account Not Created using Account with AccountNo: {account.AccountIdentifier}. No Borrowers.");
}
var repoAccount = ConstructRepoAccountFromParts(loanDetail, account, recoveryCase);
repoAccount.Borrowers = borrowers;
repoAccount.Collaterals = ConstructCollateralFromDetails(collateralDetail);
TryUpdateCollateralRetitleDate(repoAccount, recoveryCase);
Log.Information($"Created Repo-Account for Not-Found Account. AccountNo: {account.AccountIdentifier}");
return repoAccount;
}
public List<Borrower> ConstructRepoAccountBorrowers(LoanDetail coreAccount)
{
List<Borrower> borrowers = new List<Borrower>();
try
{
foreach (var coreAccountBorrower in coreAccount.Borrowers)
{
var borrower = new Borrower()
{
IndividualName = coreAccountBorrower.Name,
IndividualId = coreAccountBorrower.IndividualId.ToString(),
OwnershipType = (int)ConvertBorrowerType(coreAccountBorrower.Type)
};
borrowers.Add(borrower);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error in ConstructRepoAccountBorrowers");
}
return borrowers;
}
public List<Collateral> ConstructCollateralFromDetails(IReadOnlyCollection<CollateralDetail> collateralDetail)
{
List<Collateral> collateralList = new List<Collateral>();
foreach (var detail in collateralDetail)
{
var collateral = new Collateral()
{
BodyStyle = null,
CollateralTypeId = null,
Description = detail.Description,
Inoperable = null,
RefCollateralId = detail.CollateralId.ToString()
};
if (detail.Other != null)
{
collateral.Vin = detail.Other.MiscellaneousInformation;
}
if (detail.Vehicle != null)
{
collateral.Make = detail.Vehicle.Make;
collateral.Model = detail.Vehicle.Model;
collateral.Vin = detail.Vehicle.VIN;
collateral.Year = detail.Vehicle.Year;
collateral.BodyStyle = detail.Vehicle.BodyStyle;
}
collateralList.Add(collateral);
}
return collateralList;
}
public RepoAccount ConstructRepoAccountFromParts(LoanDetail coreAccount, TemenosAccount account,
TemenosCase recoveryCase)
{
DateTime? lastPaymentDate = null;
DateTime? dateFirstDelinquent = null;
var lastPaymentResult = GetDateTimeFromString(account.DateLastPaid);
if (lastPaymentResult.IsSuccess)
{
lastPaymentDate = lastPaymentResult.Value;
}
else
{
Log.Warning(
$"Construct Repo Account from temenos accountNo: {account.AccountNumber}. Could not parse last payment result : {account.DateLastPaid}.",
lastPaymentResult, account);
}
var dateFirstDelinquentResult = GetDateTimeFromString(account.DateFirstDelinquent);
if (dateFirstDelinquentResult.IsSuccess)
{
dateFirstDelinquent = dateFirstDelinquentResult.Value;
}
else
{
Log.Warning(
$"Construct Repo Account from temenos accountNo: {account.AccountNumber}. failed parsing dateFirstDelinquent {account.DateFirstDelinquent}",
dateFirstDelinquentResult, account);
// This is no longer a "MissingRequirementException", per Bill Johns.
}
var sixtyPlusUserAssignedNumber = GetSixtyPlusUserNumber(account);
var repoAccount = new RepoAccount()
{
DueToPayDate = coreAccount.NextDueDate,
Balance = account.Balance,
AccountTypeCode = account.AccountType.Code,
OrigAccountTypeCode = account.AccountType.Code,
LoanNo = long.Parse(account.AccountIdentifier),
MemberNo = MemberIdFromAccountId(account.AccountIdentifier),
Branch = account.Branch != null ? int.Parse(account.Branch) : 0,
AccountNo = account.AccountIdentifier,
Status = StatusService.ConvertStatus(recoveryCase.RepoStatus),
O4RDate = recoveryCase.CreatedDate,
Collector = account.LoanOfficer,
DateFirstDelinquent = dateFirstDelinquent,
DelinquentAmount = account.DQAmount,
CurrentDelinquentAmount = account.DQAmount,
SubStatus = "",
DaysDelinquent = account.DaysDq,
LastPaymentDate = lastPaymentDate,
LstPmtAmt = account.AmountLastPaid,
OrigAmount = account.OriginalLoanAmount,
RequiredPaymentAmount = account.DQAmount,
PrimaryIndividualId = coreAccount.IndividualId[0].ToString(),
Bankruptcy = $"{account.BankruptcyFlag}",
DateFirstIntellect = recoveryCase.CreatedDate,
ReferenceId = account.AccountId,
ReferenceCaseId = recoveryCase.CaseId,
AccountLastProcessed = DateTime.Now,
ReferenceStatus = account.AccountStatus,
WrittenOffDate = coreAccount.Status == WrittenOff ? coreAccount.WrittenOffDate : (DateTime?)null,
SixtyPlusCollectorId = sixtyPlusUserAssignedNumber
};
return repoAccount;
}
public long MemberIdFromAccountId(string accountId)
{
return _coreDao.RetrieveMemberAsync(accountId).GetAwaiter().GetResult().MemberId;
}
public RepoAccount UpdateRepoAccountFromParts(RepoAccountProcessingSlip slip)
{
RepoAccount originalRepoAccount = slip.ExistingRepoAccount;
TemenosAccount account = slip.RecoveryAccount;
TemenosCase recoveryCase = slip.RecoveryCase;
IReadOnlyCollection<RepoLetter> letters = slip.Letters;
var redemptionExpiredDate = _redemptionExpiredDateCalculator.CalculateAsync(recoveryCase, letters);
DateTime? lastPaymentDate = null;
DateTime? dateFirstDelinquent = originalRepoAccount.DateFirstDelinquent;
var lastPaymentResult = GetDateTimeFromString(account.DateLastPaid);
if (lastPaymentResult.IsSuccess)
{
lastPaymentDate = lastPaymentResult.Value;
}
else
{
Log.Warning(
$"Repo account {originalRepoAccount.RepoId} ({originalRepoAccount.AccountNo}). Could not parse last payment result : {account.DateLastPaid}.",
lastPaymentResult);
}
var dateFirstDelinquentResult = GetDateTimeFromString(account.DateFirstDelinquent);
if (dateFirstDelinquentResult.IsSuccess)
{
dateFirstDelinquent = dateFirstDelinquentResult.Value;
}
else
{
Log.Error(
$"Repo account {originalRepoAccount.RepoId} ({originalRepoAccount.AccountNo}) failed parsing dateFirstDelinquent {account.DateFirstDelinquent}",
dateFirstDelinquentResult);
// This is no longer a "MissingRequirementException", per Bill Johns.
}
var sixtyPlusUserAssignedNumber = GetSixtyPlusUserNumber(account);
var repoAccount = new RepoAccount()
{
RepoId = originalRepoAccount.RepoId,
DueToPayDate = account.DueDate,
Balance = account.Balance,
AccountTypeCode = account.AccountType.Code,
OrigAccountTypeCode = originalRepoAccount.OrigAccountTypeCode,
LoanNo = originalRepoAccount.LoanNo,
MemberNo = originalRepoAccount.MemberNo,
Branch = originalRepoAccount.Branch,
AccountNo = originalRepoAccount.AccountNo,
Status = StatusService.ConvertStatus(recoveryCase.RepoStatus),
O4RDate = originalRepoAccount.O4RDate,
Collector = account.LoanOfficer,
DateFirstDelinquent = dateFirstDelinquent,
DelinquentAmount = account.DQAmount,
CurrentDelinquentAmount = account.DQAmount,
SubStatus = "",
DaysDelinquent = originalRepoAccount.DaysDelinquent, //leave this set to days delinquent when added.
LastPaymentDate = lastPaymentDate,
LstPmtAmt = account.AmountLastPaid ?? 0,
OrigAmount = account.OriginalLoanAmount ?? 0,
RedemptionExpiredDate = redemptionExpiredDate,
RequiredPaymentAmount = account.DQAmount,
PrimaryIndividualId = originalRepoAccount.PrimaryIndividualId,
Bankruptcy = $"{account.BankruptcyFlag}",
Collaterals = originalRepoAccount.Collaterals,
RepoStatusChanges = originalRepoAccount.RepoStatusChanges,
ManualRedemptionDate = originalRepoAccount.ManualRedemptionDate,
DateFirstIntellect = originalRepoAccount.DateFirstIntellect,
Borrowers = originalRepoAccount.Borrowers,
PaidSkipAmount = originalRepoAccount.PaidSkipAmount,
RefundStatus = originalRepoAccount.RefundStatus,
ReferenceId = account.AccountId,
ReferenceCaseId = recoveryCase.CaseId,
AccountLastProcessed = DateTime.Now,
ReferenceStatus = account.AccountStatus,
WrittenOffDate = slip.Loan.Status == WrittenOff ? slip.Loan.WrittenOffDate : (DateTime?)null,
SixtyPlusCollectorId = sixtyPlusUserAssignedNumber
};
return repoAccount;
}
/**
* High Risk function because of string comparison and unknown mapping.
* The old system got the integer from the database.
* The new system gets a string from the web service.
* This maps the new string to the old integer, if borrower types start
* showing up wrong, look here...
*/
public OwnershipType ConvertBorrowerType(string type)
{
if (type != null && string.Equals(type.Trim(), "Borrower", StringComparison.OrdinalIgnoreCase))
{
return OwnershipType.Borrower;
}
if (type != null && string.Equals(type.Trim(), "Co-Borrower", StringComparison.OrdinalIgnoreCase))
{
return OwnershipType.Coborrower;
}
return OwnershipType.Other;
}
public void CheckForRejectedStatusChange(RepoAccount originalRepoAccount, RepoAccount updatedRepoAccount,
Settings settings)
{
if (!IsStatusMatch(originalRepoAccount, updatedRepoAccount.Status) &&
!IsStatusChangeAllowed(originalRepoAccount.Status, updatedRepoAccount.Status,
settings.AllowedStatusChanges))
{
Log.Warning(
$"Status Change Reverted for Account {updatedRepoAccount.AccountNo} from {originalRepoAccount.Status} to {updatedRepoAccount.Status}");
_accountAlertCheckService.ManualAlert(new EventAlert()
{
RepoId = updatedRepoAccount.RepoId,
AccountNo = updatedRepoAccount.AccountNo,
CreatedBy = "Importer",
DateAdded = DateTime.Now,
Message = "Collections System Status Does Not Match Repo Management",
SubMessage =
$"Invalid Status Change from {originalRepoAccount.Status} to {updatedRepoAccount.Status}"
});
updatedRepoAccount.Status = originalRepoAccount.Status;
}
}
public bool IsStatusChangeAllowed(string oldStatus, string newStatus,
IEnumerable<AllowedStatusChange> settingsAllowedStatusChanges)
{
foreach (var allowedStatusChange in settingsAllowedStatusChanges)
{
if (IsStringMatch(allowedStatusChange.CurrentStatus, oldStatus) &&
IsStringMatch(allowedStatusChange.AllowedChangeStatus, newStatus))
{
return true;
}
}
return false;
}
public bool IsStatusMatch(RepoAccount repo, string status)
{
if (repo?.Status == null || status == null)
{
return false;
}
return string.Equals(repo.Status.Trim(), status.Trim(), StringComparison.OrdinalIgnoreCase);
}
public bool IsStringMatch(string x, string y)
{
if (x == null || y == null)
{
return false;
}
return string.Equals(x.Trim(), y.Trim(), StringComparison.OrdinalIgnoreCase);
}
public void UpdateRepoAccountDateClosed(RepoAccount repoAccount)
{
// Close the account after four days in certain statuses
if ((repoAccount.Status == PaidSkipStatus || repoAccount.Status == RedeemedStatus || repoAccount.Status == RepoCancelledStatus)
&& repoAccount.RepoStatusChanges.LastOrDefault(x => x.StatusTo == repoAccount.Status)?.ChangedDate <
DateTime.Now.AddDays(-3).Date)
{
repoAccount.DateClosed = DateTime.Now;
repoAccount.AccountLastProcessed = DateTime.Now;
// TODO calling this still has a side effect.
_eventAlertService.MarkAllAsRead(repoAccount.AccountNo).GetAwaiter().GetResult();
}
}
public void TryUpdateCollateralRetitleDate(RepoAccount repo, TemenosCase temenosCase)
{
DateTime? retitledDate = (temenosCase.TitleOrdered) ? temenosCase.TitleOrderedDate : null;
foreach (Collateral collateral in repo.Collaterals)
{
collateral.RetitledInLandmarkName = retitledDate;
}
}
// This exists to update accounts not originally created from Temenos
public void AddAccountUpdateRecord(RepoAccountProcessingSlip slip)
{
slip.ExistingRepoAccount.ReferenceId = slip.RecoveryAccount.AccountId;
slip.ExistingRepoAccount.ReferenceCaseId = slip.RecoveryCase.CaseId;
slip.ExistingRepoAccount.AccountLastProcessed = DateTime.Now;
}
public void TryUpdateOrCreateRepoCompanyAssignment(RepoAccount repoAccount, TemenosCase temenosCase,
Settings settings)
{
//Temenos may send blank string instead of null
var repoCompanyIdFromCase = string.IsNullOrEmpty(temenosCase.RepoAgentId)
? null
: MapRepoAgentIdToRepoCompanyId(temenosCase.RepoAgentId, settings.RepoCompanies);
foreach (var collateral in repoAccount.Collaterals)
{
var mostRecent = collateral.RepoCompanySelections.OrderByDescending(x => x.DateTimeCreated)
.FirstOrDefault();
// TODO, move to Slip, save elsewhere.
var updated =
UpdateOrCreateRepoCompanyAssignment(collateral, mostRecent, temenosCase, repoCompanyIdFromCase);
// This can no longer happen, but watch for it anyway.
if (updated.Any(repoCompanySelection =>
repoCompanySelection.DateRepoed != null &&
repoCompanySelection.RepoCompanyAssignmentDate == null))
{
Log.Error(
$"Tried to set DateRepoed before RepoCompanyAssignment for accountNo {repoAccount.AccountNo}. Please Investigate.");
throw new MissingRequirementException(
$"RepoCompanyAssignment for repo account {repoAccount.AccountNo} Missing RepoCompanyAssignmentDate",
$"Cannot assign a DateRepoed before a RepoCompanyAssignment Date.");
}
if (updated.Any())
{
_repoCompanySelectionService.SaveChangesAsync(updated).GetAwaiter().GetResult();
}
}
}
public List<RepoCompanySelection> UpdateOrCreateRepoCompanyAssignment(Collateral collateral, RepoCompanySelection mostRecentSelection,
TemenosCase temenosCase, int? repoCompanyIdFromCase)
{
var selectionUpdates = new List<RepoCompanySelection>();
//If this is the first repoCompanySelection create when temenos has data
if (mostRecentSelection == null)
{
if (temenosCase.RepoAgentId != null && temenosCase.RepoType != null &&
temenosCase.DateRepoAssigned != null)
{
var newRepoCompanyEntry = CreateNewRepoCompanyAssignment(collateral, temenosCase, repoCompanyIdFromCase);
collateral.RepoCompanySelections = collateral.RepoCompanySelections.Append(newRepoCompanyEntry).ToList();
selectionUpdates.Add(newRepoCompanyEntry);
}
return selectionUpdates;
}
//If the repoCompany has changed update with temenos data
if (mostRecentSelection.RepoCompanyId != repoCompanyIdFromCase ||
mostRecentSelection.RepoType != temenosCase.RepoType ||
mostRecentSelection.RepoCompanyAssignmentDate != temenosCase.DateRepoAssigned)
{
if (mostRecentSelection.DateRepoed == null)
{
mostRecentSelection.DateRepoed = DateTime.Now;
if (mostRecentSelection.RepoCompanyAssignmentDate == null)
{
mostRecentSelection.RepoCompanyAssignmentDate = mostRecentSelection.DateTimeCreated ?? DateTime.Now;
}
selectionUpdates.Add(mostRecentSelection);
}
var newRepoCompanyEntry = CreateNewRepoCompanyAssignment(collateral, temenosCase, repoCompanyIdFromCase);
collateral.RepoCompanySelections = collateral.RepoCompanySelections.Append(newRepoCompanyEntry).ToList();
selectionUpdates.Add(newRepoCompanyEntry);
return selectionUpdates;
}
//If no need to update or create, sync Repoed Date
mostRecentSelection.DateRepoed = temenosCase.DateRepossessed;
selectionUpdates.Add(mostRecentSelection);
return selectionUpdates;
}
public RepoCompanySelection CreateNewRepoCompanyAssignment(Collateral collateral, TemenosCase temenosCase, int? newRepoCompanyId)
{
// DateRepoAssigned is defaulted to Today. The business (Bill) doesn't want it to throw, if they start having trouble
// with the dates; change this to throw a MissingRequirementException on the field.
var newRepoCompanySelection = new RepoCompanySelection()
{
RepoCompanyId = newRepoCompanyId,
RepoType = temenosCase.RepoType ?? RepoType.Unknown,
RepoCompanyAssignmentDate = temenosCase.DateRepoAssigned ?? DateTime.Now,
DateRepoed = temenosCase.DateRepossessed,
CollateralId = collateral.CollateralId,
DateTimeCreated = DateTime.Now,
Id = null
};
return newRepoCompanySelection;
}
public int? MapRepoAgentIdToRepoCompanyId(string repoAgentId, IReadOnlyCollection<RepoCompany> repoCompanies)
{
try
{
var repoCompany =
repoCompanies.FirstOrDefault(company => company.ReferenceId == int.Parse(repoAgentId));
if (repoCompany != null)
{
return repoCompany.ID;
}
}
catch (Exception ex)
{
Log.Error(ex, $"Error when trying to map RepoAgentID: {repoAgentId} to a RepoCompany ReferenceID", repoCompanies);
}
// If you get this error, ensure there is a matching RepoCompany and that it has a RepoAgentID that matches the config call in Collections API.
throw new MissingRequirementException("RepoCompanyAssignment - RepoCompanyID", $"Ensure a RepoCompany with RepoAgentID: {repoAgentId} is present");
}
public IResult<DateTime?> GetDateTimeFromString(string dtString)
{
if (string.IsNullOrEmpty(dtString))
{
Log.Error("Cannot parse null or empty date string");
return new DateTimeResult()
{
IsSuccess = false,
ErrorMessage = "Cannot Parse Empty Date String",
};
}
try
{
return new DateTimeResult()
{
IsSuccess = true,
Value = DateTime.Parse(dtString)
};
}
catch (Exception ex)
{
Log.Error(ex, $"Cannot Parse Date String {dtString}");
return new DateTimeResult()
{
IsSuccess = false,
ErrorMessage = $"There was an Exception when processing the date string: {dtString}"
};
}
}
public int? GetSixtyPlusUserNumber(TemenosAccount account)
{
try
{
account.CustomFieldMap.TryGetValue(SixtyPLusUserAssignedLabel, out string sixtyPlusUser);
if (String.IsNullOrWhiteSpace(sixtyPlusUser))
{
return null;
}
Int32.TryParse(sixtyPlusUser, out int sixtyPlusUserNumber);
return sixtyPlusUserNumber;
}
catch(NullReferenceException nullEx)
{
Log.Warning($"60+ Collector key not found in Temenos Account for Account Number: {account.AccountNumber}. Error: {nullEx}");
return null;
}
catch(Exception ex)
{
Log.Error($"Error while attempting to Parse 60+ Collector ID for Account Number: {account.AccountNumber}. Error: {ex}");
return null;
}
}
}
}
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RefundStatusService : IRefundStatusService
{
private IRefundStatusRepository _refundStatusRepository;
private IUnitOfWork _unitOfWork;
private IRepoAccountService _repoAccountService;
private IEmailer _emailer;
public RefundStatusService(
IRefundStatusRepository refundStatusRepository,
IUnitOfWork unitOfWork,
IRepoAccountService repoAccountService,
IEmailer emailer)
{
_refundStatusRepository = refundStatusRepository;
_unitOfWork = unitOfWork;
_repoAccountService = repoAccountService;
_emailer = emailer;
}
public async Task<IReadOnlyCollection<RefundStatus>> ListAllAsync()
{
return await _refundStatusRepository.ListAsync();
}
public async Task<RefundStatus> GetAsync(int repoId)
{
RefundStatus refundStatus;
try
{
refundStatus = await _refundStatusRepository.FindByRepoIdAsync(repoId);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Refund Status for repoId: {repoId}");
throw new NotFoundException($"Refund Status with repoId {repoId} not found");
}
if (refundStatus == null)
{
throw new NotFoundException($"Refund Status with repoId {repoId} not found");
}
return refundStatus;
}
public async Task<RefundStatus> SaveAsync(RefundStatus refundStatus)
{
await _repoAccountService.Get(refundStatus.RepoId.Value);
try
{
var newRefundStatus = await _refundStatusRepository.AddAsync(refundStatus);
await _unitOfWork.CompleteAsync();
return newRefundStatus;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Refund Status: {refundStatus}", refundStatus);
throw new ServerErrorException();
}
}
public async Task<RefundStatus> UpdateAsync(RefundStatus refundStatus)
{
var existingRefundStatus = await GetAsync(refundStatus.RepoId.Value);
var existingRepoAccount = await _repoAccountService.Get(refundStatus.RepoId.Value);
var isInBankruptcy = existingRepoAccount.Bankruptcy != null;
var isEmailNeeded = refundStatus.RefundsReceivedDate != null && existingRefundStatus.RefundsReceivedDate != refundStatus.RefundsReceivedDate && !isInBankruptcy;
try
{
existingRefundStatus.WarrantyRefundStatus = refundStatus.WarrantyRefundStatus;
existingRefundStatus.InsuranceRefundStatus = refundStatus.InsuranceRefundStatus;
existingRefundStatus.PremiumDefiRefundStatus = refundStatus.PremiumDefiRefundStatus;
existingRefundStatus.CPIRefundStatus = refundStatus.CPIRefundStatus;
existingRefundStatus.RefundsReceivedDate = refundStatus.RefundsReceivedDate;
_refundStatusRepository.Update(existingRefundStatus);
await _unitOfWork.CompleteAsync();
if (isEmailNeeded)
{
await _emailer.SendSurplusDeficiencyLetterEmailAsync(existingRepoAccount);
}
return existingRefundStatus;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Refund Status: {refundStatus}", refundStatus);
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models;
using LCU.API.Collections.RepoApi.Models.Filters;
using LCU.API.Collections.RepoApi.Models.Requests;
using LCU.Shared.Collections.RepoApiLib.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Models.Producer;
using LCU.Shared.Collections.RepoApiLib.Models.Results;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RepoAccountService : IRepoAccountService
{
private readonly IRepoAccountRepository _repoAccountRepository;
private readonly IRepoStatusChangeRepository _repoStatusChangeRepository;
private readonly IRepoManualStatusRepository _repoManualStatusRepository;
private readonly IUnitOfWork _unitOfWork;
public RepoAccountService(
IRepoAccountRepository repoAccountRepository,
IRepoStatusChangeRepository repoStatusChangeRepository,
IRepoManualStatusRepository repoManualStatusRepository,
IUnitOfWork unitOfWork)
{
_repoAccountRepository = repoAccountRepository;
_repoStatusChangeRepository = repoStatusChangeRepository;
_repoManualStatusRepository = repoManualStatusRepository;
_unitOfWork = unitOfWork;
}
public async Task<PagedResult<RepoAccount>> ListAll(PageFilter pageFilter, RepoAccountPostFilter repoAccountFilter)
{
try
{
return await _repoAccountRepository.ListAsync(
repoAccountFilter != null ? repoAccountFilter.RepoIdList : new List<int?>(),
pageFilter.Page,
pageFilter.Limit,
repoAccountFilter?.Status,
repoAccountFilter?.AuctionHouseId,
repoAccountFilter?.RepoCompanyId,
repoAccountFilter?.OnlyOpenAccounts);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving RepoAccounts");
throw new ServerErrorException();
}
}
public async Task<RepoAccount> FindByAuctionAssignmentId(int auctionAssignmentId)
{
try
{
return await _repoAccountRepository.FindByAuctionAssignmentIdAsync(auctionAssignmentId);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving RepoAccount");
throw new ServerErrorException();
}
}
public async Task<RepoAccount> SaveAsync(RepoAccount repoAccount)
{
try
{
var newAccount = await _repoAccountRepository.AddAsync(repoAccount);
RepoStatusChange change = new RepoStatusChange()
{
RepoId = newAccount.RepoId,
ChangedBy = "System",
StatusFrom = "None",
StatusTo = repoAccount.Status,
ChangedDate = DateTime.Now
};
await _repoStatusChangeRepository.AddAsync(change);
await _unitOfWork.CompleteAsync();
return repoAccount;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Repo Account: {repoAccount}", repoAccount);
throw new ServerErrorException();
}
}
public async Task<RepoAccount> Get(int id)
{
RepoAccount repoAccount;
try
{
repoAccount = await _repoAccountRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Repo Account with id: {id}");
throw new ServerErrorException();
}
if (repoAccount == null)
{
throw new NotFoundException($" Repo Accounts with id {id} not found.");
}
return repoAccount;
}
public async Task<IReadOnlyCollection<RepoAccount>> GetByAccountNo(string accountNo)
{
IReadOnlyCollection<RepoAccount> repoAccounts;
try
{
repoAccounts = await _repoAccountRepository.FindByAccountNoAsync(accountNo);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Repo Account by accountNo with accountNo: {accountNo}");
throw new ServerErrorException();
}
if (repoAccounts == null || !repoAccounts.Any())
{
throw new NotFoundException($" Repo Accounts with id {accountNo} not found.");
}
return repoAccounts;
}
public async Task<RepoAccount> UpdateAsync(RepoAccount repoAccount, AppUser currentUser)
{
var existingRepoAccount = await _repoAccountRepository.FindByIdAsync(repoAccount.RepoId);
if (existingRepoAccount == null)
{
throw new NotFoundException($"Repo Account with repoId {repoAccount.RepoId} not found.");
}
var originalStatus = existingRepoAccount.Status;
existingRepoAccount.AccountLastProcessed = repoAccount.AccountLastProcessed;
existingRepoAccount.AccountNo = repoAccount.AccountNo;
existingRepoAccount.AccountTypeCode = repoAccount.AccountTypeCode;
existingRepoAccount.Balance = repoAccount.Balance;
existingRepoAccount.Bankruptcy = repoAccount.Bankruptcy;
existingRepoAccount.Borrowers = repoAccount.Borrowers;
existingRepoAccount.Branch = repoAccount.Branch;
// Only one field can change in collaterals when updating the account.
if (existingRepoAccount.Collaterals != null)
{
foreach (var collateral in existingRepoAccount.Collaterals)
{
var newCollateral =
repoAccount.Collaterals.FirstOrDefault(x => x.CollateralId == collateral.CollateralId);
if (newCollateral != null)
{
collateral.RetitledInLandmarkName = newCollateral.RetitledInLandmarkName;
}
}
}
// existingRepoAccount.Collaterals = repoAccount.Collaterals; This will fail, save separately.
existingRepoAccount.Collector = repoAccount.Collector;
existingRepoAccount.CurrentDelinquentAmount = repoAccount.CurrentDelinquentAmount;
existingRepoAccount.DateClosed = repoAccount.DateClosed;
existingRepoAccount.DateFirstDelinquent = repoAccount.DateFirstDelinquent;
existingRepoAccount.DateFirstIntellect = repoAccount.DateFirstIntellect;
existingRepoAccount.DaysDelinquent = repoAccount.DaysDelinquent;
existingRepoAccount.DelinquentAmount = repoAccount.DelinquentAmount;
existingRepoAccount.DueToPayDate = repoAccount.DueToPayDate;
existingRepoAccount.LastPaymentDate = repoAccount.LastPaymentDate;
existingRepoAccount.LoanNo = repoAccount.LoanNo;
existingRepoAccount.LstPmtAmt = repoAccount.LstPmtAmt;
existingRepoAccount.ManualRedemptionDate = repoAccount.ManualRedemptionDate;
existingRepoAccount.MemberNo = repoAccount.MemberNo;
existingRepoAccount.O4RDate = repoAccount.O4RDate;
existingRepoAccount.OrigAmount = repoAccount.OrigAmount;
existingRepoAccount.OrigAccountTypeCode = repoAccount.OrigAccountTypeCode;
existingRepoAccount.PaidSkipAmount = repoAccount.PaidSkipAmount;
existingRepoAccount.PrimaryIndividualId = repoAccount.PrimaryIndividualId;
existingRepoAccount.RedemptionExpiredDate = repoAccount.RedemptionExpiredDate;
existingRepoAccount.RefundStatus = repoAccount.RefundStatus;
existingRepoAccount.RequiredPaymentAmount = repoAccount.RequiredPaymentAmount;
existingRepoAccount.ReferenceId = repoAccount.ReferenceId;
existingRepoAccount.ReferenceCaseId = repoAccount.ReferenceCaseId;
existingRepoAccount.ReferenceStatus = repoAccount.ReferenceStatus;
existingRepoAccount.Status = repoAccount.Status;
existingRepoAccount.SubStatus = repoAccount.SubStatus;
existingRepoAccount.WrittenOffDate = repoAccount.WrittenOffDate;
existingRepoAccount.SixtyPlusCollectorId = repoAccount.SixtyPlusCollectorId;
try
{
_repoAccountRepository.Update(existingRepoAccount);
if (originalStatus != repoAccount.Status)
{
var change = new RepoStatusChange()
{
RepoId = repoAccount.RepoId,
ChangedBy = currentUser.FirstName + " " + currentUser.LastName,
StatusFrom = originalStatus,
StatusTo = repoAccount.Status,
ChangedDate = DateTime.Now
};
await _repoStatusChangeRepository.AddAsync(change);
}
await _unitOfWork.CompleteAsync();
return await _repoAccountRepository.FindByIdAsync(repoAccount.RepoId);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Repo Account: {repoAccount}", repoAccount);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Repo Account: {repoAccount}", repoAccount);
throw new ServerErrorException();
}
}
public async Task<RepoAccount> DeleteAsync(int id)
{
RepoAccount existingRepoAccount;
try
{
existingRepoAccount = await _repoAccountRepository.FindByIdAsync(id);
}
catch(Exception ex)
{
Log.Error(ex, $"Error finding an existing repo to delete with account ID: {id}");
throw new ServerErrorException();
}
if (existingRepoAccount == null)
{
throw new NotFoundException($" Repo Account with id {id} not found.");
}
try
{
_repoAccountRepository.Remove(existingRepoAccount);
await _unitOfWork.CompleteAsync();
return existingRepoAccount;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting Repo Account with id: {id}", id);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting Repo Account with id: {id}", id);
throw new ServerErrorException();
}
}
public async Task<IReadOnlyCollection<ProcessingItem>> GetItemsForProcessing()
{
try
{
var watchStatus = await _repoManualStatusRepository.ListWatchStatusesAsync();
var repoAccountNos = await _repoAccountRepository.GetItemsForProcessing(watchStatus);
return repoAccountNos;
}
catch (Exception ex)
{
Log.Error(ex, "Failure getting Repo Account Nos for polling");
throw new ServerErrorException();
}
}
public async Task<IReadOnlyCollection<RepoAccount>> SearchRepoAccounts(string searchPhrase, int maxAllowedResults)
{
try
{
var matchedRepoAccounts = await _repoAccountRepository.SearchRepoAccounts(searchPhrase, maxAllowedResults);
return matchedRepoAccounts;
}
catch (MaxItemException ex)
{
Log.Error(ex, $"Search Phrase Returned More Items Than Max (${maxAllowedResults}) searchPhrase:{searchPhrase}", searchPhrase);
throw new MaxItemException(ex.ItemCount);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure Searching For Repo Accounts searchPhrase: {searchPhrase}", searchPhrase);
throw new ServerErrorException();
}
}
public async Task<RepoAccount> GetNewestOpenAccount(string accountNo)
{
IReadOnlyCollection<RepoAccount> repoAccounts;
try
{
repoAccounts = await _repoAccountRepository.FindByAccountNoAsync(accountNo);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Repo Account by accountNo with accountNo: {accountNo}");
throw new ServerErrorException();
}
if (repoAccounts == null || !repoAccounts.Any())
{
// You can't throw here, it's not an exception case, there simply isn't an account. (404)
Log.Debug($" Repo Accounts with id {accountNo} not found.");
return null;
}
return SelectFirstOpenAccount(repoAccounts);
}
/**
* Attempts to select the repo account that should be updated. Since we have to go by AccountNo and there
* may be more than one. Try to return an open account, and log if it detects more than one.
*/
public RepoAccount SelectFirstOpenAccount(IReadOnlyCollection<RepoAccount> repoAccounts)
{
if (repoAccounts.Count == 1)
{
return repoAccounts.First();
}
RepoAccount openAccount = null;
foreach (var repoAccount in repoAccounts.OrderBy(x => x.RepoId))
{
if (repoAccount.DateClosed == null)
{
if (openAccount != null)
{
Log.Fatal($"Multiple -OPEN- Repos with AccountNo: {openAccount.AccountNo}, {openAccount.RepoId} / {repoAccount.RepoId}");
return openAccount;
}
openAccount = repoAccount;
}
}
if (openAccount != null)
{
return openAccount;
}
return repoAccounts.OrderByDescending(x => x.DateClosed).ToList().FirstOrDefault();
}
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RepoCompanySelectionService : IRepoCompanySelectionService
{
private readonly IRepoCompanySelectionRepository _repoCompanySelectionRepository;
private readonly IUnitOfWork _unitOfWork;
public RepoCompanySelectionService(
IRepoCompanySelectionRepository repoCompanySelectionRepository,
IUnitOfWork unitOfWork)
{
_repoCompanySelectionRepository = repoCompanySelectionRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<RepoCompanySelection>> ListAllAsync()
{
try
{
return await _repoCompanySelectionRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Companies");
throw new ServerErrorException();
}
}
public async Task<RepoCompanySelection> GetAsync(int id)
{
RepoCompanySelection repoCompanySelection;
try
{
repoCompanySelection = await _repoCompanySelectionRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Repo Company Selection with id: {id}");
throw new NotFoundException($"Repo Company Selection with id {id} not found");
}
if (repoCompanySelection == null)
{
throw new NotFoundException($"Repo Company Selection with id {id} not found");
}
return repoCompanySelection;
}
public async Task SaveChangesAsync(IList<RepoCompanySelection> repoCompanySelections)
{
_repoCompanySelectionRepository.SaveRange(repoCompanySelections);
await _unitOfWork.CompleteAsync();
}
}
}
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Models;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RepoCompanyService : IRepoCompanyService
{
private IRepoCompanyRepository _repoCompanyRepository;
private IUnitOfWork _unitOfWork;
public RepoCompanyService(
IRepoCompanyRepository repoCompanyRepository,
IUnitOfWork unitOfWork)
{
_repoCompanyRepository = repoCompanyRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<RepoCompany>> ListAll()
{
try
{
return await _repoCompanyRepository.ListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Companies");
throw new ServerErrorException();
}
}
public async Task<RepoCompany> Get(int id)
{
RepoCompany repoCompany;
try
{
repoCompany = await _repoCompanyRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Repo Company with id: {id}");
throw new NotFoundException($"Repo Company with id {id} not found");
}
if (repoCompany == null)
{
throw new NotFoundException($"Repo Company with id {id} not found");
}
return repoCompany;
}
public async Task<RepoCompany> Update(RepoCompany repoCompany, AppUser user)
{
var existing = await _repoCompanyRepository.FindByIdAsync(repoCompany.ID);
if (existing == null)
{
throw new NotFoundException($"Repo Company with id {repoCompany.ID} not found");
}
existing.Name = repoCompany.Name;
existing.Email = repoCompany.Email;
existing.Address_Line_1 = repoCompany.Address_Line_1;
existing.Address_Line_2 = repoCompany.Address_Line_2;
existing.City = repoCompany.City;
existing.State = repoCompany.State;
existing.ZIP = repoCompany.ZIP;
existing.Country = repoCompany.Country;
existing.Phone = repoCompany.Phone;
existing.Fax = repoCompany.Fax;
existing.ReferenceId = repoCompany.ReferenceId;
if (existing.DisabledDate != repoCompany.DisabledDate)
{
if (repoCompany.DisabledDate == null)
{
existing.DisabledDate = null;
existing.DisabledBy = null;
} else
{
existing.DisabledDate = repoCompany.DisabledDate;
existing.DisabledBy = user.FirstName + " " + user.LastName;
}
}
try
{
_repoCompanyRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return existing;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Filure updating Repo Company: {repoCompany}", repoCompany);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Repo Repo Company: {repoCompany}", repoCompany);
throw new ServerErrorException();
}
}
public async Task<RepoCompany> Save(RepoCompany repoCompany)
{
try
{
var newCompany = await _repoCompanyRepository.AddAsync(repoCompany);
await _unitOfWork.CompleteAsync();
return newCompany;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Repo Company: {repoCompany}", repoCompany);
throw new ServerErrorException();
}
}
public async Task<RepoCompany> Delete(int id)
{
var existingRepoCompany = await _repoCompanyRepository.FindByIdAsync(id);
if (existingRepoCompany == null)
{
throw new NotFoundException($" Repo Company with id {id} not found.");
}
try
{
_repoCompanyRepository.Remove(existingRepoCompany);
await _unitOfWork.CompleteAsync();
return existingRepoCompany;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting Repo Company with id: {id}");
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting Repo Company with id: {id}");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.DataAccess;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RepoLetterService : IRepoLetterService
{
private readonly ILetterRepository _letterRepository;
private readonly ICollectionsDao _collectionsDao;
public RepoLetterService(ILetterRepository letterRepository,
ICollectionsDao collectionsDao)
{
_letterRepository = letterRepository;
_collectionsDao = collectionsDao;
}
public async Task<IReadOnlyCollection<RepoLetter>> ListByAccountIdAsync(int accountId)
{
try
{
var collectionsHistory = await _collectionsDao.RetrieveHistoryAsync(accountId);
var collectionsLetters =
collectionsHistory.Where(history => history.Type == HistoryType.Letter).ToList();
if (!collectionsLetters.Any())
{
return new List<RepoLetter>();
}
var letters = await _letterRepository.ListAsync();
return ProcessLetters(collectionsLetters, letters.ToList());
}
catch (MissingRequirementException ex)
{
throw;
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Letter List");
throw new ServerErrorException();
}
}
public static IReadOnlyCollection<RepoLetter> ProcessLetters(List<TemenosHistory> collectionsLetters,
List<Letter> letters)
{
// GJF: Are you here to fix a bug? Probably. These don't match up, who wrote this?!?! This code is fueled by hope.
// Ok, so you really need to fix it. Go out to Temenos, go to System Management (the button is labelled 'tools'!)
// Find the Communication section, expand it and choose 'letters & forms'. Now find the right Letter and
// update the LetterDescription column in the Letters table of RepoDb for the correct letter.
var resultList = new List<RepoLetter>();
foreach (var letter in letters)
{
foreach (var collectionsLetter in collectionsLetters)
{
if (string.IsNullOrWhiteSpace(letter.LetterDescription))
{
// If this exception is happening, see LetterUpdate.sql. Also See Above.
throw new MissingRequirementException("LetterDescription", "Letter table requires LetterDescription Column entry.");
}
if (collectionsLetter.Message.Trim().ToUpper()
.Contains(letter.LetterDescription.Trim().ToUpper()))
{
resultList.Add(new RepoLetter
{
NoticeType = collectionsLetter.Message,
DateSent = collectionsLetter.Created,
DateExpired = collectionsLetter.Created.AddDays(letter.DaysToWait ?? 0),
DaysToRedeem = letter.DaysToWait ?? 0,
ActionCode = letter.ActionCode.Trim()
});
}
}
}
return resultList
.GroupBy(x => x.NoticeType.Trim(),
(key, g) => g.OrderByDescending(x => x.DateSent).FirstOrDefault())
.OrderByDescending(x => x.DateSent).ToList();
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class RepoStatusChangeService : IRepoStatusChangeService
{
private readonly IRepoStatusChangeRepository _repoStatusChangeRepository;
public RepoStatusChangeService(
IRepoStatusChangeRepository repoStatusChangeRepository)
{
_repoStatusChangeRepository = repoStatusChangeRepository;
}
public async Task<IEnumerable<RepoStatusChange>> ListByAccount(int repoId)
{
try
{
return await _repoStatusChangeRepository.ListForAccountAsync(repoId);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Account Status Change List");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,282 @@
using System;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Factories;
using LCU.Shared.Collections.RepoApiLib.Formatting;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public class AuctionReportingService : IAuctionReportingService
{
private readonly IReportingRepository _reportingRepository;
public AuctionReportingService(IReportingRepository reportingRepository)
{
_reportingRepository = reportingRepository;
}
public async Task<ReportTableResponse> GetAssignedBalance(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateRepoed(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.Balance,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.DateRepoed,
(reportEntry) => reportEntry.RepoId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.DateRepoed(),
StdReportEntryColumns.DateEnteredAutoIMS(),
StdReportEntryColumns.ScheduledSaleDate())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Gross Active Balance Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAssignedCollateralCount(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateRepoed(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumber,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) => 1,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.DateRepoed,
(reportEntry) => reportEntry.RepoId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.DateRepoed(),
StdReportEntryColumns.Grade(),
StdReportEntryColumns.FloorPrice(),
StdReportEntryColumns.Auction())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Active Inventory Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetSalePriceReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateSoldInAutoIMS(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.GrossSalePrice,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.SoldDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Grade(),
StdReportEntryColumns.FloorPrice(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.DateSold(),
StdReportEntryColumns.GrossSalePrice(),
StdReportEntryColumns.GrossPercentFloor(),
StdReportEntryColumns.NetPercentFloor(),
StdReportEntryColumns.TotalCharges())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failture Retrieving Sold Price Report.");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAuctionFeeReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateSoldInAutoIMS(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.TotalCharges,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.SoldDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.DateSold(),
StdReportEntryColumns.TotalCharges())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failture Retrieving Auction Fee Report.");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgDaysAssignedToSold(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateSoldInAutoIMS(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumberAvg,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) =>
{
if (reportEntry.SoldDate != null && reportEntry.DateEnteredAutoIMS != null)
{
return (decimal) (reportEntry.SoldDate.Value - reportEntry.DateEnteredAutoIMS.Value).TotalDays;
}
return null;
},
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.SoldDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.DateEnteredAutoIMS("Date Assigned"),
StdReportEntryColumns.DateSold(),
StdReportEntryColumns.AutoIMSAssignmentToSold("Days Assigned to Sold"))
};
}
catch (Exception ex)
{
Log.Error(ex, "Failture Retrieving Avg. Days Assigned to Sold Report.");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgNetOfFloor(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateSoldInAutoIMS(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.PercentageAvg,
reportData,
"Auctions",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.NetPercentFloor,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.SoldDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.NetPercentFloor(),
StdReportEntryColumns.DateSold())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failture Retrieving Avg. Days Assigned to Sold Report.");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public interface IAuctionReportingService
{
Task<ReportTableResponse> GetAssignedBalance(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAssignedCollateralCount(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetSalePriceReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAuctionFeeReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgDaysAssignedToSold(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgNetOfFloor(DateTime lowDateTime, DateTime highDateTime);
}
}
@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public interface ILandmarkReportingService
{
Task<ReportTableResponse> GetIsActiveAccountReport(DateTime reportingRequestLowDate, DateTime reportingRequestHighDate);
Task<ReportTableResponse> GetIsClosedAccountReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgDaysRepoToSoldReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetRedemptionCountReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgDaysDelqWhenAddedReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetCountOfSalesReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgSurplusReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetRepoBalanceReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetRedemptionBalanceReport(DateTime lowDateTime, DateTime highDateTime);
}
}
@@ -0,0 +1,14 @@
using System;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public interface IRepoCompanyReportingService
{
Task<ReportTableResponse> GetAssignmentCountReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetRepoedCountReport(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAvgDaysAssignedToRepoed(DateTime lowDateTime, DateTime highDateTime);
Task<ReportTableResponse> GetAssignedBalance(DateTime lowDateTime, DateTime highDateTime);
}
}
@@ -0,0 +1,474 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Factories;
using LCU.Shared.Collections.RepoApiLib.Formatting;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public class LandmarkReportingService : ILandmarkReportingService
{
private readonly IReportingRepository _reportingRepository;
private const string RedeemedStatus = "REDEEMED";
public LandmarkReportingService(IReportingRepository reportingRepository)
{
_reportingRepository = reportingRepository;
}
public async Task<ReportTableResponse> GetIsActiveAccountReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetActiveAccounts();
return new ReportTableResponse()
{
DataTable = new DataTable()
{
Headers = new string[]
{
"Member Name", "Account No", "Status", "Days In Status", "Days Delinquent", "Last Payment Amount", "Last Payment Date",
"Delinquent Amount", "Repo Type", "Balance", "Account Code", "Bankruptcy", "Collector", "Year", "Make", "Model",
"Grade", "Floor Price", "Auction House", "Date Entered AutoIMS","Sold Date", "Gross % Floor", "Net % Floor", "Gross Sale Price",
"Total Charges", "Repo Company", "Date Assigned", "Date Repoed", "Scheduled Sale Date"
},
Rows = reportData.Select(reportRow => new RepoAccountReferenceRow()
{
RepoId = reportRow.RepoId,
Cells = new ReportCell[]
{
ReportCellFormatting.CreateStringCell(reportRow.IndividualName),
ReportCellFormatting.CreateStringCell(reportRow.AccountNo),
ReportCellFormatting.CreateStringCell(reportRow.Status),
ReportCellFormatting.CreateIntCell(reportRow.DaysInStatus),
ReportCellFormatting.CreateIntCell(reportRow.DaysDelinquent),
ReportCellFormatting.CreateCurrencyCell(reportRow.LastPaymentAmount),
ReportCellFormatting.CreateDateCell(reportRow.LastPaymentDate),
ReportCellFormatting.CreateCurrencyCell(reportRow.DelinquentAmount),
ReportCellFormatting.CreateStringCell(reportRow.RepoType),
ReportCellFormatting.CreateCurrencyCell(reportRow.Balance),
ReportCellFormatting.CreateStringCell(reportRow.AccountTypeCode),
ReportCellFormatting.CreateStringCell(reportRow.Bankruptcy),
ReportCellFormatting.CreateStringCell(reportRow.Collector),
ReportCellFormatting.CreateIntCell(reportRow.Year),
ReportCellFormatting.CreateStringCell(reportRow.Make),
ReportCellFormatting.CreateStringCell(reportRow.Model),
ReportCellFormatting.CreateDecimalCell(reportRow.Grade),
ReportCellFormatting.CreateCurrencyCell(reportRow.FloorPrice),
ReportCellFormatting.CreateStringCell(reportRow.AuctionHouse),
ReportCellFormatting.CreateDateCell(reportRow.DateEnteredAutoIMS),
ReportCellFormatting.CreateDateCell(reportRow.SoldDate),
ReportCellFormatting.CreatePercentageCell(reportRow.GrossPercentFloor),
ReportCellFormatting.CreatePercentageCell(reportRow.NetPercentFloor),
ReportCellFormatting.CreateCurrencyCell(reportRow.GrossSalePrice),
ReportCellFormatting.CreateCurrencyCell(reportRow.TotalCharges),
ReportCellFormatting.CreateStringCell(reportRow.RepoCompany),
ReportCellFormatting.CreateDateCell(reportRow.DateAssigned),
ReportCellFormatting.CreateDateCell(reportRow.DateRepoed),
ReportCellFormatting.CreateDateCell(reportRow.ScheduledSaleDate)
}
})
}
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Is Active Account Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetIsClosedAccountReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetClosedAccounts(lowDateTime, highDateTime);
return new ReportTableResponse()
{
DataTable = new DataTable()
{
Headers = new string[]
{
"Member Name", "Account No", "Date Closed", "Status", "Days In Status", "Days Delinquent", "Last Payment Amount", "Last Payment Date",
"Delinquent Amount", "Repo Type", "Balance", "Account Code", "Bankruptcy", "Collector", "Year", "Make", "Model",
"Grade", "Floor Price", "Auction House", "Date Entered AutoIMS","Sold Date", "Gross % Floor", "Net % Floor", "Gross Sale Price",
"Total Charges", "Repo Company", "Date Assigned", "Date Repoed", "Scheduled Sale Date"
},
Rows = reportData.Select(reportRow => new RepoAccountReferenceRow()
{
RepoId = reportRow.RepoId,
Cells = new ReportCell[]
{
ReportCellFormatting.CreateStringCell(reportRow.IndividualName),
ReportCellFormatting.CreateStringCell(reportRow.AccountNo),
ReportCellFormatting.CreateDateCell(reportRow.DateClosed),
ReportCellFormatting.CreateStringCell(reportRow.Status),
ReportCellFormatting.CreateIntCell(reportRow.DaysInStatus),
ReportCellFormatting.CreateIntCell(reportRow.DaysDelinquent),
ReportCellFormatting.CreateCurrencyCell(reportRow.LastPaymentAmount),
ReportCellFormatting.CreateDateCell(reportRow.LastPaymentDate),
ReportCellFormatting.CreateCurrencyCell(reportRow.DelinquentAmount),
ReportCellFormatting.CreateStringCell(reportRow.RepoType),
ReportCellFormatting.CreateCurrencyCell(reportRow.Balance),
ReportCellFormatting.CreateStringCell(reportRow.AccountTypeCode),
ReportCellFormatting.CreateStringCell(reportRow.Bankruptcy),
ReportCellFormatting.CreateStringCell(reportRow.Collector),
ReportCellFormatting.CreateIntCell(reportRow.Year),
ReportCellFormatting.CreateStringCell(reportRow.Make),
ReportCellFormatting.CreateStringCell(reportRow.Model),
ReportCellFormatting.CreateDecimalCell(reportRow.Grade),
ReportCellFormatting.CreateCurrencyCell(reportRow.FloorPrice),
ReportCellFormatting.CreateStringCell(reportRow.AuctionHouse),
ReportCellFormatting.CreateDateCell(reportRow.DateEnteredAutoIMS),
ReportCellFormatting.CreateDateCell(reportRow.SoldDate),
ReportCellFormatting.CreatePercentageCell(reportRow.GrossPercentFloor),
ReportCellFormatting.CreatePercentageCell(reportRow.NetPercentFloor),
ReportCellFormatting.CreateCurrencyCell(reportRow.GrossSalePrice),
ReportCellFormatting.CreateCurrencyCell(reportRow.TotalCharges),
ReportCellFormatting.CreateStringCell(reportRow.RepoCompany),
ReportCellFormatting.CreateDateCell(reportRow.DateAssigned),
ReportCellFormatting.CreateDateCell(reportRow.DateRepoed),
ReportCellFormatting.CreateDateCell(reportRow.ScheduledSaleDate)
}
})
}
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Is Closed Account Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgDaysRepoToSoldReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByStatusChangeInPeriod("SOLD", lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumberAvg,
reportData,
"Sold Accounts",
lowDateTime,
highDateTime,
(reportEntry) =>
{
if (reportEntry.FilteredStatusChangeDate != null && reportEntry.DateRepoed != null)
{
return (decimal)(reportEntry.FilteredStatusChangeDate.Value - reportEntry.DateRepoed.Value).TotalDays;
}
return null;
},
(reportEntry) => null,
(reportEntry) => reportEntry.FilteredStatusChangeDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysCurrentDelq(),
StdReportEntryColumns.DaysDelqWhenAdded(),
StdReportEntryColumns.LastPaymentAmount(),
StdReportEntryColumns.LastPaymentDate(),
StdReportEntryColumns.DelinquentAmount(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.RepoDuration(),
StdReportEntryColumns.DateAssigned(),
StdReportEntryColumns.DateRepoed(),
StdReportEntryColumns.FilteredStatusChangeDate("Date Sold"))
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Avg Days Repot to Sold Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetRedemptionCountReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByStatusChangeInPeriod(RedeemedStatus, lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumber,
reportData,
"Redemptions",
lowDateTime,
highDateTime,
(reportEntry) => 1,
(reportEntry) => null,
(reportEntry) => reportEntry.FilteredStatusChangeDate,
(reportEntry) => $"{reportEntry.RepoId}_{reportEntry.FilteredStatusChangeDate}",
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entity) => entity.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.DaysCurrentDelq(),
StdReportEntryColumns.LastPaymentDate(),
StdReportEntryColumns.LastPaymentDate(),
StdReportEntryColumns.DelinquentAmount(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Grade(),
StdReportEntryColumns.FloorPrice(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.DateEnteredAutoIMS(),
StdReportEntryColumns.TotalCharges(),
StdReportEntryColumns.DateAssigned(),
StdReportEntryColumns.RepoDuration(),
StdReportEntryColumns.DateRepoed())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Redemption Count Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgDaysDelqWhenAddedReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByO4RDate(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumberAvg,
reportData,
"Accounts",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.DaysDelqWhenAdded,
(reportEntry) => null,
(reportEntry) => reportEntry.O4RDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(reportRow) => reportRow.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysCurrentDelq(),
StdReportEntryColumns.DaysDelqWhenAdded(),
StdReportEntryColumns.LastPaymentDate(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.O4RDate())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Avg Days Delq When Added Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetCountOfSalesReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByStatusChangeInPeriod("SOLD", lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumber,
reportData,
"Sold Accounts",
lowDateTime,
highDateTime,
(reportEntry) => 1,
(reportEntry) => null,
(reportEntry) => reportEntry.FilteredStatusChangeDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.GrossSalePrice(),
StdReportEntryColumns.GrossPercentFloor(),
StdReportEntryColumns.NetPercentFloor(),
StdReportEntryColumns.TotalCharges(),
StdReportEntryColumns.FilteredStatusChangeDate("Date Sold"))
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Avg Days Repot to Sold Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgSurplusReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateSoldInAutoIMS(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.CurrencyAvg,
reportData,
"Sold Accounts",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.GrossSalePrice - reportEntry.Balance,
(reportEntry) => reportEntry.AuctionName,
(reportEntry) => reportEntry.SoldDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(reportRow) => reportRow.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.GrossSalePrice(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.SurplusDeficiency(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.Auction(),
StdReportEntryColumns.DateSold())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Avg Surplus or Defiency Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetRepoBalanceReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateRepoed(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Companies",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.Balance,
(reportEntry) => reportEntry.RepoCompany,
(reportEntry) => reportEntry.DateRepoed,
(reportEntry) => reportEntry.RepoId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(reportRow) => reportRow.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Balance Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetRedemptionBalanceReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByStatusChangeInPeriod(RedeemedStatus, lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Redeemed Accounts",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.Balance,
(reportEntry) => null,
(reportEntry) => reportEntry.FilteredStatusChangeDate,
(reportEntry) => reportEntry.RepoId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(reportRow) => reportRow.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.FilteredStatusChangeDate("Redeemed On"))
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Redemption Balance Report");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,188 @@
using System;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.API.Collections.RepoApi.Factories;
using LCU.Shared.Collections.RepoApiLib.Formatting;
using LCU.Shared.Collections.RepoApiLib.Models.Reporting.Responses;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services.Reporting
{
public class RepoCompanyReportingService : IRepoCompanyReportingService
{
private readonly IReportingRepository _reportingRepository;
public RepoCompanyReportingService(IReportingRepository reportingRepository)
{
_reportingRepository = reportingRepository;
}
public async Task<ReportTableResponse> GetAssignedBalance(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateAssigned(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.Currency,
reportData,
"Repo Companies",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.Balance,
(reportEntry) => reportEntry.RepoCompany,
(reportEntry) => reportEntry.RepoCompanyAssignmentDate,
(reportEntry) => reportEntry.CollateralId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.DateAssigned())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Company Active Count Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAssignmentCountReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateAssigned(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumber,
reportData,
"Repo Companies",
lowDateTime,
highDateTime,
(reportEntry) => 1,
(reportEntry) => reportEntry.RepoCompany,
(reportEntry) => reportEntry.RepoCompanyAssignmentDate,
(reportEntry) => reportEntry.CollateralId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collector(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.DateAssigned())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Company Active Count Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetAvgDaysAssignedToRepoed(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateRepoed(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumberAvg,
reportData,
"Repo Companies",
lowDateTime,
highDateTime,
(reportEntry) => reportEntry.RepoDuration,
(reportEntry) => reportEntry.RepoCompany,
(reportEntry) => reportEntry.DateRepoed,
(reportEntry) => reportEntry.CollateralId,
false),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(reportRow) => reportRow.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.RepoDuration(),
StdReportEntryColumns.DateAssigned(),
StdReportEntryColumns.DateRepoed())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Avg Days Assigned to Repoed Report");
throw new ServerErrorException();
}
}
public async Task<ReportTableResponse> GetRepoedCountReport(DateTime lowDateTime, DateTime highDateTime)
{
try
{
var reportData = await _reportingRepository.GetAccountsByDateRepoed(lowDateTime, highDateTime);
return new ReportTableResponse()
{
ReportTable = ReportTableFactory.CreateHistoricalSummationTable(
ReportSumType.WholeNumber,
reportData,
"Repo Companies",
lowDateTime,
highDateTime,
(reportEntry) => 1,
(reportEntry) => reportEntry.RepoCompany,
(reportEntry) => reportEntry.DateRepoed,
(reportEntry) => reportEntry.CollateralId),
DataTable = DataTableFormatting.CreateDataTable(
reportData,
(entry) => entry.RepoId,
StdReportEntryColumns.Member(),
StdReportEntryColumns.AccountNo(),
StdReportEntryColumns.Status(),
StdReportEntryColumns.DaysInStatus(),
StdReportEntryColumns.RepoType(),
StdReportEntryColumns.Balance(),
StdReportEntryColumns.AccountCode(),
StdReportEntryColumns.Bankruptcy(),
StdReportEntryColumns.Collateral(),
StdReportEntryColumns.RepoCompany(),
StdReportEntryColumns.RepoDuration())
};
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Repo Company Active Count Report");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class SavedLocationService : ISavedLocationService
{
private ISavedLocationRepository _savedLocationRepository;
private IUnitOfWork _unitOfWork;
public SavedLocationService(
ISavedLocationRepository savedLocationRepository,
IUnitOfWork unitOfWork)
{
_savedLocationRepository = savedLocationRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<SavedLocation>> ListAllAsync()
{
try
{
return await _savedLocationRepository.ListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Saved Locations");
throw new ServerErrorException();
}
}
public async Task<SavedLocation> GetAsync(int id)
{
SavedLocation savedLocation;
try
{
savedLocation = await _savedLocationRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving Saved Location with id: {id}");
throw new NotFoundException($"Saved Location with id {id} not found");
}
if (savedLocation == null)
{
throw new NotFoundException($"Saved Location with id {id} not found");
}
return savedLocation;
}
public async Task<SavedLocation> SaveAsync(SavedLocation savedLocation)
{
try
{
var newSavedLocation = await _savedLocationRepository.AddAsync(savedLocation);
await _unitOfWork.CompleteAsync();
return newSavedLocation;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Saved Location {savedLocation}", savedLocation);
throw new ServerErrorException();
}
}
public async Task<SavedLocation> UpdateAsync(SavedLocation savedLocation)
{
var existing = await _savedLocationRepository.FindByIdAsync(savedLocation.ID);
if (existing == null)
{
throw new NotFoundException($"Saved Location with id {savedLocation.ID} not found");
}
existing.Name = savedLocation.Name;
existing.LocationType = savedLocation.LocationType;
existing.Address_Line_1 = savedLocation.Address_Line_1;
existing.Address_Line_2 = savedLocation.Address_Line_2;
existing.City = savedLocation.City;
existing.State = savedLocation.State;
existing.ZIP = savedLocation.ZIP;
existing.Country = savedLocation.Country;
existing.Phone = savedLocation.Phone;
existing.Fax = savedLocation.Fax;
try
{
_savedLocationRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return existing;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Filure updating Saved Location: {savedLocation}", savedLocation);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Saved Location {savedLocation}", savedLocation);
throw new ServerErrorException();
}
}
public async Task<SavedLocation> DeleteAsync(int id)
{
var existingSavedLocation = await _savedLocationRepository.FindByIdAsync(id);
if (existingSavedLocation == null)
{
throw new NotFoundException($" Saved Location with id {id} not found.");
}
try
{
_savedLocationRepository.Remove(existingSavedLocation);
await _unitOfWork.CompleteAsync();
return existingSavedLocation;
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure deleting Saved Location with id: {id}");
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure deleting Saved Location with id: {id}");
throw new ServerErrorException();
}
}
}
}
@@ -0,0 +1,592 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Shared.Collections.RepoApiLib.Models.Settings;
using LCU.Shared.Collections.RepoApiLib.Repositories.Settings;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
using LCU.API.Collections.RepoApi.Exceptions;
using System.Data.SqlClient;
using System.Linq;
using LCU.API.Collections.CollectionsApiLib.Models;
using Microsoft.Extensions.Caching.Memory;
using LCU.API.Collections.RepoApi.Configuration;
using LCU.API.Collections.RepoApi.DataAccess;
using LCU.Shared.Collections.RepoApiLib.Models;
namespace LCU.API.Collections.RepoApi.Services
{
public class SettingsService : ISettingsService
{
private readonly IAccountTypeRepository _accountTypeRepository;
private readonly IAllowedStatusChangeRepository _allowedStatusChangeRepository;
private readonly IBodyStyleRepository _bodyStyleRepository;
private readonly ICleaningLevelRepository _cleaningLevelRepository;
private readonly ICollateralTypeRepository _collateralTypeRepository;
private readonly ICollateralTypeToVehicleTypeRepository _collateralTypeToVehicleTypeRepository;
private readonly IHoldStatusRepository _holdStatusRepository;
private readonly ILoanCategoryRepository _loanCategoryRepository;
private readonly ILocationTypeRepository _locationTypeRepository;
private readonly IVehicleTypeRepository _vehicleTypeRepository;
private readonly IExternalUserRepository _externalUserRepository;
private readonly IRepoCompanyRepository _repoCompanyRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IMemoryCache _memoryCache;
private readonly ICollectionsDao _collectionsDao;
private readonly RepoApiConfiguration _config;
public SettingsService(
IAccountTypeRepository accountTypeCodesRepository,
IAllowedStatusChangeRepository allowedStatusChangesRepository,
IBodyStyleRepository bodyStyleCodesRepository,
ICleaningLevelRepository cleaningLevelCodesRepository,
ICollateralTypeRepository collateralTypesRepository,
ICollateralTypeToVehicleTypeRepository collateralTypesToVehicleTypesRepository,
IHoldStatusRepository holdStatusCodesRepository,
ILoanCategoryRepository loanCategoriesRepository,
ILocationTypeRepository locationTypeCodesRepository,
IVehicleTypeRepository vehicleTypeCodesRepository,
IExternalUserRepository externalUserRepository,
IRepoCompanyRepository repoCompanyRepository,
IUnitOfWork unitOfWork, IMemoryCache memoryCache,
ICollectionsDao collectionsDao,
RepoApiConfiguration configuration)
{
_accountTypeRepository = accountTypeCodesRepository;
_allowedStatusChangeRepository = allowedStatusChangesRepository;
_bodyStyleRepository = bodyStyleCodesRepository;
_cleaningLevelRepository = cleaningLevelCodesRepository;
_collateralTypeRepository = collateralTypesRepository;
_collateralTypeToVehicleTypeRepository = collateralTypesToVehicleTypesRepository;
_holdStatusRepository = holdStatusCodesRepository;
_loanCategoryRepository = loanCategoriesRepository;
_locationTypeRepository = locationTypeCodesRepository;
_vehicleTypeRepository = vehicleTypeCodesRepository;
_externalUserRepository = externalUserRepository;
_repoCompanyRepository = repoCompanyRepository;
_unitOfWork = unitOfWork;
_memoryCache = memoryCache;
_collectionsDao = collectionsDao;
_config = configuration;
}
public async Task<Settings> GetCachedRepoSettingsAsync()
{
return await _memoryCache.GetOrCreateAsync<Settings>("repoSettings", factory: (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTime.Now.AddSeconds(_config.SettingsCacheExpirationSeconds);
return ListAllSettingsAsync();
});
}
public async Task<Settings> ListAllSettingsAsync()
{
try
{
var result = new Settings
{
AccountTypes = await _accountTypeRepository.ListAllAsync(),
AllowedStatusChanges = await _allowedStatusChangeRepository.ListAllAsync(),
BodyStyles = await _bodyStyleRepository.ListAllAsync(),
CleaningLevels = await _cleaningLevelRepository.ListAllAsync(),
CollateralTypes = await _collateralTypeRepository.ListAllAsync(),
CollateralTypesToVehicleTypes = await _collateralTypeToVehicleTypeRepository.ListAllAsync(),
HoldStatuses = await _holdStatusRepository.ListAllAsync(),
LoanCategories = await _loanCategoryRepository.ListAllAsync(),
LocationTypes = await _locationTypeRepository.ListAllAsync(),
VehicleTypes = await _vehicleTypeRepository.ListAllAsync(),
BranchSet = await _collectionsDao.RetrieveBranchSetAsync(),
ExternalUsers = await _externalUserRepository.ListAsync(),
RepoCompanies = await _repoCompanyRepository.ListAsync() as IReadOnlyCollection<RepoCompany>,
TransactionTypeCodes = _config.TransactionTypeCodes,
TransactionTypeCodeQueryPrefix = _config.TransactionTypeCodeQueryPrefix,
// Convert DateTime format to uppercase for React/Moment JS ToString use
TransactionHistoryParameterDateFormat = _config.TransactionHistoryParameterDateFormat.ToUpper(),
TransactionHistoryStartDateOffset = _config.TransactionHistoryStartDateOffset,
RepoTypeMap = GetRepoTypes()
};
return result;
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving Settings.");
throw new ServerErrorException();
}
}
public async Task<AccountType> SaveAsync(AccountType accountType)
{
try
{
await _accountTypeRepository.AddAsync(accountType);
await _unitOfWork.CompleteAsync();
return accountType;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Account Type: {accountType}", accountType);
throw new ServerErrorException();
}
}
public async Task<AllowedStatusChange> SaveAsync(AllowedStatusChange statusChange)
{
try
{
await _allowedStatusChangeRepository.AddAsync(statusChange);
await _unitOfWork.CompleteAsync();
return statusChange;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Allowed Status Change: {statusChange}", statusChange);
throw new ServerErrorException();
}
}
public async Task<BodyStyle> SaveAsync(BodyStyle bodyStyle)
{
try
{
await _bodyStyleRepository.AddAsync(bodyStyle);
await _unitOfWork.CompleteAsync();
return bodyStyle;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Body Style: {bodyStyle}", bodyStyle);
throw new ServerErrorException();
}
}
public async Task<CleaningLevel> SaveAsync(CleaningLevel cleaningLevel)
{
try
{
await _cleaningLevelRepository.AddAsync(cleaningLevel);
await _unitOfWork.CompleteAsync();
return cleaningLevel;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Cleaning Level: {cleaningLevel}", cleaningLevel);
throw new ServerErrorException();
}
}
public async Task<CollateralType> SaveAsync(CollateralType collateralType)
{
try
{
await _collateralTypeRepository.AddAsync(collateralType);
await _unitOfWork.CompleteAsync();
return collateralType;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Collateral: {collateralType}", collateralType);
throw new ServerErrorException();
}
}
public async Task<CollateralTypeToVehicleType> SaveAsync(CollateralTypeToVehicleType collateralToVehicle)
{
try
{
await _collateralTypeToVehicleTypeRepository.AddAsync(collateralToVehicle);
await _unitOfWork.CompleteAsync();
return collateralToVehicle;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Collateral Type to Vehicle Type: {collateralToVehicle}",
collateralToVehicle);
throw new ServerErrorException();
}
}
public async Task<HoldStatus> SaveAsync(HoldStatus holdStatus)
{
try
{
await _holdStatusRepository.AddAsync(holdStatus);
await _unitOfWork.CompleteAsync();
return holdStatus;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Hold Status: {holdStatus}", holdStatus);
throw new ServerErrorException();
}
}
public async Task<LoanCategory> SaveAsync(LoanCategory loanCategory)
{
try
{
await _loanCategoryRepository.AddAsync(loanCategory);
await _unitOfWork.CompleteAsync();
return loanCategory;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Loan Category: {loanCategory}", loanCategory);
throw new ServerErrorException();
}
}
public async Task<LocationType> SaveAsync(LocationType locationType)
{
try
{
await _locationTypeRepository.AddAsync(locationType);
await _unitOfWork.CompleteAsync();
return locationType;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Location Type: {locationType}", locationType);
throw new ServerErrorException();
}
}
public async Task<VehicleType> SaveAsync(VehicleType vehicleType)
{
try
{
await _vehicleTypeRepository.AddAsync(vehicleType);
await _unitOfWork.CompleteAsync();
return vehicleType;
}
catch (Exception ex)
{
Log.Error(ex, $"Failure saving Vehicle Type: {vehicleType}", vehicleType);
throw new ServerErrorException();
}
}
public async Task<AccountType> UpdateAsync(AccountType accountType)
{
var existingAccountType = await _accountTypeRepository.FindByIdAsync(accountType.ID);
if (existingAccountType == null)
{
throw new NotFoundException($" Account Type with id {accountType.ID} not found.");
}
existingAccountType.AccountTypeCode = accountType.AccountTypeCode;
existingAccountType.Description = accountType.Description;
existingAccountType.IsVisible = accountType.IsVisible;
try
{
_accountTypeRepository.Update(existingAccountType);
await _unitOfWork.CompleteAsync();
return (existingAccountType);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Account Type: {accountType}", accountType);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Account Type: {accountType}", accountType);
throw new ServerErrorException();
}
}
public async Task<AllowedStatusChange> UpdateAsync(AllowedStatusChange allowedStatusChange)
{
var existingStatusChange = await _allowedStatusChangeRepository.FindByIdAsync(allowedStatusChange.ID);
if (existingStatusChange == null)
{
throw new NotFoundException($" Allowed Status Change with id {allowedStatusChange.ID} not found.");
}
existingStatusChange.CurrentStatus = allowedStatusChange.CurrentStatus;
existingStatusChange.AllowedChangeStatus = allowedStatusChange.AllowedChangeStatus;
try
{
_allowedStatusChangeRepository.Update(existingStatusChange);
await _unitOfWork.CompleteAsync();
return (existingStatusChange);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Allowed Status Change: {allowedStatusChange}",
allowedStatusChange);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Allowed Status Change {allowedStatusChange}", allowedStatusChange);
throw new ServerErrorException();
}
}
public async Task<BodyStyle> UpdateAsync(BodyStyle bodyStyle)
{
var existingBodyStyle = await _bodyStyleRepository.FindByIdAsync(bodyStyle.Code);
if (existingBodyStyle == null)
{
throw new NotFoundException($" Body Style with code {bodyStyle.Code} not found.");
}
existingBodyStyle.Description = bodyStyle.Description;
try
{
_bodyStyleRepository.Update(existingBodyStyle);
await _unitOfWork.CompleteAsync();
return (existingBodyStyle);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Body Style: {bodyStyle}", bodyStyle);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Body Style: {bodyStyle}", bodyStyle);
throw new ServerErrorException();
}
}
public async Task<CleaningLevel> UpdateAsync(CleaningLevel cleaningLevel)
{
var existingCleaningLevel = await _cleaningLevelRepository.FindByIdAsync(cleaningLevel.Code);
if (existingCleaningLevel == null)
{
throw new NotFoundException($" Cleaning Level with code {cleaningLevel.Code} not found.");
}
existingCleaningLevel.Description = cleaningLevel.Description;
try
{
_cleaningLevelRepository.Update(existingCleaningLevel);
await _unitOfWork.CompleteAsync();
return (existingCleaningLevel);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Cleaning Level: {cleaningLevel}", cleaningLevel);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Cleaning Level: {cleaningLevel}", cleaningLevel);
throw new ServerErrorException();
}
}
public async Task<CollateralType> UpdateAsync(CollateralType collateralType)
{
var existingCollateralType = await _collateralTypeRepository.FindByIdAsync(collateralType.ID);
if (existingCollateralType == null)
{
throw new NotFoundException($" Collateral Type with code {collateralType.ID} not found.");
}
existingCollateralType.Description = collateralType.Description;
existingCollateralType.LookupHandler = collateralType.LookupHandler;
try
{
_collateralTypeRepository.Update(existingCollateralType);
await _unitOfWork.CompleteAsync();
return (existingCollateralType);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Collateral Type: {collateralType}", collateralType);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Collateral Type: {collateralType}", collateralType);
throw new ServerErrorException();
}
}
public async Task<CollateralTypeToVehicleType> UpdateAsync(CollateralTypeToVehicleType collateralToVehicle)
{
var existingCollateralToVehicle =
await _collateralTypeToVehicleTypeRepository.FindByIdAsync(collateralToVehicle.ID);
if (existingCollateralToVehicle == null)
{
throw new NotFoundException(
$" Collateral Type To Vehicle Type with id {collateralToVehicle.ID} not found.");
}
existingCollateralToVehicle.CollateralTypeId = collateralToVehicle.CollateralTypeId;
existingCollateralToVehicle.VehicleTypeId = collateralToVehicle.VehicleTypeId;
try
{
_collateralTypeToVehicleTypeRepository.Update(existingCollateralToVehicle);
await _unitOfWork.CompleteAsync();
return (existingCollateralToVehicle);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Collateral Type To Vehicle Type: {collateralToVehicle}",
collateralToVehicle);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Collateral Type To Vehicle Type: {collateralToVehicle}",
collateralToVehicle);
throw new ServerErrorException();
}
}
public async Task<HoldStatus> UpdateAsync(HoldStatus holdStatus)
{
var existingHoldStatus = await _holdStatusRepository.FindByIdAsync(holdStatus.Code);
if (existingHoldStatus == null)
{
throw new NotFoundException($" Hold Status with code {holdStatus.Code} not found.");
}
existingHoldStatus.Description = holdStatus.Description;
try
{
_holdStatusRepository.Update(existingHoldStatus);
await _unitOfWork.CompleteAsync();
return (existingHoldStatus);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Hold Status: {holdStatus}", holdStatus);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Hold Status: {holdStatus}", holdStatus);
throw new ServerErrorException();
}
}
public async Task<LoanCategory> UpdateAsync(LoanCategory loanCategory)
{
var existingLoanCategory = await _loanCategoryRepository.FindByIdAsync(loanCategory.ID);
if (existingLoanCategory == null)
{
throw new NotFoundException($" Loan Category with code {loanCategory.ID} not found.");
}
existingLoanCategory.CategoryName = loanCategory.CategoryName;
try
{
_loanCategoryRepository.Update(existingLoanCategory);
await _unitOfWork.CompleteAsync();
return (existingLoanCategory);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Loan Category: {loanCategory}", loanCategory);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Loan Category: {loanCategory}", loanCategory);
throw new ServerErrorException();
}
}
public async Task<LocationType> UpdateAsync(LocationType locationType)
{
var existingLocationType = await _locationTypeRepository.FindByIdAsync(locationType.Code);
if (existingLocationType == null)
{
throw new NotFoundException($" Location Type with code {locationType.Code} not found.");
}
existingLocationType.Description = locationType.Description;
try
{
_locationTypeRepository.Update(existingLocationType);
await _unitOfWork.CompleteAsync();
return (existingLocationType);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Location Type: {locationType}", locationType);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Location Type: {locationType}", locationType);
throw new ServerErrorException();
}
}
public async Task<VehicleType> UpdateAsync(VehicleType vehicleType)
{
var existingVehicleType = await _vehicleTypeRepository.FindByIdAsync(vehicleType.Code);
if (existingVehicleType == null)
{
throw new NotFoundException($" Vehicle Type with code {vehicleType.Code} not found.");
}
existingVehicleType.Description = vehicleType.Description;
try
{
_vehicleTypeRepository.Update(existingVehicleType);
await _unitOfWork.CompleteAsync();
return (existingVehicleType);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Vehicle Type: {vehicleType}", vehicleType);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Vehicle Type: {vehicleType}", vehicleType);
throw new ServerErrorException();
}
}
public async Task SyncExternalUsersAsync(IReadOnlyCollection<ExternalUser> users)
{
await _externalUserRepository.SynchronizeSet(users);
await _unitOfWork.CompleteAsync();
}
private static Dictionary<int, string> GetRepoTypes()
{
return Enum.GetValues(typeof(RepoType))
.Cast<RepoType>()
.ToDictionary(type => (int)type, type => type.ToString() );
}
}
}
@@ -0,0 +1,36 @@
using LCU.API.Collections.CollectionsApiLib.Models;
using LCU.API.Collections.RepoApi.Exceptions;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class StatusService
{
/**
* Convert a Stage from a case to a RepoAccount status.
*/
public static string ConvertStatus(RepoStatus oldStage)
{
switch (oldStage)
{
case RepoStatus.PSKP: return "PSKP";
case RepoStatus.REHO: return "REHO";
case RepoStatus.RECA: return "RECA";
case RepoStatus.O4R: return "O4R";
case RepoStatus.REPO: return "REPO";
case RepoStatus.REDEEMED: return "REDEEMED";
case RepoStatus.SOLD: return "SOLD";
case RepoStatus.NOTACTIVE: return "SOLD";
case RepoStatus.INELIGIBLE: throw new IneligibleAccountException("CaseStage", "Not a Repo Case Stage.");
// case RepoStatus.UNKNOWN is meant to fall through to below.
}
Log.Fatal($"Attempt to convert status {oldStage}, but was not known.");
// The account is not necessarily ineligible, but is missing a requirement, the stage.
// This covers the problem of Temenos not providing a Case Stage Change event, and only
// a Case Creation event. The stage won't be available yet, so throw a MissingRequirement
// causing it to be retried on the hour - it should then run fine.
throw new MissingRequirementException("Case Stage", $"A valid case stage is required, {oldStage} is not valid.");
}
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using LCU.API.Collections.RepoApi.Exceptions;
using LCU.Shared.Collections.RepoApiLib.Models;
using LCU.Shared.Collections.RepoApiLib.Repositories;
using Serilog;
namespace LCU.API.Collections.RepoApi.Services
{
public class VehicleFloorPricingParametersService : IVehicleFloorPricingParametersService
{
private readonly IVehicleFloorPricingParametersRepository _parametersRepository;
private readonly IUnitOfWork _unitOfWork;
public VehicleFloorPricingParametersService(
IVehicleFloorPricingParametersRepository parametersRepository,
IUnitOfWork unitOfWork)
{
_parametersRepository = parametersRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<VehicleFloorPricingParameters>> ListAll()
{
try
{
return await _parametersRepository.ListAllAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving VehicleFloorPricingParameters");
throw new ServerErrorException();
}
}
public async Task<VehicleFloorPricingParameters> Get(int id)
{
VehicleFloorPricingParameters parameters;
try
{
parameters = await _parametersRepository.FindByIdAsync(id);
await _unitOfWork.CompleteAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving VehicleFloorPricingParameters: {id}", id);
throw new ServerErrorException();
}
if (parameters == null)
{
throw new NotFoundException($"VehicleFloorPricingParameters with id {id} not found.");
}
return parameters;
}
public async Task<VehicleFloorPricingParameters> UpdateAsync(VehicleFloorPricingParameters parameters)
{
VehicleFloorPricingParameters existing = null;
try
{
existing = await _parametersRepository.FindByIdAsync(parameters.Id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving existing VehicleFloorPricingParameters with Id: {parameters.Id}", parameters.Id);
throw new ServerErrorException();
}
if (existing == null)
{
throw new NotFoundException($"Vehicle Floor Pricing Parameters with id {parameters.Id} not found.");
}
existing.LowCondition = parameters.LowCondition;
existing.HighCondition = parameters.HighCondition;
existing.NADAFirstValue = parameters.NADAFirstValue;
existing.NADASecondValue = parameters.NADASecondValue;
existing.MMRAdjustedValue = parameters.MMRAdjustedValue;
existing.PercentageOf = parameters.PercentageOf;
existing.ValuationServiceType = parameters.ValuationServiceType;
try
{
_parametersRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return (existing);
}
catch (SqlException ex)
{
Log.Error(ex, $"SQL Failure updating Vehicle Floor Pricing Parameters: {parameters}", parameters);
throw new ServerErrorException();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure updating Vehicle Floor Pricing Parameters: {parameters}", parameters);
throw new ServerErrorException();
}
}
}
}