first commit
This commit is contained in:
+440
@@ -0,0 +1,440 @@
|
||||
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> _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> 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<IActionResult> 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<IActionResult> 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. <br><br>" +
|
||||
"A new deal has been submitted and is ready to be viewed and exported <br><br>" +
|
||||
$"Dealership Name: <strong>{authResponse.DealershipName}</strong><br>" +
|
||||
$"Application ID: <strong>{authResponse.ApplicationId}</strong><br>" +
|
||||
$"Primary Applicant's Last Name or Business Full Name: <strong>{authResponse.ApplicantLastName}</strong><br><br>" +
|
||||
$"Click on the following link to open the application record within ACTion: <a href=\"{_dealershipEmailSettings.Value.ActionLink}{authResponse.ApplicationId}\">{authResponse.ApplicationId}</a><br>";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use to transmit an existing loan application to CRIF Lending
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
// 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<IActionResult> 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> { 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use to transmit a new loan application directly to CRIF Lending
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Transmits a loan application directly to CRIF Lending given a valid loan application.
|
||||
/// Cookies: None required or set.
|
||||
/// Security: None required or provided.
|
||||
/// </remarks>
|
||||
// Post: api/LoanApplication/
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(LoanApplication), 201)]
|
||||
[ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)]
|
||||
[ProducesResponseType(520)]
|
||||
[ProducesResponseType(500)]
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user