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> 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> GetListByIdsAsync(IReadOnlyCollection 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 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> 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 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 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 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 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); } /** * 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 RemoveAuctionFromAutoIMS(AuctionAssignment auctionAssignment) { return await _autoImsWebService.RemoveFromAuction(auctionAssignment.AuctionCustomerNo, auctionAssignment.CollateralVIN); } public IEnumerable CreateJoiningAuctionAssignmentToCollateralsList(AuctionAssignment auctionAssignment, IEnumerable 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(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 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> _findActiveAuctionsWithMatchingCollateral(int repoId, IEnumerable 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; } } }