441 lines
21 KiB
C#
441 lines
21 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|