using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; using Serilog; using CRIF.ACTion.Services.Crif; using Landmark.OLA.Application.Configuration; using Landmark.OLA.Application.DataAccess; using Landmark.OLA.Application.DataAccessLayer; using Landmark.OLA.Application.Services.MicroApiServices; using Landmark.OLA.Models.Loan; using Landmark.OLA.Models.Loan.Enums; using Landmark.OLA.Models.Loan.Validators; using OnlineBankingAuth.Tokens; using LCU.Library.Internal.NotificationsModels.Models; using System.Net; namespace Landmark.OLA.Application.Controllers { [Route("api/[controller]")] [ApiController] public class LoanApplicationController : ControllerBase { private readonly ICrifVendorClient VendorClient; private readonly ILoanApplicationDataAccess loanApplicationStorage; private readonly IBranchDataAccess branchStorage; private readonly IBlobStorageAccess _blobStorageAccess; private readonly ITokenService _tokenService; readonly IOptions _dealershipEmailSettings; private readonly INotificationsApiService _notificationsApiService; private readonly Services.Email.IEmailTemplateService _emailTemplateService; public LoanApplicationController( ICrifVendorClient vendorClient, ILoanApplicationDataAccess loanApplicationDataAccess, IBranchDataAccess branchDataAccess, IBlobStorageAccess blobStorageAccess, ITokenService tokenService, IOptions dealershipEmailSettings, INotificationsApiService notificationsApiService, IWebHostEnvironment webHostEnvironment, Services.Email.IEmailTemplateService emailTemplateService) { VendorClient = vendorClient; loanApplicationStorage = loanApplicationDataAccess; branchStorage = branchDataAccess; _blobStorageAccess = blobStorageAccess; _tokenService = tokenService; _dealershipEmailSettings = dealershipEmailSettings; _notificationsApiService = notificationsApiService; _emailTemplateService = emailTemplateService; } [HttpPost("AuthenticateDealership")] [ProducesResponseType(200)] [ProducesResponseType(201)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(500)] public async Task AuthenticateDealership([FromBody] DealershipAuthRequest authRequest) { try { if (string.IsNullOrEmpty(authRequest.ApplicationId)) { return BadRequest("ApplicationId is required."); } authRequest.DealershipName = ""; var authResponse = await VendorClient.GetDealershipAuthDetails(authRequest.ApplicationId); if (authRequest.Equals(authResponse)) { // This portion of the try can be uncommented so that the "session" of an upload won't always be re-created. // There may be a small bug that might only affect app# 10056 if this is re-enabled. Basically the blob file was deleted, but the cosmos record has a reference. // deleting the cosmos record should resolve the issue /*try { var loanApplicationId = loanApplicationStorage.GetContainerIdByLoanId(authRequest.ApplicationId); return Ok(new { cosmosId = loanApplicationId }); } catch (KeyNotFoundException exception) {*/ var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); var loanContainer = new LoanContainer(new LoanApplication()) { Id = (Guid.NewGuid()).ToString(), LoanId = authRequest.ApplicationId }; loanApplicationStorage.CreateContainer(loanContainer); HttpContext.Response.StatusCode = 201; return Created($"/api/LoanApplication/AuthenticateDealer/{loanContainer.Id}", new { cosmosId = loanContainer.Id }); /*}*/ } return Unauthorized(); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpPost("SubmitDealershipFiles/{cosmosId}")] [ProducesResponseType(200)] [ProducesResponseType(201)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(500)] public async Task SubmitDealershipFiles(string cosmosId, [FromBody] DealershipAuthRequest authRequest) { try { if (string.IsNullOrEmpty(cosmosId)) { return BadRequest("Cosmos Id is required."); } if (string.IsNullOrEmpty(authRequest.ApplicationId)) { return BadRequest("ApplicationId is required."); } authRequest.DealershipName = ""; var authResponse = await VendorClient.GetDealershipAuthDetails(authRequest.ApplicationId); if (authRequest.Equals(authResponse)) { LoanContainer loanContainer = loanApplicationStorage.GetContainerById(cosmosId); LoanApplication loanApplication = loanContainer.LoanApplication; loanApplication.Id = authResponse.ApplicationId; // Submit files to ACTion, 1-by-1. Each time the POST request is called, it will upload 1 file. Once the last file has been sent, an email will be sent if (loanApplication.Files.Count > 0) { if (!(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.Equals("Production", StringComparison.OrdinalIgnoreCase) ?? false) && loanApplication.Files[0].Name.Equals("do-not-allow-transmission.jpg", StringComparison.OrdinalIgnoreCase)) { throw new Exception(); } var fileBase64Value = _blobStorageAccess.GetBlobAsBase64(loanApplication.Files[0]); var fileSubmitted = await VendorClient.AddFile(loanApplication.Id, loanApplication.Files[0], fileBase64Value); if (fileSubmitted) { _blobStorageAccess.DeleteBlob(loanApplication.Files[0]); loanApplication.Files.RemoveAt(0); loanApplicationStorage.UpdateContainer(loanContainer); // Return the files if there are still some left to upload. If not, send the email and return if (loanApplication.Files.Count > 0) { return StatusCode(201, new { files = loanApplication.Files }); } } else { Log.Error($"File not sent to ACTion, not deleting file: {loanApplication.Files[0]}"); } } if (_dealershipEmailSettings.Value.Recipients != null && _dealershipEmailSettings.Value.Recipients.Count > 0 && !_dealershipEmailSettings.Value.Recipients.Contains("PERENVIRONMENT") && !string.IsNullOrEmpty(_dealershipEmailSettings.Value.ActionLink) && !string.IsNullOrEmpty(_dealershipEmailSettings.Value.EmailAPIUrl)) { try { var redBannerMessage = _dealershipEmailSettings.Value.RedBannerMessage ?? string.Empty; var contentHtml = "This is an auto generated message.

" + "A new deal has been submitted and is ready to be viewed and exported

" + $"Dealership Name: {authResponse.DealershipName}
" + $"Application ID: {authResponse.ApplicationId}
" + $"Primary Applicant's Last Name or Business Full Name: {authResponse.ApplicantLastName}

" + $"Click on the following link to open the application record within ACTion: {authResponse.ApplicationId}
"; var requestBody = _emailTemplateService.BuildBrandedBody(contentHtml, redBannerMessage); var request = new EmailRequest() { Subject = $"Dealership has uploaded documents to application {loanApplication.Id}", Recipient = _dealershipEmailSettings.Value.Recipients, Body = requestBody }; Log.Information($"Sending email to {_dealershipEmailSettings.Value.Recipients} for application ID: {loanApplication.Id}"); await _notificationsApiService.SendEmailAsync(request); } catch (Exception ex) { Log.Error($"Error when sending email to loan team for application ID {loanApplication.Id}"); Log.Error(ex.Message); } } loanApplicationStorage.DeleteApplication(cosmosId); return StatusCode(200, new { files = loanApplication.Files }); } return Unauthorized(); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } /// /// Use to transmit an existing loan application to CRIF Lending /// /// /// Transmits a loan application to CRIF lending given a loan storage id (not a loan id). /// Cookies: None required or set. /// Security: None required or provided. /// // Post: api/LoanApplication/Submit/:id [HttpPost("Submit/{cosmosId}")] [ProducesResponseType(typeof(LoanApplication), 201)] [ProducesResponseType(typeof(ModelStateDictionary), 400)] [ProducesResponseType(404)] [ProducesResponseType(520)] [ProducesResponseType(500)] public async Task Post(string cosmosId) { try { if (string.IsNullOrEmpty(cosmosId)) { return BadRequest("Id is required."); } LoanContainer loanContainer = loanApplicationStorage.GetContainerById(cosmosId); if ( (loanContainer.Status != LoanStatus.In_Progress && loanContainer.Status != LoanStatus.Vendor_Error && loanContainer.Status != LoanStatus.Application_Error) || loanContainer.LoanApplication == null) { return BadRequest("Loan is not in progress. Unable to submit."); } LoanApplication loanApplication = loanContainer.LoanApplication; var memberInfo = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); if (memberInfo != null) { loanApplication.PrimaryBorrower.CustomerNumber = memberInfo.MemberID; } var validator = new LoanApplicationValidator(); var validatorResult = validator.Validate(loanApplication); var catchError = ""; if (!validatorResult.IsValid) { foreach (var error in validatorResult.Errors) { catchError += error; ModelState.AddModelError(error.PropertyName, error.ErrorMessage); } loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.Error_Submitting, catchError)); loanApplicationStorage.UpdateStatus(loanContainer.Id, LoanStatus.Application_Error); Log.Error($"Could not submit application, errors follow: {@ModelState}"); return BadRequest(ModelState); } // CRIF id will be assigned loanApplication.Id = await VendorClient.CreateApplicationAndGetId(loanApplication, branchStorage); // Submit files to ACTion foreach(var file in loanApplication.Files) { var fileBase64Value = _blobStorageAccess.GetBlobAsBase64(file); var fileSubmitted = true; bool shouldUploadFileToAction = false; var vehicleTypes = new List { LoanType.V_Vehicle, LoanType.V_VehiclePreapproval, LoanType.P_Powersport, LoanType.T_TrailerMotorhome, LoanType.M_Boat }; if (vehicleTypes.Contains((LoanType)loanApplication.LoanType)) { if (file.Description.Equals("DNR Registration Card", StringComparison.OrdinalIgnoreCase)) { shouldUploadFileToAction = true; } else { shouldUploadFileToAction = loanApplication.Purpose == LoanPurpose.Purpose.Purchase; } } else if (loanApplication.LoanType != LoanType.EQ_HELOC_Flex) { shouldUploadFileToAction = true; } else if (loanApplication.LoanType == LoanType.EQ_HELOC_Flex && loanApplication.RequestedAmount > 100000) { shouldUploadFileToAction = true; } else if (!file.Description.Equals("Proof of Income", StringComparison.OrdinalIgnoreCase)) { shouldUploadFileToAction = true; } if (shouldUploadFileToAction) { fileSubmitted = await VendorClient.AddFile(loanApplication.Id, file, fileBase64Value); } if(fileSubmitted) { _blobStorageAccess.DeleteBlob(file); } else { Log.Error($"File not sent to ACTion, not deleting file: {file}"); } } loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Submitted); loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.No_Error, "No_Error")); loanApplicationStorage.UpdateLoanId(loanContainer.Id, loanApplication.Id); loanApplicationStorage.DeleteApplication(cosmosId); return Ok(new { loanApplicationId = loanApplication.Id }); } catch (KeyNotFoundException ex) { Log.Error(ex.Message); return NotFound("An application with that Id could not be found"); } catch (InvalidDataException ie) { loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, ie.Message)); loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Application_Error); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } catch (TimeoutException te) { loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "The time taken to submit the loan application to the vendor has exceded the configured timeout length.")); Log.Error(te.Message); loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error); return BadRequest("The time taken to submit the loan application to the vendor has exceeded the configured timeout length."); } catch (ArgumentException ae) { loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Invalid argument.")); loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error); Log.Error(ae.Message); return BadRequest(ae.Message); } catch (FormatException fe) { loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Vendor returned unexpected response.")); loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error); Log.Error(fe.Message); return StatusCode(520, fe.Message); //Cloudflare uses an HTTP 520 when the external "server returns something unexpected"[1] //We return this when we don't understand CRIF's response //[1] https://support.cloudflare.com/hc/en-us/articles/206973867-Error-analytics-by-colo } catch (Exception e) { loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Unexpected exception when submitting application.")); loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error); Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } /// /// Use to transmit a new loan application directly to CRIF Lending /// /// /// Transmits a loan application directly to CRIF Lending given a valid loan application. /// Cookies: None required or set. /// Security: None required or provided. /// // Post: api/LoanApplication/ [HttpPost] [ProducesResponseType(typeof(LoanApplication), 201)] [ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)] [ProducesResponseType(520)] [ProducesResponseType(500)] public async Task Post([FromBody] LoanApplication loanApplication) { try { var validator = new LoanApplicationValidator(); var validatorResult = validator.Validate(loanApplication); if (!validatorResult.IsValid) { foreach (var error in validatorResult.Errors) { ModelState.AddModelError(error.PropertyName, error.ErrorMessage); } Log.Error($"Could not submit application, [Possibly third party], errors follow: {@ModelState}"); return BadRequest(ModelState); } var application = await VendorClient.CreateApplication(loanApplication, branchStorage); return CreatedAtRoute("GetApplication", new { loanApplicationId = application.Id }, application); } catch (TimeoutException te) { Log.Error(te.Message); return BadRequest("The time taken to submit the loan application to the vendor has exceeded the configured timeout length."); } catch (ArgumentException ae) { Log.Error(ae.Message); return BadRequest(ae.Message); } catch (FormatException fe) { Log.Error(fe.Message); return StatusCode(520, fe.Message); //Cloudflare uses an HTTP 520 when the external "server returns something unexpected"[1] //We return this when we don't understand CRIF's response //[1] https://support.cloudflare.com/hc/en-us/articles/206973867-Error-analytics-by-colo } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } } }