commit f3cb730f3b1fed2341f2abbcea2f8db5a4992df6 Author: Zeeshaun Date: Wed Mar 11 11:18:25 2026 -0500 first commit diff --git a/LCU.API.Internal.OnlineLoanApp/.dockerignore b/LCU.API.Internal.OnlineLoanApp/.dockerignore new file mode 100644 index 0000000..8b63706 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/.dockerignore @@ -0,0 +1,17 @@ +**/*.bat +**/*.log +**/*.md +**/*.yml +**/.dockerignore +**/.editorconfig +**/.git +**/.gitattributes +**/.gitignore +**/.vs +**/.vscode +**/bin +**/dist +**/dockerfile +**/node_modules +**/obj +**/package-lock.json \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/.gitignore b/LCU.API.Internal.OnlineLoanApp/.gitignore new file mode 100644 index 0000000..a9e6de6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/.gitignore @@ -0,0 +1,330 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ +>>>>>>> agentic diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/BuilderTestBase.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/BuilderTestBase.cs new file mode 100644 index 0000000..76844c3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/BuilderTestBase.cs @@ -0,0 +1,19 @@ +using System.Xml.Serialization; +using CRIF.ACTion.Common; + +namespace CRIF.ACTion.Tests +{ + public abstract class BuilderTestBase + { + protected static string Serialize(T dataToSerialize) + { + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add(CrifNamespaces.Common.Prefix, CrifNamespaces.Common.Namespace.NamespaceName); + + var stringwriter = new System.IO.StringWriter(); + var serializer = new XmlSerializer(typeof(T)); + serializer.Serialize(stringwriter, dataToSerialize, namespaces); + return stringwriter.ToString(); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/CRIF.ACTion.Tests.csproj b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/CRIF.ACTion.Tests.csproj new file mode 100644 index 0000000..d89f53b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/CRIF.ACTion.Tests.csproj @@ -0,0 +1,299 @@ + + + false + net8.0 + false + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + + Always + + + PreserveNewest + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisionStatusBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisionStatusBuilderTest.cs new file mode 100644 index 0000000..33e20b7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisionStatusBuilderTest.cs @@ -0,0 +1,33 @@ +using Xunit; +using CRIF.ACTion.Decisioning; + +namespace CRIF.ACTion.Tests.Decisioning +{ + public class DecisionStatusBuilderTest + { + private const string LoanApplicationId = "1200398"; + private const string AppDetailId = "3380"; + + [Fact] + public void Validate() + { + var createAppElement = + new DecisionStatusBuilder(LoanApplicationId, AppDetailId, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void GetDecisionStatus() + { + var createAppElement = + new DecisionStatusBuilder(LoanApplicationId, AppDetailId, TestCommon.GetTestSettings()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "GetDecisionStatus" + System.IO.Path.DirectorySeparatorChar + "GetDecisionStatus.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisioningStateResponseParserTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisioningStateResponseParserTest.cs new file mode 100644 index 0000000..d6eddde --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisioningStateResponseParserTest.cs @@ -0,0 +1,28 @@ +using System.Xml.Linq; +using Xunit; +using CRIF.ACTion.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; + +namespace CRIF.ACTion.Tests.Decisioning +{ + public class DecisioningStateResponseParserTest + { + private readonly string LoanApplicationId = "1200400"; + + [Fact] + public void SendToDecisioningResponse() + { + string successResponsePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "SendToDecisioningParser" + System.IO.Path.DirectorySeparatorChar + "SuccessResponse.xml"; + + var successResponse = (XElement)XDocument.Load(successResponsePath).FirstNode; + + var parser = new DecisioningStateResponseParser(); + + parser.SetLoanId(LoanApplicationId); + + var result = parser.Parse(successResponse); + + Assert.Equal(DecisioningState.Requested, result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsRequestBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsRequestBuilderTest.cs new file mode 100644 index 0000000..de2a852 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsRequestBuilderTest.cs @@ -0,0 +1,32 @@ +using Xunit; +using CRIF.ACTion.Decisioning; + +namespace CRIF.ACTion.Tests.Decisioning +{ + public class GetApplicationProductsRequestBuilderTest + { + private const string LoanApplicationId = "1200217"; + + [Fact] + public void Validate() + { + var getApplicationProductsElement = + new GetApplicationProductsRequestBuilder(LoanApplicationId, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(getApplicationProductsElement)); + } + + [Fact] + public void GetDecisionStatus() + { + var getApplicationProductsElement = + new GetApplicationProductsRequestBuilder(LoanApplicationId, TestCommon.GetTestSettings()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "GetApplicationProducts" + System.IO.Path.DirectorySeparatorChar + "GetApplicationProducts.xml"; + + var results = TestCommon.Compare(getApplicationProductsElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsResponseParserTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsResponseParserTest.cs new file mode 100644 index 0000000..6603916 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsResponseParserTest.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Xunit; +using CRIF.ACTion.Decisioning; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; + +namespace CRIF.ACTion.Tests.Decisioning +{ + public class GetApplicationProductsResponseParserTest + { + private const string LoanApplicationId = "1200217"; + + [Fact] + public void DecisionStatus_Review() + { + GetDecisionStatusParserResponse("CashBackCC_Under-Review.xml", + new DecisioningDetails([85,]) + { + DecisioningState = DecisioningState.Ready, + DecisioningResult = DecisioningResult.DecisionStatus_Review, + }); + } + + [Fact] + public void InvalidApplication() + { + /* + This situation would likely not occur with our use cases, but if an application has more than one + AppDetail section, the client will need to ask for a specific result. + */ + Assert.Throws(() => + GetDecisionStatusParserResponse("Multiple-AppDetails.xml", null)); + } + + [Fact] + public void InvalidApplicationIdInResponse() + { + Assert.Throws(() => + GetDecisionStatusParserResponse("InvalidApplicationId.xml", null)); + } + + + private static void GetDecisionStatusParserResponse(string fileName, DecisioningDetails expectedResult) + { + string successResponsePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "GetApplicationsProductsParser" + System.IO.Path.DirectorySeparatorChar + fileName; + + var decisionResponse = (XElement)XDocument.Load(successResponsePath).FirstNode; + + var parser = new GetApplicationProductsResponseParser(); + + parser.SetLoanId(LoanApplicationId); + + var result = parser.Parse(decisionResponse); + + Assert.Equal(expectedResult, result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/SendToDecisioningRequestBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/SendToDecisioningRequestBuilderTest.cs new file mode 100644 index 0000000..b44d1d0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/SendToDecisioningRequestBuilderTest.cs @@ -0,0 +1,32 @@ +using Xunit; +using CRIF.ACTion.Decisioning; + +namespace CRIF.ACTion.Tests.Decisioning +{ + public class SendToDecisioningRequestBuilderTest + { + private readonly string LoanApplicationId = "1200400"; + + [Fact] + public void Validate() + { + var createAppElement = + new SendToDecisioningRequestBuilder(LoanApplicationId, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void SendToDecisioning() + { + var createAppElement = + new SendToDecisioningRequestBuilder(LoanApplicationId, TestCommon.GetTestSettings()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "SendToDecisioning" + System.IO.Path.DirectorySeparatorChar + "sendToDecisioning.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddAppFilesSection/AddAppFiles.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddAppFilesSection/AddAppFiles.xml new file mode 100644 index 0000000..1a5a276 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddAppFilesSection/AddAppFiles.xml @@ -0,0 +1,9 @@ + + + + + + dGVzdA== + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-BadNamespace.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-BadNamespace.xml new file mode 100644 index 0000000..f05dabc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-BadNamespace.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-Simple.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-Simple.xml new file mode 100644 index 0000000..05b997a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-Simple.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualError.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualError.xml new file mode 100644 index 0000000..010023e Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualError.xml differ diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualSuccess.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualSuccess.xml new file mode 100644 index 0000000..393665f Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualSuccess.xml differ diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddressesSection/AddAddress.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddressesSection/AddAddress.xml new file mode 100644 index 0000000..80bc47e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddressesSection/AddAddress.xml @@ -0,0 +1,5 @@ + +
+ +
+
\ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppDetailsSection/SetAsStudentCcProduct.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppDetailsSection/SetAsStudentCcProduct.xml new file mode 100644 index 0000000..fc2945e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppDetailsSection/SetAsStudentCcProduct.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualBorrowerParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualBorrowerParty.xml new file mode 100644 index 0000000..afce97b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualBorrowerParty.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningCoApplicantSpouseParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningCoApplicantSpouseParty.xml new file mode 100644 index 0000000..e321b44 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningCoApplicantSpouseParty.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningPrimarySpouseParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningPrimarySpouseParty.xml new file mode 100644 index 0000000..d9f9732 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningPrimarySpouseParty.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualPrimaryBorrowerParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualPrimaryBorrowerParty.xml new file mode 100644 index 0000000..a12156b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualPrimaryBorrowerParty.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetServicingChannel.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetServicingChannel.xml new file mode 100644 index 0000000..965bb54 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetServicingChannel.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetSourceChannel.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetSourceChannel.xml new file mode 100644 index 0000000..a58492b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetSourceChannel.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/AppWithPromo1.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/AppWithPromo1.xml new file mode 100644 index 0000000..7a53333 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/AppWithPromo1.xml @@ -0,0 +1,4971 @@ + + + + + Success + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User + + + + + + + + + + + + + + + + + + + User + LoanOfficer + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Employer + + + + + + + + + + + + + Team + + + + + + + + + + + + + Team + + + + + + + + + + + + + + + + Vendor + + + + + + + + + + + + + + + + + + + + + + Organization + + + + + + + Vendor + + + + + + + + + + + + + Channel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplication.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplication.xml new file mode 100644 index 0000000..0149b72 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplication.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationNonSigningHistorySpouse.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationNonSigningHistorySpouse.xml new file mode 100644 index 0000000..be9254f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationNonSigningHistorySpouse.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationWithNote.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationWithNote.xml new file mode 100644 index 0000000..2bd2e2d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationWithNote.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/cc_cash_back.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/cc_cash_back.xml new file mode 100644 index 0000000..83e121f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/cc_cash_back.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/equity_heloc.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/equity_heloc.xml new file mode 100644 index 0000000..629a69b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/equity_heloc.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/unsecured_line_of_credit.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/unsecured_line_of_credit.xml new file mode 100644 index 0000000..1234ff5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/unsecured_line_of_credit.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_car_loan.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_car_loan.xml new file mode 100644 index 0000000..6b82963 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_car_loan.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_marine_2borrowers.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_marine_2borrowers.xml new file mode 100644 index 0000000..24b39dd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_marine_2borrowers.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Individual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationProducts/GetApplicationProducts.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationProducts/GetApplicationProducts.xml new file mode 100644 index 0000000..c0de3be --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationProducts/GetApplicationProducts.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/CashBackCC_Under-Review.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/CashBackCC_Under-Review.xml new file mode 100644 index 0000000..295fffe --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/CashBackCC_Under-Review.xml @@ -0,0 +1,977 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/InvalidApplicationId.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/InvalidApplicationId.xml new file mode 100644 index 0000000..750a2ba --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/InvalidApplicationId.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/Multiple-AppDetails.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/Multiple-AppDetails.xml new file mode 100644 index 0000000..9d12259 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/Multiple-AppDetails.xml @@ -0,0 +1,2778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetDecisionStatus/GetDecisionStatus.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetDecisionStatus/GetDecisionStatus.xml new file mode 100644 index 0000000..653c8de --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetDecisionStatus/GetDecisionStatus.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/HelocFlex/CreateApplication.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/HelocFlex/CreateApplication.xml new file mode 100644 index 0000000..557b9d3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/HelocFlex/CreateApplication.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateApp.xml new file mode 100644 index 0000000..dc98e8a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateApp.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateAppWithAnnualTaxes.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateAppWithAnnualTaxes.xml new file mode 100644 index 0000000..a9a669f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateAppWithAnnualTaxes.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/OrgSection/SetOrgId.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/OrgSection/SetOrgId.xml new file mode 100644 index 0000000..07f4398 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/OrgSection/SetOrgId.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddChannelParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddChannelParty.xml new file mode 100644 index 0000000..8c4ba3e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddChannelParty.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddEmployer.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddEmployer.xml new file mode 100644 index 0000000..307fdea --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddEmployer.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddIndividual.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddIndividual.xml new file mode 100644 index 0000000..c89658f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddIndividual.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddOrganizationParty.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddOrganizationParty.xml new file mode 100644 index 0000000..7a4ea1c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddOrganizationParty.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement-Current.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement-Current.xml new file mode 100644 index 0000000..fe5838e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement-Current.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement.xml new file mode 100644 index 0000000..f2e131e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismElement.xml new file mode 100644 index 0000000..5bfbb19 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismElement.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismsSectionElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismsSectionElement.xml new file mode 100644 index 0000000..08ac446 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismsSectionElement.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEmployerContactMechanismsSectionElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEmployerContactMechanismsSectionElement.xml new file mode 100644 index 0000000..c345ee1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEmployerContactMechanismsSectionElement.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement-DidNotProvide.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement-DidNotProvide.xml new file mode 100644 index 0000000..c380772 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement-DidNotProvide.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement.xml new file mode 100644 index 0000000..c303042 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetIncomesSectionElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetIncomesSectionElement.xml new file mode 100644 index 0000000..c09051a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetIncomesSectionElement.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-False.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-False.xml new file mode 100644 index 0000000..cd1d288 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-False.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-True.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-True.xml new file mode 100644 index 0000000..3c8d560 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-True.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetNoCoApplicantDetermination.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetNoCoApplicantDetermination.xml new file mode 100644 index 0000000..ac639bc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetNoCoApplicantDetermination.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetPartyRelationshipsSectionElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetPartyRelationshipsSectionElement.xml new file mode 100644 index 0000000..829f29e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetPartyRelationshipsSectionElement.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement-NotProvided.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement-NotProvided.xml new file mode 100644 index 0000000..3007958 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement-NotProvided.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement.xml new file mode 100644 index 0000000..7457d69 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes-NotProvided.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes-NotProvided.xml new file mode 100644 index 0000000..5b26b4c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes-NotProvided.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes.xml new file mode 100644 index 0000000..eb21517 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSuffixElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSuffixElement.xml new file mode 100644 index 0000000..ad87128 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSuffixElement.xml @@ -0,0 +1 @@ + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetTradesElement.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetTradesElement.xml new file mode 100644 index 0000000..3f10d46 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetTradesElement.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioning/sendToDecisioning.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioning/sendToDecisioning.xml new file mode 100644 index 0000000..1d228e3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioning/sendToDecisioning.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioningParser/SuccessResponse.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioningParser/SuccessResponse.xml new file mode 100644 index 0000000..ca6ad9c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioningParser/SuccessResponse.xml @@ -0,0 +1,25 @@ + + + + + Success + + + 1200400 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Signature/CreateApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Signature/CreateApp.xml new file mode 100644 index 0000000..53dfbd3 Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Signature/CreateApp.xml differ diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApp.xml new file mode 100644 index 0000000..0786ab6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithNoteAndVehicleSeller.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithNoteAndVehicleSeller.xml new file mode 100644 index 0000000..64ec010 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithNoteAndVehicleSeller.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithVehicleSeller.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithVehicleSeller.xml new file mode 100644 index 0000000..3ca9979 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithVehicleSeller.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateAtvApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateAtvApp.xml new file mode 100644 index 0000000..bb0429e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateAtvApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateBoatApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateBoatApp.xml new file mode 100644 index 0000000..d6cddfa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateBoatApp.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateMotorcycleApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateMotorcycleApp.xml new file mode 100644 index 0000000..81efa75 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateMotorcycleApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePreapprovalApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePreapprovalApp.xml new file mode 100644 index 0000000..a99b5fa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePreapprovalApp.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePwcApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePwcApp.xml new file mode 100644 index 0000000..8d6ba88 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePwcApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateRvPowersportApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateRvPowersportApp.xml new file mode 100644 index 0000000..cc60f51 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateRvPowersportApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateSnowmobileApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateSnowmobileApp.xml new file mode 100644 index 0000000..64631d5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateSnowmobileApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateTrailerMotorhomeApp.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateTrailerMotorhomeApp.xml new file mode 100644 index 0000000..47e7eb2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateTrailerMotorhomeApp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/AppDetailBankcardSectionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/AppDetailBankcardSectionTest.cs new file mode 100644 index 0000000..53e7c61 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/AppDetailBankcardSectionTest.cs @@ -0,0 +1,35 @@ +using System; +using Xunit; +using CRIF.ACTion.Loan; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Tests.Loan +{ + public class AppDetailBankcardSectionTest + { + [Fact] + public void SetAsStudentCcProduct() + { + var section = new AppDetailBankcardSection("", TestCommon.GetTestSettings()); + + var loanApplication = new LoanApplication + { + LoanType = LoanType.CC_Student, + Purpose = LoanPurpose.Purpose.Other, + RequestedAmount = 100, + PrimaryContactMethod = "1234567890" + }; + + section.SetDetails(loanApplication, channelRef: new Guid("8066fa2a-16ba-4148-b28c-2926ba263c5f")); + + var cashBackCcElement = section.Build(); + + var result = TestCommon.Compare(cashBackCcElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppDetailsSection" + + System.IO.Path.DirectorySeparatorChar + "SetAsStudentCcProduct.xml"); + + Assert.Empty(result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/FileRequestBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/FileRequestBuilderTest.cs new file mode 100644 index 0000000..24b73f2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/FileRequestBuilderTest.cs @@ -0,0 +1,35 @@ +using System; +using System.Text; +using Xunit; +using CRIF.ACTion.Files; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Tests.Loan +{ + public class FileRequestBuilderTest + { + private readonly LoanApplicationFile appFile = new LoanApplicationFile("12345", new Guid("b734f50d-d83e-41e9-af19-7291d9cc0622"), "test.txt", "test", "0", "Proof of Income"); + private readonly string fileBase64Value = Convert.ToBase64String(Encoding.ASCII.GetBytes("test")); + + [Fact] + public void Validate() + { + var createAppElement = new FileRequestBuilder("12345", appFile, fileBase64Value, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + + [Fact] + public void CompareFileRequestXML() + { + var createAppElement = new FileRequestBuilder("12345", appFile, fileBase64Value, TestCommon.GetTestSettings()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddAppFilesSection" + System.IO.Path.DirectorySeparatorChar + "AddAppFiles.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/HelocFlexTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/HelocFlexTest.cs new file mode 100644 index 0000000..02d3e55 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/HelocFlexTest.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using Xunit; +using CRIF.ACTion.Loan; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan +{ + public class HelocFlexTest + { + + private readonly LoanApplication _loanApplication = + new(loanDate: DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), loanId: "7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5") + { + LoanType = LoanType.EQ_HELOC_Flex, + Purpose = LoanPurpose.Purpose.Purchase, + RequestedAmount = 1000, + PrimaryContactMethod = "1234567890", + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + RealEstate = new RealEstate(realEstateId: new Guid("7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5")) + { + LienPosition = new LienPosition + { + HasFirstMortgage = true, + HasSecondMortgage = true, + PayoffFirstMortgage = false, + PayoffSecondMortgage = true + }, + CurrentlyBeingRemodeledOrImproved = false, + CurrentlyListedForSale = false, + InitialEstimatedValue = 250000m, + Type = RealEstateType.SingleFamily, + Use = PropertyUse.OwnerOccupied, + RealEstateAddress = new Address(new Guid("f370b54c-5999-4ebc-b1b6-078cc8571652")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + }, + }, + PrimaryBorrower = new Individual(new Guid("b0fb7c37-a203-4c06-89cb-e4e9f3ca20d4")) + { + Language = LanguageOption.English, + FirstName = "Jane", + MiddleInitial = "J", + LastName = "Doe", + DOB = new DateTime(2018, 01, 01), + SSN = "666666666", + Phone = "5555550100", + PhoneType = PhoneType.Home, + Email = "janej@example.net", + IsMarried = false, + Address = new CurrentResidenceAddress(new Guid("66eecd56-4877-47d9-b2b1-45d11a087322")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthlyPayment = 1000m, + MonthsResided = 120, + ResidenceType = ResidenceTypeOption.Own, + }, + HmdaValues = new Hmda + { + HmdaNoticeDisclosed = true, + Races = + [ + Race.NotProvided, + ], + Ethnicities = + [ + Ethnicity.OtherHispanicLatino, + ], + OtherHispanicLatinoEthnicity = "Argentinean", + Sexes = + [ + Sex.Female, + Sex.Male, + ], + }, + Employers = + [ + new Employer(employerId: new Guid("57df49cc-ff09-4660-aa47-54fdb5e08017")) + { + Language = LanguageOption.English, + Name = "Purple Widgets Inc", + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + MonthsEmployed = 120, + StartDate = new DateTime(2008, 01, 01), + EndDate = new DateTime(2018,01,01), + Title = "Lead Widget Painter", + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Job", + AmountPerPeriod = 100m, + Frequency = Frequency.Hourly, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + }, + }, + ], + } + }; + + [Fact] + public void Validate() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void CreateApplication() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings(), + new Guid("741be6b4-9f42-4efd-871f-f5d99c0ef122"), new Guid("e814d860-14fe-4bef-be9b-21be3dce1edf"), + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), new TestDistributionChannelLookupService()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "HelocFlex" + System.IO.Path.DirectorySeparatorChar + "CreateApplication.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddIndividualResponseParserTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddIndividualResponseParserTest.cs new file mode 100644 index 0000000..4352375 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddIndividualResponseParserTest.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Linq; +using Org.XmlUnit.Diff; +using Xunit; +using CRIF.ACTion.Common.Parsers; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan.Individuals +{ + public class AddIndividualResponseParserTest + { + private readonly IResponseParser IndividualParser; + private readonly int applicationId = 1100950; + + public AddIndividualResponseParserTest() + { + IndividualParser = new AddIndividualResponseParser(); + } + + private readonly Individual individual = new(new Guid("325f9caf-b08a-44dc-af67-bd49db6a822c")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + HasOptedOutTCPA = true, + Address = new CurrentResidenceAddress(new Guid("325f9caf-b08a-44dc-af67-bd49db6a822c")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + PostalCode = "53151", + Country = CountryOption.UnitedStates, + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + }; + + [Fact] + public void ErrorResponseParse() + { + XmlDocument doc = new(); + doc.Load("ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddIndividualError.xml"); + + XDocument response; + + using (XmlNodeReader reader = new(doc)) + { + response = XDocument.Load(reader); + } + + Exception ex = Assert.Throws(() => IndividualParser.Parse(response.Root)); + + Assert.Equal("The 'Common:isCollateralGrantorFL' attribute is not declared. on line 1 position 166", ex.Message); + } + + [Fact] + public void SuccessResponseParse() + { + XmlDocument doc = new(); + doc.Load("ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddIndividualSuccess.xml"); + + XDocument response; + + using (XmlNodeReader reader = new(doc)) + { + response = XDocument.Load(reader); + } + + var parsedIndividual = IndividualParser.Parse(response.Root); + + Assert.Equal("Jane", parsedIndividual.FirstName); + } + + [Fact] + public void Validate() + { + var addIndividual = + new AddIndividualRequestBuilder(applicationId.ToString(), individual, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(addIndividual)); + } + + [Fact] + public void CorrectBuild() + { + var addIndividual = + new AddIndividualRequestBuilder(applicationId.ToString(), individual, TestCommon.GetTestSettings()).Build(); + + var result = TestCommon.Compare(addIndividual, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddIndividual-Simple.xml"); + + Assert.Empty(result); + } + + [Fact] + public void BadNamespace() + { + var addIndividual = + new AddIndividualRequestBuilder(applicationId.ToString(), individual, TestCommon.GetTestSettings()).Build(); + + var result = TestCommon.Compare(addIndividual, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddIndividual-BadNamespace.xml"); + + Assert.NotEmpty(result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddressesSectionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddressesSectionTest.cs new file mode 100644 index 0000000..7856d70 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddressesSectionTest.cs @@ -0,0 +1,40 @@ +using System; +using Xunit; +using CRIF.ACTion.Common; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan.Individuals +{ + public class AddressesSectionTest + { + public ResidenceAddress ValidAddress = new(new Guid("a333a064-be7b-4d20-841f-9c3c306866b3")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + }; + + + [Fact] + public void AddAddress() + { + var section = new AddressesSection("", TestCommon.GetTestSettings()); + + section.AddAddress(ValidAddress, ContactMechanisms.CrifContactMechanism.CurrentAddress); + + var addressElement = section.Build(); + + var result = TestCommon.Compare(addressElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AddressesSection" + + System.IO.Path.DirectorySeparatorChar + "AddAddress.xml"); + + Assert.Empty(result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AppPartiesSectionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AppPartiesSectionTest.cs new file mode 100644 index 0000000..06e05b3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AppPartiesSectionTest.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using Xunit; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan.Individuals +{ + public class AppPartiesSectionTest + { + private static Guid TestGuid => new("a333a064-be7b-4d20-841f-9c3c306866b3"); + + private readonly Individual Individual = new(new Guid("3580f8c9-db39-42f9-a9f3-133fe4a3b002")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Address = new CurrentResidenceAddress + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + OtherIncomes = + [ + new Income + { + IncomeTypeCD = IncomeType.Alimony, + SourceName = "Child Support", + AmountPerPeriod = 500.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + ], + Employers = + [ + new Employer + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + MonthsEmployed = 60, + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ] + }; + + private readonly IIndividualHistory HistoryIndividual = new HistoryIndividual(new Guid("3580f8c9-db39-42f9-a9f3-133fe4a3b002")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + Address = new CurrentResidenceAddress + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + OtherIncomes = + [ + new Income + { + IncomeTypeCD = IncomeType.Alimony, + SourceName = "Child Support", + AmountPerPeriod = 500.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + ], + }; + + [Fact] + public void AddIndividualBorrowerParty() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.AddIndividualParty(Individual, AppPartiesSection.PartyType.borrower, false); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "AddIndividualBorrowerParty.xml"); + + Assert.Empty(result); + } + + [Fact] + public void AddIndividualNonSigningPrimarySpouseParty() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.AddIndividualParty(HistoryIndividual, AppPartiesSection.PartyType.NonSigningSpouseMarriedToPrimary); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "AddIndividualNonSigningPrimarySpouseParty.xml"); + + Assert.Empty(result); + } + + [Fact] + public void AddIndividualNonSigningCoApplicantSpouseParty() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.AddIndividualParty(HistoryIndividual, AppPartiesSection.PartyType.NonSigningSpouseMarriedToCoBorrower); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "AddIndividualNonSigningCoApplicantSpouseParty.xml"); + + Assert.Empty(result); + } + + [Fact] + public void AddRentAndAlimony() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.AddIndividualParty(Individual, AppPartiesSection.PartyType.borrower); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "AddIndividualBorrowerParty.xml"); + + Assert.Empty(result); + } + + [Fact] + public void AddIndividualPrimaryBorrowerParty() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.AddIndividualParty(Individual, AppPartiesSection.PartyType.primaryBorrower); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "AddIndividualPrimaryBorrowerParty.xml"); + + Assert.Empty(result); + } + + [Fact] + public void SetSourceChannel() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.SetSourceChannel(TestGuid); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "SetSourceChannel.xml"); + + Assert.Empty(result); + } + + [Fact] + public void SetServicingChannel() + { + var section = new AppPartiesSection("", TestCommon.GetTestSettings()); + + section.SetServicingChannel(TestGuid); + + var partyElement = section.Build(); + + var result = TestCommon.Compare(partyElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "AppPartiesSection" + + System.IO.Path.DirectorySeparatorChar + "SetServicingChannel.xml"); + + Assert.Empty(result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/ExampleXML/BuildIncomeSuccess.xml b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/ExampleXML/BuildIncomeSuccess.xml new file mode 100644 index 0000000..e8c5483 Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/ExampleXML/BuildIncomeSuccess.xml differ diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/IncomeSectionBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/IncomeSectionBuilderTest.cs new file mode 100644 index 0000000..59b3cf6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/IncomeSectionBuilderTest.cs @@ -0,0 +1,50 @@ +using System; +using System.Xml.Linq; +using Xunit; +using Xunit.Abstractions; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; + +namespace CRIF.ACTion.Tests.Loan.Individuals +{ + public class IncomeSectionBuilderTest : BuilderTestBase + { + private readonly ITestOutputHelper output; + + public IncomeSectionBuilderTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Build_Income_Fail() + { + Assert.Throws(() => new IncomeSectionBuilder(new Income()).Build()); + } + + [Fact] + public void BuildIncomeSuccess() + { + var income = new Income() + { + AmountPerPeriod = 100, + Frequency = Frequency.Annually, + HoursPerWeek = 10, + IncomeStatus = IncomeStatus.IncomeStatus_Excluded, + IncomeTypeCD = IncomeType.Commissions, + SourceName = "Job" + }; + + var incomeElement = new IncomeSectionBuilder(income).Build(); + + output.WriteLine(Serialize(incomeElement)); + + string comparePath = @"Loan\Individuals\ExampleXML\BuildIncomeSuccess.xml"; + + var results = TestCommon.Compare(incomeElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/PartiesSectionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/PartiesSectionTest.cs new file mode 100644 index 0000000..7949bdc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/PartiesSectionTest.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Xunit; +using CRIF.ACTion.Common; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan.Individuals +{ + public class PartiesSectionTest + { + private static Guid TestGuid => new("e4a1a31a-3985-4f76-955c-9227c9c94ba8"); + + private static readonly IApplicant TestIndividual = new Individual(new Guid("67755afb-b125-4a48-959e-7cedead90f8f")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + Suffix = Suffix.III, + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("b09444c1-c12f-4c3e-b787-04c44b6be0c4")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + OtherIncomes = + [ + new Income + { + IncomeTypeCD = IncomeType.SocialSecurity, + Frequency = Frequency.Monthly, + SourceName = "Freeform Text", + AmountPerPeriod = 123.45m, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + }, + new Income + { + IncomeTypeCD = IncomeType.Alimony, + Frequency = Frequency.Annually, + SourceName = "Child Support", + AmountPerPeriod = 600, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + } + ], + Employers = + [ + new Employer(employerId: new Guid("2fd067f2-bd3d-4053-990e-f4282180db63")) + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + StartDate = new DateTime(2017, 01, 01), + MonthsEmployed = 60, + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ] + }; + + public static IEnumerable GetSuffixElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + + var suffixElements = PartiesSection.GetSuffixElement(TestIndividual); + + yield return new Object[] { + suffixElements, + "GetSuffixElement.xml" + }; + + } + + public static IEnumerable GetTradesElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var mockPropertyLot = new PropertyLot + { + RentalAmt = 150 + }; + var tradesElement = PartiesSection.GetTradesElement(TestIndividual, mockPropertyLot); + + yield return new Object[] { + tradesElement, + "GetTradesElement.xml" + }; + + } + + public static IEnumerable AddChannelParty() + { + var section = new PartiesSection("", TestCommon.GetTestSettings()); + section.AddSourceChannelParty(TestGuid, LoanType.EQ_HELOC_Flex); + + yield return new Object[] { + section.Build(), + "AddChannelParty.xml" + }; + } + + public static IEnumerable AddEmployer() + { + var section = new PartiesSection("", TestCommon.GetTestSettings()); + section.AddEmployer(TestIndividual.Employers.First()); + + yield return new Object[] { + section.Build(), + "AddEmployer.xml" + }; + } + + public static IEnumerable GetContactMechanismElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var contactElement = PartiesSection.GetContactMechanismElement(TestIndividual.Phone, ContactMechanisms.CrifContactMechanism.HomePhone); + + yield return new Object[] { + contactElement, + "GetContactMechanismElement.xml" + }; + } + + public static IEnumerable GetContactMechanismsSectionElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var element = PartiesSection.GetContactMechanismsSectionElement(TestIndividual); + + yield return new Object[] { + element, + "GetContactMechanismsSectionElement.xml" + }; + } + + public static IEnumerable GetEmployerContactMechanismsSectionElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var element = PartiesSection.GetEmployerContactMechanismsSectionElement(TestIndividual.Employers.First()); + + yield return new Object[] { + element, + "GetEmployerContactMechanismsSectionElement.xml" + }; + } + + public static IEnumerable GetIncomesSectionElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var element = PartiesSection.GetIncomesSectionElement(TestIndividual); + + yield return new Object[] { + element, + "GetIncomesSectionElement.xml" + }; + } + + public static IEnumerable GetMaritalStatusElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var isMarried = PartiesSection.GetMaritalStatusElement(true); + + yield return new Object[] { + isMarried, + "GetMaritalStatusElement-True.xml" + }; + + var isNotMarried = PartiesSection.GetMaritalStatusElement(false); + + yield return new Object[] { + isNotMarried, + "GetMaritalStatusElement-False.xml" + }; + } + + public static IEnumerable GetPartyRelationshipsSectionElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var element = PartiesSection.GetApplicantPartyRelationshipsSectionElement(TestIndividual); + + yield return new Object[] { + element, + "GetPartyRelationshipsSectionElement.xml" + }; + } + + public static IEnumerable AddIndividual() + { + var section = new PartiesSection("", TestCommon.GetTestSettings()); + section.AddApplicant(TestIndividual); + + yield return new Object[] { + section.Build(), + "AddIndividual.xml" + }; + } + + public static IEnumerable AddOrganizationParty() + { + var section = new PartiesSection("", TestCommon.GetTestSettings()); + section.AddOrganizationParty(TestGuid); + + yield return new Object[] { + section.Build(), + "AddOrganizationParty.xml" + }; + } + + public static IEnumerable GetAddressContactMechanismElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + + var currentAddress = PartiesSection.GetAddressContactMechanismElement(TestGuid, ContactMechanisms.CrifContactMechanism.CurrentAddress); + + yield return new Object[] { + currentAddress, + "GetAddressContactMechanismElement-Current.xml" + }; + + var previousAddress = PartiesSection.GetAddressContactMechanismElement(TestGuid, ContactMechanisms.CrifContactMechanism.PreviousAddress); + + yield return new Object[] { + previousAddress, + "GetAddressContactMechanismElement.xml" + }; + } + + public static IEnumerable GetEthnicitiesElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + + var individualEthnicityAndDidNotFurnish = new Individual + { + HmdaValues = new Hmda + { + Ethnicities = [Ethnicity.Cuban], + } + }; + + yield return new Object[] { + PartiesSection.GetEthnicitiesElement(individualEthnicityAndDidNotFurnish), + "GetEthnicitiesElement.xml" + }; + + var individual = new Individual + { + HmdaValues = new Hmda + { + Ethnicities = + [ + Ethnicity.Cuban, + Ethnicity.NotProvided, + ] + } + }; + + yield return new Object[] { + PartiesSection.GetEthnicitiesElement(individual), + "GetEthnicitiesElement.xml" + }; + + var individualDidNotFurnish = new Individual + { + HmdaValues = new Hmda + { + Ethnicities = + [ + Ethnicity.NotProvided, + ] + } + }; + + yield return new Object[] { + PartiesSection.GetEthnicitiesElement(individualDidNotFurnish), + "GetEthnicitiesElement-DidNotProvide.xml" + }; + } + + public static IEnumerable GetRacesElement() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + + var individual = new Individual + { + HmdaValues = new Hmda + { + Races = + [ + Race.AmericanIndianAlaskaNative, + Race.OtherPacificIslander, + ], + AmericanIndianAlaskaNativeTribe = "Forest County Potawatomi", + OtherAsianRace = "Hmong", + } + }; + + yield return new Object[] { + PartiesSection.GetRacesElement(individual), + "GetRacesElement.xml" + }; + + var individualWithNotProvided = new Individual + { + HmdaValues = new Hmda + { + Races = + [ + Race.AmericanIndianAlaskaNative, + Race.OtherPacificIslander, + Race.NotProvided + ], + AmericanIndianAlaskaNativeTribe = "Forest County Potawatomi", + OtherAsianRace = "Hmong", + } + }; + + yield return new Object[] { + PartiesSection.GetRacesElement(individualWithNotProvided), + "GetRacesElement.xml" + }; + + var individualOnlyNotProvided = new Individual + { + HmdaValues = new Hmda + { + Races = + [ + Race.NotProvided + ] + } + }; + + yield return new Object[] { + PartiesSection.GetRacesElement(individualOnlyNotProvided), + "GetRacesElement-NotProvided.xml" + }; + } + + public static IEnumerable GetSexAttributes() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + + var individual = new Individual + { + HmdaValues = new Hmda + { + Sexes = + [ + Sex.Female, + Sex.NotProvided, + ] + } + }; + + var individualElement = new XElement(CrifNamespaces.Common + "individual", PartiesSection.GetSexAttributes(individual)); + + yield return new Object[] { + individualElement, + "GetSexAttributes.xml" + }; + + var individualNotProvided = new Individual + { + HmdaValues = new Hmda + { + Sexes = + [ + Sex.NotProvided, + ] + } + }; + + individualElement = new XElement(CrifNamespaces.Common + "individual", PartiesSection.GetSexAttributes(individualNotProvided)); + + yield return new Object[] { + individualElement, + "GetSexAttributes-NotProvided.xml" + }; + } + + public static IEnumerable GetNoCoApplicantDetermination() + { + _ = new PartiesSection("", TestCommon.GetTestSettings()); + var individualElement = new XElement(CrifNamespaces.Common + "determinations"); + PartiesSection.AddNonApplicantHmdaDetermination(individualElement); + + yield return new object[] + { + individualElement, + "GetNoCoApplicantDetermination.xml" + }; + + } + + [Theory] + [MemberData(nameof(AddChannelParty))] + [MemberData(nameof(AddEmployer))] + [MemberData(nameof(AddIndividual))] + [MemberData(nameof(AddOrganizationParty))] + [MemberData(nameof(GetContactMechanismElement))] + [MemberData(nameof(GetContactMechanismsSectionElement))] + [MemberData(nameof(GetEmployerContactMechanismsSectionElement))] + [MemberData(nameof(GetIncomesSectionElement))] + [MemberData(nameof(GetMaritalStatusElement))] + [MemberData(nameof(GetAddressContactMechanismElement))] + [MemberData(nameof(GetPartyRelationshipsSectionElement))] + [MemberData(nameof(GetEthnicitiesElement))] + [MemberData(nameof(GetRacesElement))] + [MemberData(nameof(GetSexAttributes))] + [MemberData(nameof(GetTradesElement))] + [MemberData(nameof(GetSuffixElement))] + [MemberData(nameof(GetNoCoApplicantDetermination))] + public void CheckXmlCreation(XElement xElement, string fileName) + { + Assert.True(ModelValidator.TryValidate(xElement)); + + var result = TestCommon.Compare(xElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "PartiesSection" + + System.IO.Path.DirectorySeparatorChar + fileName); + + Assert.Empty(result); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LienPositionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LienPositionTest.cs new file mode 100644 index 0000000..805fac4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LienPositionTest.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.IO; +using Xunit; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using CRIF.ACTion.Loan.Assets; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; + +namespace CRIF.ACTion.Tests.Loan +{ + public class LienPositionTest + { + + private readonly RealEstateBase _realEstateBase = new() + { + CurrentlyBeingRemodeledOrImproved = false, + CurrentlyListedForSale = true, + Use = PropertyUse.SecondHome, + LienPosition = new LienPosition + { + HasFirstMortgage = true, + HasSecondMortgage = true, + PayoffFirstMortgage = false, + PayoffSecondMortgage = false + } + }; + + [Fact] + public void GetFirstLienPositionTest() + { + _ = new RealEstateProperty(_realEstateBase); + + using StreamReader file = File.OpenText("Loan" + Path.DirectorySeparatorChar + + "json" + Path.DirectorySeparatorChar + "TestLienPositions.json"); + using JsonTextReader reader = new(file); + JObject allData = (JObject)JToken.ReadFrom(reader); + JArray firstLien = (JArray)allData["first"]; + IList validFirstLienPositions = firstLien.ToObject>(); + JArray secondLien = (JArray)allData["second"]; + IList validSecondLienPositions = secondLien.ToObject>(); + JArray thirdLien = (JArray)allData["third"]; + IList validThirdLienPositions = thirdLien.ToObject>(); + + foreach (var position in validFirstLienPositions) + { + LienPositionType calculatedPosition = RealEstateProperty.GetLienPosition(position); + Assert.Equal(LienPositionType.First, calculatedPosition); + } + foreach (var position in validSecondLienPositions) + { + LienPositionType calculatedPosition = RealEstateProperty.GetLienPosition(position); + Assert.Equal(LienPositionType.Second, calculatedPosition); + } + foreach (var position in validThirdLienPositions) + { + LienPositionType calculatedPosition = RealEstateProperty.GetLienPosition(position); + Assert.Equal(LienPositionType.Third, calculatedPosition); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LoanApplicationRequestBuilderTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LoanApplicationRequestBuilderTest.cs new file mode 100644 index 0000000..2ff70e4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LoanApplicationRequestBuilderTest.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using Xunit; +using CRIF.ACTion.Loan; +using CRIF.ACTion.Common.Parsers; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan +{ + public class LoanApplicationRequestBuilderTest + { + private readonly IResponseParser _loanApplicationResponseParser; + + private readonly LoanApplication _loanApplication = + new(DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), new Guid("9ed5a50f-279c-49b0-b5b4-d5b614e42501")) + { + LoanType = LoanType.CC_Secure, + RequestedAmount = 154.20m, + PrimaryContactMethod = "1234567890", + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + PromoCode = "asdfas-12341", + Purpose = LoanPurpose.Purpose.Other, + PrimaryBorrower = new Individual(new Guid("8066fa2a-16ba-4148-b28c-2926ba263c5f")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("bf7e502a-9830-43c6-b603-1a32294c7e1a")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + MailingAddress = new Address(new Guid("f84db562-36a0-4039-9ce5-346a2ba6899b")) + { + Address1 = "321 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151" + }, + Employers = + [ + new Employer(employerId: new Guid("eee4efa3-327f-4ce5-9e8b-8c3e7a865fff")) + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + MonthsEmployed = 60, + StartDate = new DateTime(2017, 01, 01), + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ] + }, + CoApplicant = new CoApplicant(new Guid("ba99037f-df92-46d4-a477-ffecea1f4837")) + { + FirstName = "John", + LastName = "Smith", + DOB = new DateTime(2018, 7, 25), + SSN = "111221111", + Phone = "5555550983", + PhoneType = PhoneType.Mobile, + Email = "john.smith@example.org", + IsMarried = true, + Language = LanguageOption.English, + HasOptedOutTCPA = true, + Address = new CurrentResidenceAddress(new Guid("ae795a5f-1ea3-4186-b4b8-e9f9b81e3059")) + { + Address1 = "339 1st St", + Address2 = "#76", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 12, + MonthlyPayment = 100, + }, + PreviousAddresses = + [ + new ResidenceAddress(new Guid("e7e881f0-d371-4371-8022-cce4f700e6d6")) + { + Address1 = "339 1st St", + Address2 = "#80", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 6, + }, + ], + Employers = + [ + new Employer(employerId: new Guid("e8436884-0e6c-4144-8de5-1f44c7936a23")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Chocolate Teapot Manfuacturing Inc", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Lead Teapot Assembler", + WorkPhone = "5556783241", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + AmountPerPeriod = 100m, + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + new Employer(employerId: new Guid("58d25f44-1260-428d-ac2c-d8804b06ed66")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Vanilla Teapot Manfuacturing Coop", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Junior Teapot Assembler", + WorkPhone = "5559572318", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + AmountPerPeriod = 50m, + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + ], + }, + }; + + private readonly LoanApplication _loanApplicationWithSpouse = + new(DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), new Guid("9ed5a50f-279c-49b0-b5b4-d5b614e42501")) + { + LoanType = LoanType.CC_Secure, + RequestedAmount = 154.20m, + Purpose = LoanPurpose.Purpose.Other, + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + PrimaryBorrower = new Individual(new Guid("8066fa2a-16ba-4148-b28c-2926ba263c5f")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("bf7e502a-9830-43c6-b603-1a32294c7e1a")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + MailingAddress = new Address(new Guid("f84db562-36a0-4039-9ce5-346a2ba6899b")) + { + Address1 = "321 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151" + }, + Employers = + [ + new Employer(employerId: new Guid("eee4efa3-327f-4ce5-9e8b-8c3e7a865fff")) + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + MonthsEmployed = 60, + StartDate = new DateTime(2017, 01, 01), + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ], + DetailedNonApplicantSpouse = new HistoryIndividual(new Guid("ba99037f-df92-46d4-a477-ffecea1f4837")) + { + FirstName = "John", + LastName = "Smith", + DOB = new DateTime(2018, 7, 25), + SSN = "111221111", + Phone = "5555550983", + PhoneType = PhoneType.Mobile, + Email = "john.smith@example.org", + Language = LanguageOption.English, + HasOptedOutTCPA = true, + Address = new CurrentResidenceAddress(new Guid("ae795a5f-1ea3-4186-b4b8-e9f9b81e3059")) + { + Address1 = "339 1st St", + Address2 = "#76", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 12, + MonthlyPayment = 100, + }, + PreviousAddresses = + [ + new ResidenceAddress(new Guid("e7e881f0-d371-4371-8022-cce4f700e6d6")) + { + Address1 = "339 1st St", + Address2 = "#80", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 6, + }, + ], + Employers = + [ + new Employer(employerId: new Guid("e8436884-0e6c-4144-8de5-1f44c7936a23")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Chocolate Teapot Manfuacturing Inc", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Lead Teapot Assembler", + WorkPhone = "5556783241", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + AmountPerPeriod = 100m, + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + new Employer(employerId: new Guid("58d25f44-1260-428d-ac2c-d8804b06ed66")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Vanilla Teapot Manfuacturing Coop", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Junior Teapot Assembler", + WorkPhone = "5559572318", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + AmountPerPeriod = 50m, + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + ], + } + } + }; + + private readonly List Guids = new List + { + new Guid("64f4efa3-327f-4ce5-9e8b-8c3e7a8656d5"), //org + new Guid("292d0c30-976b-47f9-85a4-487ce0b3e1e8"), //source channel + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), //distribution/servicing channel + }; + + public LoanApplicationRequestBuilderTest() + { + _loanApplicationResponseParser = new LoanApplicationResponseParser(); + } + + [Fact] + public void Validate() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void CreateApplication() + { + var appCreator = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings(), + Guids[0], Guids[1], Guids[2], new TestDistributionChannelLookupService()); + + var createAppElement = appCreator.Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "CreateApp" + System.IO.Path.DirectorySeparatorChar + "CreateApplication.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateApplicationWithNote() + { + _loanApplication.Note = "Example application note."; + + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings(), + Guids[0], Guids[1], Guids[2], new TestDistributionChannelLookupService()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "CreateApp" + System.IO.Path.DirectorySeparatorChar + "CreateApplicationWithNote.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateApplicationNonSingingHistorySpouse() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplicationWithSpouse, TestCommon.GetTestSettings(), + Guids[0], Guids[1], Guids[2], new TestDistributionChannelLookupService()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "CreateApp" + System.IO.Path.DirectorySeparatorChar + "CreateApplicationNonSigningHistorySpouse.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void AddPurposeOfLoanSignature() + { + _loanApplication.LoanType = LoanType.S_Signature; + _loanApplication.PurposeOfLoanSignature = "Test Purpose of Loan Signature"; + + bool check = false; + + if (_loanApplication.LoanType == LoanType.S_Signature && !string.IsNullOrWhiteSpace(_loanApplication.PurposeOfLoanSignature)) + { + check = true; + } + + Assert.True(check); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/MfgHomeAndLandTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/MfgHomeAndLandTest.cs new file mode 100644 index 0000000..ac22662 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/MfgHomeAndLandTest.cs @@ -0,0 +1,243 @@ +using System; +using Xunit; +using CRIF.ACTion.Loan; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan +{ + public class MfgHomeAndLandTest + { + + private readonly LoanApplication _loanApplication = + new(loanDate: DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), loanId: "7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5") + { + LoanType = LoanType.EQ_Mfg_Home_With_or_Without_Land, + Purpose = LoanPurpose.Purpose.Purchase, + RequestedAmount = 1000, + PrimaryContactMethod = "1234567890", + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + ManufacturedHome = new ManufacturedHome(new Guid("7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5")) + { + CurrentlyBeingRemodeledOrImproved = false, + CurrentlyListedForSale = false, + Type = ManufacturedHomeType.Double, + Use = PropertyUse.OwnerOccupied, + Lot = new PropertyLot(new Guid("f370b54c-5999-4ebc-b1b6-078cc8571652")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + IsRented = true, + RentalAmt = 123.45m, + }, + }, + PrimaryBorrower = new Individual(new Guid("b0fb7c37-a203-4c06-89cb-e4e9f3ca20d4")) + { + Language = LanguageOption.English, + FirstName = "Jane", + MiddleInitial = "J", + LastName = "Doe", + DOB = new DateTime(2018, 01, 01), + SSN = "666666666", + Phone = "5555550100", + PhoneType = PhoneType.Home, + Email = "janej@example.net", + IsMarried = false, + IsLandmarkEmployee = true, + Address = new CurrentResidenceAddress(new Guid("66eecd56-4877-47d9-b2b1-45d11a087322")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthlyPayment = 1000m, + MonthsResided = 120, + ResidenceType = ResidenceTypeOption.Own, + }, + HmdaValues = new Hmda + { + HmdaNoticeDisclosed = true, + Races = + [ + Race.NotProvided, + ], + Ethnicities = + [ + Ethnicity.OtherHispanicLatino, + ], + OtherHispanicLatinoEthnicity = "Argentinean", + Sexes = + [ + Sex.Female, + Sex.Male, + ], + }, + Employers = + [ + new Employer(employerId: new Guid("57df49cc-ff09-4660-aa47-54fdb5e08017")) + { + Language = LanguageOption.English, + Name = "Purple Widgets Inc", + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + MonthsEmployed = 120, + StartDate = new DateTime(2008, 01, 01), + EndDate = new DateTime(2018,01,01), + Title = "Lead Widget Painter", + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Job", + AmountPerPeriod = 100m, + Frequency = Frequency.Hourly, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + }, + }, + ], + } + }; + + private readonly LoanApplication _loanApplicationWithAnnualTaxes = + new(loanDate: DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), loanId: "7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5") + { + LoanType = LoanType.EQ_Mfg_Home_With_or_Without_Land, + Purpose = LoanPurpose.Purpose.Purchase, + RequestedAmount = 1000, + PrimaryContactMethod = "1234567890", + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + ManufacturedHome = new ManufacturedHome(new Guid("7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5")) + { + CurrentlyBeingRemodeledOrImproved = false, + CurrentlyListedForSale = false, + Type = ManufacturedHomeType.Double, + Use = PropertyUse.OwnerOccupied, + Lot = new PropertyLot(new Guid("f370b54c-5999-4ebc-b1b6-078cc8571652")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + IsRented = false, + AnnualPropertyTax = 4500, + }, + }, + PrimaryBorrower = new Individual(new Guid("b0fb7c37-a203-4c06-89cb-e4e9f3ca20d4")) + { + Language = LanguageOption.English, + FirstName = "Jane", + MiddleInitial = "J", + LastName = "Doe", + DOB = new DateTime(2018, 01, 01), + SSN = "666666666", + Phone = "5555550100", + PhoneType = PhoneType.Home, + Email = "janej@example.net", + IsMarried = false, + IsLandmarkEmployee = true, + Address = new CurrentResidenceAddress(new Guid("66eecd56-4877-47d9-b2b1-45d11a087322")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthlyPayment = 1000m, + MonthsResided = 120, + ResidenceType = ResidenceTypeOption.Own, + }, + HmdaValues = new Hmda + { + HmdaNoticeDisclosed = true, + Races = + [ + Race.NotProvided, + ], + Ethnicities = + [ + Ethnicity.OtherHispanicLatino, + ], + OtherHispanicLatinoEthnicity = "Argentinean", + Sexes = + [ + Sex.Female, + Sex.Male, + ], + }, + Employers = + [ + new Employer(employerId: new Guid("57df49cc-ff09-4660-aa47-54fdb5e08017")) + { + Language = LanguageOption.English, + Name = "Purple Widgets Inc", + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + MonthsEmployed = 120, + StartDate = new DateTime(2008, 01, 01), + EndDate = new DateTime(2018,01,01), + Title = "Lead Widget Painter", + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Job", + AmountPerPeriod = 100m, + Frequency = Frequency.Hourly, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + }, + }, + ], + } + }; + + [Fact] + public void Validate() + { + var createAppElement = + new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void CreateApplication() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings(), + new Guid("741be6b4-9f42-4efd-871f-f5d99c0ef122"), new Guid("e814d860-14fe-4bef-be9b-21be3dce1edf"), + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), new TestDistributionChannelLookupService()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "MfgHomeAndLand" + System.IO.Path.DirectorySeparatorChar + "CreateApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateApplicationWithAnnualTaxes() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplicationWithAnnualTaxes, TestCommon.GetTestSettings(), + new Guid("741be6b4-9f42-4efd-871f-f5d99c0ef122"), new Guid("e814d860-14fe-4bef-be9b-21be3dce1edf"), + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), new TestDistributionChannelLookupService()).Build(); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "MfgHomeAndLand" + System.IO.Path.DirectorySeparatorChar + "CreateAppWithAnnualTaxes.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/OrgSectionTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/OrgSectionTest.cs new file mode 100644 index 0000000..faf3b16 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/OrgSectionTest.cs @@ -0,0 +1,28 @@ +using System; +using Xunit; +using CRIF.ACTion.Loan; + +namespace CRIF.ACTion.Tests.Loan +{ + public class OrgSectionTest + { + private static Guid TestGuid => new("a333a064-be7b-4d20-841f-9c3c306866b3"); + + [Fact] + public void SetOrgId() + { + var section = new OrgSection("", TestCommon.GetTestSettings()); + + section.SetOrgId(TestGuid); + + var orgElement = section.Build(); + + var result = TestCommon.Compare(orgElement, + "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "OrgSection" + + System.IO.Path.DirectorySeparatorChar + "SetOrgId.xml"); + + Assert.Empty(result); + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/SignatureLoanTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/SignatureLoanTest.cs new file mode 100644 index 0000000..a6cfd38 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/SignatureLoanTest.cs @@ -0,0 +1,99 @@ +using System; +using System.Xml.Linq; +using Xunit; +using CRIF.ACTion.Common; +using CRIF.ACTion.Loan; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace CRIF.ACTion.Tests.Loan +{ + public class SignatureLoanTest + { + private readonly LoanApplication _loanApplication = + new(loanDate: DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), loanId: "7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5") + { + LoanType = LoanType.S_Signature, + Purpose = LoanPurpose.Purpose.Other, + PurposeOfLoanSignature = "Test Purpose", + RequestedAmount = 13000, + PrimaryContactMethod = "1234567890", + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + TermLengthInMonths = 36, + PrimaryBorrower = new Individual(new Guid("b0fb7c37-a203-4c06-89cb-e4e9f3ca20d4")) + { + Language = LanguageOption.English, + FirstName = "Jane", + MiddleInitial = "J", + LastName = "Doe", + DOB = new DateTime(2018, 01, 01), + SSN = "666666666", + Phone = "5555550100", + PhoneType = PhoneType.Home, + Email = "janej@example.net", + IsMarried = false, + Address = new CurrentResidenceAddress(new Guid("66eecd56-4877-47d9-b2b1-45d11a087322")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthlyPayment = 1000m, + MonthsResided = 120, + ResidenceType = ResidenceTypeOption.Own, + } + } + }; + + private static XElement BuildApplication(LoanApplication loanApplication) + { + var createAppElement = new LoanApplicationRequestBuilder(loanApplication, TestCommon.GetTestSettings(), + new Guid("741be6b4-9f42-4efd-871f-f5d99c0ef122"), new Guid("e814d860-14fe-4bef-be9b-21be3dce1edf"), + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), new TestDistributionChannelLookupService()).Build(); + + return createAppElement; + } + + [Fact] + public void Validate() + { + var createAppElement = new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + + [Fact] + public void CreateApplication() + { + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Signature" + System.IO.Path.DirectorySeparatorChar + "CreateApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void AddPurposeOfLoanSignature() + { + var appDetailElement = new XElement(CrifNamespaces.App + "Test", + new XAttribute(CrifNamespaces.App + "financedAmt", _loanApplication.RequestedAmount) + ); + + if (_loanApplication.LoanType == LoanType.S_Signature) + { + appDetailElement.Add(new XAttribute("purposeOfLoanSignature", "Testing Purpose of Loan Signature")); + } + + Assert.True(appDetailElement.Attribute("purposeOfLoanSignature").Value == "Testing Purpose of Loan Signature"); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/VehicleEtcTest.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/VehicleEtcTest.cs new file mode 100644 index 0000000..ad2c73a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/VehicleEtcTest.cs @@ -0,0 +1,320 @@ +using System; +using System.Xml.Linq; +using Xunit; +using CRIF.ACTion.Loan; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; +using Landmark.OLA.Models.Addresses; + +namespace CRIF.ACTion.Tests.Loan +{ + public class VehicleEtcTest + { + private readonly LoanApplication _loanApplication = + new(loanDate: DateTime.Parse("2018-01-01T06:00:00.0000000Z", null, System.Globalization.DateTimeStyles.AssumeLocal), loanId: "7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5") + { + LoanType = LoanType.V_Vehicle, + Purpose = LoanPurpose.Purpose.Purchase, + RequestedAmount = 7500, + PrimaryContactMethod = "1234567890", + TermLengthInMonths = 48, + DisclosureDate = DateTime.Parse("2019-03-08T19:50:26.429Z"), + IPAddress = "111.11.1111", + Vehicle = new Vehicle(valuationId: new Guid("254d06dd-9da2-4013-8021-c9aaf591807f"), + vehicleId: new Guid("7c0d9659-4d37-4ae7-93ce-e6c72ad6a0b5")) + { + ValuationType = VehicleValuationType.Invoice, + Year = 2019, + InitialEstimatedValue = 10000, + Make = "Datsun", + Model = "Go-Cross", + Mileage = 38, + Condition = NewUsed.New, + Type = VehicleType.Automobile + }, + PrimaryBorrower = new Individual(new Guid("b0fb7c37-a203-4c06-89cb-e4e9f3ca20d4")) + { + Language = LanguageOption.English, + FirstName = "Jane", + MiddleInitial = "J", + LastName = "Doe", + DOB = new DateTime(2018, 01, 01), + SSN = "666666666", + Phone = "5555550100", + PhoneType = PhoneType.Home, + Email = "janej@example.net", + IsMarried = false, + Address = new CurrentResidenceAddress(new Guid("66eecd56-4877-47d9-b2b1-45d11a087322")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthlyPayment = 1000m, + MonthsResided = 120, + ResidenceType = ResidenceTypeOption.Own, + }, + HmdaValues = new Hmda + { + HmdaNoticeDisclosed = true, + Races = + [ + Race.NotProvided, + ], + Ethnicities = + [ + Ethnicity.OtherHispanicLatino, + ], + OtherHispanicLatinoEthnicity = "Argentinean", + Sexes = + [ + Sex.Female, + Sex.Male, + ], + }, + Employers = + [ + new Employer(employerId: new Guid("57df49cc-ff09-4660-aa47-54fdb5e08017")) + { + Language = LanguageOption.English, + Name = "Purple Widgets Inc", + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + MonthsEmployed = 120, + StartDate = new DateTime(2008, 01, 01), + EndDate = new DateTime(2018,01,01), + Title = "Lead Widget Painter", + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Job", + AmountPerPeriod = 100m, + Frequency = Frequency.Hourly, + HoursPerWeek = 40, + IncomeStatus = IncomeStatus.IncomeStatus_Included, + }, + }, + ], + } + }; + + private static XElement BuildApplication(LoanApplication loanApplication) + { + var createAppElement = new LoanApplicationRequestBuilder(loanApplication, TestCommon.GetTestSettings(), + new Guid("741be6b4-9f42-4efd-871f-f5d99c0ef122"), new Guid("e814d860-14fe-4bef-be9b-21be3dce1edf"), + new Guid("3090a5a1-d493-4865-ba44-8fd390dfec44"), new TestDistributionChannelLookupService()).Build(); + + return createAppElement; + } + + [Fact] + public void Validate() + { + ModelValidator.Validate(_loanApplication.Vehicle); + + var createAppElement = + new LoanApplicationRequestBuilder(_loanApplication, TestCommon.GetTestSettings()).Build(); + + Assert.Empty(TestCommon.Validate(createAppElement)); + } + + [Fact] + public void CreateApplication() + { + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateMotorcyleApplication() + { + var motorcycleApplication = _loanApplication; + motorcycleApplication.Vehicle.Type = VehicleType.Motorcycle; + motorcycleApplication.Vehicle.Make = "Honda"; + motorcycleApplication.Vehicle.Model = "Gold Wing"; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateMotorcycleApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreatePreApprovalApplication() + { + var preApprovalApplication = _loanApplication; + preApprovalApplication.LoanType = LoanType.V_VehiclePreapproval; + preApprovalApplication.Vehicle = null; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreatePreApprovalApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateBoatApplication() + { + var boatApplication = _loanApplication; + boatApplication.LoanType = LoanType.M_Boat; + boatApplication.Vehicle.Type = VehicleType.Boat; + + boatApplication.Vehicle.BoatAddress = new Address(new Guid("60899299-c5b3-4683-bd3b-02a8ab1af35e")) + { + Address1 = "123 Boat Location Ave", + PostalCode = "12345", + City = "Schenectady", + State = StateOption.NY, + Country = CountryOption.UnitedStates + }; + + boatApplication.Vehicle.BoatLength = 16; + + boatApplication.Vehicle.TrailerMake = "Trailer Make"; + boatApplication.Vehicle.TrailerType = "TriAxle"; + boatApplication.Vehicle.TrailerYear = 2010; + + boatApplication.Vehicle.MotorMake = "Motor Make"; + boatApplication.Vehicle.MotorModel = "Motor Model"; + boatApplication.Vehicle.MotorType = 2; + boatApplication.Vehicle.MotorYear = 2005; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateBoatApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateAtvApplication() + { + var boatApplication = _loanApplication; + boatApplication.LoanType = LoanType.P_Powersport; + boatApplication.Vehicle.Type = VehicleType.ATV; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateAtvApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreatePwcApplication() + { + var boatApplication = _loanApplication; + boatApplication.LoanType = LoanType.P_Powersport; + boatApplication.Vehicle.Type = VehicleType.PWC; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreatePwcApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateRvPowersportApplication() + { + var boatApplication = _loanApplication; + boatApplication.LoanType = LoanType.P_Powersport; + boatApplication.Vehicle.Type = VehicleType.RecreationalVehicle; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateRvPowersportApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateSnowmobileApplication() + { + var boatApplication = _loanApplication; + boatApplication.LoanType = LoanType.P_Powersport; + boatApplication.Vehicle.Type = VehicleType.Snowmobile; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateSnowmobileApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateMotorhomeApplication() + { + var motorhomeApplication = _loanApplication; + motorhomeApplication.LoanType = LoanType.T_TrailerMotorhome; + motorhomeApplication.Vehicle.Type = VehicleType.TrailerMotorHome; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = "ExampleXML" + System.IO.Path.DirectorySeparatorChar + "Vehicle" + System.IO.Path.DirectorySeparatorChar + "CreateTrailerMotorhomeApp.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateApplicationWithNoteAndVehicleSeller() + { + _loanApplication.Note = "Example application note."; + _loanApplication.Vehicle.Seller = VehicleSeller.Private_Party; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = @"ExampleXML/Vehicle/CreateApplicationWithNoteAndVehicleSeller.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + + [Fact] + public void CreateApplicationWithVehicleSeller() + { + _loanApplication.Vehicle.Seller = VehicleSeller.Dealer; + + var createAppElement = BuildApplication(_loanApplication); + + string comparePath = @"ExampleXML/Vehicle/CreateApplicationWithVehicleSeller.xml"; + + var results = TestCommon.Compare(createAppElement, comparePath); + + Assert.Empty(results); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/json/TestLienPositions.json b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/json/TestLienPositions.json new file mode 100644 index 0000000..2ee1b76 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/json/TestLienPositions.json @@ -0,0 +1,54 @@ + +{ + "first": + [ + { + "hasFirstMortgage": false, + "hasSecondMortgage": false, + "payoffFirstMortgage": false, + "payoffSecondMortgage": false + }, + { + "hasFirstMortgage": true, + "hasSecondMortgage": false, + "payoffFirstMortgage": true, + "payoffSecondMortgage": false + }, + { + "hasFirstMortgage": false, + "hasSecondMortgage": true, + "payoffFirstMortgage": false, + "payoffSecondMortgage": true + } + ], + "second": + [ + { + "hasFirstMortgage": true, + "hasSecondMortgage": false, + "payoffFirstMortgage": false, + "payoffSecondMortgage": false + }, + { + "hasFirstMortgage": false, + "hasSecondMortgage": true, + "payoffFirstMortgage": false, + "payoffSecondMortgage": false + }, + { + "hasFirstMortgage": true, + "hasSecondMortgage": true, + "payoffFirstMortgage": true, + "payoffSecondMortgage": false + } + ], + "third": + [ + { + "hasFirstMortgage": true, + "hasSecondMortgage": true, + "payoffFirstMortgage": false, + "payoffSecondMortgage": false + } + ] +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestCommon.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestCommon.cs new file mode 100644 index 0000000..6b0b203 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestCommon.cs @@ -0,0 +1,254 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Org.XmlUnit.Builder; +using Org.XmlUnit.Diff; +using Org.XmlUnit.Input; +using Org.XmlUnit.Validation; +using XmlUnit = Org.XmlUnit; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Tests +{ + public static class TestCommon + { + + public static CrifSettings GetTestSettings() + { + var settings = new CrifSettings + { + ApiSettings = new ApiConnectionSettings + { + Username = "LandmarkCUImportUser", + InterfaceId = 600002, + OrgCode = "LandmarkCU", + OrgId = 4, + }, + SourceChannels = new SourceChannelSettings + { + DefaultSourceChannelId = 2399724, + CreditCardSourceChannelId = 2294092, + }, + ProductIds = new ProductIdSettings + { + CC_CashBack = 132, + CC_Rewards = 133, + CC_Student = 134, + CC_Secure = 136, + EQ_HELOC_Flex = 139, + EQ_Home_Equity_Fixed = 148, + EQ_Mtg_Refi_Express = 149, + EQ_Mfg_Home_And_Land = 146, + V_Vehicle = 125, + V_VehiclePreapproval = 126, + M_Boat = 129, + P_Powersport = 131, + T_TrailerMotorhome = 130, + S_Signature = 140 + }, + ServicingChannels = new ServicingChannelSettings + { + DefaultServicingChannel = 1698432, + }, + CustomFieldMappings = new CustomFieldMappings + { + LoanTypeToPromoId = new Dictionary() + { + { "132", "-119" }, + { "133", "-119" }, + { "134", "-119" }, + { "136", "-119" }, + { "139", "-115" }, + { "148", "-115" }, + { "149", "-115" }, + { "146", "-115" }, + { "125", "-116" }, + { "126", "-116" }, + { "129", "-116" }, + { "131", "-116" }, + { "130", "-116" }, + { "140", "-118" } + } + }, + CustomFields = new CustomFieldConfigs + { + CustomPropertyRenovationFieldData = new List() + { + { + new CustomPropertyStatusData + { + CustomFieldId = "-109", + ProductCategoryId = "28", + SortOrder = "1", + YesId = "270", + NoId = "269", + NaId = "301" + } + } + }, + CustomPropertySaleFieldData = new List() + { + { + new CustomPropertyStatusData + { + CustomFieldId = "-110", + ProductCategoryId = "28", + SortOrder = "2", + YesId = "272", + NoId = "271", + NaId = "302" + } + } + }, + CustomJointCreditFieldData = + [ + new CustomJointCreditData + { + CustomFieldId = "-127", + ProductCategoryId = "28", + SortOrder = "25", + IndividuallyId = "314", + JointlyId = "315" + }, + new CustomJointCreditData + { + CustomFieldId = "-126", + ProductCategoryId = "27", + SortOrder = "25", + IndividuallyId = "312", + JointlyId = "313" + }, + new CustomJointCreditData + { + CustomFieldId = "-125", + ProductCategoryId = "26", + SortOrder = "25", + IndividuallyId = "310", + JointlyId = "311" + }, + new CustomJointCreditData + { + CustomFieldId = "-124", + ProductCategoryId = "24", + SortOrder = "25", + IndividuallyId = "308", + JointlyId = "309" + }, + new CustomJointCreditData + { + CustomFieldId = "-122", + ProductCategoryId = "25", + SortOrder = "25", + IndividuallyId = "303", + JointlyId = "304" + } + ], + ProductIdToCategoryId = new Dictionary() + { + {"107" , "22"}, + {"108" , "22"}, + {"109" , "22"}, + {"110" , "22"}, + {"111" , "22"}, + {"112" , "22"}, + {"113" , "22"}, + {"114" , "23"}, + {"115" , "22"}, + {"116" , "22"}, + {"117" , "22"}, + {"118" , "22"}, + {"119" , "22"}, + {"120" , "22"}, + {"121" , "22"}, + {"122" , "22"}, + {"123" , "22"}, + {"124" , "22"}, + {"125" , "27"}, + {"126" , "27"}, + {"127" , "27"}, + {"128" , "27"}, + {"129" , "27"}, + {"130" , "27"}, + {"131" , "27"}, + {"132" , "24"}, + {"133" , "24"}, + {"134" , "24"}, + {"135" , "24"}, + {"136" , "24"}, + {"137" , "24"}, + {"138" , "24"}, + {"139" , "28"}, + {"140" , "25"}, + {"141" , "26"}, + {"142" , "26"}, + {"143" , "25"}, + {"144" , "25"}, + {"145" , "25"}, + {"146" , "28"}, + {"147" , "28"}, + {"148" , "28"}, + {"149" , "28"}, + {"150" , "27"}, + {"151" , "24"} + } + } + + }; + + return settings; + } + + public static ICollection Compare(XElement xml1, XElement xml2) + { + var source1 = Input.FromNode(xml1).Build(); + var source2 = Input.FromNode(xml2).Build(); + + return Compare(source1, source2); + } + + public static ICollection Compare(XElement xml1, string xmlLocation) + { + var source1 = Input.FromNode(xml1).Build(); + var source2 = Input.FromFile(xmlLocation).Build(); + + return Compare(source1, source2); + } + + private static ICollection Compare(XmlUnit.ISource source1, XmlUnit.ISource source2) + { + var diff = DiffBuilder.Compare(source2) + .WithTest(source1) + .CheckForIdentical() + .Build(); + + var similar = diff.Differences.Where(x => x.Result == ComparisonResult.SIMILAR).ToList(); + var different = diff.Differences.Where(x => x.Result == ComparisonResult.DIFFERENT).ToList(); + + + //TODO #243: Apparently this will be false if elements are in different orders, but diff.Differences will have data -_- + var result = diff.HasDifferences(); + + return different; + } + + public static IEnumerable Validate(XNode xmlData) + { + Validator v = Validator.ForLanguage(Languages.W3C_XML_SCHEMA_NS_URI); + v.SchemaSources = [ + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "actionAPI.XSD"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "appSchema.xsd"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "commonSchema.xsd"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "maintSchema.xsd"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "msgSchema.xsd"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "qoSchema.xsd"), + new StreamSource("XSDs" + System.IO.Path.DirectorySeparatorChar + "utilSchema.xsd"), + ]; + ValidationResult r = v.ValidateInstance( + Input.FromNode(xmlData) + .Build()); + + return r.Problems; + // return r.Valid; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestDistributionChannelLookupService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestDistributionChannelLookupService.cs new file mode 100644 index 0000000..7e2c507 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestDistributionChannelLookupService.cs @@ -0,0 +1,13 @@ +using CRIF.ACTion.Services.Landmark.DistributionChannel; + +namespace CRIF.ACTion.Tests +{ + public class TestDistributionChannelLookupService : IDistributionChannelLookupService + { + public int GetDistributionChannelByZipCode(string zipCode) + { + //Always return this value for testing + return TestCommon.GetTestSettings().ServicingChannels.DefaultServicingChannel; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPI.XSD b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPI.XSD new file mode 100644 index 0000000..14279bc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPI.XSD @@ -0,0 +1,1739 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPIResponse.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPIResponse.xsd new file mode 100644 index 0000000..f437ef7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPIResponse.xsd @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/appSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/appSchema.xsd new file mode 100644 index 0000000..f19a34d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/appSchema.xsd @@ -0,0 +1,3060 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + createRiskManagerExchageRequest is to indicate that the import came from external system (most likely DAO) and it requires few special steps (like RiskManagerExchange insertion) to be done + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/commonSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/commonSchema.xsd new file mode 100644 index 0000000..1d9e097 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/commonSchema.xsd @@ -0,0 +1,8712 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/maintSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/maintSchema.xsd new file mode 100644 index 0000000..7b1299d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/maintSchema.xsd @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/msgSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/msgSchema.xsd new file mode 100644 index 0000000..627464e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/msgSchema.xsd @@ -0,0 +1,1346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/qoSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/qoSchema.xsd new file mode 100644 index 0000000..532d4c9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/qoSchema.xsd @@ -0,0 +1,3289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TRUE means that non-ACTion products that can be part of cross-platform cross-sell offer should be also listed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/utilSchema.xsd b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/utilSchema.xsd new file mode 100644 index 0000000..4e9157d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/utilSchema.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/additional_test_args.runsettings b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/additional_test_args.runsettings new file mode 100644 index 0000000..f56e8d4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/additional_test_args.runsettings @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + .*microsoft.* + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/CRIF.ACTion.csproj b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/CRIF.ACTion.csproj new file mode 100644 index 0000000..60631f8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/CRIF.ACTion.csproj @@ -0,0 +1,40 @@ + + + + false + enable + net8.0 + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderBase.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderBase.cs new file mode 100644 index 0000000..d30d86e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderBase.cs @@ -0,0 +1,33 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Common.Builders +{ + public abstract class BuilderBase : IBuildable + { + public readonly XNamespace CrifRequestSectionNamespace; + + private protected CrifSettings _settings; + + public BuilderBase(XNamespace mainSectionNamespace, CrifSettings settings) + { + CrifRequestSectionNamespace = mainSectionNamespace; + + _settings = settings; + } + + public virtual XElement Build(params XElement[] content) + { + return new XElement(CrifNamespaces.Empty + "actionAPI", + new XAttribute("version", 1), + new XAttribute("xmlns", ""), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.Common.Prefix, CrifNamespaces.Common.ToString()), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.App.Prefix, CrifNamespaces.App.ToString()), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.Messages.Prefix, CrifNamespaces.Messages.ToString()), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.QO.Prefix, CrifNamespaces.QO.ToString()), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.Maint.Prefix, CrifNamespaces.Maint.ToString()), + new XAttribute(XNamespace.Xmlns + CrifNamespaces.XSI.Prefix, CrifNamespaces.XSI.ToString()), + content); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderUtils.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderUtils.cs new file mode 100644 index 0000000..9ffd804 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderUtils.cs @@ -0,0 +1,10 @@ +namespace CRIF.ACTion.Common.Builders +{ + public class BuilderUtils + { + public static string FormatCrifData(string data) + { + return data?.ToUpper(); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/CrifRequestSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/CrifRequestSection.cs new file mode 100644 index 0000000..5dfc771 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/CrifRequestSection.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Common.Builders +{ + public class CrifRequestSection : IBuildable + { + private readonly XNamespace SectionNamespace; + private readonly string SectionName; + private readonly List Content; + private readonly List Attributes; + + private protected readonly CrifSettings _settings; + + private protected CrifRequestSection(XNamespace sectionNamespace, string sectionName, CrifSettings settings) + { + SectionNamespace = sectionNamespace; + SectionName = sectionName; + + Content = []; + Attributes = []; + + _settings = settings; + } + + public XElement Build(params XElement[] content) + { + var element = new XElement(SectionNamespace + SectionName, Attributes); + element.Add([.. Content]); + + return element; + } + + public void AddContent(XObject element) + { + Content.Add(element); + } + + public void AddAttribute(XObject attribute) + { + Attributes.Add(attribute); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/IBuildable.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/IBuildable.cs new file mode 100644 index 0000000..5f907bb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/IBuildable.cs @@ -0,0 +1,9 @@ +using System.Xml.Linq; + +namespace CRIF.ACTion.Common.Builders +{ + public interface IBuildable + { + XElement Build(params XElement[] content); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ContactMechanisms.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ContactMechanisms.cs new file mode 100644 index 0000000..4ad6545 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ContactMechanisms.cs @@ -0,0 +1,107 @@ +using Landmark.OLA.Models.Individuals.Enums; + +namespace CRIF.ACTion.Common +{ + public static class ContactMechanisms + { + public static CrifContactMechanism GetPhoneType(PhoneType type) + { + switch (type) + { + case PhoneType.Home: + return CrifContactMechanism.HomePhone; + case PhoneType.Mobile: + return CrifContactMechanism.CellPhone; + default: + throw new ArgumentOutOfRangeException(nameof(type), "Unexpected PhoneType value"); + } + } + + public static ContactMechanismCategory GetContactMechanismCategory(CrifContactMechanism mechanism) + { + switch (mechanism) + { + case CrifContactMechanism.HomePhone: + case CrifContactMechanism.WorkPhone: + case CrifContactMechanism.CellPhone: + case CrifContactMechanism.Fax: + return ContactMechanismCategory.Phone; + case CrifContactMechanism.EMail: + case CrifContactMechanism.URL: + return ContactMechanismCategory.EmailOrUrl; + case CrifContactMechanism.CurrentAddress: + case CrifContactMechanism.MailingAddress: + case CrifContactMechanism.PreviousAddress: + case CrifContactMechanism.RealEstateAddress: + case CrifContactMechanism.ParkContactAddress: + return ContactMechanismCategory.Address; + case CrifContactMechanism.NoneSelected: + default: + throw new ArgumentOutOfRangeException(nameof(mechanism), "ContactMechanismCategory not determined"); + } + } + + /// + /// ContactMechanismTypeEnum values from CRIF's commonSchema.xsd + /// + public enum CrifContactMechanism + { + NoneSelected, + HomePhone, + WorkPhone, + CellPhone, + Fax, + EMail, + CurrentAddress, + URL, + IM, + DriversLicenceState, + MailingAddress, + PreviousAddress, + ContractAddress, + LandlordPostalAddress, + LandlordPhone, + LandlordFax, + InventoryAddress, + AttorneyPostalAddress, + AttorneyPhone, + AttorneyFax, + AttorneyEMailAddress, + CPAPostalAddress, + CPAPhone, + CPAFax, + CPAEMailAddress, + VendorOFACEmail, + RealEstateAddress, + ParkContactAddress, + ParkPhone, + MarinaAddress, + MarinaStorageAddress, + InsurerPostalAddress, + InsurerPhone, + InsurerFax, + SellerPostalAddress, + SellerPhone, + SellerFax, + TitleClerkFax, + Pager, + AdHocPhone, + AdHocFax, + AdHocEMail, + AdHocAddress, + StatementPostalAddress, + BillingPostalAddress, + PropertyContactPhone, + } + + /// + /// Per CRIF Data Mapping document + /// + public enum ContactMechanismCategory + { + Phone = 1, + Address = 2, + EmailOrUrl = 3, + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/CrifNamespaces.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/CrifNamespaces.cs new file mode 100644 index 0000000..8a6f942 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/CrifNamespaces.cs @@ -0,0 +1,70 @@ +using System.Xml; +using System.Xml.Linq; + +namespace CRIF.ACTion.Common +{ + public class CrifNamespaces + { + public static CrifNamespace App => new("App"); + + public static CrifNamespace Common => new("Common"); + + public static CrifNamespace Maint => new("Maint"); + + public static CrifNamespace Messages => new("msg", "Messages"); + + public static CrifNamespace QO => new("QO"); + + public static CrifNamespace Util => new("Util"); + + public static XNamespace Empty => ""; + + public static CrifNamespace XSI => new("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + + public static XmlNamespaceManager GetNamespaceManager(XElement xElement) + { + var namespaceManager = new XmlNamespaceManager(xElement.CreateReader().NameTable); + + namespaceManager.AddNamespace(Messages.Prefix, Messages.Namespace.ToString()); + namespaceManager.AddNamespace(App.Prefix, App.Namespace.ToString()); + namespaceManager.AddNamespace(Maint.Prefix, Maint.Namespace.ToString()); + namespaceManager.AddNamespace(Util.Prefix, Util.Namespace.ToString()); + namespaceManager.AddNamespace(Common.Prefix, Common.Namespace.ToString()); + + return namespaceManager; + } + } + + public class CrifNamespace + { + public XNamespace Namespace { get; private set; } + public string Prefix { get; private set; } + + public CrifNamespace(string prefix, XNamespace namespaceValue) + { + Prefix = prefix; + Namespace = namespaceValue; + } + + public CrifNamespace(XNamespace namespaceValue) + { + Prefix = namespaceValue.ToString().ToLower(); + Namespace = namespaceValue; + } + + public override string ToString() + { + return Namespace.ToString(); + } + + public static implicit operator XNamespace(CrifNamespace crifNamespace) + { + return crifNamespace.Namespace; + } + + public static XName operator +(CrifNamespace crifNamespace, string localName) + { + return crifNamespace.Namespace + localName; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Dates.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Dates.cs new file mode 100644 index 0000000..c7eba7d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Dates.cs @@ -0,0 +1,16 @@ + +namespace CRIF.ACTion.Common +{ + public static class Dates + { + public static string GetFormattedDOB(DateTime dob) + { + return dob.ToString("yyyy-MM-dd"); + } + + public static string GetFormattedApplicationDate(DateTime applicationDate) + { + return applicationDate.ToUniversalTime().ToString("o"); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ICrifNamespaces.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ICrifNamespaces.cs new file mode 100644 index 0000000..0c942ed --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ICrifNamespaces.cs @@ -0,0 +1,19 @@ +using System.Xml; +using System.Xml.Linq; + +namespace CRIF.ACTion.Common +{ + public interface ICrifNamespaces + { + XNamespace App { get; } + XNamespace Common { get; } + XNamespace Maint { get; } + XNamespace Messages { get; } + XNamespace QO { get; } + XNamespace Util { get; } + + XNamespace Empty { get; } + + XmlNamespaceManager GetNamespaceManager(XElement xElement); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/IResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/IResponseParser.cs new file mode 100644 index 0000000..9b93d49 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/IResponseParser.cs @@ -0,0 +1,9 @@ +using System.Xml.Linq; + +namespace CRIF.ACTion.Common.Parsers +{ + public interface IResponseParser + { + T Parse(XElement xElement); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/XElementExtensions.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/XElementExtensions.cs new file mode 100644 index 0000000..bb688e7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/XElementExtensions.cs @@ -0,0 +1,34 @@ +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace CRIF.ACTion.Common.Parsers +{ + public static class XElementExtensions + { + /* + Using XPathSelectElement on an XElement starts with the XElement's parent XDocument, + unless you convert the XElement itself to an XDocument + */ + + public static XElement XElementXPathSelectElement(this XElement node, string expression) + { + return (new XDocument(node)).XPathSelectElement(expression); + } + + public static XElement XElementXPathSelectElement(this XNode node, string expression, IXmlNamespaceResolver resolver) + { + return (new XDocument(node)).XPathSelectElement(expression, resolver); + } + + public static IEnumerable XElementXPathSelectElements(this XElement node, string expression) + { + return (new XDocument(node)).XPathSelectElements(expression); + } + + public static IEnumerable XElementXPathSelectElements(this XElement node, string expression, IXmlNamespaceResolver resolver) + { + return (new XDocument(node)).XPathSelectElements(expression, resolver); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestBuilder.cs new file mode 100644 index 0000000..193b519 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestBuilder.cs @@ -0,0 +1,24 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; + +namespace CRIF.ACTion.Configuration +{ + public class ConfigurationRequestBuilder : BuilderBase + { + private readonly string SettingName; + + public ConfigurationRequestBuilder(string settingName, CrifSettings settings) + : base(CrifNamespaces.Empty, settings) + { + SettingName = settingName; + } + + public override XElement Build(params XElement[] content) + { + var retrieveCriffConfigurationElement = new XElement(CrifNamespaces.Empty + SettingName); + return base.Build(retrieveCriffConfigurationElement); + } + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestResponseParser.cs new file mode 100644 index 0000000..c2a627c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestResponseParser.cs @@ -0,0 +1,78 @@ +using System.Xml.Linq; +using Serilog; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Parsers; + +namespace CRIF.ACTion.Configuration +{ + public class ConfigurationRequestResponseParser : IResponseParser> + { + private const string _channelsPath = "/actionAPI/getChannels/msg:maintExportResponse/msg:maintenanceEntities/maint:parties/common:channel/common:channelClassifications/common:channelClassification/common:channelClassificationItem"; + + public ConfigurationRequestResponseParser() + { + } + + public SortedList Parse(XElement xElement) + { + SortedList mappingList = []; + + try + { + var namespaceManager = CrifNamespaces.GetNamespaceManager(xElement); + IEnumerable channelNodeList = xElement.XElementXPathSelectElements(_channelsPath, namespaceManager); + + foreach (XElement channelNode in channelNodeList) + { + try + { + string nodeAttributeValue = StandardizedStringValue(channelNode.Attribute(CrifNamespaces.Common + "name").Value); + if (ChannelIsActive(nodeAttributeValue)) + { + + string partyIdString = channelNode.Parent.Parent.Parent.Attribute(CrifNamespaces.Common + "partyID").Value; + string name = channelNode.Parent.Parent.Parent.Attribute(CrifNamespaces.Common + "name").Value; + _ = int.TryParse(partyIdString, out int partyId); + mappingList.Add(partyId, name); + } + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF BranchName Configuration Response", ex.Message, ex.StackTrace); + } + } + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF BranchName Configuration Response", ex.Message, ex.StackTrace); + } + return mappingList; + } + + private static bool ChannelIsActive(string nodeAttributeValue) + { + bool channelIsActive = false; + if (!string.IsNullOrWhiteSpace(nodeAttributeValue)) + { + channelIsActive = + ( + nodeAttributeValue.Equals("BRANCH") || + nodeAttributeValue.Equals("INTERNET") || + nodeAttributeValue.Equals("ON-LINE") + ); + } + return channelIsActive; + } + + private static string StandardizedStringValue(string InputString) + { + string standardizedString = ""; + if (!string.IsNullOrWhiteSpace(InputString)) + { + standardizedString = InputString.Trim().ToUpper(); + } + return standardizedString; + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifConstants.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifConstants.cs new file mode 100644 index 0000000..15138bc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifConstants.cs @@ -0,0 +1,86 @@ + +namespace CRIF.ACTion +{ + + /************ + This file should be migrated to appsettings.json and the CrifSettings class + *************/ + public class CrifConstants + { + public static TimeSpan SubmissionTimeoutInMinutes => new(0, 2, 0); + + public static int EQ_ListedForSale_Field_Id = -110; + public static int EQ_ListedForSale_Field_True = 272; + public static int EQ_ListedForSale_Field_False = 271; + + public static int EQ_CurrentlyBeingRemodeledOrImproved_Field_Id = -109; + public static int EQ_CurrentlyBeingRemodeledOrImproved_Field_True = 270; + public static int EQ_CurrentlyBeingRemodeledOrImproved_Field_False = 269; + + public static int Borrower_IsApplicantNonSigningSpouse_Field_Id = -73; + public static int Borrower_IsApplicantNonSigningSpouse_Field_Borrower = 85; + public static int Borrower_IsApplicantNonSigningSpouse_Field_NonSigningMarriedToPrimary = 84; + public static int Borrower_IsApplicantNonSigningSpouse_Field_NonSigningMarriedToCo = 83; + + public static int TCPA_Opt_Out_Field_Id = -112; + + public static Dictionary CollateralIds = new Dictionary + { + //Take from CRIF database: select * from v_RptLuCollateralType_v001 + {Collateral.Vehicle, 1}, + {Collateral.ResidentialRealEstate, 2}, + {Collateral.Marine, 3}, + {Collateral.ManufacturedHome, 4}, + {Collateral.RecreationalVehicle, 5}, + {Collateral.Stocks, 6}, + {Collateral.Bonds, 7}, + {Collateral.Insurance, 8}, + {Collateral.CDs, 9}, + {Collateral.CDsOrSavings, 18}, + {Collateral.Collateral401k, 10}, + {Collateral.CommercialRealEstate, 11}, + {Collateral.AccountsReceivable, 12}, + {Collateral.SecuredOther, 13}, + {Collateral.ManufacturedHomeAndLand, 14}, + {Collateral.Motorcycle, 15}, + {Collateral.ATV, 16}, + {Collateral.Snowmobile, 17}, + }; + + public static int SpecialHandlingCode_OneBorrower_with_NonapplicantSpouse = 8; + + public static Dictionary StipulationsToShow = new Dictionary + { + //matches occur by int ID. Text is only used to easily identify stipulations. + {20, "A Personal Guaranty form is required for all business loans" }, + {92, "Cards can only be issued to business owner and owner's spouse"}, + {58, "Final Approval is dependent on Appraisal, Title, and Flood"}, + {82, "Landmark Business savings account must be established."}, + {85, "Must show proof borrower is a current student."}, + {59, "Two years of income verification is required"}, + }; + + public enum Collateral + { + //These enum int values have no relation to CRIF's int values. Use the dictionary above instead. + Vehicle, + ResidentialRealEstate, + Marine, + ManufacturedHome, + RecreationalVehicle, + Stocks, + Bonds, + Insurance, + CDs, + CDsOrSavings, + Collateral401k, //enums must start with a letter + CommercialRealEstate, + AccountsReceivable, + SecuredOther, + ManufacturedHomeAndLand, + Motorcycle, + ATV, + Snowmobile, + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifSettings.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifSettings.cs new file mode 100644 index 0000000..a9ad558 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifSettings.cs @@ -0,0 +1,106 @@ + +namespace CRIF.ACTion.Configuration +{ + public class CrifSettings + { + public ApiConnectionSettings ApiSettings { get; set; } + public SourceChannelSettings SourceChannels { get; set; } + public ProductIdSettings ProductIds { get; set; } + public ServicingChannelSettings ServicingChannels { get; set; } + public CustomFieldMappings CustomFieldMappings { get; set; } + public CustomFieldConfigs CustomFields { get; set; } + public FormatCategorySettings FormatCategoryMappings { get; set; } + } + + public class CustomJointCreditData + { + public string CustomFieldId { get; set; } + + public string ProductCategoryId { get; set; } + + public string SortOrder { get; set; } + + public string IndividuallyId { get; set; } + + public string JointlyId { get; set; } + } + + public class CustomPropertyStatusData + { + public string CustomFieldId { get; set; } + + public string ProductCategoryId { get; set; } + + public string SortOrder { get; set; } + + public string YesId { get; set; } + + public string NoId { get; set; } + + public string NaId { get; set; } + } + + public class CustomFieldConfigs + { + public List CustomPropertyRenovationFieldData { get; set; } + public List CustomPropertySaleFieldData { get; set; } + public List CustomJointCreditFieldData { get; set; } + public Dictionary ProductIdToCategoryId { get; set; } + } + + public class CustomFieldMappings + { + public Dictionary LoanTypeToPromoId { get; set; } + public Dictionary CustomFieldID { get; set; } + public Dictionary MunicipalityTypeCode { get; set; } + } + + public class CustomFieldIDMappings + { + public Dictionary CustomFieldID { get; set; } + } + + public class ApiConnectionSettings + { + public string EndpointUrl { get; set; } + public string Username { get; set; } + public int InterfaceId { get; set; } + public string OrgCode { get; set; } + public int OrgId { get; set; } + } + + public class SourceChannelSettings + { + public int DefaultSourceChannelId { get; set; } + public int CreditCardSourceChannelId { get; set; } + } + + public class ProductIdSettings + { + public int CC_CashBack { get; set; } + public int CC_Rewards { get; set; } + public int CC_Student { get; set; } + public int CC_Secure { get; set; } + public int EQ_HELOC_Flex { get; set; } + public int EQ_Home_Equity_Fixed { get; set; } + public int EQ_Mtg_Refi_Express { get; set; } + public int EQ_Mfg_Home_And_Land { get; set; } + public int V_Vehicle { get; set; } + public int V_VehiclePreapproval { get; set; } + public int M_Boat { get; set; } + public int P_Powersport { get; set; } + public int T_TrailerMotorhome { get; set; } + public int S_Signature { get; set; } + } + + public class FormatCategorySettings + { + public Dictionary Categories { get; set; } + public Dictionary DocumentToCategory { get; set; } + } + + public class ServicingChannelSettings + { + public int DefaultServicingChannel { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CustomFieldRequestResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CustomFieldRequestResponseParser.cs new file mode 100644 index 0000000..ef73b68 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CustomFieldRequestResponseParser.cs @@ -0,0 +1,222 @@ +using System.Xml.Linq; +using Serilog; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Parsers; + +namespace CRIF.ACTion.Configuration +{ + public class CustomFieldRequestResponseParser : IResponseParser + { + private const string _customFieldsPath = "/actionAPI/getCustomFields/msg:maintExportResponse/msg:maintenanceEntities/maint:maintenanceEntity/maint:fields/common:customLookupField"; + + public CustomFieldRequestResponseParser() + { + } + + public CustomFieldConfigs Parse(XElement xElement) + { + CustomFieldConfigs customFieldConfig = new(); + + try + { + var namespaceManager = CrifNamespaces.GetNamespaceManager(xElement); + IEnumerable customFieldNodeList = xElement.XElementXPathSelectElements(_customFieldsPath, namespaceManager); + List RenovationMatchCriteria = ["IS THE PROPERTY CURRENTLY BEING REMODELED OR IMPROVED?"]; + List SaleMatchCriteria = ["IS THE PROPERTY CURRENTLY LISTED FOR SALE?"]; + List JointMatchCriteria = ["INTENT FOR JOINT CREDIT"]; + + customFieldConfig.CustomPropertyRenovationFieldData = ParseCustomPropertyStatusData(customFieldNodeList, RenovationMatchCriteria); + customFieldConfig.CustomPropertySaleFieldData = ParseCustomPropertyStatusData(customFieldNodeList, SaleMatchCriteria); + customFieldConfig.CustomJointCreditFieldData = ParseCustomJointCreditData(customFieldNodeList, JointMatchCriteria); + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF Custom Field Configuration Response", ex.Message, ex.StackTrace); + } + return customFieldConfig; + } + + private List ParseCustomJointCreditData(IEnumerable customFieldNodeList, List MatchCriteria) + { + List customDataSet = []; + + foreach (XElement node in customFieldNodeList) + { + try + { + string nodeAttributeValue = node.Attribute(CrifNamespaces.Common + "name").Value; + if (CustomFieldIsActive(nodeAttributeValue, MatchCriteria)) + { + string individuallyId = ""; + string jointlyId = ""; + string productCategoryId = ""; + + IEnumerable childNodes = node.Descendants().Elements(); + + foreach (XElement option in childNodes) + { + if (option.Name.LocalName.Equals("FIELDSELECTION", StringComparison.CurrentCultureIgnoreCase)) + { + var attributes = option.Attributes(); + + foreach (var attribute in attributes) + { + string idValue = ""; + string nameValue = ""; + + if (attribute.Name.LocalName.Equals("FIELDSELECTIONID", StringComparison.CurrentCultureIgnoreCase)) + { + idValue = attribute.Value; + nameValue = attribute.NextAttribute.Value.ToUpper(); + + switch (nameValue) + { + case "INDIVIDUALLY": + individuallyId = idValue; + break; + case "JOINTLY": + jointlyId = idValue; + break; + default: + break; + } + } + } + } + else if (option.Name.LocalName.ToUpper() == "PRODUCTCATEGORY") + { + productCategoryId = option.LastAttribute.Value; + } + } + + CustomJointCreditData data = new() + { + CustomFieldId = node.Attribute(CrifNamespaces.Common + "ID").Value, + ProductCategoryId = productCategoryId, + SortOrder = node.Attribute(CrifNamespaces.Common + "sortOrder").Value, + JointlyId = jointlyId, + IndividuallyId = individuallyId + }; + + customDataSet.Add(data); + } + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF Custom Field Configuration Response", ex.Message, ex.StackTrace); + } + } + + return customDataSet; + } + + private List ParseCustomPropertyStatusData(IEnumerable customFieldNodeList, List MatchCriteria) + { + List customDataSet = []; + + foreach (XElement node in customFieldNodeList) + { + try + { + string nodeAttributeValue = node.Attribute(CrifNamespaces.Common + "name").Value; + if (CustomFieldIsActive(nodeAttributeValue, MatchCriteria)) + { + string yesId = ""; + string noId = ""; + string naId = ""; + string productCategoryId = ""; + + IEnumerable childNodes = node.Descendants().Elements(); + + foreach (XElement option in childNodes) + { + if (option.Name.LocalName.Equals("FIELDSELECTION", StringComparison.CurrentCultureIgnoreCase)) + { + var attributes = option.Attributes(); + + foreach (var attribute in attributes) + { + string idValue = ""; + string nameValue = ""; + + if (attribute.Name.LocalName.ToUpper() == "FIELDSELECTIONID") + { + idValue = attribute.Value; + nameValue = attribute.NextAttribute.Value.ToUpper(); + + switch (nameValue) + { + case "YES": + yesId = idValue; + break; + case "NO": + noId = idValue; + break; + case "N/A (FLEX FIXED DRAWS ONLY)": + naId = idValue; + break; + default: + break; + } + } + } + } + else if (option.Name.LocalName.ToUpper() == "PRODUCTCATEGORY") + { + productCategoryId = option.LastAttribute.Value; + } + } + + CustomPropertyStatusData data = new CustomPropertyStatusData + { + CustomFieldId = node.Attribute(CrifNamespaces.Common + "ID").Value, + ProductCategoryId = productCategoryId, + SortOrder = node.Attribute(CrifNamespaces.Common + "sortOrder").Value, + NoId = noId, + YesId = yesId, + NaId = naId + }; + + customDataSet.Add(data); + } + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF Custom Field Configuration Response", ex.Message, ex.StackTrace); + } + } + + return customDataSet; + } + + private bool CustomFieldIsActive(string NodeAttributeValue, List ActiveMatchCriteria) + { + bool isActive = false; + foreach (string compare in ActiveMatchCriteria) + { + string standardizedNodeString = StandardizedStringValue(NodeAttributeValue); + string standardizedCompareString = StandardizedStringValue(compare); + + bool validCompareInput = !string.IsNullOrWhiteSpace(standardizedNodeString) && !string.IsNullOrWhiteSpace(compare); + if (validCompareInput && !isActive) + { + isActive = + ( + standardizedNodeString.Equals(standardizedCompareString) + ); + } + } + return isActive; + } + + private string StandardizedStringValue(string InputString) + { + string standardizedString = ""; + if (!string.IsNullOrWhiteSpace(InputString)) + { + standardizedString = InputString.Trim().ToUpper(); + } + return standardizedString; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ProductDetailsResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ProductDetailsResponseParser.cs new file mode 100644 index 0000000..f05bfde --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ProductDetailsResponseParser.cs @@ -0,0 +1,47 @@ +using System.Xml.Linq; +using Serilog; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Parsers; + +namespace CRIF.ACTion.Configuration +{ + public class ProductDetailsResponseParser : IResponseParser> + { + private const string _productDetailsPath = "/actionAPI/getProducts/msg:maintExportResponse/msg:maintenanceEntities/maint:maintenanceEntity/maint:products/common:product/common:productCategory"; + + public ProductDetailsResponseParser() + { + } + + public Dictionary Parse(XElement xElement) + { + Dictionary mappingList = []; + + try + { + var namespaceManager = CrifNamespaces.GetNamespaceManager(xElement); + IEnumerable channelNodeList = xElement.XElementXPathSelectElements(_productDetailsPath, namespaceManager); + + foreach (XElement channelNode in channelNodeList) + { + try + { + string productId = channelNode.Parent.Attribute(CrifNamespaces.Common + "productID").Value; + string categoryId = channelNode.Attribute(CrifNamespaces.Common + "productCategoryID").Value; + mappingList.Add(productId, categoryId); + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF Product information from node in Configuration Response", ex.Message, ex.StackTrace); + } + } + } + catch (Exception ex) + { + Log.Error("Error trying to parse CRIF Products Configuration Response", ex.Message, ex.StackTrace); + } + return mappingList; + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisionStatusBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisionStatusBuilder.cs new file mode 100644 index 0000000..c05f56e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisionStatusBuilder.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Decisioning +{ + public class DecisionStatusBuilder : BuilderBase + { + private readonly string ApplicationId; + private readonly string AppDetailId; + + public DecisionStatusBuilder(string applicationId, string appDetailId, CrifSettings settings) + : base(CrifNamespaces.Empty, settings) + { + ApplicationId = applicationId; + AppDetailId = appDetailId; + } + + public override XElement Build(params XElement[] content) + { + var getDecisioningElement = new XElement(CrifNamespaces.Empty + "getDecisionStatus", + new XAttribute(CrifNamespaces.Empty + "applicationID", ApplicationId), + new XAttribute(CrifNamespaces.Empty + "appDetailID", AppDetailId)); + + return base.Build(getDecisioningElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisioningStateResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisioningStateResponseParser.cs new file mode 100644 index 0000000..933c9dd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisioningStateResponseParser.cs @@ -0,0 +1,47 @@ +using System.Xml.Linq; +using Serilog; +using CRIF.ACTion.Common.Parsers; +using Landmark.OLA.Models.Decisioning.Enums; + +namespace CRIF.ACTion.Decisioning +{ + public class DecisioningStateResponseParser : IResponseParser + { + private const string SuccessMessage = "Success"; + private const string MissingRequredInformation = "WorkFlowBC::SendToWorkflow::(Decisioning) Process Instances not in correct state Please correct the Process Errors."; + + private string ExpectedLoanId; + + public DecisioningStateResponseParser() + { + } + + public void SetLoanId(string expectedLoanId) + { + ExpectedLoanId = expectedLoanId; + } + + public DecisioningState Parse(XElement xElement) + { + if (string.IsNullOrWhiteSpace(ExpectedLoanId)) + { + throw new ArgumentException("Must set valid ExpectedLoanId with SetLoanId(expectedLoanId)"); + } + + if (xElement.Value.Equals(SuccessMessage + ExpectedLoanId)) + { + return DecisioningState.Requested; + } + if (xElement.Value.Equals(MissingRequredInformation)) + { + Log.Debug("Application {loanId} is likely missing information required for decisioning.", SuccessMessage, ExpectedLoanId); + return DecisioningState.MissingRequiredInformation; + } + else + { + Log.Debug("Did not receive {SuccessMessage} for a SendToDecisioning call for Application {loanId}.", SuccessMessage, ExpectedLoanId); + return DecisioningState.Unknown; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsRequestBuilder.cs new file mode 100644 index 0000000..d29872d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsRequestBuilder.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Decisioning +{ + public class GetApplicationProductsRequestBuilder : BuilderBase + { + private readonly string ApplicationId; + + public GetApplicationProductsRequestBuilder(string applicationId, CrifSettings settings) + : base(CrifNamespaces.Empty, settings) + { + ApplicationId = applicationId; + } + + public override XElement Build(params XElement[] content) + { + var sendToDecisioningElement = new XElement(CrifNamespaces.Empty + "getApplicationProducts", + new XAttribute(CrifNamespaces.Empty + "applicationID", ApplicationId)); + + return base.Build(sendToDecisioningElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsResponseParser.cs new file mode 100644 index 0000000..3ba75a7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsResponseParser.cs @@ -0,0 +1,165 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Parsers; +using Serilog; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; + +namespace CRIF.ACTion.Decisioning +{ + public class GetApplicationProductsResponseParser : IResponseParser + { + private string LoanApplicationId; + + private const string ApplicationsPath = "/actionAPI/getApplicationProducts/msg:appExportResponse/msg:applications/app:application"; + private const string AppDetailsPath = "/app:application/app:appDetails"; + private readonly XName AppDetailStatusesName = CrifNamespaces.App + "appDetailStatuses"; + private readonly XName AppDetailStatusName = CrifNamespaces.App + "appDetailStatus"; + private readonly XName StatusAttribute = CrifNamespaces.App + "statusType"; + private const string DecisionTypeValue = "DecisionStatus"; + private readonly XName DecisionResultName = CrifNamespaces.App + "status"; + private readonly XName AppDetailStipulationsName = CrifNamespaces.App + "appDetailStipulations"; + private readonly XName StipulationName = CrifNamespaces.App + "stipulation"; + private readonly XName StipulationIdName = CrifNamespaces.Common + "stipulationID"; + + public GetApplicationProductsResponseParser() + { + } + + public void SetLoanId(string loanApplicationId) + { + LoanApplicationId = loanApplicationId; + } + + public DecisioningDetails Parse(XElement xElement) + { + if (string.IsNullOrWhiteSpace(LoanApplicationId)) + { + throw new ArgumentException("Must set valid LoanApplicationId with SetLoanId(loanApplicationId)"); + } + + var namespaceManager = CrifNamespaces.GetNamespaceManager(xElement); + + var applicationElement = xElement.XElementXPathSelectElement(ApplicationsPath, namespaceManager); + + if (applicationElement == null) + { + //CRIF didn't recognize this application ID + throw new ArgumentException("The Application Id was not recognized by the decisioning system."); + } + + if (!applicationElement.Attribute(CrifNamespaces.App + "applicationID").Value.Equals(LoanApplicationId)) + { + Log.Fatal("CRIF XML does not match expected loan ID {loanId}", LoanApplicationId); + throw new InvalidOperationException("Unexpected vendor response."); + } + + var appDetailElements = applicationElement.XElementXPathSelectElement(AppDetailsPath, namespaceManager).Elements().ToList(); + + for (int detailIndex = appDetailElements.Count - 1; detailIndex >= 0; detailIndex--) + { + var detail = appDetailElements[detailIndex]; + + var statuses = GetElementFromElement(detail, AppDetailStatusesName); + + var decision = GetElementFromElementWithAttributeIdentifier(statuses, AppDetailStatusName, StatusAttribute, DecisionTypeValue); + + if (decision == null) + { + appDetailElements.Remove(detail); + continue; + } + + var decisionResult = GetAttributeFromElement(decision, DecisionResultName); + + var status = (DecisioningResult)Enum.Parse(typeof(DecisioningResult), decisionResult.Value); + + if (status == DecisioningResult.DecisionStatus_Cancelled) + { + appDetailElements.Remove(detail); + } + } + + if (appDetailElements.Count != 1) + { + if (appDetailElements.Count == 0) + { + return new DecisioningDetails() + { + DecisioningState = DecisioningState.Unknown + }; + } + + throw new ArgumentException($"Application contains {appDetailElements.Count} appDetail elements. An appDetailID is required."); + } + + var appDetailStatuses = GetElementFromElement(appDetailElements.SingleOrDefault(), AppDetailStatusesName); + + var decisionStatus = GetElementFromElementWithAttributeIdentifier(appDetailStatuses, AppDetailStatusName, StatusAttribute, DecisionTypeValue); + + var stipulations = GetStipulations(appDetailElements.Single().Element(AppDetailStipulationsName)); + + if (decisionStatus == null) + { + return new DecisioningDetails(stipulations) + { + DecisioningState = DecisioningState.Unknown, + }; + } + else + { + var decisioningResult = GetAttributeFromElement(decisionStatus, DecisionResultName); + + var statusResult = (DecisioningResult)Enum.Parse(typeof(DecisioningResult), decisioningResult.Value); + + return new DecisioningDetails(stipulations) + { + DecisioningState = DecisioningState.Ready, + DecisioningResult = statusResult, + }; + } + } + + private ICollection GetStipulations(XElement appDetailStipulations) + { + var stipulations = new List(); + + if (appDetailStipulations is null) + { + return stipulations; + } + + foreach (var stipulationElement in appDetailStipulations.Elements()) + { + var appDetailStipulation = stipulationElement.Element(StipulationName); + + var stipulationIdString = appDetailStipulation.Attribute(StipulationIdName).Value; + + if (int.TryParse(stipulationIdString, out int stipulationId)) + { + if (CrifConstants.StipulationsToShow.ContainsKey(stipulationId)) + { + stipulations.Add(stipulationId); + } + } + } + + return stipulations; + } + + private static XElement GetElementFromElement(XElement element, XName elementToFind) + { + return element.Element(elementToFind); + } + + private static XElement GetElementFromElementWithAttributeIdentifier(XElement element, XName elementToSearch, XName attribute, string identifier) + { + return element.Elements(elementToSearch).SingleOrDefault(x => x.Attribute(attribute).Value.Equals(identifier)); + } + + private static XAttribute GetAttributeFromElement(XElement element, XName attribute) + { + return element.Attribute(attribute); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/SendToDecisioningRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/SendToDecisioningRequestBuilder.cs new file mode 100644 index 0000000..f01e558 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/SendToDecisioningRequestBuilder.cs @@ -0,0 +1,26 @@ +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using System.Xml.Linq; + +namespace CRIF.ACTion.Decisioning +{ + public class SendToDecisioningRequestBuilder : BuilderBase + { + private readonly string ApplicationId; + + public SendToDecisioningRequestBuilder(string applicationId, CrifSettings settings) + : base(CrifNamespaces.Empty, settings) + { + ApplicationId = applicationId; + } + + public override XElement Build(params XElement[] content) + { + var sendToDecisioningElement = new XElement(CrifNamespaces.Empty + "sendToDecisioning", + new XAttribute(CrifNamespaces.Empty + "applicationID", ApplicationId)); + + return base.Build(sendToDecisioningElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Documentation/CRIF API Risks and Issues.md b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Documentation/CRIF API Risks and Issues.md new file mode 100644 index 0000000..32c8d27 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Documentation/CRIF API Risks and Issues.md @@ -0,0 +1,32 @@ +# CRIF API Risks and Issues + +*Some of this content is copied from NVISIA's API Risk Analysis document.* + + +## Important Considerations +* The API WSDLs (description files) point to non-secure HTTP endpoint URLs. These URLs should not be used. +* CRIF can enable IP address whitelisting upon request. NVISIA recommends that this be used, and it is currently enabled in the production environment. +* The XML any-typed body fields mean that changes on CRIF's end cannot be automatically detected or updated via the API WSDL. Maintaining data models may require a large amount of manual effort as the vendor was unfamiliar with any code generation process. +* CRIF states that the “ACTion API only supports the Transactions, Elements, and Fields as described in [the documentation]”. Certain transactions or fields desired by Landmark Credit Union may not be available. +* Documentation is spread throughout a few places, including CRIF’s in-progress data mapping document. Documentation has been missing or wrong in some cases. + + +## Known Issues +* The XML formatting of the UAT system’s error responses is different than described in CRIF’s documentation. +* State abbreviations in addresses are validated against `StateEnum` in *commonSchema.xsd* and must also match a listing in the `v_RptGeoCode_v001` table. As of September 2018 `FM` and `MH` are listed in the XSD but are not listed in the database. Addresses submitted with either of those values results in a foreign key SQL error from CRIF. `FM` and `MH` are also available in the ACTion web interface and will prevent the loan from being saved. +* The CRIF update deployed to Landmark's UAT environment around July 23rd contains a bug that prevents most, if not all, applications from being saved due to an 'IX_PartyRelationship_ParentPartyID_BeginDate_ChildPartyID_ChildBeginDate_RelationshipTypeID' SQL error. CRIF has a fix that has now been applied to the UAT and Production environments. +* Not all custom field types can be written to, and the documentation is wrong in this regard. Product-level and borrower-level fields are supported. Application-level and collateral-level fields are not currently supported. +* The UAT environment does not accept all of the API calls defined in CRIF's reference XSDs (*addIndividualApplicant*, for example, is not supported). +* Many of the calls used to edit existing loans do not work as expected even when accepted by the server: + * *addApplicant* calls appear to prevent the updated loan from being opened in the ACTion web application as an IIS error screen appears instead. + * *addIndividual* calls do not save martial status or anything sent in the *trades* element. + * *updateApp* does not appear to be able to save relationship data (that is, indicating that two parties are married to each other), though the call is accepted by the server. + * *updateApp* calls adding individuals to a loan results in a SQL error response from the API server + + +## Other Risks +* The CRIF API server does respond over HTTP (port 80) but appears to only respond with system data over HTTPS. +* CRIF has informed us that the API layout and definition is generally stable from a development standpoint but there will be changes made as the underlying data models for the core applications change over time. These changes do not seem to correlate to ACTion version numbers. +* Mismatches between API-required fields, business-required fields, CRIF-required fields, and documented fields can complicate data submission. +* Custom fields, such as channels (that is, LCU branches), may have different IDs in each environment/version. The UAT environment may especially change over time when it is refreshed from the production environment. +* Submitting a loan that references a disabled custom field will result in a loan that cannot be view in the ACTion web application. \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/AddAppFilesSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/AddAppFilesSection.cs new file mode 100644 index 0000000..41058a4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/AddAppFilesSection.cs @@ -0,0 +1,82 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Files +{ + public class AddAppFilesSection : CrifRequestSection + { + public AddAppFilesSection(CrifSettings settings) : base(CrifNamespaces.Empty, "addAppFiles", settings) + {} + + public virtual void SetLoanId(string loanId) + { + AddAttribute(new XAttribute("applicationID", loanId)); + } + + public virtual void SetFile(LoanApplicationFile file, string fileBase64Value) + { + XAttribute description; + + if (file.Section?.Length > 0) { + description = new XAttribute(CrifNamespaces.Common + "description", $"{file.Section} - {file.SectionId}: {file.Description}"); + } else { + description = new XAttribute(CrifNamespaces.Common + "description", $"{file.SectionId}: {file.Description}"); + } + + var addAppFilesElement = new XElement(CrifNamespaces.Common + "file", + new XAttribute(CrifNamespaces.Common + "id", file.FileId), + new XAttribute(CrifNamespaces.Common + "name", file.Name), + description, + new XAttribute(CrifNamespaces.Common + "size", CalculateFileSize(fileBase64Value))); + + addAppFilesElement.Add(new XElement(CrifNamespaces.Common + "formatType", new XAttribute(CrifNamespaces.Common + "CD", GetFormatType()))); + + addAppFilesElement.Add(new XElement(CrifNamespaces.Common + "formatCategory", new XAttribute(CrifNamespaces.Common + "CD", GetFormatCategory(file.Description)))); + + var fileElement = new XElement(CrifNamespaces.Common + "file"); + fileElement.SetValue(fileBase64Value); + + addAppFilesElement.Add(fileElement); + + AddContent(addAppFilesElement); + } + + public static string CalculateFileSize(string fileBase64Value) + { + int size = fileBase64Value.Length / 4 * 3; + + if (fileBase64Value.EndsWith("==")) + { + size -= 2; + } + else if (fileBase64Value.EndsWith("=")) + { + size -= 1; + } + + return size.ToString(); + } + + public int GetFormatType() + { + // Always return 1 since it seems that there are no other types used. Creating this in case that ever changes. + return 1; + } + + // Configurable in appsettings.json, defaults to 1 + public int GetFormatCategory(string description) + { + try + { + var category = _settings.FormatCategoryMappings.DocumentToCategory.GetValueOrDefault(description, "Attachment"); + return _settings.FormatCategoryMappings.Categories.GetValueOrDefault(category, 1); + } catch (Exception) + { + return 1; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/FileRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/FileRequestBuilder.cs new file mode 100644 index 0000000..ffa8111 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/FileRequestBuilder.cs @@ -0,0 +1,34 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Files +{ + public class FileRequestBuilder : BuilderBase, IAddAppFiles + { + string _loanId; + LoanApplicationFile _file; + public AddAppFilesSection AddAppFiles { get; set; } + public string _fileBase64Value; + + public FileRequestBuilder(string loanId, LoanApplicationFile file, string fileBase64Value, CrifSettings settings) : base(CrifNamespaces.Empty, settings) + { + AddAppFiles = new AddAppFilesSection(settings); + _loanId = loanId; + _file = file; + _fileBase64Value = fileBase64Value; + } + + public override XElement Build(params XElement[] content) + { + AddAppFiles.SetLoanId(_loanId); + AddAppFiles.SetFile(_file, _fileBase64Value); + + var baseElement = base.Build(AddAppFiles.Build()); + + return baseElement; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IAddAppFiles.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IAddAppFiles.cs new file mode 100644 index 0000000..a5b9f15 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IAddAppFiles.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Files +{ + public interface IAddAppFiles + { + AddAppFilesSection AddAppFiles { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IFileRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IFileRequestBuilder.cs new file mode 100644 index 0000000..376f3d7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IFileRequestBuilder.cs @@ -0,0 +1,8 @@ + +namespace CRIF.ACTion.Files +{ + public interface IFileRequestBuilder + { + void SetFile(FileRequestBuilder createApp); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailBankcardSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailBankcardSection.cs new file mode 100644 index 0000000..f20a593 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailBankcardSection.cs @@ -0,0 +1,14 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class AppDetailBankcardSection : AppDetailsSection + { + public AppDetailBankcardSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "appDetailBankcard", settings) + { + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailHelocSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailHelocSection.cs new file mode 100644 index 0000000..10c5291 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailHelocSection.cs @@ -0,0 +1,89 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Loan +{ + public class AppDetailHelocSection : AppDetailsSection + { + private readonly List _content; + + private readonly XElement _fieldsElement; + + public AppDetailHelocSection(XNamespace sectionNamespace, CrifSettings settings, + string interestRateType = null, string calculationType = null) + : base(sectionNamespace, "appDetailHELOC", settings, interestRateType, calculationType) + { + _content = []; + } + + private protected AppDetailHelocSection(XNamespace sectionNamespace, CrifSettings settings, + string elementName, string interestRateType = null, string calculationType = null) + : base(sectionNamespace, elementName, settings, interestRateType, calculationType) + { + _content = []; + } + + public virtual void AddPropertyDetails(RealEstateBase realEstateBase, bool? mfgHomeIncludesLand = null) + { + var appDetailPropertiesElement = new XElement(CrifNamespaces.App + "appDetailProperties"); + + var propertyType = "RealEstate"; + var collateralType = CrifConstants.CollateralIds[CrifConstants.Collateral.ResidentialRealEstate]; + + string subType; + if (mfgHomeIncludesLand.HasValue) + { + propertyType = "MfgHomeAndLand"; + collateralType = mfgHomeIncludesLand.Value ? + CrifConstants.CollateralIds[CrifConstants.Collateral.ManufacturedHomeAndLand] : + CrifConstants.CollateralIds[CrifConstants.Collateral.ManufacturedHome]; + + subType = ((ManufacturedHome)realEstateBase).Type.Value.ToString(); + } + else + { + subType = ((RealEstate)realEstateBase).Type.Value.ToString(); + } + + var statusID = "Active"; + var includeInLTVCalculations = true; + + + var appDetailPropertyElement = new XElement(CrifNamespaces.App + "appDetailProperty", + new XAttribute(CrifNamespaces.Common + "idRef", realEstateBase.GetOrCreateGuid()), + new XAttribute(CrifNamespaces.Common + "propertyType", propertyType), + new XAttribute(CrifNamespaces.Common + "propertySubType", subType), + new XAttribute(CrifNamespaces.App + "isCollateralFL", realEstateBase.IsCollateral), + new XAttribute(CrifNamespaces.App + "isTradeInFL", realEstateBase.IsTradeIn), + new XAttribute(CrifNamespaces.App + "statusID", statusID), + new XAttribute(CrifNamespaces.App + "includeInLTVCalculations", includeInLTVCalculations), + new XAttribute(CrifNamespaces.App + "collateralTypeID", collateralType) + ); + + appDetailPropertiesElement.Add(appDetailPropertyElement); + + _content.Add(appDetailPropertiesElement); + } + + public void AddHmdaDisclosed(bool? acceptedDisclosure) + { + if ((!acceptedDisclosure.HasValue) || !acceptedDisclosure.Value) + { + return; + } + + var hmdaElement = new XElement(CrifNamespaces.App + "appDetailHMDA2018", + new XAttribute(CrifNamespaces.App + "isNoticeDisclosedFL", acceptedDisclosure.Value)); + + _content.Add(hmdaElement); + } + + public override void SetDetails(LoanApplication loanApplication, Guid channelRef, object[] additionalContent = null, XElement fieldsElement = null, XElement vehicleValuationsElement = null) + { + base.SetDetails(loanApplication, channelRef, [.. _content], _fieldsElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailSignatureSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailSignatureSection.cs new file mode 100644 index 0000000..ca06bd1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailSignatureSection.cs @@ -0,0 +1,13 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class AppDetailSignatureSection : AppDetailsSection + { + public AppDetailSignatureSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "appDetail", settings, "Fixed", "Installment") + { + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailsSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailsSection.cs new file mode 100644 index 0000000..bfe6d03 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailsSection.cs @@ -0,0 +1,237 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Services.Landmark; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Loan +{ + public class AppDetailsSection : CrifRequestSection + { + private readonly string _elementName; + private readonly string _interestRateType; + private readonly string _calculationType; + private readonly ProductIdService _productIdTranslator; + + protected AppDetailsSection(XNamespace sectionNamespace, string elementName, CrifSettings settings, + string interestRateType = null, string calculationType = null) + : base(sectionNamespace, "appDetails", settings) + { + _elementName = elementName; + + _interestRateType = string.IsNullOrWhiteSpace(interestRateType) ? "Variable" : interestRateType; + _calculationType = string.IsNullOrWhiteSpace(calculationType) ? "OpenEnd" : calculationType; + + _productIdTranslator = new ProductIdService(settings); + + } + + public static bool IsHMDA(LoanType? loanType) + { + HMDALoanType hmdaType; + try + { + hmdaType = (HMDALoanType)Enum.Parse(typeof(HMDALoanType), loanType.ToString()); + } + catch + { + return false; + } + + return true; + + } + + public virtual void SetDetails(LoanApplication loanApplication, Guid channelRef, object[] additionalContent = null, XElement fieldsElement = null, XElement vehicleValuationsElement = null) + { + var appDetailElement = new XElement(CrifNamespaces.App + _elementName, + new XAttribute(CrifNamespaces.App + "contractRate", 0m), //Per Ben V. at Landmark, we should send in 0 so that CRIF can set the correct rate + new XAttribute(CrifNamespaces.App + "financedAmt", loanApplication.RequestedAmount), + new XAttribute(CrifNamespaces.App + "termFrequencyCD", "Monthly"), + new XAttribute(CrifNamespaces.App + "calculationType", _calculationType), + new XAttribute(CrifNamespaces.App + "loanType", "Consumer"), + new XAttribute(CrifNamespaces.App + "interestRateType", _interestRateType), + new XAttribute(CrifNamespaces.App + "rankNum", 1), + new XAttribute(CrifNamespaces.App + "isApplicantRequestedInterestRate", false), + new XAttribute(CrifNamespaces.App + "isApplicantRequestedAmountRequested", true), + new XAttribute(CrifNamespaces.App + "principal", loanApplication.RequestedAmount), //TODO #234: Why is the loan amount listed twice? + new XAttribute(CrifNamespaces.App + "isApplicantRequestedTerm", true), + new XAttribute(CrifNamespaces.App + "isHMDAFL", IsHMDA(loanApplication.LoanType)) + ); + + if (loanApplication.TermLengthInMonths.HasValue) + { + appDetailElement.Add(new XAttribute(CrifNamespaces.App + "termInMonths", loanApplication.TermLengthInMonths.Value)); + } + + var appDetailParties = new XElement(CrifNamespaces.App + "appDetailParties", + new XElement(CrifNamespaces.App + "appDetailParty", + new XAttribute(CrifNamespaces.Common + "idRef", channelRef), + new XAttribute(CrifNamespaces.Common + "partyType", "Channel"), + new XAttribute(CrifNamespaces.App + "appPartyType", "ServicingChannel"))); + + var appDetailRequestedAmts = new XElement(CrifNamespaces.App + "appDetailRequestedAmts", + new XElement(CrifNamespaces.App + "appDetailRequestedAmt", + new XAttribute(CrifNamespaces.App + "requestedAmount", loanApplication.RequestedAmount), //TODO #234: Financed amount is listed 3 times? Are the other 2 from the calculator? + new XAttribute(CrifNamespaces.App + "requestedAmountType", "Generic"))); + + var product = new XElement(CrifNamespaces.App + "product", + new XAttribute(CrifNamespaces.Common + "productID", + _productIdTranslator.GetProductId(loanApplication.LoanType.Value))); + + + var loanPurposes = new XElement(CrifNamespaces.App + "loanPurposes", + new XElement(CrifNamespaces.App + "appDetailLoanPurpose", + new XAttribute(CrifNamespaces.App + "rank", 1), + new XElement(CrifNamespaces.App + "loanPurpose", + new XAttribute(CrifNamespaces.Common + "CD", (int)loanApplication.Purpose.Value) + ))); + + appDetailElement.Add(appDetailParties); + + appDetailElement.Add(additionalContent); + + appDetailElement.Add(appDetailRequestedAmts); + appDetailElement.Add(product); + + string productId = _productIdTranslator.GetProductId(loanApplication.LoanType.Value).ToString(); + + XElement appFieldsElement = null; + + + if (_settings.CustomFields != null && _settings.CustomFields.ProductIdToCategoryId.TryGetValue(productId, out string productCategoryId)) + { + CustomPropertyStatusData customPropertyRenovationData = _settings.CustomFields.CustomPropertyRenovationFieldData.Find(x => x.ProductCategoryId == productCategoryId); + if (customPropertyRenovationData != null) + { + bool realEstateNotNullValue = loanApplication.RealEstate?.CurrentlyBeingRemodeledOrImproved ?? false; + string propertyRenovationValueCode = realEstateNotNullValue ? customPropertyRenovationData.YesId : customPropertyRenovationData.NoId; + appFieldsElement = GetLookupFieldLinkElement(customPropertyRenovationData.CustomFieldId, propertyRenovationValueCode, "AppDetail", appFieldsElement); + } + + CustomPropertyStatusData customPropertySaleData = _settings.CustomFields.CustomPropertySaleFieldData.Find(x => x.ProductCategoryId == productCategoryId); + if (customPropertySaleData != null) + { + bool realEstateNotNullValue = loanApplication.RealEstate?.CurrentlyListedForSale ?? false; + string propertySaleValueCode = realEstateNotNullValue ? customPropertySaleData.YesId : customPropertySaleData.NoId; + appFieldsElement = GetLookupFieldLinkElement(customPropertySaleData.CustomFieldId, propertySaleValueCode, "AppDetail", appFieldsElement); + } + + CustomJointCreditData customJointCreditData = _settings.CustomFields.CustomJointCreditFieldData.Find(x => x.ProductCategoryId == productCategoryId); + if (customJointCreditData != null) + { + string jointCreditValueCode = loanApplication.CoApplicant != null ? customJointCreditData.JointlyId : customJointCreditData.IndividuallyId; + appFieldsElement = GetLookupFieldLinkElement(customJointCreditData.CustomFieldId, jointCreditValueCode, "ProductCategory", appFieldsElement); + } + } + + if (!string.IsNullOrWhiteSpace(loanApplication.PromoCode)) + { + if (_settings.CustomFieldMappings.LoanTypeToPromoId.TryGetValue(productId, out string promoFieldId)) + { + appFieldsElement = GetStringFieldLinkElement(promoFieldId, loanApplication.PromoCode, "ProductCategory", appFieldsElement); + } + } + + if (loanApplication.Vehicle != null) + { + if (!string.IsNullOrWhiteSpace(loanApplication.Vehicle.LicensePlateNumber)) + { + if (_settings.CustomFieldMappings.CustomFieldID.TryGetValue("LicensePlateNumber", out string customFieldId)) + { + appFieldsElement = GetStringFieldLinkElement(customFieldId, loanApplication.Vehicle.LicensePlateNumber, "ProductCategory", appFieldsElement); + } + } + + if (!string.IsNullOrWhiteSpace(loanApplication.Vehicle.CountyTitled)) + { + if (_settings.CustomFieldMappings.CustomFieldID.TryGetValue("CountyTitled", out string customFieldId)) + { + appFieldsElement = GetStringFieldLinkElement(customFieldId, loanApplication.Vehicle.CountyTitled, "ProductCategory", appFieldsElement); + } + } + + if (!string.IsNullOrWhiteSpace(loanApplication.Vehicle.MunicipalityWhereKept)) + { + if (_settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityWhereKept", out string customFieldId)) + { + appFieldsElement = GetStringFieldLinkElement(customFieldId, loanApplication.Vehicle.MunicipalityWhereKept, "ProductCategory", appFieldsElement); + } + } + + var municipalityTypeCode = GetMunicipalityTypeCode(loanApplication.Vehicle.MunicipalityType); + if (!string.IsNullOrWhiteSpace(municipalityTypeCode)) + { + if (_settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityType", out string customFieldId)) + { + appFieldsElement = GetLookupFieldLinkElement(customFieldId, municipalityTypeCode, "ProductCategory", appFieldsElement); + } + } + } + + if (appFieldsElement != null) + { + appDetailElement.Add(appFieldsElement); //Order matters for this element. + } + + appDetailElement.Add(loanPurposes); + appDetailElement.Add(vehicleValuationsElement); //Order matters for this element. + + AddContent(appDetailElement); + } + + private static XElement GetStringFieldLinkElement(string customFieldId, string value, string fieldLevel, XElement fieldsElement) + { + fieldsElement = CreateFieldsElementIfNull(fieldsElement); + var stringFieldLinkElement = new XElement(CrifNamespaces.Common + "stringFieldLink", + new XAttribute(CrifNamespaces.Common + "value", value)); + var customFieldElement = new XElement(CrifNamespaces.Common + "customField", + new XAttribute(CrifNamespaces.Common + "ID", customFieldId), + new XAttribute(CrifNamespaces.Common + "fieldLevel", fieldLevel)); + fieldsElement.Add(stringFieldLinkElement); + stringFieldLinkElement.Add(customFieldElement); + return fieldsElement; + } + + private static XElement GetLookupFieldLinkElement(string Id, string value, string fieldLevel, XElement fieldsElement) + { + fieldsElement = CreateFieldsElementIfNull(fieldsElement); + var lookupFieldLinkElement = new XElement(CrifNamespaces.Common + "lookupFieldLink", + new XAttribute(CrifNamespaces.Common + "value", value), + new XElement(CrifNamespaces.Common + "customLookupField", + new XAttribute(CrifNamespaces.Common + "ID", Id), + new XAttribute(CrifNamespaces.Common + "fieldLevel", fieldLevel))); + fieldsElement.Add(lookupFieldLinkElement); + return fieldsElement; + } + + private static XElement CreateFieldsElementIfNull(XElement fieldsElement) + { + if (fieldsElement == null) + { + fieldsElement = new XElement(CrifNamespaces.App + "fields"); + } + return fieldsElement; + } + + private string GetMunicipalityTypeCode(string municipalityType) + { + if (string.IsNullOrWhiteSpace(municipalityType)) + { + return null; + } + + if (_settings?.CustomFieldMappings?.MunicipalityTypeCode == null) + { + return null; + } + + var key = municipalityType.Trim().ToUpperInvariant(); + return _settings.CustomFieldMappings.MunicipalityTypeCode.TryGetValue(key, out string code) + ? code + : null; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppNotesSectionBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppNotesSectionBuilder.cs new file mode 100644 index 0000000..b317401 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppNotesSectionBuilder.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class AppNotesSectionBuilder : CrifRequestSection + { + public AppNotesSectionBuilder(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "appNotes", settings) + { + } + + public void AddAppNote(string appNoteContent) + { + + var appNote = new XElement(CrifNamespaces.App + "appNote", + new XAttribute(CrifNamespaces.App + "noteEntry", BuilderUtils.FormatCrifData(appNoteContent)), + new XAttribute(CrifNamespaces.App + "noteTypeDisplayName", "Application Note"), + new XAttribute(CrifNamespaces.App + "noteTypeCategoryDisplayName", "Application Note")); + + var noteType = new XElement(CrifNamespaces.App + "noteType", + new XAttribute(CrifNamespaces.Common + "CD", 1)); + + var noteTypeCategory = new XElement(CrifNamespaces.App + "noteTypeCategory", + new XAttribute(CrifNamespaces.Common + "lookupType", "NoteTypeCategory"), + new XAttribute(CrifNamespaces.Common + "CD", 1)); + + appNote.Add(noteType, noteTypeCategory); + + AddContent(appNote); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailMfgHomeAndLandSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailMfgHomeAndLandSection.cs new file mode 100644 index 0000000..a0a739b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailMfgHomeAndLandSection.cs @@ -0,0 +1,23 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Assets; + +namespace CRIF.ACTion.Loan.Assets +{ + public class AppDetailMfgHomeAndLandSection : AppDetailHelocSection + { + //AppDetailMfgHomeAndLand is generally similar to AppDetailHeloc. + public AppDetailMfgHomeAndLandSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, settings, "appDetailMfgHousingAndLand", "Fixed", "Installment") + { + } + + public override void AddPropertyDetails(RealEstateBase realEstateBase, bool? mfgHomeIncludesLand = null) + { + if (!mfgHomeIncludesLand.HasValue) + throw new ArgumentException("Manufactured Home Loans must state if the land is included."); + + base.AddPropertyDetails(realEstateBase, mfgHomeIncludesLand); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailRealEstateSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailRealEstateSection.cs new file mode 100644 index 0000000..2be0b4b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailRealEstateSection.cs @@ -0,0 +1,16 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan.Assets +{ + public class AppDetailRealEstateSection : AppDetailHelocSection + { + + //So far, AppDetailRealEstate is used exactly the same as AppDetailHeloc. + //The only difference is the element name and term types. + public AppDetailRealEstateSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, settings, "appDetailRealEstate", "Fixed", "Installment") + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailVehicleSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailVehicleSection.cs new file mode 100644 index 0000000..940c2d6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailVehicleSection.cs @@ -0,0 +1,69 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Loan.Assets +{ + public class AppDetailVehicleSection : AppDetailsSection + { + private readonly List _content; + private XElement _vehicleValuationsElement; + + public AppDetailVehicleSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "appDetailVehicle", settings, "Fixed", "Installment") + { + _content = []; + } + + public void AddPropertyDetails(Vehicle vehicle) + { + //If there's no vehicle type, it's likely a preapproval and doesn't need the appDetailProperties element + if (vehicle is null || vehicle.Type is null) + { + return; + } + + var appDetailPropertiesElement = new XElement(CrifNamespaces.App + "appDetailProperties"); + + var propertyValues = PropertyValues.GetPropertyValues(vehicle.Type.Value); + + var isCollateralFL = true; + var isTradeInFL = false; + var statusID = "Active"; + var includeInLTVCalculations = true; + + var appDetailPropertyElement = new XElement(CrifNamespaces.App + "appDetailProperty", + new XAttribute(CrifNamespaces.Common + "idRef", vehicle.GetOrCreateGuid()), + new XAttribute(CrifNamespaces.Common + "propertyType", propertyValues.PropertyType), + new XAttribute(CrifNamespaces.Common + "propertySubType", propertyValues.PropertySubType), + new XAttribute(CrifNamespaces.App + "isCollateralFL", isCollateralFL), + new XAttribute(CrifNamespaces.App + "isTradeInFL", isTradeInFL), + new XAttribute(CrifNamespaces.App + "statusID", statusID), + new XAttribute(CrifNamespaces.App + "includeInLTVCalculations", includeInLTVCalculations), + new XAttribute(CrifNamespaces.App + "collateralTypeID", propertyValues.CollateralId) + ); + + appDetailPropertiesElement.Add(appDetailPropertyElement); + + _vehicleValuationsElement = new XElement(CrifNamespaces.App + "vehicleValuations", + new XElement(CrifNamespaces.App + "appDetailVehicleValuation", + new XAttribute(CrifNamespaces.Common + "idRef", vehicle.ValuationId), + new XAttribute(CrifNamespaces.App + "valueType", vehicle.ValuationType.Value.ToString()))); + //TODO: What does valuationType mean? + + _content.Add(appDetailPropertiesElement); + } + + public void AddSalesPriceDetails(LoanApplication loanApplication) + { + _content.Add(new XAttribute(CrifNamespaces.App + "initialSalesPrice", loanApplication.RequestedAmount)); + } + + public override void SetDetails(LoanApplication loanApplication, Guid channelRef, object[] additionalContent = null, XElement fieldsElement = null, XElement vehicleValuationsElement = null) + { + base.SetDetails(loanApplication, channelRef, [.. _content], vehicleValuationsElement: _vehicleValuationsElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/IVehicleValuations.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/IVehicleValuations.cs new file mode 100644 index 0000000..57d92ed --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/IVehicleValuations.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan.Assets +{ + public interface IVehicleValuations + { + VehicleValuationsSection VehicleValuations { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingAndLandAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingAndLandAppBuilder.cs new file mode 100644 index 0000000..120b22e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingAndLandAppBuilder.cs @@ -0,0 +1,36 @@ +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan.Assets +{ + public class MfgHousingAndLandAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + + public MfgHousingAndLandAppBuilder(CrifSettings settings) + { + _settings = settings; + } + + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailMfgHomeAndLandSection(createApp.CrifRequestSectionNamespace, _settings); + + + var includesLand = true; + if (createApp.LoanApplication.ManufacturedHome?.Lot?.IsRented ?? false) + { + includesLand = false; + } + + + ((AppDetailMfgHomeAndLandSection)createApp.AppDetails).AddPropertyDetails(createApp.LoanApplication.ManufacturedHome, includesLand); + ((AppDetailMfgHomeAndLandSection)createApp.AppDetails).AddHmdaDisclosed(createApp.LoanApplication.PrimaryBorrower.HmdaValues.HmdaNoticeDisclosed); + createApp.Properties.AddProperty(new MfgHousingProperty(createApp.LoanApplication.ManufacturedHome, includesLand)); + createApp.Addresses.AddAddress(createApp.LoanApplication.ManufacturedHome.Lot, ContactMechanisms.CrifContactMechanism.RealEstateAddress); + + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingProperty.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingProperty.cs new file mode 100644 index 0000000..fc32d62 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingProperty.cs @@ -0,0 +1,12 @@ +using Landmark.OLA.Models.Assets; + +namespace CRIF.ACTion.Loan.Assets +{ + public class MfgHousingProperty : RealEstateProperty + { + public MfgHousingProperty(ManufacturedHome manufacturedHome, bool includesLand) + : base(manufacturedHome, "propertyMfgHousing", "MfgHomeAndLand") + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/PropertyValues.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/PropertyValues.cs new file mode 100644 index 0000000..348e080 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/PropertyValues.cs @@ -0,0 +1,32 @@ +using Landmark.OLA.Models.Assets.Enums; + +namespace CRIF.ACTion.Loan.Assets +{ + public static class PropertyValues + { + public static (int CollateralId, string PropertyType, string PropertySubType, string elementName) GetPropertyValues(VehicleType type) + { + switch (type) + { + case VehicleType.Automobile: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.Vehicle], "Auto", "Automobile", "propertyAuto"); + case VehicleType.Motorcycle: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.Motorcycle], "Motorcycle", "NoneSelected", "propertyAuto"); + case VehicleType.Boat: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.Marine], "Marine", "NoneSelected", "propertyMarine"); + case VehicleType.ATV: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.ATV], "ATV", "NoneSelected", "propertyAuto"); + case VehicleType.PWC: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.Marine], "Marine", "NoneSelected", "propertyMarine"); + case VehicleType.RecreationalVehicle: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.RecreationalVehicle], "RV", "CampingTrailer", "propertyAuto"); //"CampingTrailer" is from CRIF example + case VehicleType.Snowmobile: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.Snowmobile], "Snowmobile", "NoneSelected", "propertyAuto"); + case VehicleType.TrailerMotorHome: + return (CrifConstants.CollateralIds[CrifConstants.Collateral.RecreationalVehicle], "RV", "Other", "propertyAuto"); //"Other" is from sprint requirements doc + default: + throw new NotImplementedException("This vehicle type is not implemented."); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateEquityAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateEquityAppBuilder.cs new file mode 100644 index 0000000..b67de1b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateEquityAppBuilder.cs @@ -0,0 +1,27 @@ +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan.Assets +{ + public class RealEstateEquityAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + + public RealEstateEquityAppBuilder(CrifSettings settings) + { + _settings = settings; + } + + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailRealEstateSection(createApp.CrifRequestSectionNamespace, _settings); + ((AppDetailRealEstateSection)createApp.AppDetails).AddPropertyDetails(createApp.LoanApplication.RealEstate); + ((AppDetailRealEstateSection)createApp.AppDetails).AddHmdaDisclosed(createApp.LoanApplication.PrimaryBorrower.HmdaValues.HmdaNoticeDisclosed); + createApp.Properties.AddProperty(new RealEstateProperty(createApp.LoanApplication.RealEstate)); + createApp.Addresses.AddAddress(createApp.LoanApplication.RealEstate.RealEstateAddress, ContactMechanisms.CrifContactMechanism.RealEstateAddress); + + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateProperty.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateProperty.cs new file mode 100644 index 0000000..1ab1065 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateProperty.cs @@ -0,0 +1,221 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; + +using static CRIF.ACTion.Common.ContactMechanisms; + +namespace CRIF.ACTion.Loan.Assets +{ + public class RealEstateProperty : IProperty + { + private protected readonly RealEstateBase _realEstateBase; + private protected readonly string _elementName; + private protected readonly string _propertyType; + + public RealEstateProperty(RealEstateBase realEstateBase, string elementName = "propertyRealEstate", + string propertyType = "RealEstate") + { + _realEstateBase = realEstateBase; + _elementName = elementName; + _propertyType = propertyType; + } + + public XElement Build(params XElement[] content) + { + var classType = _realEstateBase.GetType(); + + if (classType.Equals(typeof(ManufacturedHome)) || classType.IsSubclassOf(typeof(ManufacturedHome))) + { + var realEstateElement = GetRealEstateElement((ManufacturedHome)_realEstateBase); + realEstateElement.Add( + new XAttribute(CrifNamespaces.Common + "lienPositionID", LienPositionType.First) + ); + return realEstateElement; + } + else if (classType.Equals(typeof(RealEstate)) || classType.IsSubclassOf(typeof(RealEstate))) + { + var realEstateElement = GetRealEstateElement((RealEstate)_realEstateBase); + realEstateElement.Add( + new XAttribute(CrifNamespaces.Common + "lienPositionID", GetLienPosition(_realEstateBase.LienPosition)) + ); + return realEstateElement; + + } + else + { + throw new ArgumentException("Unexpected object given to constructor for RealEstateProperty"); + } + + } + + private XElement GetRealEstateElement(RealEstate realEstate) + { + return GetRealEstateElement(realEstate, realEstate.Type.Value.ToString()); + } + + private XElement GetRealEstateElement(ManufacturedHome manufacturedHome) + { + var element = GetRealEstateElement(manufacturedHome, manufacturedHome.Type.Value.ToString(), + manufacturedHome.EstimatedLandValuation, manufacturedHome.EstimatedHomeValuation); + //The valuation fields above are suppose to go in totalValue and finalEstimatedValue as written. + //It's a little strange but that's how CRIF stores that data. + + // This has to be before propertyLot, otherwise CRIF will complain! + if (manufacturedHome.Lot.AnnualPropertyTax.HasValue) + { + var propertyTaxesElement = new XElement(CrifNamespaces.Common + "propertyTaxes"); + var propertyTaxElement = new XElement( + CrifNamespaces.Common + "propertyTax", + new XAttribute(CrifNamespaces.Common + "taxEntity", "IRS"), + new XAttribute(CrifNamespaces.Common + "amount", manufacturedHome.Lot.AnnualPropertyTax.Value.ToString()), + new XAttribute(CrifNamespaces.Common + "paymentFrequency", "Annually") + ); + propertyTaxesElement.Add(propertyTaxElement); + element.Add(propertyTaxesElement); + } + + var propertyLotElement = new XElement(CrifNamespaces.Common + "propertyLot"); + + if (manufacturedHome.Lot.RentalAmt.HasValue) + { + propertyLotElement.Add(new XAttribute(CrifNamespaces.Common + "rentalAmt", manufacturedHome.Lot.RentalAmt.Value)); + } + + element.Add(propertyLotElement); + + return element; + } + + private XElement GetRealEstateElement(RealEstateBase realEstateBase, string subtype, decimal? finalEstimatedValue = null, decimal? totalValue = null) + { + var propertyRealEstateElement = new XElement(CrifNamespaces.Common + _elementName, + new XAttribute(CrifNamespaces.Common + "id", realEstateBase.Id), + new XAttribute(CrifNamespaces.Common + "propertySubType", subtype), + // new XAttribute(CrifNamespaces.Common + "hasClearTitleFL", realEstate.HasClearTitle), + new XAttribute(CrifNamespaces.Common + "initialEstimatedValue", realEstateBase.InitialEstimatedValue), + new XAttribute(CrifNamespaces.Common + "propertyType", _propertyType), + new XAttribute(CrifNamespaces.Common + "ownerOccupiedTypeID", realEstateBase.Use.Value)); + + if(finalEstimatedValue.HasValue) + { + propertyRealEstateElement.Add(new XAttribute(CrifNamespaces.Common + "finalEstimatedValue", finalEstimatedValue)); + } + + if (totalValue.HasValue) + { + propertyRealEstateElement.Add(new XAttribute(CrifNamespaces.Common + "totalValue", totalValue)); + } + + + var contactMechanismsElement = GetContactMechanisms(realEstateBase); + + propertyRealEstateElement.Add(GetPropertyDetails(realEstateBase)); + + propertyRealEstateElement.Add(contactMechanismsElement); + + return propertyRealEstateElement; + } + + private static XElement GetContactMechanisms(RealEstateBase realEstateBase) + { + var contactMechanismType = CrifContactMechanism.RealEstateAddress; + var idRef = ""; + + var classType = realEstateBase.GetType(); + if (classType.Equals(typeof(ManufacturedHome)) || classType.IsSubclassOf(typeof(ManufacturedHome))) + { + contactMechanismType = CrifContactMechanism.ParkContactAddress; + idRef = ((ManufacturedHome)realEstateBase).Lot.Id.ToString(); + } + else if (classType.Equals(typeof(RealEstate)) || classType.IsSubclassOf(typeof(RealEstate))) + { + idRef = ((RealEstate)realEstateBase).RealEstateAddress.Id.ToString(); + } + + var contactMechanismsElement = new XElement(CrifNamespaces.Common + "contactMechanisms", + new XElement(CrifNamespaces.Common + "contactMechanism", + new XAttribute(CrifNamespaces.Common + "contactMechanismType", contactMechanismType.ToString()), + new XElement(CrifNamespaces.Common + "addressRef", + new XAttribute(CrifNamespaces.Common + "idRef", idRef)), + new XElement(CrifNamespaces.Common + "contactMechanismCategory", + new XAttribute(CrifNamespaces.Common + "CD", (int)GetContactMechanismCategory(contactMechanismType))) + )); + + return contactMechanismsElement; + } + + private static XAttribute[] GetPropertyDetails(RealEstateBase realEstateBase) + { + var attributeList = new List(); + + if (realEstateBase.HasSpa.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "hasSpaFL", realEstateBase.HasSpa.Value)); + } + if (realEstateBase.HasPool.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "hasPoolFL", realEstateBase.HasPool.Value)); + } + if (realEstateBase.HasBasement.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "hasBasementFL", realEstateBase.HasBasement.Value)); + } + if (realEstateBase.HasCentralAir.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "hasCentralAirFL", realEstateBase.HasCentralAir.Value)); + } + if (realEstateBase.HasGarage.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "hasGarageFL", realEstateBase.HasGarage.Value)); + } + if (realEstateBase.InFloodZone.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "inFloodZoneFL", realEstateBase.InFloodZone.Value)); + } + if (realEstateBase.InEquity.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "inEquityFL", realEstateBase.InEquity.Value)); + } + if (realEstateBase.IsPropertyHeldInTrust.HasValue) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "isPropertyHeldInTrust", realEstateBase.IsPropertyHeldInTrust.Value)); + } + + return [.. attributeList]; + } + + public static LienPositionType GetLienPosition(LienPosition lienPositionData) + { + if (lienPositionData.HasFirstMortgage && lienPositionData.HasSecondMortgage) + { + if (lienPositionData.PayoffFirstMortgage && lienPositionData.PayoffSecondMortgage) + { + return LienPositionType.First; + } + else if (!lienPositionData.PayoffFirstMortgage && !lienPositionData.PayoffSecondMortgage) + { + return LienPositionType.Third; + } + else if (lienPositionData.PayoffFirstMortgage || lienPositionData.PayoffSecondMortgage) + { + return LienPositionType.Second; + } + + } + else if (lienPositionData.HasFirstMortgage || lienPositionData.HasSecondMortgage) + { + if (lienPositionData.PayoffFirstMortgage || lienPositionData.PayoffSecondMortgage) + { + return LienPositionType.First; + } + else + { + return LienPositionType.Second; + } + } + + return LienPositionType.First; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleAppBuilder.cs new file mode 100644 index 0000000..18d4fc8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleAppBuilder.cs @@ -0,0 +1,45 @@ +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Loan.Assets +{ + public class VehicleAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + + public VehicleAppBuilder(CrifSettings settings) + { + _settings = settings; + } + + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailVehicleSection(createApp.CrifRequestSectionNamespace, _settings); + + if (createApp.LoanApplication.LoanType.Value == LoanType.V_VehiclePreapproval + && createApp.LoanApplication.Vehicle is null) + { + createApp.LoanApplication.Vehicle = new Vehicle + { + InitialEstimatedValue = createApp.LoanApplication.RequestedAmount + }; + } + + ((AppDetailVehicleSection)createApp.AppDetails).AddPropertyDetails(createApp.LoanApplication.Vehicle); + ((AppDetailVehicleSection)createApp.AppDetails).AddSalesPriceDetails(createApp.LoanApplication); + createApp.Properties.AddProperty(new VehicleProperty(createApp.LoanApplication.Vehicle)); + createApp.VehicleValuations.AddVehicleValuation(createApp.LoanApplication.Vehicle); + + if (createApp.LoanApplication.LoanType == LoanType.M_Boat && + !(createApp.LoanApplication.Vehicle.BoatAddress is null)) + { + createApp.Addresses.AddAddress(createApp.LoanApplication.Vehicle.BoatAddress, ContactMechanisms.CrifContactMechanism.MarinaAddress); + } + + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleProperty.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleProperty.cs new file mode 100644 index 0000000..dbc1202 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleProperty.cs @@ -0,0 +1,167 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Loan.Enums; +using static CRIF.ACTion.Common.ContactMechanisms; + +namespace CRIF.ACTion.Loan.Assets +{ + public class VehicleProperty : IProperty + { + private readonly Vehicle _vehicle; + + public VehicleProperty(Vehicle vehicle) + { + _vehicle = vehicle; + } + + public XElement Build(params XElement[] content) + { + //If there's no vehicle type, it's likely a preapproval and doesn't need this element + if (_vehicle is null || _vehicle.Type is null) + return null; + + var propertyValues = PropertyValues.GetPropertyValues(_vehicle.Type.Value); + + var propertyElement = new XElement(CrifNamespaces.Common + propertyValues.elementName, + new XAttribute(CrifNamespaces.Common + "id", _vehicle.GetOrCreateGuid()), + new XAttribute(CrifNamespaces.Common + "propertySubType", propertyValues.PropertySubType), + new XAttribute(CrifNamespaces.Common + "yearBuilt", _vehicle.Year.Value), + new XAttribute(CrifNamespaces.Common + "initialEstimatedValue", _vehicle.InitialEstimatedValue.Value), + new XAttribute(CrifNamespaces.Common + "propertyType", propertyValues.PropertyType)); + + if ( _vehicle.Type != Landmark.OLA.Models.Assets.Enums.VehicleType.Automobile ) { + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "totalValue", + _vehicle.InitialEstimatedValue.Value)); + } + + if (_vehicle.HasClearTitle.HasValue) + { + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "hasClearTitleFL", + _vehicle.HasClearTitle.Value)); + } + + if (_vehicle.IsForBusinessUse.HasValue) + { + var usageType = _vehicle.IsForBusinessUse.Value ? "Business" : "Personal"; + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "propertyUsageTypeID", + usageType)); + } + + if (_vehicle.IsOwnerOperator.HasValue) + { + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "isOwnerOperatorFL", + _vehicle.IsOwnerOperator.Value)); + } + + if ((_vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.PWC || _vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.Boat) + && !string.IsNullOrWhiteSpace(_vehicle.VIN)) + { + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "hullNum", + BuilderUtils.FormatCrifData(_vehicle.VIN))); + } + + if (_vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.Boat && (_vehicle.BoatLength ?? 0) != 0) + { + propertyElement.Add(new XAttribute(CrifNamespaces.Common + "lengthDesc", + _vehicle.BoatLength)); + } + + // Order matters for elements + + if (_vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.Boat + && _vehicle.BoatAddress is not null) + { + var contactMechanisms = new XElement(CrifNamespaces.Common + "contactMechanisms"); + contactMechanisms.Add(GetAddressContactMechanismElement(_vehicle.BoatAddress.Id, CrifContactMechanism.MarinaAddress)); + propertyElement.Add(contactMechanisms); + } + + var valuationsLinkElement = new XElement(CrifNamespaces.Common + "propertyVehicleValuations", + new XElement(CrifNamespaces.Common + "propertyVehicleValuation", + new XAttribute(CrifNamespaces.Common + "idRef", _vehicle.ValuationId))); + + propertyElement.Add(valuationsLinkElement); + + if (_vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.Boat) + { + propertyElement.Add(CreateMotorElement(_vehicle)); + propertyElement.Add(CreateTrailerElement(_vehicle)); + } + + return propertyElement; + } + + public static XElement CreateMotorElement(Vehicle vehicle) + { + var motorElement = new XElement(CrifNamespaces.Common + "motors"); + + var propertyMotorElement = new XElement(CrifNamespaces.Common + "propertyMotor", + new XAttribute(CrifNamespaces.Common + "motorNum", 1)); + + if (!string.IsNullOrWhiteSpace(vehicle.MotorMake)) + { + propertyMotorElement.Add(new XAttribute(CrifNamespaces.Common + "make", vehicle.MotorMake)); + } + + if (vehicle.MotorYear is not null) + { + propertyMotorElement.Add(new XAttribute(CrifNamespaces.Common + "yearBuilt", vehicle.MotorYear.Value)); + } + + if (!string.IsNullOrWhiteSpace(vehicle.MotorModel)) + { + propertyMotorElement.Add(new XAttribute(CrifNamespaces.Common + "model", vehicle.MotorModel)); + } + + if (vehicle.MotorType is not null) + { + propertyMotorElement.Add(new XAttribute(CrifNamespaces.Common + "engineTypeID", vehicle.MotorType)); + } + + motorElement.Add(propertyMotorElement); + + return motorElement; + } + + public static XElement CreateTrailerElement(Vehicle vehicle) + { + var trailerElement = new XElement(CrifNamespaces.Common + "trailer", + new XAttribute(CrifNamespaces.Common + "registrationState", "WI")); + + if (!string.IsNullOrWhiteSpace(vehicle.TrailerType)) + { + trailerElement.Add(new XAttribute(CrifNamespaces.Common + "propertySubType", vehicle.TrailerType)); + } + + if (vehicle.TrailerYear is not null) + { + trailerElement.Add(new XAttribute(CrifNamespaces.Common + "yearBuild", vehicle.TrailerYear.Value)); + } + + if (!string.IsNullOrWhiteSpace(vehicle.TrailerMake)) + { + trailerElement.Add(new XAttribute(CrifNamespaces.Common + "make", vehicle.TrailerMake)); + } + + return trailerElement; + } + + public static XElement GetAddressContactMechanismElement(Guid addressRef, CrifContactMechanism contactMechanismType) + { + + var contactMechanism = new XElement(CrifNamespaces.Common + "contactMechanism", + new XAttribute(CrifNamespaces.Common + "contactMechanismType", contactMechanismType), + new XElement(CrifNamespaces.Common + "addressRef", + new XAttribute(CrifNamespaces.Common + "idRef", addressRef) + ), + new XElement(CrifNamespaces.Common + "contactMechanismCategory", + new XAttribute(CrifNamespaces.Common + "CD", 2) + ) + ); + + return contactMechanism; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleValuationsSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleValuationsSection.cs new file mode 100644 index 0000000..dc33aa5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleValuationsSection.cs @@ -0,0 +1,79 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Assets; + +namespace CRIF.ACTion.Loan.Assets +{ + public class VehicleValuationsSection : CrifRequestSection + { + public VehicleValuationsSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "vehicleValuations", settings) + { + + } + + public void AddVehicleValuation(Vehicle vehicle) + { + //If there's no vehicle type, it's likely a preapproval and doesn't need this element + if (vehicle is null || vehicle.Type is null) + return; + + var valuationElement = new XElement(CrifNamespaces.Common + "vehicleValuation", + new XAttribute(CrifNamespaces.Common + "id", vehicle.ValuationId), + new XAttribute(CrifNamespaces.Common + "valuationType", "Manual"), + new XAttribute(CrifNamespaces.Common + "make", BuilderUtils.FormatCrifData(vehicle.Make)), + new XAttribute(CrifNamespaces.Common + "model", BuilderUtils.FormatCrifData(vehicle.Model)), + new XAttribute(CrifNamespaces.Common + "year", vehicle.Year.Value), + new XAttribute(CrifNamespaces.Common + "newUsed", vehicle.Condition.Value.ToString()), + new XAttribute(CrifNamespaces.Common + "estimatedValue", vehicle.InitialEstimatedValue)); + + var valuationValueTypesElement = new XElement(CrifNamespaces.Common + "vehicleValuationValueTypes"); + valuationValueTypesElement.Add(new XElement(CrifNamespaces.Common + "vehicleValuationValueType", + new XAttribute(CrifNamespaces.Common + "valueType", "Manual"), + new XAttribute(CrifNamespaces.Common + "baseValue", vehicle.InitialEstimatedValue) + )); + + valuationElement.Add(valuationValueTypesElement); + + if (vehicle.Mileage.HasValue) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "mileage", vehicle.Mileage.Value)); + } + if (!string.IsNullOrWhiteSpace(vehicle.Color)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "color", BuilderUtils.FormatCrifData(vehicle.Color))); + } + if (!string.IsNullOrWhiteSpace(vehicle.Body)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "body", BuilderUtils.FormatCrifData(vehicle.Body))); + } + if (!string.IsNullOrWhiteSpace(vehicle.Series)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "series", BuilderUtils.FormatCrifData(vehicle.Series))); + } + if (!string.IsNullOrWhiteSpace(vehicle.Style)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "style", BuilderUtils.FormatCrifData(vehicle.Style))); + } + if (!string.IsNullOrWhiteSpace(vehicle.Trim)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "trim", BuilderUtils.FormatCrifData(vehicle.Trim))); + } + if (!string.IsNullOrWhiteSpace(vehicle.VIN)) + { + if (vehicle.Type != Landmark.OLA.Models.Assets.Enums.VehicleType.PWC) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "vin", BuilderUtils.FormatCrifData(vehicle.VIN))); + } + } + if (!string.IsNullOrWhiteSpace(vehicle.StateTitled)) + { + valuationElement.Add(new XAttribute(CrifNamespaces.Common + "state", BuilderUtils.FormatCrifData(vehicle.StateTitled))); + } + + AddContent(valuationElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/CreditCardAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/CreditCardAppBuilder.cs new file mode 100644 index 0000000..400be85 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/CreditCardAppBuilder.cs @@ -0,0 +1,45 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Loan +{ + public class CreditCardAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + + public CreditCardAppBuilder(CrifSettings settings) + { + _settings = settings; + } + + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailBankcardSection(createApp.CrifRequestSectionNamespace, _settings); + + var additionalContent = new List(); + + if (createApp.LoanApplication.LoanType == LoanType.CC_Secure) + { + var collateralId = createApp.LoanApplication.SecuredCardCollateralId; + + //per Ben V., all Secure CC loans should have a savings account collateral equal to the CC limit + createApp.Properties.AddProperty(new SavingsAccountProperty(collateralId, createApp.LoanApplication.RequestedAmount.Value)); + + var appDetailProperties = new XElement(CrifNamespaces.App + "appDetailProperties", + new XElement(CrifNamespaces.App + "appDetailProperty", + new XAttribute(CrifNamespaces.Common + "idRef", collateralId), + new XAttribute(CrifNamespaces.Common + "propertyType", "CD"), //from CRIF example + new XAttribute(CrifNamespaces.Common + "propertySubType", "NoneSelected"), //from CRIF example + new XAttribute(CrifNamespaces.App + "statusID", "Active"), //from CRIF example + new XAttribute(CrifNamespaces.App + "collateralTypeID", CrifConstants.CollateralIds[CrifConstants.Collateral.CDsOrSavings]))); + + additionalContent.Add(appDetailProperties); + } + + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel, additionalContent: additionalContent.ToArray()); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipRequestBuilder.cs new file mode 100644 index 0000000..920d338 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipRequestBuilder.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class DealershipRequestBuilder : BuilderBase + { + + public readonly string applicationId; + + public DealershipRequestBuilder(string applicationId, + CrifSettings settings) + : base(CrifNamespaces.App.Namespace, settings) + { + this.applicationId = applicationId; + } + + public override XElement Build(params XElement[] content) + { + var createAppElement = GetAppElement(applicationId); + + return base.Build(createAppElement); + } + + public static XElement GetAppElement(string applicationId) + { + var element = new XElement(CrifNamespaces.Empty + "getApplication"); + element.SetAttributeValue("applicationID", applicationId); + + return element; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipResponseParser.cs new file mode 100644 index 0000000..a964e91 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipResponseParser.cs @@ -0,0 +1,96 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common.Parsers; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Loan +{ + public class DealershipResponseParser : IResponseParser + { + public DealershipAuthRequest Parse(XElement xdoc) + { + try + { + var application = xdoc.Descendants(Common.CrifNamespaces.App + "application").GetEnumerator(); + application.MoveNext(); + + if(application.Current == null) + { + return null; + } + + var applicationId = application.Current.Attribute(Common.CrifNamespaces.App + "applicationID")?.Value; + + var primaryBorrower = xdoc.Descendants(Common.CrifNamespaces.App + "primaryBorrower").GetEnumerator(); + primaryBorrower.MoveNext(); + var primaryBorrowerRef = primaryBorrower.Current.Attribute(Common.CrifNamespaces.Common + "idRef").Value; + + string applicantLastName = null; + var individual = xdoc.Descendants(Common.CrifNamespaces.Common + "individual").GetEnumerator(); + while(individual.MoveNext()) + { + if(individual.Current.Attribute(Common.CrifNamespaces.Common + "id").Value.Equals(primaryBorrowerRef)) + { + applicantLastName = individual.Current.Attribute(Common.CrifNamespaces.Common + "lastName")?.Value; + break; + } + } + + if (applicantLastName == null) + { + // Check for Company as primary applicant + var company = xdoc.Descendants(Common.CrifNamespaces.Common + "company").GetEnumerator(); + while (company.MoveNext()) + { + if (company.Current.Attribute(Common.CrifNamespaces.Common + "id").Value.Equals(primaryBorrowerRef)) + { + applicantLastName = company.Current.Attribute(Common.CrifNamespaces.Common + "tradeName").Value; + break; + } + } + } + + /*if (applicantLastName == null) + { + // Check for DBA/Trust Name as primary applicant + var company = xdoc.Descendants(Common.CrifNamespaces.Common + "company").GetEnumerator(); + while (company.MoveNext()) + { + if (company.Current.Attribute(Common.CrifNamespaces.Common + "id").Value.Equals(primaryBorrowerRef)) + { + applicantLastName = company.Current.Attribute(Common.CrifNamespaces.Common + "code").Value; + break; + } + } + }*/ + + var channel = xdoc.Descendants(Common.CrifNamespaces.Common + "channel"); + var channelEnum = channel.GetEnumerator(); + channelEnum.MoveNext(); + var dealershipCode = channelEnum.Current.Attribute(Common.CrifNamespaces.Common + "code")?.Value; + var dealershipName = channelEnum.Current.Attribute(Common.CrifNamespaces.Common + "dbaName")?.Value; + var dealershipPassword = channelEnum.Current.Attribute(Common.CrifNamespaces.Common + "costCenterCode")?.Value; + + var isActiveFlag = channelEnum.Current.Attribute(Common.CrifNamespaces.Common + "isActiveFL")?.Value; + if(isActiveFlag == "false") + { + return null; + } + + var classificationItems = channel.Descendants(Common.CrifNamespaces.Common + "channelClassificationItem").GetEnumerator(); + while (classificationItems.MoveNext()) + { + if(string.Equals(classificationItems.Current.Attribute(Common.CrifNamespaces.Common + "name").Value, "Dealer", StringComparison.OrdinalIgnoreCase)) + { + return new DealershipAuthRequest(dealershipCode, dealershipName, dealershipPassword, applicantLastName, applicationId); + } + } + + return null; + } + catch + { + throw new FormatException("CRIF XML is in an unexpected layout."); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/HelocFlexAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/HelocFlexAppBuilder.cs new file mode 100644 index 0000000..7d40bde --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/HelocFlexAppBuilder.cs @@ -0,0 +1,28 @@ +using CRIF.ACTion.Common; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Loan.Assets; + +namespace CRIF.ACTion.Loan +{ + public class HelocFlexAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + + public HelocFlexAppBuilder(CrifSettings settings) + { + _settings = settings; + } + + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailHelocSection(createApp.CrifRequestSectionNamespace, _settings); + ((AppDetailHelocSection)createApp.AppDetails).AddPropertyDetails(createApp.LoanApplication.RealEstate); + ((AppDetailHelocSection)createApp.AppDetails).AddHmdaDisclosed(createApp.LoanApplication.PrimaryBorrower.HmdaValues.HmdaNoticeDisclosed); + createApp.Properties.AddProperty(new RealEstateProperty(createApp.LoanApplication.RealEstate)); + createApp.Addresses.AddAddress(createApp.LoanApplication.RealEstate.RealEstateAddress, ContactMechanisms.CrifContactMechanism.RealEstateAddress); + + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppDetails.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppDetails.cs new file mode 100644 index 0000000..e202945 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppDetails.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan +{ + public interface IAppDetails + { + AppDetailsSection AppDetails { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppNotes.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppNotes.cs new file mode 100644 index 0000000..63b022e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppNotes.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan +{ + public interface IAppNotes + { + AppNotesSectionBuilder AppNotes { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/ILoanApplicationRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/ILoanApplicationRequestBuilder.cs new file mode 100644 index 0000000..22cad85 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/ILoanApplicationRequestBuilder.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan +{ + public interface ILoanApplicationRequestBuilder + { + void SetDetails(LoanApplicationRequestBuilder createApp); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IOrg.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IOrg.cs new file mode 100644 index 0000000..4fcc8f1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IOrg.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan +{ + public interface IOrg + { + OrgSection Org { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperties.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperties.cs new file mode 100644 index 0000000..2539377 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperties.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan +{ + public interface IProperties + { + PropertiesSection Properties { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperty.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperty.cs new file mode 100644 index 0000000..52dee50 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperty.cs @@ -0,0 +1,8 @@ +using CRIF.ACTion.Common.Builders; + +namespace CRIF.ACTion.Loan +{ + public interface IProperty : IBuildable + { + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualRequestBuilder.cs new file mode 100644 index 0000000..9b65407 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualRequestBuilder.cs @@ -0,0 +1,65 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class AddIndividualRequestBuilder : BuilderBase, IAppParties, IParties, IAddresses + { + private readonly IApplicant Individual; + + private readonly string ApplicationId; + + public AppPartiesSection AppParties { get; set; } + + public PartiesSection Parties { get; set; } + + public AddressesSection Addresses { get; set; } + + public AddIndividualRequestBuilder(string applicationId, IApplicant individual, CrifSettings settings) + : base(CrifNamespaces.Empty, settings) + { + Individual = individual; + ApplicationId = applicationId; + + AppParties = new AppPartiesSection(CrifRequestSectionNamespace, _settings); + Parties = new PartiesSection(CrifRequestSectionNamespace, _settings); + Addresses = new AddressesSection(CrifRequestSectionNamespace, _settings); + } + + public override XElement Build(params XElement[] content) + { + + Parties.AddApplicant(Individual); + AppParties.AddIndividualParty(Individual, AppPartiesSection.PartyType.borrower, Individual.HasOptedOutTCPA); + + Addresses.AddAddress(Individual.Address, ContactMechanisms.CrifContactMechanism.CurrentAddress); + + if (Individual.MailingAddress is not null) + { + Addresses.AddAddress(Individual.MailingAddress, ContactMechanisms.CrifContactMechanism.MailingAddress); + } + + if (Individual.PreviousAddresses?.Count > 0) + { + foreach (Address previousAddress in Individual.PreviousAddresses) + { + Addresses.AddAddress(previousAddress, ContactMechanisms.CrifContactMechanism.PreviousAddress); + } + } + + var addIndividualElement = new XElement(CrifNamespaces.Empty + "addIndividual", + new XAttribute("applicationID", ApplicationId), + AppParties.Build(), + Parties.Build(), + Addresses.Build()); + + var baseElement = base.Build(addIndividualElement); + + return baseElement; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualResponseParser.cs new file mode 100644 index 0000000..542de7c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualResponseParser.cs @@ -0,0 +1,42 @@ +using System.Xml.Linq; +using System.Xml.XPath; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Parsers; +using Landmark.OLA.Models.Individuals; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class AddIndividualResponseParser: IResponseParser + { + public Individual Parse(XElement xElement) + { + CheckErrors(xElement); + + return ParseIndividual(xElement); + } + + private static void CheckErrors(XElement xElement) + { + var error = xElement.XPathSelectElement("/actionAPI/addIndividual/errors/error", + CrifNamespaces.GetNamespaceManager(xElement)); + + if (error != null) + { + throw new Exception(error.Value); + } + } + + public static Individual ParseIndividual(XElement xElement) + { + var individual = xElement.XPathSelectElement("/actionAPI/addIndividual/msg:appImportResponse/msg:applications/app:application/app:parties/common:individual", + CrifNamespaces.GetNamespaceManager(xElement)); + + var parsedIndividual = new Individual + { + FirstName = individual.Attribute(CrifNamespaces.Common + "firstName").Value + }; + + return parsedIndividual; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddressesSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddressesSection.cs new file mode 100644 index 0000000..dc448c5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddressesSection.cs @@ -0,0 +1,68 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Residences; +using static CRIF.ACTion.Common.ContactMechanisms; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class AddressesSection : CrifRequestSection + { + public AddressesSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "addresses", settings) + { + + } + + public void AddAddress(IAddress address, CrifContactMechanism contactMechanismType) + { + ModelValidator.Validate(address); + + var isCurrentAddress = contactMechanismType == CrifContactMechanism.CurrentAddress + ? true : false; + + var addressElement = new XElement(CrifNamespaces.Common + "address", + new XAttribute(CrifNamespaces.Common + "id", address.Id), + new XAttribute(CrifNamespaces.Common + "street1", BuilderUtils.FormatCrifData(address.Address1)), + new XAttribute(CrifNamespaces.Common + "cityName", BuilderUtils.FormatCrifData(address.City)), + new XAttribute(CrifNamespaces.Common + "state", address.State), + new XAttribute(CrifNamespaces.Common + "postalCode", address.PostalCode), + new XAttribute(CrifNamespaces.Common + "country", address.Country)); + + var addressType = address.GetType(); + if (addressType.Equals(typeof(ResidenceAddress)) || addressType.IsSubclassOf(typeof(ResidenceAddress))) + { + var residence = (ResidenceAddress)address; + + addressElement.Add( + new XAttribute(CrifNamespaces.Common + "isCurrentResFL", isCurrentAddress), + new XAttribute(CrifNamespaces.Common + "monthsResided", residence.MonthsResided)); + + var typeCode = CrifResidenceType.GetCrifResidenceTypeCode(residence.ResidenceType.Value); + + if (typeCode != null) + { + var residenceTypeElement = new XElement(CrifNamespaces.Common + "residenceType", + new XAttribute(CrifNamespaces.Common + "CD", typeCode)); + + addressElement.Add(residenceTypeElement); + } + } + + if (!string.IsNullOrWhiteSpace(address.Address2)) + { + addressElement.Add(new XAttribute(CrifNamespaces.Common + "street2", BuilderUtils.FormatCrifData(address.Address2))); + } + + if (!string.IsNullOrWhiteSpace(address.PostalCodeExt)) + { + addressElement.Add(new XAttribute(CrifNamespaces.Common + "zipExt", address.PostalCodeExt)); + } + + AddContent(addressElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AppPartiesSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AppPartiesSection.cs new file mode 100644 index 0000000..92144fb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AppPartiesSection.cs @@ -0,0 +1,111 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class AppPartiesSection : CrifRequestSection + { + public AppPartiesSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "appParties", settings) + { + + } + + public void AddIndividualParty(IReferenceable individual, PartyType partyType, bool hasOptedOutTCPA = false) + { + var borrowerTypeString = partyType == PartyType.primaryBorrower ? "primaryBorrower" : "borrower"; + + var individualParty = new XElement(CrifNamespaces.App + borrowerTypeString, + new XAttribute(CrifNamespaces.Common + "idRef", individual.Id), + new XAttribute(CrifNamespaces.Common + "partyType", "Individual"), + new XAttribute(CrifNamespaces.App + "isCollateralGrantorFL", false) + ); + + individualParty.Add(GetCustomField(partyType, hasOptedOutTCPA)); + + AddContent(individualParty); + } + + private XElement GetTCPACustomField() + { + var customField = GetBoolFieldLinkElement(CrifConstants.TCPA_Opt_Out_Field_Id, + true, "IndividualApplicant"); + + return customField; + } + + private XElement GetCustomField(PartyType type, bool includeTCPAOptOut) + { + var fieldValue = type switch + { + PartyType.NonSigningSpouseMarriedToCoBorrower => CrifConstants.Borrower_IsApplicantNonSigningSpouse_Field_NonSigningMarriedToCo, + PartyType.NonSigningSpouseMarriedToPrimary => CrifConstants.Borrower_IsApplicantNonSigningSpouse_Field_NonSigningMarriedToPrimary, + _ => CrifConstants.Borrower_IsApplicantNonSigningSpouse_Field_Borrower, + }; + var customField = GetLookupFieldLinkElement(CrifConstants.Borrower_IsApplicantNonSigningSpouse_Field_Id, + fieldValue, "IndividualApplicant"); + + if (includeTCPAOptOut) + { + var tcpaField = GetTCPACustomField(); + var fieldsElementWithTCPA = new XElement(CrifNamespaces.App + "fields", customField, tcpaField); + return fieldsElementWithTCPA; + } + + var fieldsElement = new XElement(CrifNamespaces.App + "fields", customField); + + return fieldsElement; + } + + public void SetSourceChannel(Guid channelId) + { + var sourceChannel = new XElement(CrifNamespaces.App + "sourceChannel", + new XAttribute(CrifNamespaces.Common + "idRef", channelId), + new XAttribute(CrifNamespaces.Common + "partyType", "Channel")); + + AddContent(sourceChannel); + } + + public void SetServicingChannel(Guid channelId) + { + var servicingChannel = new XElement(CrifNamespaces.App + "servicingChannel", + new XAttribute(CrifNamespaces.Common + "idRef", channelId), + new XAttribute(CrifNamespaces.Common + "partyType", "Channel")); + + AddContent(servicingChannel); + } + + private static XElement GetLookupFieldLinkElement(int Id, int value, string fieldLevel) + { + var lookupFieldLinkElement = new XElement(CrifNamespaces.Common + "lookupFieldLink", + new XAttribute(CrifNamespaces.Common + "value", value), + new XElement(CrifNamespaces.Common + "customLookupField", + new XAttribute(CrifNamespaces.Common + "ID", Id), + new XAttribute(CrifNamespaces.Common + "fieldLevel", fieldLevel))); + + return lookupFieldLinkElement; + } + + private static XElement GetBoolFieldLinkElement(int Id, bool value, string fieldLevel) + { + var lookupFieldLinkElement = new XElement(CrifNamespaces.Common + "boolFieldLink", + new XAttribute(CrifNamespaces.Common + "value", value), + new XElement(CrifNamespaces.Common + "customLookupField", + new XAttribute(CrifNamespaces.Common + "ID", Id), + new XAttribute(CrifNamespaces.Common + "fieldLevel", fieldLevel))); + + return lookupFieldLinkElement; + } + + public enum PartyType + { + primaryBorrower, + borrower, + NonSigningSpouseMarriedToPrimary, + NonSigningSpouseMarriedToCoBorrower, + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/CrifResidenceType.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/CrifResidenceType.cs new file mode 100644 index 0000000..6d861c6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/CrifResidenceType.cs @@ -0,0 +1,29 @@ +using Landmark.OLA.Models.Addresses.Enums; + +namespace CRIF.ACTion.Loan.Individuals +{ + public static class CrifResidenceType + { + public static string GetCrifResidenceTypeCode(ResidenceTypeOption residenceTypeOption) + { + return residenceTypeOption switch + { + ResidenceTypeOption.Own => "N", + ResidenceTypeOption.Rent => "R", + ResidenceTypeOption.Other => "O", + _ => throw new ArgumentOutOfRangeException(nameof(residenceTypeOption), "Unexpected ResidenceTypeOptions value"), + }; + } + + public static string GetCrifResidenceTypeName(ResidenceTypeOption residenceTypeOption) + { + return residenceTypeOption switch + { + ResidenceTypeOption.Own => "Own", + ResidenceTypeOption.Rent => "Rent", + ResidenceTypeOption.Other => "Other", + _ => throw new ArgumentOutOfRangeException(nameof(residenceTypeOption), "Unexpected ResidenceTypeOptions value"), + }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAddresses.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAddresses.cs new file mode 100644 index 0000000..3a5a1b2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAddresses.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan.Individuals +{ + public interface IAddresses + { + AddressesSection Addresses { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAppParties.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAppParties.cs new file mode 100644 index 0000000..243dad7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAppParties.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan.Individuals +{ + public interface IAppParties + { + AppPartiesSection AppParties { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IParties.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IParties.cs new file mode 100644 index 0000000..c41d8ac --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IParties.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Loan.Individuals +{ + public interface IParties + { + PartiesSection Parties { get; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IncomeSectionBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IncomeSectionBuilder.cs new file mode 100644 index 0000000..79c6e9a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IncomeSectionBuilder.cs @@ -0,0 +1,36 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using Landmark.OLA.Models.Individuals; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class IncomeSectionBuilder + { + private readonly Income _income; + + public IncomeSectionBuilder(Income income) + { + _income = income; + } + + public XElement Build() + { + var incomeElement = new XElement(CrifNamespaces.Common + "income", + new XAttribute(CrifNamespaces.Common + "IncomeTypeCD", _income.IncomeTypeCD), + new XAttribute(CrifNamespaces.Common + "frequency", _income.Frequency), + new XAttribute(CrifNamespaces.Common + "amountPerPeriod", _income.AmountPerPeriod), + new XAttribute(CrifNamespaces.Common + "incomeStatus", _income.IncomeStatus), + new XAttribute(CrifNamespaces.Common + "incomeAdjustmentPercentage", 1m), + new XAttribute(CrifNamespaces.Common + "adjustedIncome", _income.AmountPerPeriod), + new XAttribute(CrifNamespaces.Common + "sourceName", BuilderUtils.FormatCrifData(_income.SourceName))); + + if (_income.HoursPerWeek.HasValue) + { + incomeElement.Add(new XAttribute(CrifNamespaces.Common + "hoursPerWeek", _income.HoursPerWeek.Value)); + } + + return incomeElement; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/MaritalStatus.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/MaritalStatus.cs new file mode 100644 index 0000000..4dd6ce0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/MaritalStatus.cs @@ -0,0 +1,29 @@ +namespace CRIF.ACTion.Loan.Individuals +{ + public static class MaritalStatus + { + public static string GetMaritalStatusCode(bool isMarried) + { + if (isMarried) + { + return "M"; + } + else + { + return "U"; + } + } + + public static string GetMaritalStatusName(bool isMarried) + { + if (isMarried) + { + return "Married"; + } + else + { + return "Unmarried"; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/PartiesSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/PartiesSection.cs new file mode 100644 index 0000000..fc1fecf --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/PartiesSection.cs @@ -0,0 +1,788 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using CRIF.ACTion.Services.Landmark.SourceChannel; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan.Enums; +using static CRIF.ACTion.Common.ContactMechanisms; + +namespace CRIF.ACTion.Loan.Individuals +{ + public class PartiesSection : CrifRequestSection + { + + private enum CrifGender + { + NoneSelected, + Female, + NotProvided, + Male, + NotApplicable, + MaleFemale, + } + + private readonly IDistributionChannelLookupService _distributionChannelLookupService; + + + public PartiesSection(XNamespace sectionNamespace, + CrifSettings settings, + IDistributionChannelLookupService distributionChannelLookupService = null) + : base(sectionNamespace, "parties", settings) + { + _distributionChannelLookupService = distributionChannelLookupService ?? + new LandmarkDistributionChannelLookupService(settings); + } + + public void AddNonApplicant(INonApplicant individual, bool isMarried = false) + { + var individualElement = GetNonApplicantElement(individual, isMarried); + + AddContent(individualElement); + } + + public void AddApplicant(IApplicant individual, + Guid? externalSpouseId = null, + PropertyLot mfgHomeLot = null) + { + XElement partyRelationships = null; + + if (externalSpouseId.HasValue || + individual.HasAnyNonApplicantSpouse() || + individual.Employers?.Count > 0) + { + partyRelationships = GetApplicantPartyRelationshipsSectionElement(individual, externalSpouseId); + } + + var individualElement = GetInitialIndividualElement(individual, partyRelationships, mfgHomeLot); + + SetHmdaData(individual, individualElement); + + //this must occur last within the individual element + var maritalStatusElement = GetMaritalStatusElement(individual.IsMarried ?? false); + individualElement.Add(maritalStatusElement); + + AddContent(individualElement); + } + + public void AddHistory(IIndividualHistory individual, bool isMarried = false, bool requiresHMDA = false) + { + XElement partyRelationships = null; + + if (individual.Employers?.Count > 0) + { + partyRelationships = GetHistoryPartyRelationshipsSectionElement(individual); + } + + var individualElement = GetInitialIndividualElement(individual, partyRelationships); + if (requiresHMDA) + { + AddNonApplicantHmdaDetermination(individualElement); + } + //this must occur last within the individual element + var maritalStatusElement = GetMaritalStatusElement(isMarried); + individualElement.Add(maritalStatusElement); + + AddContent(individualElement); + } + + public static void AddNonApplicantHmdaDetermination(XElement individualElement) + { + var ethnicityDeterminationElement = new XElement(CrifNamespaces.Common + "ethnicityDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "T"), + new XAttribute(CrifNamespaces.Common + "name", "No Co-Applicant"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(ethnicityDeterminationElement); + var raceDeterminationElement = new XElement(CrifNamespaces.Common + "raceDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "T"), + new XAttribute(CrifNamespaces.Common + "name", "No Co-Applicant"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(raceDeterminationElement); + var sexDeterminationElement = new XElement(CrifNamespaces.Common + "sexDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "T"), + new XAttribute(CrifNamespaces.Common + "name", "No Co-Applicant"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(sexDeterminationElement); + var applicationTakenMethodElement = new XElement(CrifNamespaces.Common + "applicationTakenMethod", + new XAttribute(CrifNamespaces.Common + "lookupType", "ApplicationTakenMethod"), + new XAttribute(CrifNamespaces.Common + "CD", "2"), + new XAttribute(CrifNamespaces.Common + "name", "Internet"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "description", "Internet")); + individualElement.Add(applicationTakenMethodElement); + } + + private XElement GetNonApplicantElement(INonApplicant individual, bool isMarried = false) + { + var individualElement = new XElement(CrifNamespaces.Common + "individual", + new XAttribute(CrifNamespaces.Common + "id", individual.Id), + new XAttribute(CrifNamespaces.Common + "languageID", individual.Language.Value), + new XAttribute(CrifNamespaces.Common + "firstName", BuilderUtils.FormatCrifData(individual.FirstName)), + new XAttribute(CrifNamespaces.Common + "lastName", BuilderUtils.FormatCrifData(individual.LastName)), + new XAttribute(CrifNamespaces.Common + "ssn", individual.SSN ?? "") + ); + + if (!string.IsNullOrWhiteSpace(individual.MiddleInitial)) + { + individualElement.Add(new XAttribute(CrifNamespaces.Common + "middleName", BuilderUtils.FormatCrifData(individual.MiddleInitial))); + } + + var contact = new XElement(CrifNamespaces.Common + "contactMechanisms"); + var currentAddress = GetAddressContactMechanismElement(individual.MailingAddress.Id, CrifContactMechanism.CurrentAddress); + + contact.Add(currentAddress); + individualElement.Add(contact); + + //this must occur last within the individual element + var maritalStatusElement = GetMaritalStatusElement(isMarried); + individualElement.Add(maritalStatusElement); + + return individualElement; + } + + private XElement GetInitialIndividualElement(IIndividualHistory individual, + XElement partyRelationshipElement = null, + PropertyLot mfgHomeLot = null) + { + ModelValidator.Validate(individual); + + var individualElement = new XElement(CrifNamespaces.Common + "individual", + new XAttribute(CrifNamespaces.Common + "id", individual.Id), + new XAttribute(CrifNamespaces.Common + "languageID", individual.Language.Value), + new XAttribute(CrifNamespaces.Common + "firstName", BuilderUtils.FormatCrifData(individual.FirstName)), + new XAttribute(CrifNamespaces.Common + "lastName", BuilderUtils.FormatCrifData(individual.LastName)), + new XAttribute(CrifNamespaces.Common + "dob", Dates.GetFormattedDOB(individual.DOB.Value)), + new XAttribute(CrifNamespaces.Common + "ssn", individual.SSN), + new XAttribute(CrifNamespaces.Common + "customerNum", individual.CustomerNumber) + ); + + if (individual.IsLandmarkEmployee.HasValue) + { + individualElement.Add(new XAttribute(CrifNamespaces.Common + "isEmployeeFL", individual.IsLandmarkEmployee.Value)); + } + + var contact = GetContactMechanismsSectionElement(individual); + individualElement.Add(contact); + + individualElement.Add(partyRelationshipElement); //order matters for this element + + if (individual.Employers?.Count > 0 || individual.OtherIncomes?.Count > 0) + { + var incomes = GetIncomesSectionElement(individual); + individualElement.Add(incomes);; + } + + var tradesElement = GetTradesElement(individual, mfgHomeLot); + individualElement.Add(tradesElement); + + if (individual.Suffix != null) + { + var suffixElement = GetSuffixElement(individual); + individualElement.Add(suffixElement); + } + + if (!string.IsNullOrWhiteSpace(individual.MiddleInitial)) + { + individualElement.Add(new XAttribute(CrifNamespaces.Common + "middleName", BuilderUtils.FormatCrifData(individual.MiddleInitial))); + } + + return individualElement; + } + + private void SetHmdaData(IApplicant individual, XElement individualElement) + { + var racesDetermination = false; + if (individual.HmdaValues.Races != null && individual.HmdaValues.Races.Count > 0) + { + var racesElement = GetRacesElement(individual); + individualElement.Add(racesElement); + + racesDetermination = true; + } + + var ethnicitiesDetermination = false; + if (individual.HmdaValues.Ethnicities != null && individual.HmdaValues.Ethnicities.Count > 0) + { + var ethnicitiesElement = GetEthnicitiesElement(individual); + individualElement.Add(ethnicitiesElement); + + ethnicitiesDetermination = true; + } + + if (ethnicitiesDetermination) + { + if (!(individual.HmdaValues.Ethnicities.Count == 1 && individual.HmdaValues.Ethnicities.Contains(Ethnicity.NotProvided))) + { + var ethnicityDeterminationElement = new XElement(CrifNamespaces.Common + "ethnicityDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "N"), + new XAttribute(CrifNamespaces.Common + "name", "Not Collected on the Basis of Visual Observation or Surname"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(ethnicityDeterminationElement); + + } + + } + + if (racesDetermination) + { + if (!(individual.HmdaValues.Races.Count == 1 && individual.HmdaValues.Races.Contains(Race.NotProvided))) + { + var raceDeterminationElement = new XElement(CrifNamespaces.Common + "raceDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "N"), + new XAttribute(CrifNamespaces.Common + "name", "Not Collected on the Basis of Visual Observation or Surname"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(raceDeterminationElement); + } + + } + + if (individual.HmdaValues.Sexes != null && individual.HmdaValues.Sexes.Count > 0) + { + var sexElements = GetSexAttributes(individual); + individualElement.Add(sexElements); + + if (!(individual.HmdaValues.Sexes.Count == 1 && individual.HmdaValues.Sexes.Contains(Sex.NotProvided))) + { + var sexDeterminationElement = new XElement(CrifNamespaces.Common + "sexDetermination", + new XAttribute(CrifNamespaces.Common + "lookupType", "GMIDetermination"), + new XAttribute(CrifNamespaces.Common + "CD", "N"), + new XAttribute(CrifNamespaces.Common + "name", "Not Collected on the Basis of Visual Observation or Surname"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "externalSystemCode", "3")); + individualElement.Add(sexDeterminationElement); + } + + + } + + var applicationTakenMethodElement = GetApplicationTakenMethodElement(individual); + individualElement.Add(applicationTakenMethodElement); + } + + public XElement GetApplicationTakenMethodElement(IApplicant individual) + { + if (!GetApplicationTakenMethodElementRequired(individual)) + { + return null; + } + + var applicationTakenMethodElement = new XElement(CrifNamespaces.Common + "applicationTakenMethod", + new XAttribute(CrifNamespaces.Common + "lookupType", "ApplicationTakenMethod"), + new XAttribute(CrifNamespaces.Common + "CD", "2"), + new XAttribute(CrifNamespaces.Common + "name", "Internet"), + new XAttribute(CrifNamespaces.Common + "isActive", true), + new XAttribute(CrifNamespaces.Common + "description", "Internet")); + + return applicationTakenMethodElement; + } + + public static XElement GetSuffixElement(IIndividualHistory individual) + { + var suffixType = SuffixMap.GetSuffixType(individual.Suffix); + + var suffixElement = new XElement(CrifNamespaces.Common + "suffix", + new XAttribute(CrifNamespaces.Common + "lookupType", "Suffix"), + new XAttribute(CrifNamespaces.Common + "CD", suffixType.CD), + new XAttribute(CrifNamespaces.Common + "name", suffixType.Name), + new XAttribute(CrifNamespaces.Common + "isActive", true)); + + return suffixElement; + + } + + public static XElement GetTradesElement(IIndividualHistory individual, PropertyLot mfgHomeLot) + { + var tradesElement = new XElement(CrifNamespaces.Common + "trades"); + + var mortgageOrRentTradeElement = new XElement(CrifNamespaces.Common + "trade", + new XAttribute(CrifNamespaces.Common + "accountTypeCD", "RealEstateMortgage"), + new XAttribute(CrifNamespaces.Common + "tradeTypeID", "Mortgage"), + new XAttribute(CrifNamespaces.Common + "creditorName", "Rent - Mortgage"), + new XAttribute(CrifNamespaces.Common + "debtStatusCD", "Include"), + new XAttribute(CrifNamespaces.Common + "ecoaCD", "Individual"), + new XAttribute(CrifNamespaces.Common + "isCurrResidenceRentMortgage", true), + new XAttribute(CrifNamespaces.Common + "monthlyPayment", individual.Address.MonthlyPayment), + + new XElement(CrifNamespaces.Common + "kindOfBusiness", + new XAttribute(CrifNamespaces.Common + "lookupType", "KOB"), + new XAttribute(CrifNamespaces.Common + "CD", "FM"))); + + tradesElement.Add(mortgageOrRentTradeElement); + + if ((!(mfgHomeLot is null)) && mfgHomeLot.RentalAmt.HasValue) + { + var mfgHomeLotRentalElement = new XElement(CrifNamespaces.Common + "trade", + new XAttribute(CrifNamespaces.Common + "tradeTypeID", "Mortgage"), + new XAttribute(CrifNamespaces.Common + "creditorName", "Lot Rent"), + new XAttribute(CrifNamespaces.Common + "debtStatusCD", "Include"), + new XAttribute(CrifNamespaces.Common + "ecoaCD", "Individual"), + new XAttribute(CrifNamespaces.Common + "monthlyPayment", mfgHomeLot.RentalAmt.Value), + + new XElement(CrifNamespaces.Common + "kindOfBusiness", + new XAttribute(CrifNamespaces.Common + "lookupType", "KOB"), + new XAttribute(CrifNamespaces.Common + "CD", "FM"))); + + tradesElement.Add(mfgHomeLotRentalElement); + } + + if (individual.OtherIncomes?.Count > 0) + { + foreach (var income in individual.OtherIncomes) + { + if (income.IncomeTypeCD == IncomeType.Alimony) + { + double monthlyAlimony = (double)income.AmountPerPeriod / 12; + var alimonyElement = new XElement(CrifNamespaces.Common + "trade", + new XAttribute(CrifNamespaces.Common + "tradeTypeID", "Open"), + new XAttribute(CrifNamespaces.Common + "creditorName", "Alimony/Child Support - Payment"), + new XAttribute(CrifNamespaces.Common + "debtStatusCD", "Include"), + new XAttribute(CrifNamespaces.Common + "ecoaCD", "Individual"), + new XAttribute(CrifNamespaces.Common + "monthlyPayment", monthlyAlimony), + new XElement(CrifNamespaces.Common + "kindOfBusiness", + new XAttribute(CrifNamespaces.Common + "lookupType", "KOB"), + new XAttribute(CrifNamespaces.Common + "CD", "VK"))); + tradesElement.Add(alimonyElement); + } + } + } + + + return tradesElement; + } + + private static bool GetApplicationTakenMethodElementRequired(IApplicant individual) + { + var hasRaces = individual.HmdaValues.Races != null && individual.HmdaValues.Races.Count > 0; + var hasEthnicities = individual.HmdaValues.Ethnicities != null && individual.HmdaValues.Ethnicities.Count > 0; + var hasSexes = individual.HmdaValues.Sexes != null && individual.HmdaValues.Sexes.Count > 0; + + return hasRaces || hasEthnicities || hasSexes; + } + + public static XAttribute[] GetSexAttributes(IApplicant individual) + { + var attributeList = new List(); + + var hasMale = individual.HmdaValues.Sexes.Contains(Sex.Male); + var hasFemale = individual.HmdaValues.Sexes.Contains(Sex.Female); + var isNotProvided = individual.HmdaValues.Sexes.Contains(Sex.NotProvided); + + var genderCDValue = CrifGender.NoneSelected; + + if (isNotProvided && !hasMale && !hasFemale) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "genderCD", Sex.NotProvided)); + return [.. attributeList]; + } + + if (hasMale) + { + if (hasFemale) + { + genderCDValue = CrifGender.MaleFemale; + } + else + { + genderCDValue = CrifGender.Male; + } + } + else if (hasFemale) + { + genderCDValue = CrifGender.Female; + } + + if (genderCDValue != CrifGender.NoneSelected) + { + attributeList.Add(new XAttribute(CrifNamespaces.Common + "genderCD", genderCDValue.ToString())); + } + + //Yes, you both could provide an answer and say that you don't wish to provide an answer + //Validation will likely restrict that + //Ignore Not Provided if Other Values are Selected + attributeList.Add(new XAttribute(CrifNamespaces.Common + "refusedToFurnishGenderFL", false)); + return [.. attributeList]; + } + + public static XElement GetRacesElement(IApplicant individual) + { + var racesElement = new XElement(CrifNamespaces.Common + "races"); + + if (individual.HmdaValues.Races.Count == 1 && individual.HmdaValues.Races.Contains(Race.NotProvided)){ + var raceElement = new XElement(CrifNamespaces.Common + "race", + new XAttribute(CrifNamespaces.Common + "isCheckedFL", true), + new XAttribute(CrifNamespaces.Common + "race", Race.NotProvided.ToString())); + + racesElement.RemoveNodes(); + racesElement.Add(raceElement); + return racesElement; + } + + foreach (Race raceOption in (Race[])Enum.GetValues(typeof(Race))) + { + var isChecked = individual.HmdaValues.Races.Contains(raceOption); + + bool isNotProvided = (raceOption == Race.NotProvided); + + string otherRaceInfoValue = null; + switch (raceOption) + { + case Race.AmericanIndianAlaskaNative: + otherRaceInfoValue = individual.HmdaValues.AmericanIndianAlaskaNativeTribe; + break; + case Race.OtherAsian: + otherRaceInfoValue = individual.HmdaValues.OtherAsianRace; + break; + case Race.OtherPacificIslander: + otherRaceInfoValue = individual.HmdaValues.OtherPacificIslanderRace; + break; + } + var hasOtherValue = !string.IsNullOrWhiteSpace(otherRaceInfoValue); + + //Ignore not provided if it is not the only ethnicity selected + if ( (isChecked || hasOtherValue) && !isNotProvided) + { + + var raceElement = new XElement(CrifNamespaces.Common + "race", + new XAttribute(CrifNamespaces.Common + "isCheckedFL", isChecked), + new XAttribute(CrifNamespaces.Common + "race", raceOption.ToString())); + + if (hasOtherValue) + { + raceElement.Add(new XAttribute(CrifNamespaces.Common + "otherRaceInfo", BuilderUtils.FormatCrifData(otherRaceInfoValue))); + } + + racesElement.Add(raceElement); + } + } + + return racesElement; + } + + public static XElement GetEthnicitiesElement(IApplicant individual) + { + var ethnicitiesElement = new XElement(CrifNamespaces.Common + "ethnicities"); + + if (individual.HmdaValues.Ethnicities.Count == 1 && individual.HmdaValues.Ethnicities.Contains(Ethnicity.NotProvided)){ + var ethnicityElement = new XElement(CrifNamespaces.Common + "ethnicity", + new XAttribute(CrifNamespaces.Common + "isCheckedFL", true), + new XAttribute(CrifNamespaces.Common + "ethnicity", Ethnicity.NotProvided.ToString())); + + ethnicitiesElement.RemoveNodes(); + ethnicitiesElement.Add(ethnicityElement); + return ethnicitiesElement; + } + + foreach (Ethnicity ethnicityOption in (Ethnicity[])Enum.GetValues(typeof(Ethnicity))) + { + var isChecked = individual.HmdaValues.Ethnicities.Contains(ethnicityOption); + + bool isNotProvided = (ethnicityOption == Ethnicity.NotProvided); + + string otherEthnicityInfo = null; + switch (ethnicityOption) + { + case Ethnicity.OtherHispanicLatino: + otherEthnicityInfo = individual.HmdaValues.OtherHispanicLatinoEthnicity; + break; + } + var hasOtherValue = !string.IsNullOrWhiteSpace(otherEthnicityInfo); + + //Ignore not provided if it is not the only ethnicity selected + if ( (isChecked || hasOtherValue) && !isNotProvided) + { + var ethnicityElement = new XElement(CrifNamespaces.Common + "ethnicity", + new XAttribute(CrifNamespaces.Common + "isCheckedFL", isChecked), + new XAttribute(CrifNamespaces.Common + "ethnicity", ethnicityOption.ToString())); + + if (hasOtherValue) + { + ethnicityElement.Add(new XAttribute(CrifNamespaces.Common + "otherEthnicityInfo", BuilderUtils.FormatCrifData(otherEthnicityInfo))); + } + + ethnicitiesElement.Add(ethnicityElement); + } + } + + return ethnicitiesElement; + } + + public static XElement GetIncomesSectionElement(IIndividualHistory individual) + { + var incomesElement = new XElement(CrifNamespaces.Common + "incomes"); + if (individual.Employers?.Count > 0) + { + foreach (var employer in individual.Employers) + { + var income = employer.Income; + + ModelValidator.Validate(income); + + var incomeElement = new IncomeSectionBuilder(income).Build(); + + var employerRefElement = new XElement(CrifNamespaces.Common + "employerRef", + new XAttribute(CrifNamespaces.Common + "idRef", employer.GetOrCreateGuid())); + incomeElement.Add(employerRefElement); + incomesElement.Add(incomeElement); + } + } + if (individual.OtherIncomes?.Count > 0) + { + foreach (var income in individual.OtherIncomes) + { + if (income.IncomeTypeCD != IncomeType.Alimony) + { + var incomeElement = new IncomeSectionBuilder(income).Build(); + incomesElement.Add(incomeElement); + } + } + } + return incomesElement; + } + + public static XElement GetHistoryPartyRelationshipsSectionElement(IIndividualHistory individual) + { + var prElement = new XElement(CrifNamespaces.Common + "partyRelationships"); + if (individual.Employers?.Count > 0) + { + foreach (var employer in individual.Employers) + { + ModelValidator.Validate(employer); + + var employmentType = employer.EmploymentType; + + var employmentElement = new XElement(CrifNamespaces.Common + "employment", + new XAttribute(CrifNamespaces.Common + "relationshipType", "Employment")); + var partyRefElement = new XElement(CrifNamespaces.Common + "partyRef", + new XAttribute(CrifNamespaces.XSI + "type", $"{CrifNamespaces.Common.Prefix}:EmployerRef"), + new XAttribute(CrifNamespaces.Common + "idRef", employer.GetOrCreateGuid()), + new XAttribute(CrifNamespaces.Common + "partyType", "Employer")); + var employmentSubRelElement = new XElement(CrifNamespaces.Common + "employmentSubRel", + new XAttribute(CrifNamespaces.Common + "isCurrentFL", employer.CurrentEmployer), + new XAttribute(CrifNamespaces.Common + "title", BuilderUtils.FormatCrifData(employer.Title)), + new XAttribute(CrifNamespaces.Common + "monthsEmployed", employer.MonthsEmployed), + new XAttribute(CrifNamespaces.Common + "isSelfEmployed", false), //TODO #239: is this always false? + new XAttribute(CrifNamespaces.Common + "employmentType", employmentType)); + var userEmploymentTypeElement = new XElement(CrifNamespaces.Common + "userEmploymentType", + new XAttribute(CrifNamespaces.Common + "lookupType", "UserEmploymentType"), //TODO #240: Does this have other values? + new XAttribute(CrifNamespaces.Common + "CD", UserEmploymentType.GetCrifUserEmploymentTypeIndex(employmentType)), //TODO #240 + new XAttribute(CrifNamespaces.Common + "name", UserEmploymentType.GetCrifUserEmploymentTypeName(employmentType))); + + employmentSubRelElement.Add(userEmploymentTypeElement); + employmentElement.Add(partyRefElement); + employmentElement.Add(employmentSubRelElement); + prElement.Add(employmentElement); + } + } + + return prElement; + } + + public static XElement GetApplicantPartyRelationshipsSectionElement(IApplicant individual, + Guid? externalSpouseId = null) + { + var prElement = GetHistoryPartyRelationshipsSectionElement(individual); + + prElement.Add(GetMarriageElement(individual, externalSpouseId)); + + return prElement; + } + + private static XElement GetMarriageElement(IApplicant individual, Guid? externalSpouseId = null) + { + if (!(individual.HasAnyNonApplicantSpouse() || externalSpouseId.HasValue)) + { + return null; + } + + Guid spouseId; + + if (externalSpouseId.HasValue) + { + spouseId = externalSpouseId.Value; + } + else + { + if (individual.NonApplicantSpouse is not null) + { + spouseId = individual.NonApplicantSpouse.Id; + } + else + { + spouseId = individual.DetailedNonApplicantSpouse.Id; + } + } + + var partyRef = new XElement(CrifNamespaces.Common + "partyRef", + new XAttribute(CrifNamespaces.XSI + "type", $"{CrifNamespaces.Common.Prefix}:IndividualRef"), + new XAttribute(CrifNamespaces.Common + "idRef", spouseId), + new XAttribute(CrifNamespaces.Common + "partyType", "Individual")); + + var marriageRelationshipElement = new XElement(CrifNamespaces.Common + "partyRelationship", + new XAttribute(CrifNamespaces.Common + "relationshipType", "Marriage"), + partyRef); + + return marriageRelationshipElement; + } + + public static XElement GetMaritalStatusElement(bool isMarried) + { + var maritalStatusElement = new XElement(CrifNamespaces.Common + "maritalStatus", + new XAttribute(CrifNamespaces.Common + "lookupType", "MaritalStatus"), + new XAttribute(CrifNamespaces.Common + "CD", MaritalStatus.GetMaritalStatusCode(isMarried)), + new XAttribute(CrifNamespaces.Common + "name", MaritalStatus.GetMaritalStatusName(isMarried))); + + return maritalStatusElement; + } + + public static XElement GetContactMechanismsSectionElement(IIndividualHistory individual) + { + var contact = new XElement(CrifNamespaces.Common + "contactMechanisms"); + + var mechanismType = ContactMechanisms.GetPhoneType(individual.PhoneType.Value); + var primaryPhone = GetContactMechanismElement(individual.Phone, mechanismType); + + var email = GetContactMechanismElement(individual.Email, ContactMechanisms.CrifContactMechanism.EMail); + + var currentAddress = GetAddressContactMechanismElement(individual.Address.Id, ContactMechanisms.CrifContactMechanism.CurrentAddress); + + contact.Add(primaryPhone); + contact.Add(email); + contact.Add(currentAddress); + + if (individual.SecondaryPhone?.Length > 0) + { + mechanismType = ContactMechanisms.GetPhoneType(individual.SecondaryPhoneType.Value); + var secondaryPhone = GetContactMechanismElement(individual.SecondaryPhone, mechanismType); + + contact.Add(secondaryPhone); + } + + if (individual.MailingAddress is not null) + { + var mailingAddressElement = GetAddressContactMechanismElement(individual.MailingAddress.Id, ContactMechanisms.CrifContactMechanism.MailingAddress); + contact.Add(mailingAddressElement); + } + else + { + var mailingAddressElement = GetAddressContactMechanismElement(individual.Address.Id,ContactMechanisms.CrifContactMechanism.MailingAddress); + contact.Add(mailingAddressElement); + } + + if (individual.PreviousAddresses?.Count > 0) + { + foreach (var previousAddress in individual.PreviousAddresses) + { + var previousAddressElement = GetAddressContactMechanismElement(previousAddress.Id, ContactMechanisms.CrifContactMechanism.PreviousAddress); + contact.Add(previousAddressElement); + } + } + + return contact; + } + + public static XElement GetAddressContactMechanismElement(Guid addressRef, ContactMechanisms.CrifContactMechanism contactMechanismType) + { + var contactMechanismCategory = ContactMechanisms.GetContactMechanismCategory(contactMechanismType); + + var contactMechanism = new XElement(CrifNamespaces.Common + "contactMechanism", + new XAttribute(CrifNamespaces.Common + "contactMechanismType", contactMechanismType), + new XElement(CrifNamespaces.Common + "addressRef", + new XAttribute(CrifNamespaces.Common + "idRef", addressRef) + ), + new XElement(CrifNamespaces.Common + "contactMechanismCategory", + new XAttribute(CrifNamespaces.Common + "CD", (int)contactMechanismCategory) + ) + ); + + return contactMechanism; + } + + public static XElement GetEmployerContactMechanismsSectionElement(Employer employer) + { + var contact = new XElement(CrifNamespaces.Common + "contactMechanisms"); + if (!string.IsNullOrWhiteSpace(employer.WorkPhone)) + { + contact.Add(GetContactMechanismElement(employer.WorkPhone, CrifContactMechanism.WorkPhone)); + } + return contact; + } + + public static XElement GetContactMechanismElement(string contactString, CrifContactMechanism contactMechanismType) + { + var contactMechanismCategory = GetContactMechanismCategory(contactMechanismType); + + var contactMechanism = new XElement(CrifNamespaces.Common + "contactMechanism", + new XAttribute(CrifNamespaces.Common + "contactMechanismType", contactMechanismType), + new XAttribute(CrifNamespaces.Common + "contactString", BuilderUtils.FormatCrifData(contactString)), + new XElement(CrifNamespaces.Common + "contactMechanismCategory", + new XAttribute(CrifNamespaces.Common + "CD", (int)contactMechanismCategory) + ) + ); + + return contactMechanism; + } + + public void AddOrganizationParty(Guid orgId) + { + var organization = new XElement(CrifNamespaces.Common + "organization", + new XAttribute(CrifNamespaces.Common + "id", orgId), + new XAttribute(CrifNamespaces.Common + "partyID", 4)); //Per CRIF Documentation + + AddContent(organization); + } + + public void AddSourceChannelParty(Guid channelId, LoanType loanType) + { + var channelLookup = new LandmarkSourceChannelLookupService(_settings); + + int partyId = channelLookup.GetSourceChannelByLoanType(loanType); + + AddChannelParty(channelId, partyId); + } + + public void AddServicingChannelParty(Guid channelId, string zipCode) + { + var partyId = _distributionChannelLookupService.GetDistributionChannelByZipCode(zipCode); + + AddChannelParty(channelId, partyId); + } + + private void AddChannelParty(Guid channelId, int partyId) + { + var channel = new XElement(CrifNamespaces.Common + "channel", + new XAttribute(CrifNamespaces.Common + "id", channelId), + new XAttribute(CrifNamespaces.Common + "partyID", partyId)); + + AddContent(channel); + } + + public void AddEmployer(Employer employer) + { + ModelValidator.Validate(employer); + + var employerElement = new XElement(CrifNamespaces.Common + "employer", + new XAttribute(CrifNamespaces.Common + "id", employer.GetOrCreateGuid()), + new XAttribute(CrifNamespaces.Common + "name", BuilderUtils.FormatCrifData(employer.Name)), + new XAttribute(CrifNamespaces.Common + "languageID", employer.Language)); + + var contact = GetEmployerContactMechanismsSectionElement(employer); + + employerElement.Add(contact); + AddContent(employerElement); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/UserEmploymentType.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/UserEmploymentType.cs new file mode 100644 index 0000000..3c443b1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/UserEmploymentType.cs @@ -0,0 +1,38 @@ +using Landmark.OLA.Models.Individuals.Enums; + +namespace CRIF.ACTion.Loan.Individuals +{ + public static class UserEmploymentType + { + + public static string GetCrifUserEmploymentTypeIndex(EmploymentTypeOption userEmploymentTypeOption) + { + return userEmploymentTypeOption switch + { + EmploymentTypeOption.NoneSelected => "0", + EmploymentTypeOption.Contract => "1", + EmploymentTypeOption.FullTime => "2", + EmploymentTypeOption.PartTime => "3", + EmploymentTypeOption.SelfEmployed => "4", + EmploymentTypeOption.Retired => "5", + EmploymentTypeOption.Temporary => "6", + _ => throw new ArgumentOutOfRangeException(nameof(userEmploymentTypeOption), "Unexpected UserEmploymentType value"), + }; + } + + public static string GetCrifUserEmploymentTypeName(EmploymentTypeOption userEmploymentTypeOption) + { + return userEmploymentTypeOption switch + { + EmploymentTypeOption.NoneSelected => "None Selected", + EmploymentTypeOption.Contract => "Contract", + EmploymentTypeOption.FullTime => "Full Time", + EmploymentTypeOption.PartTime => "Part Time", + EmploymentTypeOption.SelfEmployed => "Self Employed", + EmploymentTypeOption.Retired => "Retired", + EmploymentTypeOption.Temporary => "Temporary", + _ => throw new ArgumentOutOfRangeException(nameof(userEmploymentTypeOption), "Unexpected UserEmploymentType value"), + }; + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationRequestBuilder.cs new file mode 100644 index 0000000..ee6bd95 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationRequestBuilder.cs @@ -0,0 +1,384 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using CRIF.ACTion.Loan.Assets; +using CRIF.ACTion.Loan.Individuals; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Loan +{ + public class LoanApplicationRequestBuilder : BuilderBase, IParties, IAppDetails, IAppParties, + IAddresses, IOrg, IProperties, IVehicleValuations, IAppNotes + { + public readonly LoanApplication LoanApplication; + private readonly Guid OrgRef; + public readonly Guid _sourceChannel; + public readonly Guid _servicingChannel; + + public AppPartiesSection AppParties { get; set; } + + public PartiesSection Parties { get; set; } + + public AppDetailsSection AppDetails { get; set; } + + public AddressesSection Addresses { get; set; } + + public AppNotesSectionBuilder AppNotes { get; set; } + + public OrgSection Org { get; set; } + + public PropertiesSection Properties { get; set; } + + public VehicleValuationsSection VehicleValuations { get; set; } + + public LoanApplicationRequestBuilder(LoanApplication loanApplication, + CrifSettings settings, + Guid? orgRef = null, + Guid? sourceChannel = null, + Guid? servicingChannel = null, + IDistributionChannelLookupService distributionChannelLookupService = null) + : base(CrifNamespaces.App.Namespace, settings) + { + LoanApplication = loanApplication; + + OrgRef = orgRef ?? Guid.NewGuid(); + _sourceChannel = sourceChannel ?? Guid.NewGuid(); + _servicingChannel = servicingChannel ?? Guid.NewGuid(); + + AppParties = new AppPartiesSection(CrifRequestSectionNamespace, settings); + Parties = new PartiesSection(CrifRequestSectionNamespace, settings, distributionChannelLookupService); + Addresses = new AddressesSection(CrifRequestSectionNamespace, settings); + AppNotes = new AppNotesSectionBuilder(CrifRequestSectionNamespace, settings); + Org = new OrgSection(CrifRequestSectionNamespace, settings); + Properties = new PropertiesSection(CrifRequestSectionNamespace, settings); + VehicleValuations = new VehicleValuationsSection(CrifRequestSectionNamespace, settings); + } + + public override XElement Build(params XElement[] content) + { + Org.SetOrgId(OrgRef); + + Parties.AddOrganizationParty(OrgRef); + + Parties.AddSourceChannelParty(_sourceChannel, LoanApplication.LoanType.Value); + + Parties.AddServicingChannelParty(_servicingChannel, LoanApplication?.PrimaryBorrower?.Address?.PostalCode ?? ""); + + AppParties.SetSourceChannel(_sourceChannel); + + AppParties.SetServicingChannel(_servicingChannel); + + AddApplicationNotes(); + + var productDetails = CreateProductDetails(LoanApplication.LoanType, _settings); + productDetails.SetDetails(this); + + Guid? externalSpouseId = null; + if (LoanApplication.CoApplicant is not null && LoanApplication.CoApplicant.IsMarriedToPrimaryBorrower) + { + externalSpouseId = LoanApplication.CoApplicant.Id; + } + + + PropertyLot mfgHomeLot = null; + + if (!(LoanApplication.ManufacturedHome?.Lot?.RentalAmt is null)) + { + //needed to pass long possible Lot rent amount + mfgHomeLot = LoanApplication.ManufacturedHome.Lot; + } + + ProcessIndividual(LoanApplication.PrimaryBorrower, + AppPartiesSection.PartyType.primaryBorrower, + externalSpouseId, + mfgHomeLot); + + if (LoanApplication.CoApplicant is not null) + { + ProcessIndividual(LoanApplication.CoApplicant, AppPartiesSection.PartyType.borrower); + + CheckForAndProcessSpouses(LoanApplication.CoApplicant); + } + + CheckForAndProcessSpouses(LoanApplication.PrimaryBorrower, true); + + var createAppElement = GetCreateAppElement(LoanApplication.Date, + AppParties.Build(), + Parties.Build(), + AppDetails.Build(), + Properties.Build(), + Addresses.Build(), + AppNotes.Build(), + Org.Build(), + VehicleValuations.Build()); + + var baseElement = base.Build(createAppElement); + + return baseElement; + } + + private void AddApplicationNotes() + { + AppNotes.AddAppNote("IP: " + LoanApplication.IPAddress + ", DISCLOSURE DATE: " + ConvertUtcToCentral(LoanApplication.DisclosureDate)); + if (!string.IsNullOrWhiteSpace(LoanApplication.PrimaryContactMethod)) + { + AppNotes.AddAppNote("PRIMARY CONTACT METHOD: " + LoanApplication.PrimaryContactMethod); + } + + if (!string.IsNullOrWhiteSpace(LoanApplication.PromoCode)) + { + AppNotes.AddAppNote("PROMO CODE: " + LoanApplication.PromoCode); + } + + if (!string.IsNullOrWhiteSpace(LoanApplication.Note)) + { + AppNotes.AddAppNote(LoanApplication.Note); + } + + if (LoanApplication.RealEstate is not null && LoanApplication.RealEstate.LienPosition is not null && + LoanApplication.LoanTitle != "Heloc Rate Modification") + { + if (LoanApplication.RealEstate.LienPosition.PayoffFirstMortgage) + { + AppNotes.AddAppNote("I INTEND TO PAYOFF MY FIRST MORTGAGE WITH THIS LOAN."); + } + if (LoanApplication.RealEstate.LienPosition.PayoffSecondMortgage) + { + AppNotes.AddAppNote("I INTEND TO PAYOFF MY SECOND MORTGAGE WITH THIS LOAN."); + } + } + + if (LoanApplication.LoanType == LoanType.S_Special) + { + AppNotes.AddAppNote("Special Loan: " + LoanApplication.LoanTitle); + AppNotes.AddAppNote("Client Message: " + LoanApplication.ClientMessage); + } + + if (LoanApplication.LoanType == LoanType.S_Signature && !string.IsNullOrWhiteSpace(LoanApplication.PurposeOfLoanSignature)) + { + AppNotes.AddAppNote("PURPOSE OF LOAN: " + LoanApplication.PurposeOfLoanSignature); + } + + if (LoanApplication.LoanType == LoanType.EQ_HELOC_Flex) { + if (!string.IsNullOrWhiteSpace(LoanApplication.ModFeeAccount)) + { + AppNotes.AddAppNote("MOD FEE ACCOUNT: " + LoanApplication.ModFeeAccount); + } + + if (!string.IsNullOrWhiteSpace(LoanApplication.PrimaryBorrower.PrimaryApplicantEmail)) + { + AppNotes.AddAppNote("PRIMARY APPLICANT EMAIL (E-DOC SIGNATURE): " + LoanApplication.PrimaryBorrower.PrimaryApplicantEmail); + } + + if (!string.IsNullOrWhiteSpace(LoanApplication.PrimaryBorrower.CoApplicantEmail)) + { + AppNotes.AddAppNote("CO-APPLICANT EMAIL (E-DOC SIGNATURE): " + LoanApplication.PrimaryBorrower.CoApplicantEmail); + } + } + + if (LoanApplication.Vehicle != null) + { + if (LoanApplication.Vehicle.Type == Landmark.OLA.Models.Assets.Enums.VehicleType.Motorcycle + && !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.StateTitled)) + { + AppNotes.AddAppNote("State of Registration: " + LoanApplication.Vehicle.StateTitled); + } + + if (LoanApplication.Vehicle.Lien == "yes") + { + AppNotes.AddAppNote("Vehicle has a lien"); + } + + var seller = LoanApplication.Vehicle?.Seller; + + if (seller != null) + { + + var sellerStr = "INTERESTED IN BUYING FROM A " + seller.Value; + sellerStr = sellerStr.Replace('_', ' '); + + if (!string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerName) || !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerPhone)) + { + bool bothSeller = !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerName) && !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerPhone); + + sellerStr += ":"; + sellerStr += !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerName) ? " " + LoanApplication.Vehicle.DealerName : ""; + sellerStr += bothSeller ? " - " : ""; + sellerStr += !string.IsNullOrWhiteSpace(LoanApplication.Vehicle.DealerPhone) ? " " + String.Format("{0:(###) ###-####}", LoanApplication.Vehicle.DealerPhone) : ""; + } + + AppNotes.AddAppNote(sellerStr); + } + } + } + + private void CheckForAndProcessSpouses(IApplicant individual, bool isPrimaryBorrowerSpouse = false) + { + var partyType = isPrimaryBorrowerSpouse ? AppPartiesSection.PartyType.NonSigningSpouseMarriedToPrimary + : AppPartiesSection.PartyType.NonSigningSpouseMarriedToCoBorrower; + + //Validation can esnure that only one of these holds data + + if (individual.NonApplicantSpouse is not null) + { + ProcessIndividual(individual.NonApplicantSpouse, partyType); + } + + if (individual.DetailedNonApplicantSpouse is not null) + { + ProcessIndividual(individual.DetailedNonApplicantSpouse, partyType); + } + } + + private void ProcessIndividual(INonApplicant individual, AppPartiesSection.PartyType partyType) + { + var isMarried = partyType == AppPartiesSection.PartyType.NonSigningSpouseMarriedToCoBorrower + || partyType == AppPartiesSection.PartyType.NonSigningSpouseMarriedToPrimary; + + Parties.AddNonApplicant(individual, isMarried); + + Addresses.AddAddress(individual.MailingAddress, + ContactMechanisms.CrifContactMechanism.CurrentAddress); + } + + private void ProcessIndividual(IApplicant individual, + AppPartiesSection.PartyType partyType, + Guid? externalSpouseId = null, + PropertyLot mfgHomeLot = null) + { + Parties.AddApplicant(individual, externalSpouseId, mfgHomeLot); + + ProcessHistory(individual, partyType); + } + + private void ProcessIndividual(IIndividualHistory individual, AppPartiesSection.PartyType partyType) + { + var isMarried = partyType == AppPartiesSection.PartyType.NonSigningSpouseMarriedToCoBorrower + || partyType == AppPartiesSection.PartyType.NonSigningSpouseMarriedToPrimary; + + bool requiresHMDA = IsHMDARequired(LoanApplication.LoanType.ToString()); + Parties.AddHistory(individual, isMarried, requiresHMDA); + + ProcessHistory(individual, partyType); + } + + public static bool IsHMDARequired(string loanType) + { + HMDALoanType parsedLoanType; + if (Enum.TryParse(loanType, out parsedLoanType)) + { + return true; + } + else + { + return false; + } + } + + private void ProcessHistory(IIndividualHistory individual, AppPartiesSection.PartyType partyType) + { + AddHistory(individual, partyType); + + if (individual.Employers?.Count > 0) + { + foreach (var employer in individual.Employers) + { + Parties.AddEmployer(employer); + } + } + } + + public static XElement GetCreateAppElement(DateTime date, params XElement[] content) + { + var element = new XElement(CrifNamespaces.Empty + "createApp", + new XElement(CrifNamespaces.App + "application", + new XAttribute(CrifNamespaces.App + "appReceivedDate", + Dates.GetFormattedApplicationDate(date)), + new XAttribute(CrifNamespaces.App + "appInitiatedDate", + Dates.GetFormattedApplicationDate(date)), + new XAttribute(CrifNamespaces.App + "priorityCD", "High"), //Per vendor examples + content)); + + return element; + } + + private void AddHistory(IIndividualHistory individual, AppPartiesSection.PartyType partyType) + { + AppParties.AddIndividualParty(individual, partyType, individual.HasOptedOutTCPA); + + Addresses.AddAddress(individual.Address, + ContactMechanisms.CrifContactMechanism.CurrentAddress); + + if (!(individual.MailingAddress is null)) + { + Addresses.AddAddress(individual.MailingAddress, ContactMechanisms.CrifContactMechanism.MailingAddress); + } + + if (individual.PreviousAddresses?.Count > 0) + { + foreach (var previousAddress in individual.PreviousAddresses) + { + Addresses.AddAddress(previousAddress, ContactMechanisms.CrifContactMechanism.PreviousAddress); + } + } + } + + private ILoanApplicationRequestBuilder CreateProductDetails(LoanType? loanType, CrifSettings settings) + { + if (!loanType.HasValue) + throw new ArgumentException("LoanType must have a value."); + + if (settings == null) + throw new ArgumentException("Product Details requires CrifSettings"); + + ILoanApplicationRequestBuilder appBuilder = null; + + switch (loanType.Value) + { + case LoanType.CC_CashBack: + case LoanType.CC_Rewards: + case LoanType.CC_Student: + case LoanType.CC_Secure: + appBuilder = new CreditCardAppBuilder(settings); + break; + case LoanType.EQ_HELOC_Flex: + appBuilder = new HelocFlexAppBuilder(settings); + break; + case LoanType.EQ_Mtg_Refi_Express: + case LoanType.EQ_Home_Equity_Fixed: + appBuilder = new RealEstateEquityAppBuilder(settings); + break; + case LoanType.EQ_Mfg_Home_With_or_Without_Land: + appBuilder = new MfgHousingAndLandAppBuilder(settings); + break; + case LoanType.V_VehiclePreapproval: + case LoanType.V_Vehicle: + case LoanType.M_Boat: + case LoanType.P_Powersport: + case LoanType.T_TrailerMotorhome: + appBuilder = new VehicleAppBuilder(settings); + break; + case LoanType.S_Special: + case LoanType.S_Signature: + appBuilder = new SignatureAppBuilder(settings); + break; + default: + throw new ArgumentOutOfRangeException(nameof(loanType), $"The requested loan type {loanType.Value} is not implemented."); + } + + return appBuilder; + } + + private static string ConvertUtcToCentral(DateTime date) + { + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); + DateTime centralDate = TimeZoneInfo.ConvertTimeFromUtc(date.ToUniversalTime(), timeZone); + return centralDate.ToString("g"); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationResponseParser.cs new file mode 100644 index 0000000..e1e029e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationResponseParser.cs @@ -0,0 +1,25 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common.Parsers; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Loan +{ + public class LoanApplicationResponseParser : IResponseParser + { + public LoanApplication Parse(XElement xdoc) + { + try + { + //TODO: This entire parser should be rewritten to use proper XPath-style lookups + + var idElement = (XElement)((XElement)((XElement)((XElement)xdoc.FirstNode).FirstNode).FirstNode).NextNode; + + return new LoanApplication(loanId: idElement.Value); + } + catch + { + throw new FormatException("CRIF XML is in an unexpected layout."); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/OrgSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/OrgSection.cs new file mode 100644 index 0000000..a3a3f9a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/OrgSection.cs @@ -0,0 +1,24 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class OrgSection : CrifRequestSection + { + public OrgSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "org", settings) + { + + } + + public void SetOrgId(Guid orgId) + { + var orgAttribute = new XAttribute(CrifNamespaces.Common + "idRef", orgId); + + AddContent(orgAttribute); + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/PropertiesSection.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/PropertiesSection.cs new file mode 100644 index 0000000..ccabf0b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/PropertiesSection.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common.Builders; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class PropertiesSection : CrifRequestSection + { + public PropertiesSection(XNamespace sectionNamespace, CrifSettings settings) + : base(sectionNamespace, "properties", settings) + { + } + + public void AddProperty(IProperty property) + { + AddContent(property.Build()); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SavingsAccountProperty.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SavingsAccountProperty.cs new file mode 100644 index 0000000..5c63a84 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SavingsAccountProperty.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; +using CRIF.ACTion.Common; + +namespace CRIF.ACTion.Loan +{ + public class SavingsAccountProperty : IProperty + { + private readonly Guid _Id; + private readonly decimal _Value; + + public SavingsAccountProperty(Guid id, decimal value) + { + _Id = id; + _Value = value; + } + + + public XElement Build(params XElement[] content) + { + var element = new XElement(CrifNamespaces.Common + "propertyStocksAndBonds", + new XAttribute(CrifNamespaces.Common + "id", _Id.ToString()), + new XAttribute(CrifNamespaces.Common + "finalEstimatedValue", _Value), + new XAttribute(CrifNamespaces.Common + "propertyType", "CD"), //from CRIF Example + new XAttribute(CrifNamespaces.Common + "description", "CDs or Savings")); //from CRIF Example + + return element; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SignatureAppBuilder.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SignatureAppBuilder.cs new file mode 100644 index 0000000..ab164e7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SignatureAppBuilder.cs @@ -0,0 +1,21 @@ +using System.Xml.Linq; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Loan +{ + public class SignatureAppBuilder : ILoanApplicationRequestBuilder + { + private readonly CrifSettings _settings; + public SignatureAppBuilder(CrifSettings settings) + { + _settings = settings; + } + public void SetDetails(LoanApplicationRequestBuilder createApp) + { + createApp.AppDetails = new AppDetailSignatureSection(createApp.CrifRequestSectionNamespace, _settings); + var additionalContent = new List(); + createApp.AppDetails.SetDetails(createApp.LoanApplication, + createApp._sourceChannel, additionalContent: [.. additionalContent]); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/README.md b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/README.md new file mode 100644 index 0000000..920991f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/README.md @@ -0,0 +1,9 @@ +# Generating Service Client + +1. Install dotnet-svcutil (https://docs.microsoft.com/en-us/dotnet/core/additional-tools/dotnet-svcutil-guide) +2. run `dotnet svcutil https://www-lc3cst.myappro.com/ACS/ConnectorService.svc -n "*,CRIF.ACTion"` +3. move Reference.cs into desired directory structure + +* This process can also be accomplished within Visual Studio directly: https://docs.microsoft.com/en-us/visualstudio/data-tools/how-to-add-update-or-remove-a-wcf-data-service-reference + +Note that the generated Service Model Binding will reject HTTPS URLs due to the use of `BasicHttpBinding`, which results from the WSDL's listed (but unusable) HTTP endpoints. A `BasicHttpsBinding` object should be used instead. \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/ServiceReference/Reference.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/ServiceReference/Reference.cs new file mode 100644 index 0000000..2bbebf1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/ServiceReference/Reference.cs @@ -0,0 +1,778 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// // +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CRIF.ACTion +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.ServiceModel.ServiceContractAttribute(ConfigurationName="CRIF.ACTion.IConnectorService")] + public interface IConnectorService + { + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IConnectorService/SubmitEx", ReplyAction="http://tempuri.org/IConnectorService/SubmitExResponse")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + System.Threading.Tasks.Task SubmitExAsync(CRIF.ACTion.btMessage theXML); + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IConnectorService/Submit", ReplyAction="http://tempuri.org/IConnectorService/SubmitResponse")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + System.Threading.Tasks.Task SubmitAsync(CRIF.ACTion.btMessage theXML); + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IConnectorService/SubmitX", ReplyAction="http://tempuri.org/IConnectorService/SubmitXResponse")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + System.Threading.Tasks.Task SubmitXAsync(System.Xml.Linq.XElement part); + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class btMessage + { + + private Routing routingField; + + private System.Xml.Linq.XElement bodyField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public Routing routing + { + get + { + return this.routingField; + } + set + { + this.routingField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public System.Xml.Linq.XElement body + { + get + { + return this.bodyField; + } + set + { + this.bodyField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class Routing + { + + private Org orgField; + + private Action actionField; + + private User userField; + + private Interface interfaceField; + + private ApproSystem systemField; + + private ApproResult approResultField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public Org org + { + get + { + return this.orgField; + } + set + { + this.orgField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public Action action + { + get + { + return this.actionField; + } + set + { + this.actionField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public User user + { + get + { + return this.userField; + } + set + { + this.userField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public Interface @interface + { + get + { + return this.interfaceField; + } + set + { + this.interfaceField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public ApproSystem system + { + get + { + return this.systemField; + } + set + { + this.systemField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public ApproResult approResult + { + get + { + return this.approResultField; + } + set + { + this.approResultField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class Org + { + + private int orgIDField; + + private bool orgIDFieldSpecified; + + private string orgCodeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public int orgID + { + get + { + return this.orgIDField; + } + set + { + this.orgIDField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool orgIDSpecified + { + get + { + return this.orgIDFieldSpecified; + } + set + { + this.orgIDFieldSpecified = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string orgCode + { + get + { + return this.orgCodeField; + } + set + { + this.orgCodeField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class ApproResult + { + + private ApproResultEnum resultField; + + private bool resultFieldSpecified; + + private string errorInfoField; + + private int exceptionCodeField; + + private bool exceptionCodeFieldSpecified; + + private string exceptionTextField; + + private string stackTraceField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public ApproResultEnum result + { + get + { + return this.resultField; + } + set + { + this.resultField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool resultSpecified + { + get + { + return this.resultFieldSpecified; + } + set + { + this.resultFieldSpecified = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string errorInfo + { + get + { + return this.errorInfoField; + } + set + { + this.errorInfoField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int exceptionCode + { + get + { + return this.exceptionCodeField; + } + set + { + this.exceptionCodeField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool exceptionCodeSpecified + { + get + { + return this.exceptionCodeFieldSpecified; + } + set + { + this.exceptionCodeFieldSpecified = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string exceptionText + { + get + { + return this.exceptionTextField; + } + set + { + this.exceptionTextField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string stackTrace + { + get + { + return this.stackTraceField; + } + set + { + this.stackTraceField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public enum ApproResultEnum + { + + /// + Success, + + /// + Exception, + + /// + SqlError, + + /// + NotImplemented, + + /// + InvalidArg, + + /// + UnexpectedType, + + /// + InvalidXML, + + /// + NoDataFound, + + /// + InvalidOperation, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class ApproSystem + { + + private string systemVersionField; + + private string systemVirtualDirectoryField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string systemVersion + { + get + { + return this.systemVersionField; + } + set + { + this.systemVersionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string systemVirtualDirectory + { + get + { + return this.systemVirtualDirectoryField; + } + set + { + this.systemVirtualDirectoryField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class InterfaceConfig + { + + private string settingNumField; + + private string nameField; + + private string valueField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string settingNum + { + get + { + return this.settingNumField; + } + set + { + this.settingNumField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string name + { + get + { + return this.nameField; + } + set + { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string value + { + get + { + return this.valueField; + } + set + { + this.valueField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class Interface + { + + private InterfaceConfig[] interfaceConfigField; + + private int interfaceIDField; + + private bool interfaceIDFieldSpecified; + + private string interfaceNameField; + + private int productIDField; + + private bool productIDFieldSpecified; + + /// + [System.Xml.Serialization.XmlElementAttribute("interfaceConfig", Order=0)] + public InterfaceConfig[] interfaceConfig + { + get + { + return this.interfaceConfigField; + } + set + { + this.interfaceConfigField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public int interfaceID + { + get + { + return this.interfaceIDField; + } + set + { + this.interfaceIDField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool interfaceIDSpecified + { + get + { + return this.interfaceIDFieldSpecified; + } + set + { + this.interfaceIDFieldSpecified = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string interfaceName + { + get + { + return this.interfaceNameField; + } + set + { + this.interfaceNameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public int productID + { + get + { + return this.productIDField; + } + set + { + this.productIDField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool productIDSpecified + { + get + { + return this.productIDFieldSpecified; + } + set + { + this.productIDFieldSpecified = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class User + { + + private int userIDField; + + private bool userIDFieldSpecified; + + private string userNameField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public int userID + { + get + { + return this.userIDField; + } + set + { + this.userIDField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool userIDSpecified + { + get + { + return this.userIDFieldSpecified; + } + set + { + this.userIDFieldSpecified = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string userName + { + get + { + return this.userNameField; + } + set + { + this.userNameField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="BizTalkMessage")] + public partial class Action + { + + private string transactionTypeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string transactionType + { + get + { + return this.transactionTypeField; + } + set + { + this.transactionTypeField = value; + } + } + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + public interface IConnectorServiceChannel : CRIF.ACTion.IConnectorService, System.ServiceModel.IClientChannel + { + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("dotnet-svcutil", "1.0.0.0")] + public partial class ConnectorServiceClient : System.ServiceModel.ClientBase, CRIF.ACTion.IConnectorService + { + + /// + /// Implement this partial method to configure the service endpoint. + /// + /// The endpoint to configure + /// The client credentials + static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials); + + public ConnectorServiceClient(EndpointConfiguration endpointConfiguration) : + base(ConnectorServiceClient.GetBindingForEndpoint(endpointConfiguration), ConnectorServiceClient.GetEndpointAddress(endpointConfiguration)) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public ConnectorServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) : + base(ConnectorServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress)) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public ConnectorServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) : + base(ConnectorServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public ConnectorServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : + base(binding, remoteAddress) + { + } + + public System.Threading.Tasks.Task SubmitExAsync(CRIF.ACTion.btMessage theXML) + { + return base.Channel.SubmitExAsync(theXML); + } + + public System.Threading.Tasks.Task SubmitAsync(CRIF.ACTion.btMessage theXML) + { + return base.Channel.SubmitAsync(theXML); + } + + public System.Threading.Tasks.Task SubmitXAsync(System.Xml.Linq.XElement part) + { + return base.Channel.SubmitXAsync(part); + } + + public virtual System.Threading.Tasks.Task OpenAsync() + { + return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action(((System.ServiceModel.ICommunicationObject)(this)).EndOpen)); + } + + public virtual System.Threading.Tasks.Task CloseAsync() + { + return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginClose(null, null), new System.Action(((System.ServiceModel.ICommunicationObject)(this)).EndClose)); + } + + private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration) + { + if ((endpointConfiguration == EndpointConfiguration.ExternalEndpoint)) + { + System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding(); + result.MaxBufferSize = int.MaxValue; + result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max; + result.MaxReceivedMessageSize = int.MaxValue; + result.AllowCookies = true; + return result; + } + if ((endpointConfiguration == EndpointConfiguration.InternalEndpoint)) + { + System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding(); + result.MaxBufferSize = int.MaxValue; + result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max; + result.MaxReceivedMessageSize = int.MaxValue; + result.AllowCookies = true; + return result; + } + throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration)); + } + + private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration) + { + if ((endpointConfiguration == EndpointConfiguration.ExternalEndpoint)) + { + return new System.ServiceModel.EndpointAddress("http://www-lc3cst.myappro.com/ACS/ConnectorService.svc/ConnectorService"); + } + if ((endpointConfiguration == EndpointConfiguration.InternalEndpoint)) + { + return new System.ServiceModel.EndpointAddress("http://www-lc3cst.myappro.com/ACS/ConnectorService.svc/InternalService"); + } + throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration)); + } + + public enum EndpointConfiguration + { + + ExternalEndpoint, + + InternalEndpoint, + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifService.cs new file mode 100644 index 0000000..5c2da5a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifService.cs @@ -0,0 +1,139 @@ +using System.Reflection; +using System.ServiceModel; +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using Serilog; +using Serilog.Events; +using CRIF.ACTion.Configuration; + +namespace CRIF.ACTion.Services.Crif +{ + public class CrifService : ICrifService + { + private ConnectorServiceClient serviceClient; + private readonly IWcfInspectorBehavior WcfInspectorBehavior; + private readonly CrifSettings _settings; + + public CrifService(IWcfInspectorBehavior wcfInspectorBehavior, + IOptions settings) + { + WcfInspectorBehavior = wcfInspectorBehavior; + + _settings = settings.Value; + IsAnyNullOrEmpty(_settings.ApiSettings); + + CreateServiceClient(); + } + + private void CreateServiceClient() + { + Log.Debug("Creating client binding and object"); + + var binding = new BasicHttpsBinding + { + MaxBufferSize = int.MaxValue, + ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max, + MaxReceivedMessageSize = int.MaxValue, + AllowCookies = true, + SendTimeout = CrifConstants.SubmissionTimeoutInMinutes + }; + + var endpoint = new EndpointAddress(_settings.ApiSettings.EndpointUrl); + serviceClient = new ConnectorServiceClient(binding, endpoint); + + if (Log.IsEnabled(LogEventLevel.Debug)) + { + serviceClient.Endpoint.EndpointBehaviors.Add(WcfInspectorBehavior); + } + } + + public btMessage BuildMessage(XElement actionApi) + { + Log.Information("Building btMessage"); + + btMessage message = new btMessage + { + body = actionApi, + routing = new Routing + { + org = new Org + { + orgCode = _settings.ApiSettings.OrgCode, + orgID = _settings.ApiSettings.OrgId, + orgIDSpecified = true, + }, + user = new User + { + userName = _settings.ApiSettings.Username, + }, + @interface = new Interface + { + interfaceID = _settings.ApiSettings.InterfaceId, + interfaceIDSpecified = true, + }, + action = new CRIF.ACTion.Action + { + transactionType = "API", + }, + system = new ApproSystem(), // empty object required + approResult = new ApproResult(), // empty object required + } + }; + + return message; + } + + public async Task SendMessage(XElement request) + { + var message = BuildMessage(request); + XElement body; + try + { + Log.Information("Sending message to CRIF"); + var response = await serviceClient.SubmitExAsync(message); + + if (response.routing.approResult.exceptionCodeSpecified) + { + //Note that approResult.result will likely equal ApproResultEnum.Success + //even with a failed submission + + var errorResult = response.routing.approResult; + var errorText = "CRIF responded with error " + errorResult.exceptionCode + " - " + errorResult.exceptionText; + if (errorResult.exceptionCode == 13) + { + // Go ahead and catch the error 13 that CRIF won't resolve. + throw new Error13Exception(errorText + "[KnownReoccuringError]"); + } else { + throw new FormatException("CRIF responded with error " + + errorResult.exceptionCode + " - " + errorResult.exceptionText); + } + } + + body = response.body; + } + catch (Exception e) + { + Log.Debug("{exception}", e.Message); + throw; + } + + return body; + } + + public static bool IsAnyNullOrEmpty(ApiConnectionSettings myObject) + { + foreach (PropertyInfo pi in myObject.GetType().GetProperties()) + { + if (pi.PropertyType == typeof(string)) + { + string value = (string)pi.GetValue(myObject); + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException(pi.ToString()); + } + } + } + return false; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifVendorClient.cs new file mode 100644 index 0000000..8df8e08 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifVendorClient.cs @@ -0,0 +1,265 @@ +using Microsoft.Extensions.Options; +using Serilog; +using CRIF.ACTion.Common.Parsers; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Decisioning; +using CRIF.ACTion.Files; +using CRIF.ACTion.Loan; +using CRIF.ACTion.Loan.Individuals; +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Services.Crif +{ + public class CrifVendorClient : ICrifVendorClient + { + private readonly string _getCustomFieldsString = "getCustomFields"; + private readonly string _getProductDetailsString = "getProducts"; + private readonly int retryLimit = 6; + private readonly ICrifService _crifService; + private readonly IResponseParser _individualResponseParser; + private readonly IResponseParser _loanApplicationResponseParser; + private readonly IResponseParser _decisioningStateResponseParser; + private readonly IResponseParser _decisioningDetailsResponseParser; + private readonly IResponseParser> _configurationResponseParser; + private readonly IResponseParser> _productDetailsResponseParser; + private readonly IResponseParser _customFieldResponseParser; + private readonly IResponseParser _dealershipResponseParser; + private readonly CrifSettings _settings; + + public CrifVendorClient(ICrifService crifService, + IResponseParser individualResponseParser, + IResponseParser loanApplicationResponseParser, + IResponseParser decisioningStateResponseParser, + IResponseParser decisioningDetailsResponseParser, + IResponseParser> crifConfigurationResponseParser, + IResponseParser> productDetailsResponseParser, + IResponseParser customFieldResponseParser, + IResponseParser dealershipResponseParser, + IOptions settings) + { + _crifService = crifService; + _individualResponseParser = individualResponseParser; + _loanApplicationResponseParser = loanApplicationResponseParser; + _decisioningStateResponseParser = decisioningStateResponseParser; + _decisioningDetailsResponseParser = decisioningDetailsResponseParser; + _configurationResponseParser = crifConfigurationResponseParser; + _productDetailsResponseParser = productDetailsResponseParser; + _customFieldResponseParser = customFieldResponseParser; + _dealershipResponseParser = dealershipResponseParser; + _settings = settings.Value; + _settings.CustomFields = GetCustomFieldConfiguration().GetAwaiter().GetResult(); + } + + public async Task GetDealershipAuthDetails(string applicationId) + { + var getApp = new DealershipRequestBuilder(applicationId, _settings); + + var xmlApp = getApp.Build(); + + try + { + var response = await _crifService.SendMessage(xmlApp); + return _dealershipResponseParser.Parse(response); + } + catch (Exception e) + { + Log.Fatal("Error parsing response: " + e.Message); + throw; + } + } + + public async Task CreateApplication(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null) + { + Log.Debug("Building Loan Application"); + + ModelValidator.Validate(loanApplication); + + var createApp = new LoanApplicationRequestBuilder(loanApplication, _settings, null, null, null, channelService); + + var xmlApp = createApp.Build(); + + var response = await _crifService.SendMessage(xmlApp); + + try + { + return _loanApplicationResponseParser.Parse(response); + } + catch (Exception) + { + Log.Fatal("Error parsing response."); + throw; + } + } + + public async Task CreateApplicationAndGetId(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null) + { + var parsedLoanApplication = await CreateApplication(loanApplication, channelService); + return parsedLoanApplication.Id; + } + + public async Task AddFile(string applicationId, LoanApplicationFile file, string fileBase64Value) + { + try + { + var addFile = new FileRequestBuilder(applicationId, file, fileBase64Value, _settings); + var response = await _crifService.SendMessage(addFile.Build()); + return true; + } + catch (Exception e) + { + Log.Fatal("Error uploading file to loan application: " + applicationId); + Log.Fatal(e.ToString()); + } + return false; + } + + public async Task AddIndividual(string applicationId, Individual individual) + { + ModelValidator.Validate(individual); + + var addIndividual = new AddIndividualRequestBuilder(applicationId, individual, _settings); + + var response = await _crifService.SendMessage(addIndividual.Build()); + + try + { + return _individualResponseParser.Parse(response); + } + catch (Exception) + { + Log.Fatal("Error parsing response."); + throw; + } + } + + public async Task SendToDecisioning(string applicationId) + { + var sendToDecisioning = new SendToDecisioningRequestBuilder(applicationId, _settings); + + var response = await _crifService.SendMessage(sendToDecisioning.Build()); + + //TODO: This cast is not particularly Dependency Injection-y + //TODO: Requiring the Application Id is really just a double check, and may not matter all that much + ((DecisioningStateResponseParser)_decisioningStateResponseParser).SetLoanId(applicationId); + + var result = _decisioningStateResponseParser.Parse(response); + + if (result == DecisioningState.MissingRequiredInformation) + { + throw new InvalidOperationException("Loan application cannot be sent to decisioning, and is likely missing required information."); + } + + if (result == DecisioningState.Requested || result == DecisioningState.Ready) + { + return true; + } + else + { + return false; + } + } + + public async Task GetDecisionStatus(string applicationId) + { + //TODO: This cast is not particularly Dependency Injection-y + //TODO: Requiring the Application Id is really just a double check, and may not matter all that much + ((GetApplicationProductsResponseParser)_decisioningDetailsResponseParser).SetLoanId(applicationId); + + var result = new DecisioningDetails() + { + DecisioningState = DecisioningState.Unknown + }; + int retryAttempts = 0; + Thread.Sleep(2500); + + do + { + Thread.Sleep((retryAttempts * 200) + 1500); + var getDecisionStatus = new GetApplicationProductsRequestBuilder(applicationId, _settings); + var response = await _crifService.SendMessage(getDecisionStatus.Build()); + result = _decisioningDetailsResponseParser.Parse(response); + retryAttempts++; + } while (retryAttempts < retryLimit && result.DecisioningState == DecisioningState.Unknown); + + if (retryAttempts >= retryLimit && result.DecisioningState == DecisioningState.Unknown) + { + result.DecisioningState = DecisioningState.Ready; + result.DecisioningResult = DecisioningResult.DecisionStatus_Review; + } + + Log.Information($"Application ID: {applicationId}, Number of Attempts to Get DecisionStatus: {retryAttempts}"); + return result; + } + + public async Task> GetBranchNameConfiguration(string ConfigurationSectionName) + { + var configurationRequest = new ConfigurationRequestBuilder(ConfigurationSectionName, _settings); + + var response = await _crifService.SendMessage(configurationRequest.Build()); + + var result = _configurationResponseParser.Parse(response); + + return result; + } + + public async Task GetCustomFieldConfiguration() + { + CustomFieldConfigs customFieldConfig = new CustomFieldConfigs(); + string municipalityTypeId = null; + string municipalityWhereKeptId = null; + + try + { + var configurationRequest = new ConfigurationRequestBuilder(_getCustomFieldsString, _settings); + + var response = await _crifService.SendMessage(configurationRequest.Build()); + + customFieldConfig = _customFieldResponseParser.Parse(response); + + if (_settings?.CustomFieldMappings?.CustomFieldID != null) + { + _settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityType", out municipalityTypeId); + _settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityWhereKept", out municipalityWhereKeptId); + } + + Log.Information( + "CRIF custom field mappings loaded: MunicipalityType={MunicipalityTypeId}, MunicipalityWhereKept={MunicipalityWhereKeptId}", + municipalityTypeId ?? "", + municipalityWhereKeptId ?? ""); + } + catch (Exception ex) + { + Log.Fatal("Error parsing CRIF custom fields configuration response." + ex.StackTrace); + } + + customFieldConfig.ProductIdToCategoryId = await GetProductDetailsConfiguration(); + + return customFieldConfig; + } + + public async Task> GetProductDetailsConfiguration() + { + Dictionary mappingList = []; + + try + { + var configurationRequest = new ConfigurationRequestBuilder(_getProductDetailsString, _settings); + + var response = await _crifService.SendMessage(configurationRequest.Build()); + + mappingList = _productDetailsResponseParser.Parse(response); + } + catch (Exception ex) + { + Log.Fatal("Error parsing CRIF custom fields configuration response." + ex.StackTrace); + } + + return mappingList; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/Error13Exception.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/Error13Exception.cs new file mode 100644 index 0000000..cff8969 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/Error13Exception.cs @@ -0,0 +1,10 @@ + +namespace CRIF.ACTion.Services.Crif +{ + public class Error13Exception : Exception + { + public Error13Exception(string message) : base(message) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifService.cs new file mode 100644 index 0000000..3410b37 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifService.cs @@ -0,0 +1,11 @@ +using System.Xml.Linq; + +namespace CRIF.ACTion.Services.Crif +{ + public interface ICrifService + { + btMessage BuildMessage(XElement actionApi); + + Task SendMessage(XElement request); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifVendorClient.cs new file mode 100644 index 0000000..91a91f0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifVendorClient.cs @@ -0,0 +1,29 @@ +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Services.Crif +{ + public interface ICrifVendorClient + { + Task GetDealershipAuthDetails(string applicationId); + + Task CreateApplication(LoanApplication loanApplication, IDistributionChannelLookupService channelService); + + Task CreateApplicationAndGetId(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null); + + Task AddFile(string applicationId, LoanApplicationFile file, string fileBase64Value); + + Task AddIndividual(string applicationId, Individual individual); + + Task SendToDecisioning(string applicationId); + + Task GetDecisionStatus(string applicationId); + + Task> GetBranchNameConfiguration(string ConfigurationSectionName); + + Task GetCustomFieldConfiguration(); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/IWcfInspectorBehavior.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/IWcfInspectorBehavior.cs new file mode 100644 index 0000000..8f84bf7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/IWcfInspectorBehavior.cs @@ -0,0 +1,9 @@ +using System.ServiceModel.Description; +using System.ServiceModel.Dispatcher; + +namespace CRIF.ACTion.Services.Crif +{ + public interface IWcfInspectorBehavior: IClientMessageInspector, IEndpointBehavior + { + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/WcfInspectorBehavior.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/WcfInspectorBehavior.cs new file mode 100644 index 0000000..b79f62c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/WcfInspectorBehavior.cs @@ -0,0 +1,46 @@ +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.ServiceModel.Dispatcher; +using Serilog; + +namespace CRIF.ACTion.Services.Crif +{ + public class WcfInspectorBehavior : IWcfInspectorBehavior + { + public WcfInspectorBehavior() + { + } + + + public void AfterReceiveReply(ref Message reply, object correlationState) + { + Log.Debug("Reply received: {reply}", reply); + } + + public object BeforeSendRequest(ref Message request, IClientChannel channel) + { + + Log.Debug("Sending request: {request}", request); + + return null; + } + + public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) + { + } + + public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) + { + clientRuntime.ClientMessageInspectors.Add(this); + } + + public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) + { + } + + public void Validate(ServiceEndpoint endpoint) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/IDistributionChannelLookupService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/IDistributionChannelLookupService.cs new file mode 100644 index 0000000..1822fdf --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/IDistributionChannelLookupService.cs @@ -0,0 +1,7 @@ +namespace CRIF.ACTion.Services.Landmark.DistributionChannel +{ + public interface IDistributionChannelLookupService + { + int GetDistributionChannelByZipCode(string zipCode); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/LandmarkDistributionChannelLookupService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/LandmarkDistributionChannelLookupService.cs new file mode 100644 index 0000000..10f2425 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/LandmarkDistributionChannelLookupService.cs @@ -0,0 +1,23 @@ +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan; + +namespace CRIF.ACTion.Services.Landmark.DistributionChannel +{ + public class LandmarkDistributionChannelLookupService : IDistributionChannelLookupService + { + private readonly ServicingChannelSettings _settings; + + public LandmarkDistributionChannelLookupService(CrifSettings settings) + { + _settings = settings.ServicingChannels; + } + + public int GetDistributionChannelByZipCode(string zipCode) + { + var partyId = BranchMapping.BranchCodesByZip.TryGetValue(zipCode, out int foundValue) + ? foundValue : _settings.DefaultServicingChannel; + + return partyId; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/ProductIdService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/ProductIdService.cs new file mode 100644 index 0000000..fceacc5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/ProductIdService.cs @@ -0,0 +1,38 @@ +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Services.Landmark +{ + public class ProductIdService + { + private readonly CrifSettings _settings; + + public ProductIdService(CrifSettings settings) + { + _settings = settings; + } + + public int GetProductId(LoanType loanType) + { + return loanType switch + { + LoanType.CC_CashBack => _settings.ProductIds.CC_CashBack, + LoanType.CC_Rewards => _settings.ProductIds.CC_Rewards, + LoanType.CC_Student => _settings.ProductIds.CC_Student, + LoanType.CC_Secure => _settings.ProductIds.CC_Secure, + LoanType.EQ_HELOC_Flex => _settings.ProductIds.EQ_HELOC_Flex, + LoanType.EQ_Home_Equity_Fixed => _settings.ProductIds.EQ_Home_Equity_Fixed, + LoanType.EQ_Mtg_Refi_Express => _settings.ProductIds.EQ_Mtg_Refi_Express, + LoanType.EQ_Mfg_Home_With_or_Without_Land => _settings.ProductIds.EQ_Mfg_Home_And_Land, + LoanType.V_Vehicle => _settings.ProductIds.V_Vehicle, + LoanType.V_VehiclePreapproval => _settings.ProductIds.V_VehiclePreapproval, + LoanType.M_Boat => _settings.ProductIds.M_Boat, + LoanType.P_Powersport => _settings.ProductIds.P_Powersport, + LoanType.T_TrailerMotorhome => _settings.ProductIds.T_TrailerMotorhome, + LoanType.S_Signature or LoanType.S_Special => _settings.ProductIds.S_Signature, + _ => throw new ArgumentOutOfRangeException(nameof(LoanApplication.LoanType), $"The requested loan type {loanType} is not implemented."), + }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/ISourceChannelLookupService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/ISourceChannelLookupService.cs new file mode 100644 index 0000000..cea4138 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/ISourceChannelLookupService.cs @@ -0,0 +1,9 @@ +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Services.Landmark.SourceChannel +{ + public interface ISourceChannelLookupService + { + int GetSourceChannelByLoanType(LoanType loanType); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/LandmarkSourceChannelLookupService.cs b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/LandmarkSourceChannelLookupService.cs new file mode 100644 index 0000000..788d94a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/LandmarkSourceChannelLookupService.cs @@ -0,0 +1,40 @@ +using System.Collections.ObjectModel; +using CRIF.ACTion.Configuration; +using Landmark.OLA.Models.Loan.Enums; + +namespace CRIF.ACTion.Services.Landmark.SourceChannel +{ + public class LandmarkSourceChannelLookupService : ISourceChannelLookupService + { + private readonly CrifSettings _settings; + private ReadOnlyDictionary _loanTypeToSourceChannel; + + public LandmarkSourceChannelLookupService(CrifSettings settings) + { + _settings = settings; + + ConfigureMapping(); + } + + private void ConfigureMapping() + { + var mapping = new Dictionary + { + {LoanType.CC_CashBack, _settings.SourceChannels.CreditCardSourceChannelId}, + {LoanType.CC_Rewards, _settings.SourceChannels.CreditCardSourceChannelId}, + {LoanType.CC_Secure, _settings.SourceChannels.CreditCardSourceChannelId}, + {LoanType.CC_Student, _settings.SourceChannels.CreditCardSourceChannelId}, + }; + + _loanTypeToSourceChannel = new ReadOnlyDictionary(mapping); + } + + public int GetSourceChannelByLoanType(LoanType loanType) + { + var channelId = _loanTypeToSourceChannel.TryGetValue(loanType, out int foundValue) + ? foundValue : _settings.SourceChannels.DefaultSourceChannelId; + + return channelId; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Dockerfile b/LCU.API.Internal.OnlineLoanApp/Dockerfile new file mode 100644 index 0000000..e3ed909 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Dockerfile @@ -0,0 +1,65 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY ["CRIF.ACTion/CRIF.ACTion.csproj", "CRIF.ACTion/"] +COPY ["CRIF.ACTion.Tests/CRIF.ACTion.Tests.csproj", "CRIF.ACTion.Tests/"] +COPY ["Landmark.OLA.Application/Landmark.OLA.Application.csproj", "Landmark.OLA.Application/"] +COPY ["Landmark.OLA.Integration.Tests/Landmark.OLA.Integration.Tests.csproj", "Landmark.OLA.Integration.Tests/"] +COPY ["Landmark.OLA.Models/Landmark.OLA.Models.csproj", "Landmark.OLA.Models/"] +COPY ["Landmark.OLA.Tests/Landmark.OLA.Tests.csproj", "Landmark.OLA.Tests/"] +COPY ["OnlineBankingAuth/OnlineBankingAuth.csproj", "OnlineBankingAuth/"] +COPY ["OnlineBankingAuth.Tests/OnlineBankingAuth.Tests.csproj", "OnlineBankingAuth.Tests/"] +COPY ["USPS/USPS.csproj", "USPS/"] +COPY ["USPS.Tests/USPS.Tests.csproj", "USPS.Tests/"] + +# Install Credential Provider and set env variables to enable Nuget restore with auth +ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Download the NuGet Credential Provider directly (Docker's ADD uses trusted TLS) +ADD https://github.com/microsoft/artifacts-credprovider/releases/latest/download/Microsoft.Net6.NuGet.CredentialProvider.tar.gz /tmp/ + +# Extract and install Credential Provider +RUN mkdir -p /root/.nuget/plugins/netcore && \ + tar --strip-components=3 -xzf /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz -C /root/.nuget/plugins/netcore && \ + rm /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz + +ENV NUGET_PLUGIN_PATHS=/root/.nuget/plugins/netcore/CredentialProvider.Microsoft.dll +ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true +ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}" + +RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" "Landmark.OLA.Application/Landmark.OLA.Application.csproj" + +COPY ["CRIF.ACTion/", "CRIF.ACTion/"] +COPY ["CRIF.ACTion.Tests/", "CRIF.ACTion.Tests/"] +COPY ["Landmark.OLA.Application/", "Landmark.OLA.Application/"] +COPY ["Landmark.OLA.Integration.Tests/", "Landmark.OLA.Integration.Tests/"] +COPY ["Landmark.OLA.Models/", "Landmark.OLA.Models/"] +COPY ["Landmark.OLA.Tests/", "Landmark.OLA.Tests/"] +COPY ["OnlineBankingAuth/", "OnlineBankingAuth/"] +COPY ["OnlineBankingAuth.Tests/", "OnlineBankingAuth.Tests/"] +COPY ["USPS/", "USPS/"] +COPY ["USPS.Tests/", "USPS.Tests/"] + +RUN dotnet build "Landmark.OLA.Application/Landmark.OLA.Application.csproj" + +FROM build AS test +WORKDIR /src +LABEL test=true +RUN dotnet test -c Release --results-directory /app/test --logger "trx;LogFileName=test.trx" + +FROM build AS publish +WORKDIR /src +RUN dotnet publish "Landmark.OLA.Application/Landmark.OLA.Application.csproj" -c Release -o /app/publish /p:UseAppHost=false +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ENV ASPNETCORE_ENVIRONMENT=Development + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Landmark.OLA.Application.dll"] diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/EmailRequest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/EmailRequest.cs new file mode 100644 index 0000000..6049d8f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/EmailRequest.cs @@ -0,0 +1,10 @@ + +namespace Landmark.OLA.Application.Common.Domain +{ + public class EmailRequest + { + public string Recipient { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/StoredSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/StoredSettings.cs new file mode 100644 index 0000000..c3c4e51 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/StoredSettings.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models; + +namespace Landmark.OLA.Application.Common.Domain +{ + public class StoredSettings : OlaModel + { + public DateTime LastUpdated { get; set; } + [Required] + public string LastUpdatedBy { get; set; } + public bool ApplicationDisabled { get; set; } + public string AppDisabledMessage { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/UiSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/UiSettings.cs new file mode 100644 index 0000000..fd0640e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/UiSettings.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using static Landmark.OLA.Models.Loan.TermLengths; + +namespace Landmark.OLA.Application.Common.Domain +{ + public class UiSettings + { + public string AbaRouting => "275079714"; + + public Dictionary Contact => new Dictionary + { + { "HelpPhone", "262-796-4500" }, + }; + + public Dictionary Links => new Dictionary + { + { "PrivacyPolicy", "https://www.example.com/" }, + { "MemberSite", "https://www.example.net" }, + { "HomePage", "https://www.landmarkcu.com/"}, + }; + + public Dictionary> MfdHomeAndRvByAmount = TermLengths.MfdHomeAndRvByAmount; + + public List HomeEquity = TermLengths.HomeEquity; + + public List Special = TermLengths.Special; + + public List MortgageRefiExpress = TermLengths.MortgageRefiExpress; + + public List VehiclePreApproval = TermLengths.VehiclePreApproval; + + public Dictionary> TermsByYear = TermLengths.TermsByYear; + + public Dictionary> BoatLoansByAmount = TermLengths.BoatLoansByAmount; + + public Dictionary> PowersportByAmount = TermLengths.PowersportByAmount; + + public Dictionary> SignatureByAmount = TermLengths.SignatureByAmount; + + public Dictionary> TypePurposes = LoanPurpose.TypePurposes; + + public Dictionary Limits = LoanLimits.Limits; + + public List StatesRequiringSpouseInformation = ValidationConstants.StatesRequiringSpouseInformation; + + public List StateOptions = ((StateOption[])Enum.GetValues(typeof(StateOption))).ToList(); + + public Dictionary IncomeLimits = Models.Individuals.IncomeLimits.Limits; + + public BranchConfig BranchMapping = null; // Provided by MongoDB + + public BranchNameConfig BranchNameMapping = null; // Provided by MongoDB + + public StoredSettings Configuration = null; // Provided by MongoDB + + public List TermOptions = AvailableTermOptions; + + public List TermOptionsNoYear = AvailableTermOptionsWithoutYears; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureActiveDirectoryOptions.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureActiveDirectoryOptions.cs new file mode 100644 index 0000000..4da2d64 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureActiveDirectoryOptions.cs @@ -0,0 +1,12 @@ +namespace Landmark.OLA.Application.Common.Security +{ + public class AzureActiveDirectoryOptions + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string Instance { get; set; } + public string Domain { get; set; } + public string TenantId { get; set; } + public string ValidAudience { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureAdServiceCollectionExtensions.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureAdServiceCollectionExtensions.cs new file mode 100644 index 0000000..04ea856 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureAdServiceCollectionExtensions.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Landmark.OLA.Application.Common.Security +{ + public static class AzureAdServiceCollectionExtensions + { + public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder) + => builder.AddAzureAdBearer(_ => { }); + + public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddJwtBearer(); + return builder; + } + + private class ConfigureAzureOptions : IConfigureNamedOptions + { + private readonly AzureActiveDirectoryOptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, JwtBearerOptions options) + { + options.Audience = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}"; + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidAudience = _azureOptions.ValidAudience + }; + } + + public void Configure(JwtBearerOptions options) + { + Configure(Options.DefaultName, options); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CookieService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CookieService.cs new file mode 100644 index 0000000..840734f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CookieService.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.AspNetCore.Http; +using Landmark.OLA.Models.OnlineBanking; + +namespace Landmark.OLA.Application.Common.Security +{ + public class CookieService + { + + public static void SetAuthenticationCookie(IResponseCookies cookies, string token) + { + if (!string.IsNullOrEmpty(token)) + { + var expiry = new DateTimeOffset(DateTime.UtcNow.AddMinutes(15)); + var options = new CookieOptions + { + SameSite = SameSiteMode.Lax, + HttpOnly = true, + Secure = true, + Expires = expiry, + Domain = "landmarkcu.com" + }; + cookies + .Append(Constants.TokenPayload, token, options); + } + } + + public static void SetAuthorizationCookie(IResponseCookies cookies, string authorization) + { + if (!string.IsNullOrEmpty(authorization)) + { + var expiry = new DateTimeOffset(DateTime.UtcNow.AddMinutes(15)); + var options = new CookieOptions + { + SameSite = SameSiteMode.Lax, + HttpOnly = false, + Secure = true, + Expires = expiry, + Domain = "landmarkcu.com", + }; + cookies + .Append(Constants.TokenAuthorization, authorization, options); + } + } + + public static void ExpireCookies(HttpContext context) + { + var currentCookies = context.Request.Cookies; + var expiry = new DateTimeOffset(DateTime.UtcNow.AddDays(-1)); + var options = new CookieOptions + { + HttpOnly = false, + Secure = true, + Expires = expiry, + Domain = ".landmarkcu.com" + }; + if (currentCookies[Constants.TokenPayload] != null) + { + context.Response.Cookies + .Delete(Constants.TokenPayload, options); + } + + if (currentCookies[Constants.TokenAuthorization] != null) + { + context.Response.Cookies + .Delete(Constants.TokenAuthorization, options); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CosmosIdService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CosmosIdService.cs new file mode 100644 index 0000000..7eb0aa9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CosmosIdService.cs @@ -0,0 +1,22 @@ +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Application.Common.Security +{ + public static class CosmosIdService + { + public static LoanContainer AppendCosmosId(LoanContainer loanContainer, string cosmosId) + { + if (!string.IsNullOrEmpty(cosmosId)) + { + loanContainer.Id = cosmosId; + } + return loanContainer; + } + + public static LoanContainer RemoveCosmosId(LoanContainer loanContainer) + { + loanContainer.LoanId = null; + return loanContainer; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/ForwardedHeaderService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/ForwardedHeaderService.cs new file mode 100644 index 0000000..05f0540 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/ForwardedHeaderService.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.Http; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Application.Common.Security +{ + public static class ForwardedHeaderService + { + public static string GetRemoteIPAddress(HttpContext context) + { + var header = (context.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? context.Request.Headers["X-Forwarded-For"].FirstOrDefault()); + if (IPAddress.TryParse(header, out IPAddress ip)) + { + return ip.ToString(); + } + return context.Connection.RemoteIpAddress.ToString(); + } + + public static LoanContainer AppendIPAddress(LoanContainer loanContainer, HttpContext context) + { + loanContainer.LoanApplication.IPAddress = GetRemoteIPAddress(context); + return loanContainer; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/MemberIdService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/MemberIdService.cs new file mode 100644 index 0000000..6269eae --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/MemberIdService.cs @@ -0,0 +1,28 @@ +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.OnlineBanking; + +namespace Landmark.OLA.Application.Common.Security +{ + public static class MemberIdService + { + public static LoanContainer AppendMemberInfo(LoanContainer loanContainer, OnlineBankingToken token) + { + if (!string.IsNullOrEmpty(token?.MemberID) ) + { + loanContainer.MemberId = token.MemberID; + } + if (!string.IsNullOrEmpty(token?.IndividualID) ) + { + loanContainer.IndividualId = token.IndividualID; + } + return loanContainer; + } + + public static LoanContainer RemoveMemberId(LoanContainer loanContainer) + { + loanContainer.MemberId = null; + loanContainer.IndividualId = null; + return loanContainer; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/RenewAuthenticatedSessionMiddleware.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/RenewAuthenticatedSessionMiddleware.cs new file mode 100644 index 0000000..61a136e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/RenewAuthenticatedSessionMiddleware.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Serilog; +using OnlineBankingAuth.Tokens; + +namespace Landmark.OLA.Application.Common.Security +{ + public class RenewAuthenticatedSessionMiddleware + { + private readonly RequestDelegate _next; + + public RenewAuthenticatedSessionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, ITokenService tokenService) + { + await _next(context); + try + { + CookieService.SetAuthenticationCookie(context.Response.Cookies, tokenService.RenewOnlineBankingJWT(context)); + CookieService.SetAuthorizationCookie(context.Response.Cookies, tokenService.RenewAuthorization(context)); + } + catch(Exception ex) + { + Log.Fatal("Error renewing cookied", ex); + // Could not refresh token :-( no biggie? + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/ApplicationSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/ApplicationSettings.cs new file mode 100644 index 0000000..eeb8e59 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/ApplicationSettings.cs @@ -0,0 +1,13 @@ +namespace Landmark.OLA.Application.Configuration +{ + public class ApplicationSettings + { + public string AppName { get; set; } + public string Environment { get; set; } + public string SeqUrl { get; set; } + public string SeqKey { get; set; } + public string LogFilePath { get; set; } + public string LogFileInterval { get; set; } + + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageConnectionSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageConnectionSettings.cs new file mode 100644 index 0000000..da16338 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageConnectionSettings.cs @@ -0,0 +1,9 @@ + +namespace Landmark.OLA.Application.Configuration +{ + public class BlobStorageConnectionSettings + { + public string AccountName { get; set; } + public string AccountKey { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageSettings.cs new file mode 100644 index 0000000..de6cc03 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageSettings.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Landmark.OLA.Application.Configuration +{ + public class BlobStorageSettings + { + public float MaxFileSizeInMB { get; set; } + public List AllowedFileTypes { get; set; } + public int FileLimit { get; set; } + + + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/DealershipEmailSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/DealershipEmailSettings.cs new file mode 100644 index 0000000..6921065 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/DealershipEmailSettings.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Landmark.OLA.Application.Configuration +{ + public class DealershipEmailSettings + { + public string RedBannerMessage { get; set; } + public List Recipients { get; set; } + public string ActionLink { get; set; } + public string EmailAPIUrl { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/NotificationsApiConfiguration.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/NotificationsApiConfiguration.cs new file mode 100644 index 0000000..b7f22e2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/NotificationsApiConfiguration.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Landmark.OLA.Application.Configuration +{ + public class NotificationsApiConfiguration + { + public string Url { get; set; } + public string SendEmail { get; set; } + public List ErrorRecipients { get; set; } = []; + + [NotMapped] + public string SendEmailEndpointUrl => Url + SendEmail; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/AddressValidationController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/AddressValidationController.cs new file mode 100644 index 0000000..0ed4db6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/AddressValidationController.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Serilog; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Validators; +using Landmark.OLA.Models.Vendor; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AddressValidationController : ControllerBase + { + private readonly IUspsVendorClient UspsVendor; + + public AddressValidationController(IUspsVendorClient uspsVendor) + { + UspsVendor = uspsVendor; + } + + /// + /// Use to correct a postal address + /// + /// + /// Returns a service corrected postal address, given a postal address. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + [HttpPost] + [ProducesResponseType(typeof(Address), 201)] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + public async Task Post([FromBody] Address address) + { + try + { + var validator = new AddressValidator(); + + var validatorResult = validator.Validate(address); + + if (!validatorResult.IsValid) + { + foreach (var error in validatorResult.Errors) + { + ModelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + return BadRequest(ModelState); + } + + var result = await UspsVendor.CompareAddress(address); + return Ok(result); + } + catch (TimeoutException te) + { + Log.Error(te.Message); + return BadRequest("The time taken to submit the address to the vendor has exceeded the configured timeout length."); + } + catch (ArgumentException ) + { + // This happens when the address is valid in form, but not found. + return StatusCode(204); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode(204); + } + } + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/BranchConfigController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/BranchConfigController.cs new file mode 100644 index 0000000..11a3f96 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/BranchConfigController.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Serilog; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Validators; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class BranchConfigController : ControllerBase + { + private readonly IBranchDataAccess branchStorage; + + public BranchConfigController( + IBranchDataAccess branchDataAccess) + { + branchStorage = branchDataAccess; + } + + /// + /// Use to get the branch configuration + /// + /// + /// Returns a full branch configuration. + /// Cookies: None required or set. + /// Security: None. + /// + // GET: api/BranchConfig + [HttpGet(Name = "GetBranchConfig")] + [ProducesResponseType(typeof(BranchConfig), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public IActionResult Get() + { + try + { + BranchConfig config = branchStorage.GetBranchConfig(); + return Ok(config); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get the branch names configuration + /// + /// + /// Returns a full branch configuration. + /// Cookies: None required or set. + /// Security: None. + /// + // GET: api/BranchNameConfig + [HttpGet("BranchNameConfig", Name = "GetBranchNameConfig")] + [ProducesResponseType(typeof(BranchNameConfig), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public IActionResult GetBranchNameConfig() + { + try + { + BranchNameConfig config = branchStorage.GetBranchNameConfig(); + return Ok(config); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to save the initial branch config. + /// + /// + /// Returns a saved branch config with id. This call will fail if ANY configs currently exist. + /// Cookies: None required or set. + /// Security: Requires Azure AD Jwt token and admin role. + /// + // POST: api/BranchConfig + [HttpPost] + [ProducesResponseType(typeof(string), 201)] + [ProducesResponseType(403)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public object Post([FromBody] BranchConfig branchConfig) + { + try + { + if (branchConfig != null) + { + var validator = new BranchConfigValidator(); + var validatorResult = validator.Validate(branchConfig); + + var catchError = ""; + if (!validatorResult.IsValid) + { + foreach (var error in validatorResult.Errors) + { + catchError += error; + ModelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + + return BadRequest(ModelState); + } + + branchConfig.Id = (Guid.NewGuid()).ToString(); + branchConfig.Date = DateTime.Now; + branchStorage.CreateBranchConfig(branchConfig); + + HttpContext.Response.StatusCode = 201; + return new {id = branchConfig.Id, message = "Successfully saved document"}; + } + + return BadRequest("Branch Config is required."); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use update the branch config + /// + /// + /// Returns a status after attempting to update the branch configuration. + /// Cookies: None required or set. + /// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + /// + // PUT: api/Storage + [HttpPut] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(typeof(ModelStateDictionary), 400)] + [ProducesResponseType(403)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult Put([FromBody] BranchConfig branchConfig) + { + try + { + var validator = new BranchConfigValidator(); + var validatorResult = validator.Validate(branchConfig); + + if (!validatorResult.IsValid) + { + foreach (var error in validatorResult.Errors) + { + ModelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + + return BadRequest(ModelState); + } + + var cosmosId = branchConfig?.Id; + if (!string.IsNullOrEmpty(cosmosId)) + { + branchConfig.Date = DateTime.Now; + branchStorage.UpdateBranchConfig(branchConfig); + return CreatedAtAction(nameof(Put), new {id = branchConfig.Id}, branchConfig); + } + + return BadRequest("Branch config invalid."); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get an updated branch names configuration + /// + /// + /// Returns a full branch names configuration after updating it internally. + /// Cookies: None required or set. + /// Security: None. + /// + // GET: api/UpdateBranchNameConfig + [HttpGet("UpdateBranchNameConfig", Name = "GetUpdateBranchNameConfig")] + [ProducesResponseType(typeof(BranchNameConfig), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public IActionResult UpdateBranchNameConfig() + { + try + { + branchStorage.UpdateBranchNameConfig(); + BranchNameConfig config = branchStorage.GetBranchNameConfig(); + return Ok(config); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/DecisioningController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/DecisioningController.cs new file mode 100644 index 0000000..0ba8fe1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/DecisioningController.cs @@ -0,0 +1,142 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Serilog; +using Microsoft.AspNetCore.Mvc; +using CRIF.ACTion.Services.Crif; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Application.DataAccessLayer; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; +using Landmark.OLA.Models.Loan.Enums; + + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/LoanApplication/{loanApplicationId}/DecisioningDetails")] + [ApiController] + public class DecisioningController : ControllerBase + { + private readonly ICrifVendorClient VendorClient; + private readonly ILoanApplicationDataAccess LoanApplicationStorage; + + public DecisioningController( + ICrifVendorClient vendorClient, + ILoanApplicationDataAccess loanApplicationDataAccess) + { + VendorClient = vendorClient; + LoanApplicationStorage = loanApplicationDataAccess; + } + + /// + /// Use to start decisioning on a loan + /// + /// + /// Returns status of decision start, on 202 you can begin requesting details. + /// Cookies: Will expire existing cookies. + /// Security: None required or provided. + /// + // PUT: api/Application/{loanApplicationId}/DecisioningDetails + [HttpPut] + [ProducesResponseType(202)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Put(string loanApplicationId, [FromBody] DecisioningDetails decisioningDetails) + { + if (decisioningDetails.DecisioningState != DecisioningState.Requested) + { + Log.Error("DecisioningState was not set to Requested."); + return BadRequest("Must PUT a DecisioningState of Requested."); + } + + Log.Information($"Sending loan {loanApplicationId} to decisioning."); + try + { + var decisioningRequestSuccess = await VendorClient.SendToDecisioning(loanApplicationId); + + if (decisioningRequestSuccess) + { + CookieService.ExpireCookies(HttpContext); + return new AcceptedResult(); + } + else + { + // SendToDecisioning did not succeed, "That's an error. That's all we know.", Check the server logs! + Log.Error($"Loan #{loanApplicationId} could not be submitted to decisioning."); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + 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 (InvalidOperationException ioe) + { + Log.Error(ioe.Message); + return BadRequest(ioe.Message); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get details of a decision + /// + /// + /// Returns a decision status, which may be empty if the decision is not ready. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // GET: api/Application/{loanApplicationId}/DecisioningDetails + [HttpGet] + [ProducesResponseType(typeof(DecisioningDetails), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Get(string loanApplicationId) + { + try + { + var decisioningResult = await VendorClient.GetDecisionStatus(loanApplicationId); + if (decisioningResult.DecisioningState == DecisioningState.Ready) + { + var cosmosId = LoanApplicationStorage.GetContainerIdByLoanId(loanApplicationId); + LoanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Decisioned); + } + + return Ok(decisioningResult); + } + catch (TimeoutException te) + { + Log.Error(te.Message); + return BadRequest( + "The time taken to decision the loan application has exceeded the configured timeout length."); + } + catch (ArgumentException ae) + { + Log.Error(ae.Message); + return BadRequest(ae.Message); + } + catch (Error13Exception e13) + { + // We will remove these logs on + Log.Error(e13.Message); + var decisionResult = new DecisioningDetails() + { + DecisioningState = DecisioningState.Unknown, + DecisioningResult = DecisioningResult.ProcessStatus_Incomplete + }; + return Ok(decisionResult); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/IndividualController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/IndividualController.cs new file mode 100644 index 0000000..f922149 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/IndividualController.cs @@ -0,0 +1,63 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Serilog; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Application.Services.Exceptions; +using Landmark.OLA.Models.Individuals; +using OnlineBankingAuth.Tokens; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class IndividualController : ControllerBase + { + private readonly ICoreService _coreService; + private readonly ITokenService _tokenService; + + public IndividualController(ITokenService tokenService, + ICoreService coreService) + { + _coreService = coreService; + _tokenService = tokenService; + } + + // GET: api/Individual/self + [HttpGet("self", Name = "GetIndividual")] + [ProducesResponseType(typeof(Individual), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + [ProducesResponseType(503)] + public async Task Get() + { + try + { + var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + if (token?.IndividualID == null ) + { + Log.Error("Unauthorized request for 'Self' in Loan API, denying."); + if (token?.MemberID != null) + { + // This isn't logging the security token, but the actual token content. + Log.Error($"Unauthorized request was on behalf of {@token}"); + } + return Unauthorized(); + } + var individual = await _coreService.GetIndividual(token.MemberID); + return Ok(individual); + } + catch (CoreUnavailableException cue) + { + Log.Error(cue.Message); + return StatusCode((int)HttpStatusCode.ServiceUnavailable); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoanApplicationController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoanApplicationController.cs new file mode 100644 index 0000000..1cd9da6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoanApplicationController.cs @@ -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; + 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); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoggingController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoggingController.cs new file mode 100644 index 0000000..3aadc88 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoggingController.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Landmark.OLA.Models; +using Serilog; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class LoggingController : ControllerBase + { + public LoggingController() + { + } + + /// + /// Use to send something to the logger from the client + /// + /// + /// Logs a message from a client. + /// Cookies: None required or set. + /// Security: None set or required [yet]. It may be best to require a token here, but then we'll miss startup errors. + /// + // Post: api/Logging/Error + [HttpPost("Error")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(403)] + [ProducesResponseType(500)] + public IActionResult Post([FromBody] ClientError error) + { + try + { + Log.Error(error.Message, error.StackTrace); + return Ok(); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/OnlineIdentityFormData.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/OnlineIdentityFormData.cs new file mode 100644 index 0000000..844c43f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/OnlineIdentityFormData.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Application.Controllers +{ + public class OnlineIdentityFormData + { + public string DateOfBirth { get; set; } + public string SocialSecurityNumber { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/PdfController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/PdfController.cs new file mode 100644 index 0000000..eca62aa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/PdfController.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Serilog; +using Landmark.OLA.Models; +using Landmark.OLA.Application.Services; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/GetPdf/")] + [ApiController] + public class PdfController : ControllerBase + { + private readonly IPdfService _pdfService; + private readonly ApplicationControlConfiguration _appControlConfig; + + public PdfController(IOptions settings, + IPdfService pdfService) + { + _pdfService = pdfService; + _appControlConfig = settings.Value; + } + + // Get: api/GetPdf/ImportantTerms + [HttpGet("ImportantTerms")] + [ProducesResponseType(200)] + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public IActionResult GetPdfImportantTerms() + { + try + { + var result = _pdfService.GetFileBytes(_appControlConfig.ImportantTermsPdfFilePath); + var payload = new FileContentResult(result, "application/octet-stream"); + return payload; + } + catch (ArgumentException ae) + { + Log.Error(ae, $"Request for bad file path for IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf"); + return BadRequest("File path not found"); + } + catch (Exception ex) + { + Log.Error(ex, $"Unexpected error retrieving file path IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + // Get: api/GetPdf/WhatYouShouldKnow + [HttpGet("WhatYouShouldKnow")] + [ProducesResponseType(200)] + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public IActionResult GetPdfWhatYouShouldKnow() + { + try + { + var result = _pdfService.GetFileBytes(_appControlConfig.WhatYouShouldKnowPdfFilePath); + var payload = new FileContentResult(result, "application/octet-stream"); + return payload; + } + catch (ArgumentException ae) + { + Log.Error(ae, $"Request for bad file path What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf"); + return BadRequest("File path not found"); + } + catch (Exception ex) + { + Log.Error(ex, $"Unexpected error retrieving file path What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/SAMLAssertionConsumerController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/SAMLAssertionConsumerController.cs new file mode 100644 index 0000000..16297fe --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/SAMLAssertionConsumerController.cs @@ -0,0 +1,213 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using System.Security.Authentication; +using Serilog; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc; +using OnlineBankingAuth.Tokens; +using Landmark.OLA.Models.OnlineBanking; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Models.Core; +using OnlineBankingAuth.Parsers; +using OnlineBankingAuth.Services; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class SamlAssertionConsumerController : ControllerBase + { + private readonly IDigitalInsightVendorClient _diVendor; + private readonly ITokenService _tokenService; + + public SamlAssertionConsumerController + ( + IDigitalInsightVendorClient diVendor, + ITokenService tokenService + ) + { + _diVendor = diVendor; + _tokenService = tokenService; + } + + /// + /// Use to refer a user to the application with a valid SAML assertion + /// + /// + /// Returns an application redirect, adding in a SAML assertion via a JWT in a secure http-only cookie. + /// Cookies: Set a standard pair of authentication and authorization cookies. + /// Security: Passed document must include a valid, signed SAML assertion, provides member authentication. + /// + // POST: api/SAMLAssertionConsumer + [HttpPost] + [ProducesResponseType(403)] + [ProducesResponseType(401)] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ModelStateDictionary), 401)] + public async Task Post([FromForm] SAMLMessaging document) + { + try + { + var decodedString = Encoding.UTF8.GetString(Convert.FromBase64String(document.SAMLResponse)); + + var parsedXml = XElement.Parse(decodedString); + var xmlDocument = new XmlDocument + { + PreserveWhitespace = true + }; + xmlDocument.Load(parsedXml.CreateReader()); + + // @TODO: Should catch error here, move element query to static service #1400b + var x509Payload = (XmlElement)xmlDocument.GetElementsByTagName("ds:X509Certificate")[0]; + var vendorCertMatches = _diVendor.ValidateDIPublicKey(x509Payload); + if (!vendorCertMatches) + { + throw new AuthenticationException("The vendor's public certificate did not match! "); + } + + var assertion = _diVendor.GetSAMLPayload(xmlDocument); + + // Check that assertion is still valid + var datesValid = _diVendor.ValidateTimeStamp(assertion); + if (!datesValid) + { + throw new AuthenticationException("The Authentication has expired!"); + } + + var obToken = new OnlineBankingToken(assertion, null, true) + { + IsTwoFactor = true + }; + obToken.IndividualID = obToken.MemberID; + + AssignTokens(obToken); + HttpContext.Response.Redirect(_diVendor.GetSuccessRedirectUrl()); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to validate SAML in {GetType().Name}"); + HttpContext.Response.Redirect(_diVendor.GetFailureRedirectUrl(ex.Message)); + } + } + + private void AssignTokens(OnlineBankingToken obToken) + { + var meta = new MemberMeta + { + IsTwoFactor = obToken.IsTwoFactor, + IsMember = (obToken.MemberID != null), + IsIndividual = (obToken.IndividualID != null) + }; + + var privateToken = _tokenService.CreateOnlineBankingJWT(obToken); + var publicToken = _tokenService.CreateAuthMetaToken(meta); + CookieService.SetAuthenticationCookie(HttpContext.Response.Cookies, privateToken); + CookieService.SetAuthorizationCookie(HttpContext.Response.Cookies, publicToken); + } + + /// + /// Use to create an empty cookie session + /// + /// + /// Returns a pair of empty authorization and authentication cookies. + /// Cookies: Sets empty Authentication and Authorization cookies. + /// Security: None required or provided. + /// + [HttpGet("InitSession")] + [ProducesResponseType(200)] + public IActionResult Get() + { + var obToken = new OnlineBankingToken + { + MemberID = null, IndividualID = null, FI_ID = "0", IsTwoFactor = false + }; + + AssignTokens(obToken); + return Ok(); + } + + /// + /// Use to create a cookie session with credentials + /// + /// + /// Returns a pair of authorization and authentication cookies when provided with valid credentials. + /// Cookies: Sets Authentication and Authorization cookies. + /// Security: Require. + /// + [HttpPost("Authenticate")] + [ProducesResponseType(403)] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ModelStateDictionary), 401)] + public async Task PostAsync([FromBody] OnlineBankingFormData document) + { + try + { + var diResponse = await _diVendor.AuthenticateOnlineBankingAccount(document); + + if (diResponse.Token == null) + { + Log.Error($"Member failed non-two factor login, {@document.Username}"); + return Ok(diResponse.Status); + } + + var assertion = new DISAMLAssertion + { + MemberID = diResponse.Token.DiMemberNumber, + FI_ID = diResponse.Token.DiFiid + }; + + var obToken = new OnlineBankingToken(assertion, null, false) + { + IsTwoFactor = false + }; + obToken.IndividualID = obToken.MemberID; + + AssignTokens(obToken); + return Ok(diResponse.Status); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to authenticate user in {GetType().Name}"); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to identify a unique individual on the member account. + /// + /// + /// Returns OK if an individual ID was added to the cookie. + /// Cookies: Sets Individual ID in cookie. + /// Security: Requires existing cookie, with member. + /// + [HttpPost("Identify")] + [ProducesResponseType(403)] + [ProducesResponseType(400)] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ModelStateDictionary), 401)] + public async Task PostIdentify([FromBody] OnlineIdentityFormData document) + { + try + { + var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + if (token == null) + { + Log.Error($"Non-authenticated attempt at individual confirmation! Using info {@document}, Refusing."); + return Unauthorized(); + } + + token.IndividualID = token.MemberID; + AssignTokens(token); + return Ok(); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to identify user in {GetType().Name}"); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StatusController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StatusController.cs new file mode 100644 index 0000000..2846e22 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StatusController.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Models; +using Serilog; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class StatusController : ControllerBase + { + private readonly IStatus _status; + private readonly ApplicationControlConfiguration _appControlConfig; + private readonly CoreSettings _coreSettings; + + public StatusController(IOptions settings, + IStatus status, + CoreSettings coreSettings) + { + _appControlConfig = settings.Value; + _status = status; + _coreSettings = coreSettings; + } + + /// + /// Attempts to load the root of the API result in a web-app redirect. + /// + /// + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // GET: Root + [HttpGet()] + [ProducesResponseType(302)] + public void GetRoot() + { + HttpContext.Response.Redirect(_appControlConfig.HomePageUrl); + } + + /// + /// Use to get the status of the api + /// + /// + /// Returns a simple object describing the build time, version, and environment of the running API. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // GET: api/Status + [HttpGet("api/Status")] + [ProducesResponseType(typeof(IStatus), 200)] + [ProducesResponseType(500)] + public IActionResult Get() + { + try + { + return new ObjectResult(_status); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get the core connection settings of the api + /// + /// + /// Returns a simple object describing the core settings being used by the API. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // GET: api/Status + [HttpGet("api/GetCoreConnection")] + [ProducesResponseType(typeof(CoreSettings), 200)] + [ProducesResponseType(500)] + public IActionResult GetCoreConnection() + { + try + { + return new ObjectResult(_coreSettings); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int) System.Net.HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StorageController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StorageController.cs new file mode 100644 index 0000000..8d8a27b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StorageController.cs @@ -0,0 +1,577 @@ +using System; +using System.Net; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Serilog; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Application.Configuration; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Application.DataAccessLayer; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using OnlineBankingAuth.Tokens; + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class StorageController : ControllerBase + { + + private readonly ILoanApplicationDataAccess loanApplicationStorage; + private readonly ITokenService _tokenService; + private readonly BlobStorageSettings _blobStorageSettings; + private readonly IBlobStorageAccess _blobStorageAccess; + + public StorageController( + ILoanApplicationDataAccess loanApplicationDataAccess, + ITokenService tokenService, + IOptions blobStorageSettings, + IBlobStorageAccess blobStorageAccess) + { + loanApplicationStorage = loanApplicationDataAccess; + _tokenService = tokenService; + _blobStorageSettings = blobStorageSettings.Value; + _blobStorageAccess = blobStorageAccess; + } + + /// + /// Use to retrieve a set of Loan Containers by filter + /// + /// + /// Returns an array of Loan Containers given a filter + /// Cookies: None required or set. + /// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + /// + // Post: api/Storage/Filter + [HttpPost("Filter")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(403)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult Post([FromBody] Filter filter) + { + try + { + return Ok(loanApplicationStorage.GetLoanContainers(filter)); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get a specific loan container FOR AN ADMIN + /// + /// + /// Returns a loan container given a loan id. + /// Cookies: None required or set. + /// Security: Must have a valid JWT token retrieved from Azure AD. + /// + // GET: api/Storage/5 + [HttpGet("{id}", Name = "Get")] + [ProducesResponseType(typeof(LoanContainer), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(403)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public IActionResult Get(string id) + { + try + { + if (String.IsNullOrEmpty(id)) + { + return BadRequest("Id is required."); + } + LoanContainer container = loanApplicationStorage.GetContainerById(id); + return Ok(container); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound("An application with that Id could not be found"); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to get loan containers for an authenticated member + /// + /// + /// Returns a list of loan containers given a valid cookie with member id. + /// Cookies: Requires authenticated cookie with member id. + /// Security: None required or provided. + /// + // GET: api/Storage + [HttpGet("GetMemberApplications", Name = "GetMemberApplications")] + [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(403)] + [ProducesResponseType(500)] + public IActionResult GetMemberApplications() + { + try + { + var assertion = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + if (string.IsNullOrEmpty(assertion?.MemberID) || string.IsNullOrEmpty(assertion?.IndividualID)) + { + Log.Error(assertion.MemberID + "/" + assertion.IndividualID, "no stacktrace"); + return StatusCode((int)System.Net.HttpStatusCode.Forbidden); + } + List containers = loanApplicationStorage.GetAllMemberContainers(assertion.MemberID, assertion.IndividualID); + return Ok(containers); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to save a loan container + /// + /// + /// Returns a saved loan container with id and possibly member id + /// Cookies: Authenticated member cookie optional, will cause id to be saved. + /// Security: Currently creates authentication and authorization cookies. + /// + // POST: api/Storage + [HttpPost] + [ProducesResponseType(typeof(string), 201)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public object Post([FromBody] LoanContainer loanContainer) + { + try + { + if (loanContainer != null) + { + var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + loanContainer.Id = (Guid.NewGuid()).ToString(); + loanContainer = MemberIdService.AppendMemberInfo(loanContainer, token); + loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); + + loanApplicationStorage.CreateContainer(loanContainer); + HttpContext.Response.StatusCode = 201; + return new { id = loanContainer.Id, message = "Successfully saved document" }; + } + return BadRequest("Loan Application is required."); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + [HttpPost("New")] + [ProducesResponseType(typeof(string), 201)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public object PostNew() + { + try + { + var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + var loanContainer = new LoanContainer(new LoanApplication()) + { + Id = (Guid.NewGuid()).ToString() + }; + loanContainer = MemberIdService.AppendMemberInfo(loanContainer, token); + loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); + + + loanApplicationStorage.CreateContainer(loanContainer); + HttpContext.Response.StatusCode = 201; + return new { id = loanContainer.Id, message = "Successfully saved document" }; + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + /// + /// Use update a loan container + /// + /// + /// Returns a status after attempting to update a loan container with new info. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // PUT: api/Storage + [HttpPut] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public IActionResult Put([FromBody] LoanContainer loanContainer) + { + try + { + var cosmosId = loanContainer?.Id; + if (!String.IsNullOrEmpty(cosmosId)) + { + var assertion = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + loanContainer = CosmosIdService.AppendCosmosId(loanContainer, cosmosId); + loanContainer = MemberIdService.AppendMemberInfo(loanContainer, assertion); + loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); + + // Append files + LoanContainer previousContainer = loanApplicationStorage.GetContainerById(cosmosId); + if (previousContainer.LoanApplication != null) { + loanContainer.LoanApplication.Files = previousContainer.LoanApplication.Files; + } + + loanApplicationStorage.UpdateContainer(loanContainer); + return CreatedAtAction(nameof(Post), new { id = loanContainer.Id }, loanContainer); + } + return BadRequest("Loan container invalid."); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.Error_Updating, e.Message)); + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to delete a loan + /// + /// + /// Deletes a loan given an id. + /// Cookies: None required or set. + /// Security: Requires JWT token from Azure AD, AND Admin role. + /// + // DELETE: api/Storage/Loan/5 + [HttpDelete("Loan/{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(403)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult DeleteLoan(string id) + { + try + { + if (String.IsNullOrEmpty(id)) + { + return BadRequest("Id is required."); + } + loanApplicationStorage.DeleteApplication(id); + loanApplicationStorage.UpdateStatus(id, LoanStatus.Deleted); + return Ok(id); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + loanApplicationStorage.UpdateError(id, new LoanError(LoanErrorCode.Error_Deleting, e.Message)); + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to delete an entire loan container + /// + /// + /// Deletes a loan container given a container id. + /// Cookies: None required or set. + /// Security: Requires JWT token from Azure AD, AND Admin role. + /// + // DELETE: api/Storage/LoanContainer/5 + [HttpDelete("LoanContainer/{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(403)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult DeleteContainer(string id) + { + try + { + if (String.IsNullOrEmpty(id)) + { + return BadRequest("Id is required."); + } + loanApplicationStorage.DeleteContainer(id); + return Ok(id); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + // GET: api/Storage/Settings + [HttpGet("Settings")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(500)] + public IActionResult GetSettings() + { + try + { + return Ok(new { fileSizeLimit = _blobStorageSettings.MaxFileSizeInMB, fileLimit = _blobStorageSettings.FileLimit, allowedFileTypes = _blobStorageSettings.AllowedFileTypes }); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + // GET: api/Storage/Documents + [HttpGet("Documents/{cosmosLoanId}")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public IActionResult GetDocuments(string cosmosLoanId) + { + try + { + if (String.IsNullOrEmpty(cosmosLoanId)) + { + return BadRequest(new { error = "Invalid or missing loan ID" }); + } + + LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); + + if(container.LoanApplication == null) + { + return NotFound(new { loanId = cosmosLoanId }); + } + + return Ok(new { files = container.LoanApplication.Files }); + + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + // POST: api/Storage/Documents/5 + [HttpPost("Documents/{cosmosLoanId}")] + [ProducesResponseType(typeof(string), 201)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public IActionResult PostDocument(string cosmosLoanId, [FromForm] DocumentUploadFromLoanApplication request) + { + try + { + if (String.IsNullOrEmpty(cosmosLoanId)) + { + return BadRequest(new { error = "Invalid or missing loan ID" }); + } + + LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); + + if (container.LoanApplication == null) + { + return NotFound(new { loanId = cosmosLoanId }); + } + + if (DocumentUploadFromLoanApplication.IsNullOrEmpty(request)) + { + return BadRequest(new { error = "Cannot upload empty file" }); + } + + using var ms = new MemoryStream(); + // Check file type + var filename = request.File.FileName.ToLower(); + + var validExtension = false; + + foreach (string extension in _blobStorageSettings.AllowedFileTypes) + { + if (filename.EndsWith(extension)) + { + validExtension = true; + break; + } + } + + if (!validExtension) + { + return BadRequest(new { error = "Invalid file type" }); + } + + // Check file size + if (request.File.Length > _blobStorageSettings.MaxFileSizeInMB * 1024 * 1024) + { + return BadRequest(new { error = $"File size exceeds {_blobStorageSettings.MaxFileSizeInMB} MB" }); + } + + request.File.CopyTo(ms); + + var fileBytes = ms.ToArray(); + + var fileId = Guid.NewGuid(); + + LoanApplicationFile file = new(cosmosLoanId, fileId, request.File.FileName, request.Section, request.SectionId, request.Description); + + container.LoanApplication.Files.Add(file); + + loanApplicationStorage.UpdateContainer(container); + + _blobStorageAccess.SaveBlob(file, fileBytes); + + return Created($"/api/Storage/Documents/{cosmosLoanId}/{fileId}", new { fileId }); + + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + // DELETE: api/Storage/Documents/1/5 + [HttpDelete("Documents/{cosmosLoanId}/{fileId}")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(500)] + public IActionResult DeleteDocument(string cosmosLoanId, string fileId) + { + try + { + if (String.IsNullOrEmpty(cosmosLoanId) || String.IsNullOrEmpty(fileId)) + { + return BadRequest(new { error = "Invalid or missing loan or file ID" }); + } + + LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); + + if (container.LoanApplication == null) + { + return NotFound(new { loanId = cosmosLoanId }); + } + + foreach (LoanApplicationFile file in container.LoanApplication.Files) + { + if(file.FileId.ToString().Equals(fileId, StringComparison.OrdinalIgnoreCase)) + { + _blobStorageAccess.DeleteBlob(file); + + container.LoanApplication.Files.Remove(file); + + loanApplicationStorage.UpdateContainer(container); + + return Ok(new { message = "file deleted", fileId }); + } + } + + return NotFound(new { fileId }); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + // DELETE: api/Storage/Documents/1/5 + [HttpPost("Documents/DeleteAll/{cosmosLoanId}")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(500)] + public IActionResult DeleteDocuments(string cosmosLoanId, [FromForm] string files) + { + try + { + /*if (String.IsNullOrEmpty(cosmosLoanId) || String.IsNullOrEmpty(fileIds)) + { + return BadRequest(new { error = "Invalid or missing loan or file ID" }); + }*/ + + LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); + + if (container.LoanApplication == null) + { + return NotFound(new { loanId = cosmosLoanId }); + } + List filesDeleted = new List(); + foreach (LoanApplicationFile file in container.LoanApplication.Files) + { + + if (files.ToString().Contains(file.FileId.ToString())) + { + _blobStorageAccess.DeleteBlob(file); + + filesDeleted.Add(file); + } + } + + + foreach (LoanApplicationFile file in filesDeleted) + { + container.LoanApplication.Files.Remove(file); + } + + loanApplicationStorage.UpdateContainer(container); + + return Ok(new { message = "files deleted", filesDeleted }); + + /*return NotFound(new { fileId });*/ + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + [HttpGet("/GetLog")] + [ProducesResponseType(typeof(string), 200)] + public IActionResult GetLog() + { + try + { + Log.Error("Test log error from localhost"); + return Ok("Logging to Seq"); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/UiSettingsController.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/UiSettingsController.cs new file mode 100644 index 0000000..1dcf2ed --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/UiSettingsController.cs @@ -0,0 +1,165 @@ +using System; +using System.Net; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Serilog; +using Landmark.OLA.Application.Common.Domain; +using Landmark.OLA.Application.DataAccess; + + +namespace Landmark.OLA.Application.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UiSettingsController : ControllerBase + { + private readonly UiSettings _uiSettings; + private readonly IBranchDataAccess _branchStorage; + private readonly IConfigurationAccess _configurationAccess; + + public UiSettingsController(UiSettings uiSettings, + IBranchDataAccess branchDataAccess, + IConfigurationAccess configurationAccess) + { + _uiSettings = uiSettings; + _branchStorage = branchDataAccess; + _configurationAccess = configurationAccess; + } + + /// + /// Use to retrieve UI Settings + /// + /// + /// Returns the UI Settings; with the dynamic branch mappings injected. + /// Cookies: None required or set. + /// Security: None required or provided. + /// + // GET: api/Content/ + [HttpGet] + [ProducesResponseType(typeof(UiSettings), 200)] + [ProducesResponseType(500)] + public IActionResult Get() + { + try + { + var settings = _uiSettings; + settings.BranchMapping = _branchStorage.GetBranchConfig(); + settings.BranchNameMapping = _branchStorage.GetBranchNameConfig(); + settings.Configuration = _configurationAccess.GetStoredSettings(); + return Ok(settings); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + + } + + /// + /// Use to get stored config (run-time changeable) + /// + /// + /// Returns the current StoredConfig + /// Cookies: None required or set. + /// Security: Requires Azure AD token and Admin role. + /// + // GET: api/UiSettings/StoredSettings + [HttpGet("StoredSettings", Name = "GetStoredSettings")] + [ProducesResponseType(typeof(StoredSettings), 200)] + [ProducesResponseType(500)] + //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult GetStoredSettings() + { + try + { + var config = _configurationAccess.GetStoredSettings(); + return Ok(config); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + /// + /// Use to save the initial storedConfig. + /// + /// + /// Returns a saved configuration with id. This call will fail if ANY configs currently exist. + /// Cookies: None required or set. + /// Security: Requires Azure AD Jwt token and admin role. + /// + // POST: api/UiSettings/StoredSettings + [HttpPost("StoredSettings", Name = "CreateStoredSettings")] + [ProducesResponseType(typeof(string), 201)] + [ProducesResponseType(403)] + [ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public object CreateStoredSettings([FromBody] StoredSettings config) + { + try + { + if (config != null) + { + config.Id = (Guid.NewGuid()).ToString(); + config.LastUpdated = DateTime.Now; + _configurationAccess.CreateStoredSettings(config); + + HttpContext.Response.StatusCode = 201; + return new { id = config.Id, message = "Successfully saved configuration" }; + } + return BadRequest("StoredConfig is required."); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + /// + /// Use update the StoredConfig + /// + /// + /// Returns a status after attempting to update the StoredConfig. + /// Cookies: None required or set. + /// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + /// + // PUT: api/UiSettings/StoredSettings + [HttpPut("StoredSettings", Name = "UpdateStoredSettings")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)] + [ProducesResponseType(403)] + [ProducesResponseType(500)] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] + public IActionResult UpdateStoredSettings([FromBody] StoredSettings config) + { + try + { + var cosmosId = config?.Id; + if (!String.IsNullOrEmpty(cosmosId)) + { + config.LastUpdated = DateTime.Now; + _configurationAccess.UpdateStoredSettings(config); + return CreatedAtAction(nameof(UpdateStoredSettings), new { id = config.Id }, config); + } + return BadRequest("StoredSettings invalid."); + } + catch (KeyNotFoundException ex) + { + Log.Error(ex.Message); + return NotFound(); + } + catch (Exception e) + { + Log.Error(e.Message); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BlobStorageAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BlobStorageAccess.cs new file mode 100644 index 0000000..4762594 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BlobStorageAccess.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.Extensions.Options; +using Azure.Storage.Blobs; +using Landmark.OLA.Application.Configuration; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Application.DataAccess +{ + public class BlobStorageAccess : IBlobStorageAccess + { + readonly BlobServiceClient _blobServiceClient; + readonly string _containerName; + readonly BlobContainerClient _containerClient; + readonly BlobStorageConnectionSettings _settings; + + public BlobStorageAccess(IOptions settings) + { + _settings = settings.Value; + + _blobServiceClient = new BlobServiceClient($"AccountName={_settings.AccountName};AccountKey={_settings.AccountKey}"); + _containerName = "documents"; + _containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + } + + public byte[] GetBlobAsBytes(LoanApplicationFile file) + { + if (file == null) + { + return []; + } + + var blob = _containerClient.GetBlobClient(file.GetBlobPath()); + + var fileContent = blob.DownloadContent(); + var fileBytes = fileContent.Value.Content.ToArray(); + + return fileBytes; + } + + public string GetBlobAsBase64(LoanApplicationFile file) + { + var fileBytes = GetBlobAsBytes(file); + + return Convert.ToBase64String(fileBytes); + } + + public bool SaveBlob(LoanApplicationFile file, byte[] fileBytes) + { + if(file == null || fileBytes == null || fileBytes.Length == 0) + { + return false; + } + + BlobClient blobClient = _containerClient.GetBlobClient(file.GetBlobPath()); + blobClient.Upload(new BinaryData(fileBytes)); + blobClient.SetMetadata(file.GetMetadata()); + return true; + } + + public bool DeleteBlob(LoanApplicationFile file) + { + if (file == null) + { + return false; + } + + BlobClient blobClient = _containerClient.GetBlobClient(file.GetBlobPath()); + return blobClient.DeleteIfExists(); + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BranchDataAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BranchDataAccess.cs new file mode 100644 index 0000000..dc77a00 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BranchDataAccess.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Services.Crif; +using Landmark.OLA.Models.Loan; + + +namespace Landmark.OLA.Application.DataAccess +{ + public class BranchDataAccess : IDisposable, IBranchDataAccess + { + private bool disposed = false; + private protected readonly CrifSettings _crifSettings; + private readonly IDataClient _client; + private readonly ICrifVendorClient _vendorClient; + private readonly string _collectionName = "Branches"; + private readonly string _branchNameCollectionName = "ServicingChannelToBranchName"; + private readonly List _branchNameDataListDefaults = new List + { + new() { ChannelId = 0, BranchName = "Not Found" }, + new() { ChannelId = 1, BranchName = "ALL" } + }; + + public BranchDataAccess( + IDataClient dataClient, + IOptions crifSettings, + ICrifVendorClient vendorClient) + { + _client = dataClient; + _crifSettings = crifSettings.Value; + _vendorClient = vendorClient; + } + + private IMongoCollection GetBranchContainersCollection() + { + return _client.GetDatabase().GetCollection(_collectionName); + } + + private IMongoCollection GetBranchNameContainersCollection() + { + return _client.GetDatabase().GetCollection(_branchNameCollectionName); + } + + public BranchConfig GetBranchConfig() + { + try + { + var collection = GetBranchContainersCollection(); + var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault(); + return result ?? throw new Exception("Could not find branch mapping"); + } + catch (MongoConnectionException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Could not load branch config"); + } + } + + public void CreateBranchConfig(BranchConfig branchConfig) + { + try + { + var collection = GetBranchContainersCollection(); + + if (collection.Find(new BsonDocument()).ToList().Count > 0) + { + throw new Exception("Can only insert 1 branch config"); + } + collection.InsertOne(branchConfig); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while creating branch config"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while creating branch config"); + } + } + + public void UpdateBranchConfig(BranchConfig branchConfig) + { + try + { + var collection = GetBranchContainersCollection(); + + ReplaceOneResult result = collection.ReplaceOne(eachLoan => eachLoan.Id == branchConfig.Id, branchConfig); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while creating updating branch config"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while creating updating branch config"); + } + } + + public BranchNameConfig GetBranchNameConfig() + { + try + { + var collection = GetBranchNameContainersCollection(); + var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault(); + + if (result == null) + { + UpsertBranchNameConfig(); + } + else if (result != null && result.BranchNamesByChannelId.Count <= _branchNameDataListDefaults.Count) + { + UpsertBranchNameConfig(true); + } + + collection = GetBranchNameContainersCollection(); + result = collection.Find(new BsonDocument()).ToList().FirstOrDefault(); + return result; + } + catch (MongoConnectionException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Could not load branch name config"); + } + } + + public void UpsertBranchNameConfig(bool IsUpdate = false) + { + try + { + var collection = GetBranchNameContainersCollection(); + + if (collection.Find(new BsonDocument()).ToList().Count > 0 && !IsUpdate) + { + throw new Exception("Can only insert 1 branch name config"); + } + + SortedList configurationDetails = _vendorClient.GetBranchNameConfiguration("getChannels").GetAwaiter().GetResult(); + BranchNameConfig branchNameConfig = new(); + + List branchNameDataList = CreateBranchNameDataList(configurationDetails); + + branchNameConfig.Id = (Guid.NewGuid()).ToString(); + branchNameConfig.Date = DateTime.Now; + branchNameConfig.BranchNamesByChannelId = branchNameDataList; + + if (IsUpdate) + { + var oldConfig = collection.Find(new BsonDocument()).ToList().FirstOrDefault(); + branchNameConfig.Id = oldConfig.Id; + ReplaceOneResult result = collection.ReplaceOne(eachConfig => eachConfig.Id == branchNameConfig.Id, branchNameConfig); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + else + { + collection.InsertOne(branchNameConfig); + } + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while creating branch names config"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while creating branch names config"); + } + } + + public void UpdateBranchNameConfig() + { + UpsertBranchNameConfig(true); + } + + public List CreateBranchNameDataList(SortedList configurationDetails) + { + List branchNameDataList = _branchNameDataListDefaults; + + foreach (var branchNameData in configurationDetails) + { + try + { + branchNameDataList.Add(new BranchNameData { ChannelId = branchNameData.Key, BranchName = branchNameData.Value }); + } + catch (Exception ex) + { + Log.Error(ex.Message); + } + } + return branchNameDataList; + } + + #region IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + } + } + + this.disposed = true; + } + + public int GetDistributionChannelByZipCode(string zipCode) + { + var config = GetBranchConfig(); + foreach (var eachBranch in config.BranchCodesByZip) + { + if (eachBranch.Zipcode == zipCode) + { + return eachBranch.ChannelId; + } + } + return _crifSettings.ServicingChannels.DefaultServicingChannel; + } + + #endregion + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ConfigurationAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ConfigurationAccess.cs new file mode 100644 index 0000000..e4fd3df --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ConfigurationAccess.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using Landmark.OLA.Application.Common.Domain; + + +namespace Landmark.OLA.Application.DataAccess +{ + public class ConfigurationAccess : IDisposable, IConfigurationAccess + { + private bool disposed = false; + private readonly IDataClient _client; + private readonly string _collectionName = "AppConfiguration"; + + public ConfigurationAccess( + IDataClient dataClient) + { + _client = dataClient; + } + + private IMongoCollection GetConfigurationContainersCollection() + { + return _client.GetDatabase().GetCollection(_collectionName); + } + + public StoredSettings GetStoredSettings() + { + try + { + var collection = GetConfigurationContainersCollection(); + var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault(); + if( result == null ) + { + throw new Exception("Could not find app configuration."); + } + return result; + } + catch (MongoConnectionException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Could not load app configuration."); + } + } + + public void CreateStoredSettings(StoredSettings storedSettings) + { + try + { + var collection = GetConfigurationContainersCollection(); + + if (collection.Find(new BsonDocument()).ToList().Count > 0) + { + throw new Exception("Can only insert 1 configuration"); + } + collection.InsertOne(storedSettings); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while creating app configuration"); + } + } + + public void UpdateStoredSettings(StoredSettings storedSettings) + { + try + { + var collection = GetConfigurationContainersCollection(); + + ReplaceOneResult result = collection.ReplaceOne(eachConfig => eachConfig.Id == storedSettings.Id, storedSettings); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while updating app configuration"); + } + } + + #region IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + } + } + + this.disposed = true; + } + + #endregion + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataAccessConfiguration.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataAccessConfiguration.cs new file mode 100644 index 0000000..5c2aa9f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataAccessConfiguration.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Application.DataAccess +{ + public class DataAccessConnection + { + public string DatabaseName { get; set; } + public string UserName { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string Password { get; set; } + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataClient.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataClient.cs new file mode 100644 index 0000000..b97c468 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataClient.cs @@ -0,0 +1,45 @@ +using System.Security.Authentication; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace Landmark.OLA.Application.DataAccess +{ + // Reference: http://mongodb.github.io/mongo-csharp-driver/2.5 + public class DataClient : IDataClient + { + private readonly MongoClientSettings _mongoSettings; + private readonly DataAccessConnection _configuration; + private readonly MongoClient _client; + private readonly IMongoDatabase _database; + + public DataClient(IOptions settings) { + _configuration = settings.Value; + _mongoSettings = GetMongoSettings(); + _client = new MongoClient(_mongoSettings); + _database = _client.GetDatabase(_configuration.DatabaseName); + } + + public IMongoDatabase GetDatabase() + { + return _database; + } + + private MongoClientSettings GetMongoSettings() + { + MongoClientSettings settings = new() + { + Server = new MongoServerAddress(_configuration.Host, _configuration.Port), + UseTls = true, + SslSettings = new SslSettings() + }; + settings.SslSettings.EnabledSslProtocols = SslProtocols.Tls12; + + MongoIdentity identity = new MongoInternalIdentity(_configuration.DatabaseName, _configuration.UserName); + MongoIdentityEvidence evidence = new PasswordEvidence(_configuration.Password); + + settings.Credential = new MongoCredential("SCRAM-SHA-1", identity, evidence); + return settings; + } + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBlobStorageAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBlobStorageAccess.cs new file mode 100644 index 0000000..5c9467f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBlobStorageAccess.cs @@ -0,0 +1,15 @@ +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Application.DataAccess +{ + public interface IBlobStorageAccess + { + bool SaveBlob(LoanApplicationFile file, byte[] fileBytes); + + byte[] GetBlobAsBytes(LoanApplicationFile file); + + string GetBlobAsBase64(LoanApplicationFile file); + + bool DeleteBlob(LoanApplicationFile file); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBranchDataAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBranchDataAccess.cs new file mode 100644 index 0000000..c7e5145 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBranchDataAccess.cs @@ -0,0 +1,15 @@ +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Application.DataAccess +{ + public interface IBranchDataAccess : IDistributionChannelLookupService + { + void Dispose(); + BranchConfig GetBranchConfig(); + void UpdateBranchConfig(BranchConfig branchConfig); + void CreateBranchConfig(BranchConfig branchConfig); + BranchNameConfig GetBranchNameConfig(); + void UpdateBranchNameConfig(); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IConfigurationAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IConfigurationAccess.cs new file mode 100644 index 0000000..8fe10ce --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IConfigurationAccess.cs @@ -0,0 +1,12 @@ +using Landmark.OLA.Application.Common.Domain; + +namespace Landmark.OLA.Application.DataAccess +{ + public interface IConfigurationAccess + { + void Dispose(); + StoredSettings GetStoredSettings(); + void UpdateStoredSettings(StoredSettings branchConfig); + void CreateStoredSettings(StoredSettings branchConfig); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IDataClient.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IDataClient.cs new file mode 100644 index 0000000..84591f8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IDataClient.cs @@ -0,0 +1,9 @@ +using MongoDB.Driver; + +namespace Landmark.OLA.Application.DataAccess +{ + public interface IDataClient + { + IMongoDatabase GetDatabase(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ILoanApplicationDataAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ILoanApplicationDataAccess.cs new file mode 100644 index 0000000..60e5be2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ILoanApplicationDataAccess.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Application.DataAccessLayer +{ + public interface ILoanApplicationDataAccess + { + void CreateContainer(LoanContainer loanContainer); + void DeleteApplication(string id); + void DeleteContainer(string id); + void Dispose(); + List GetLoanContainers(Filter loanFilter); + List GetAllMemberContainers(string memberId, string individualId); + LoanContainer GetContainerById(string guid); + string GetContainerIdByLoanId(string loanId); + void UpdateContainer(LoanContainer loanContainer); + void UpdateStatus(string id, LoanStatus status); + void UpdateError(string id, LoanError error); + void UpdateLoanId(string id, string loanId); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/INotificationDao.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/INotificationDao.cs new file mode 100644 index 0000000..2fba281 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/INotificationDao.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using LCU.Library.Internal.NotificationsModels.Models; + +namespace Landmark.OLA.Application.DataAccess +{ + public interface INotificationDao + { + Task SendEmailAsync(EmailRequest request); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/LoanApplicationDataAccess.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/LoanApplicationDataAccess.cs new file mode 100644 index 0000000..59e21e1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/LoanApplicationDataAccess.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Application.Configuration; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Application.Services.MicroApiServices; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using LCU.Library.Internal.NotificationsModels.Models; + + + + +namespace Landmark.OLA.Application.DataAccessLayer +{ + public class LoanApplicationDataAccess : IDisposable, ILoanApplicationDataAccess + { + private bool disposed = false; + private readonly IDataClient _client; + private readonly INotificationsApiService _notificationsApiService; + private readonly string _collectionName = "Applications"; + private readonly NotificationsApiConfiguration _notificationsApiConfiguration; + private readonly Services.Email.IEmailTemplateService _emailTemplateService; + + public LoanApplicationDataAccess( + IDataClient dataClient, + INotificationsApiService notificationsApiService, + IOptions notificationsApiConfiguration, + IWebHostEnvironment webHostEnvironment, + Services.Email.IEmailTemplateService emailTemplateService) + { + _client = dataClient; + _notificationsApiService = notificationsApiService; + _notificationsApiConfiguration = notificationsApiConfiguration.Value; + _emailTemplateService = emailTemplateService; + } + + + public LoanContainer GetContainerById(string guid) + { + try + { + var collection = GetLoanContainersCollection(); + var foundItems = collection.Find(eachContainer => eachContainer.Id == guid); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + return foundItems.First(); + } + catch (MongoConnectionException) + { + return null; + } + } + + public string GetContainerIdByLoanId(string loanId) + { + try + { + var collection = GetLoanContainersCollection(); + var foundItems = collection.Find(eachContainer => eachContainer.LoanId == loanId); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + return foundItems.First().Id; + } + catch (MongoConnectionException) + { + return null; + } + } + + public List GetAllMemberContainers(string memberId, string individualId) + { + try + { + var collection = GetLoanContainersCollection(); + var foundItems = collection.Find(eachContainer => eachContainer.MemberId == memberId && eachContainer.IndividualId == individualId && eachContainer.LoanApplication != null).ToList(); + + foreach (var container in foundItems) + { + MemberIdService.RemoveMemberId(container); + } + return foundItems; + } + catch (MongoConnectionException) + { + return []; + } + } + + public List GetAllContainers() + { + try + { + var collection = GetLoanContainersCollection(); + return collection.Find(new BsonDocument()).ToList(); + } + catch (MongoConnectionException) + { + return []; + } + } + + public List GetLoanContainers(Filter filter) + { + try + { + var collection = GetLoanContainersCollection().Find(BuildFilter(filter)).Sort(BuildSorter(filter)).Limit(filter.Count).ToList(); + return collection; + } + catch (MongoConnectionException) + { + return []; + } + } + + public void CreateContainer(LoanContainer loanContainer) + { + try + { + var collection = GetLoanContainersCollection(); + + collection.InsertOne(loanContainer); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while creating container"); + } + } + + public void UpdateContainer(LoanContainer loanContainer) + { + try + { + var collection = GetLoanContainersCollection(); + + ReplaceOneResult result = collection.ReplaceOne(eachLoan => eachLoan.Id == loanContainer.Id, loanContainer); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while updating container"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while updating container"); + } + } + + public void DeleteApplication(string id) + { + try + { + var collection = GetLoanContainersCollection(); + + var foundItems = collection.Find(eachContainer => eachContainer.Id == id); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + LoanContainer loanContainer = foundItems.First(); + loanContainer.LoanApplication = null; + + ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while deleting application"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while deleting application"); + } + } + + public void UpdateStatus(string id, LoanStatus status) + { + try + { + var collection = GetLoanContainersCollection(); + + var foundItems = collection.Find(eachContainer => eachContainer.Id == id); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + LoanContainer loanContainer = foundItems.First(); + loanContainer.Status = status; + + ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + if (status == LoanStatus.Vendor_Error || status == LoanStatus.Application_Error) + { + var reportId = id; + + if ( loanContainer.LoanId != null) + { + reportId = loanContainer.LoanId; + } + + var contentHtml = "There was a " + status + " in loan application: " + reportId + ".

"; + + var request = new EmailRequest() + { + Subject = "Vendor Error In Loan Application", + Recipient = _notificationsApiConfiguration.ErrorRecipients, + Body = _emailTemplateService.BuildBrandedBody(contentHtml) + }; + + _notificationsApiService.SendEmailAsync(request); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while updating status"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while updating status"); + } + } + + public void UpdateError(string id, LoanError error) + { + try + { + var collection = GetLoanContainersCollection(); + + var foundItems = collection.Find(eachContainer => eachContainer.Id == id); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + LoanContainer loanContainer = foundItems.First(); + loanContainer.Error = error; + + ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while updating error"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while updating error"); + } + } + + public void UpdateLoanId(string id, string loanId) + { + var collection = GetLoanContainersCollection(); + try + { + var foundItems = collection.Find(eachContainer => eachContainer.Id == id); + if (foundItems.CountDocuments() != 1) + { + throw new KeyNotFoundException("Item not found"); + } + LoanContainer loanContainer = foundItems.First(); + loanContainer.LoanId = loanId; + + ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer); + if (result.MatchedCount == 0 || result.ModifiedCount == 0) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while updating loan id"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while updating loan id"); + } + } + + public void DeleteContainer(string id) + { + var collection = GetLoanContainersCollection(); + try + { + DeleteResult result = collection.DeleteOne(_ => _.Id == id); + if (result.DeletedCount != 1) + { + throw new KeyNotFoundException("Item not found"); + } + } + catch (KeyNotFoundException en) + { + Log.Error(en.Message); + throw new KeyNotFoundException(en.Message); + } + catch (MongoException ex) + { + Log.Error(ex.Message); + throw new InvalidDataException("Error occured while deleting container"); + } + catch (Exception e) + { + Log.Error(e.Message); + throw new InvalidDataException("Error occured while deleting container"); + } + } + + private IMongoCollection GetLoanContainersCollection() + { + return _client.GetDatabase().GetCollection(_collectionName); + } + + private static FilterDefinition BuildFilter(Filter filter) + { + DateTime sDate = filter.StartDate.ToUniversalTime(); + DateTime eDate = filter.EndDate.ToUniversalTime(); + var builder = Builders.Filter; + var filterValue = builder.Gt("Date", sDate) & builder.Lt("Date", eDate); + for (int i = 0; i < filter.FilterField.Length; i++) + { + var field = filter.FilterField[i]; + if (field.NotEqual) + { + filterValue = filterValue & builder.Ne(field.Field, field.FilterValue); + } + else if (field.Equal) + { + filterValue = filterValue & builder.Eq(field.Field, field.FilterValue); + } + else if (field.GreaterThan) + { + filterValue = filterValue & builder.Gt(field.Field, field.FilterValue); + } + else if (field.LessThan) + { + filterValue = filterValue & builder.Lt(field.Field, field.FilterValue); + } + } + return filterValue; + } + + private static string BuildSorter(Filter filter) + { + return "{ " + filter.SortField + ": " + (filter.SortDesc ? "-1}" : "1}"); + } + + #region IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + } + } + + this.disposed = true; + } + + #endregion + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/NotificationDao.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/NotificationDao.cs new file mode 100644 index 0000000..a8a9699 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/NotificationDao.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Landmark.OLA.Application.Configuration; +using LCU.Library.Internal.LcuHttpClient.HttpRequest; +using LCU.Library.Internal.NotificationsModels.Models; +using Microsoft.Extensions.Options; + +namespace Landmark.OLA.Application.DataAccess +{ + public class NotificationDao : INotificationDao + { + private readonly NotificationsApiConfiguration _configuration; + private readonly IPostRequest _postRequest; + + public NotificationDao(IOptions configuration, IPostRequest postRequest) + { + _configuration = configuration.Value; + _postRequest = postRequest; + } + + public async Task SendEmailAsync(EmailRequest request) + => await _postRequest.Async(new Uri(_configuration.SendEmailEndpointUrl), request); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf new file mode 100644 index 0000000..dcd66ab Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf differ diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/Logo.png b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/Logo.png new file mode 100644 index 0000000..ad4fd29 Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/Logo.png differ diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf new file mode 100644 index 0000000..0f990e7 Binary files /dev/null and b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf differ diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.csproj b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.csproj new file mode 100644 index 0000000..80e2f89 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.csproj @@ -0,0 +1,65 @@ + + + false + net8.0 + + Exe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Never + + + + + Always + + + Always + + + Always + + + PreserveNewest + + + diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.xml b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.xml new file mode 100644 index 0000000..40b6b45 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.xml @@ -0,0 +1,286 @@ + + + + Landmark.OLA.Application + + + + + Use to correct a postal address + + + Returns a service corrected postal address, given a postal address. + Cookies: None required or set. + Security: None required or provided. + + + + + Use to get the branch configuration + + + Returns a full branch configuration. + Cookies: None required or set. + Security: None. + + + + + Use to save the initial branch config. + + + Returns a saved branch config with id. This call will fail if ANY configs currently exist. + Cookies: None required or set. + Security: Requires Azure AD Jwt token and admin role. + + + + + Use update the branch config + + + Returns a status after attempting to update the branch configuration. + Cookies: None required or set. + Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + + + + + Use to start decisioning on a loan + + + Returns status of decision start, on 202 you can begin requesting details. + Cookies: Will expire existing cookies. + Security: None required or provided. + + + + + Use to get details of a decision + + + Returns a decision status, which may be empty if the decision is not ready. + Cookies: None required or set. + Security: None required or provided. + + + + + 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. + + + + + 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. + + + + + Use to send something to the logger from the client + + + Logs a message from a client. + Cookies: None required or set. + Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + + + + + Use to get the pdfs + + + Returns bytes of PDF. + Cookies: None required or set. + Security: None required or provided. + + + + + Use to refer a user to the application with a valid SAML assertion + + + Returns an application redirect, adding in a SAML assertion via a JWT in a secure http-only cookie. + Cookies: Set a standard pair of authentication and authorization cookies. + Security: Passed document must include a valid, signed SAML assertion, provides member authentication. + + + + + Use to create an empty cookie session + + + Returns a pair of empty authorization and authentication cookies. + Cookies: Sets empty Authentication and Authorization cookies. + Security: None required or provided. + + + + + Use to create a cookie session with credentials + + + Returns a pair of authorization and authentication cookies when provided with valid credentials. + Cookies: Sets Authentication and Authorization cookies. + Security: Require. + + + + + Use to identify a unique individual on the member account. + + + Returns OK if an individual ID was added to the cookie. + Cookies: Sets Individual ID in cookie. + Security: Requires existing cookie, with member. + + + + + Attempts to load the root of the API result in a web-app redirect. + + + Cookies: None required or set. + Security: None required or provided. + + + + + Attempts to load a website and return the reponse. + + + Cookies: None required or set. + Security: None required or provided. + + + + + Use to get the status of the api + + + Returns a simple object describing the build time, version, and environment of the running API. + Cookies: None required or set. + Security: None required or provided. + + + + + Use to retrieve a set of Loan Containers by filter + + + Returns an array of Loan Containers given a filter + Cookies: None required or set. + Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + + + + + Use to get a specific loan container + + + Returns a loan container given a loan id. + Cookies: None required or set. + Security: Must have a valid JWT token retrieved from Azure AD. + + + + + Use to get loan containers for an authenticated member + + + Returns a list of loan containers given a valid cookie with member id. + Cookies: Requires authenticated cookie with member id. + Security: None required or provided. + + + + + Use to save a loan container + + + Returns a saved loan container with id and possibly member id + Cookies: Authenticated member cookie optional, will cause id to be saved. + Security: Currently creates authentication and authorization cookies. + + + + + Use update a loan container + + + Returns a status after attempting to update a loan container with new info. + Cookies: None required or set. + Security: None required or provided. + + + + + Use to delete a loan + + + Deletes a loan given an id. + Cookies: None required or set. + Security: Requires JWT token from Azure AD, AND Admin role. + + + + + Use to delete an entire loan container + + + Deletes a loan container given a container id. + Cookies: None required or set. + Security: Requires JWT token from Azure AD, AND Admin role. + + + + + Use to retrieve UI Settings + + + Returns the UI Settings; with the dynamic branch mappings injected. + Cookies: None required or set. + Security: None required or provided. + + + + + Use to get stored config (run-time changeable) + + + Returns the current StoredConfig + Cookies: None required or set. + Security: Requires Azure AD token and Admin role. + + + + + Use to save the initial storedConfig. + + + Returns a saved configuration with id. This call will fail if ANY configs currently exist. + Cookies: None required or set. + Security: Requires Azure AD Jwt token and admin role. + + + + + Use update the StoredConfig + + + Returns a status after attempting to update the StoredConfig. + Cookies: None required or set. + Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Program.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Program.cs new file mode 100644 index 0000000..a884a63 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Program.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Landmark.OLA.Application +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .ConfigureAppConfiguration((context, builder) => + { + var builtConfig = builder.Build(); + + builder.AddJsonFile("/app/config/environment-appsettings/appsettings.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/app/config/workload-appsettings/appsettings.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-primary-seq-key.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-usps-settings-username.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-application-storage-password.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-crif-settings-api-settings-username.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-private-cert-azure-thumbprint.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-public-cert-azure-thumbprint.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-token-secret.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-key.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-secret.json", optional: true, reloadOnChange: true); + builder.AddJsonFile("/vault/secrets/online-loan-app-api-blob-storage-connection-settings-account-key.json", optional: true, reloadOnChange: true); + }) + .UseSerilog(); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Properties/launchSettings.json b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Properties/launchSettings.json new file mode 100644 index 0000000..2b95bdf --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56932", + "sslPort": 44384 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Landmark.OLA.Application": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreAccessService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreAccessService.cs new file mode 100644 index 0000000..bf2d09a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreAccessService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Landmark.OLA.Application.Controllers; +using Landmark.OLA.Models.Core; + +namespace Landmark.OLA.Application.Services +{ + public class CoreAccessService + { + public CoreMemberOwner FindMatchingOwner(OnlineIdentityFormData document, List owners) + { + if (owners != null && owners.Count != 0 && document != null) + { + foreach (var owner in owners) + { + if (DoesOwnerMatch(owner, document)) + { + return owner; + } + } + } + + return null; + } + + // Expects birthdate in form YYYY-MM-DD and taxid in form ###-##-#### + // Expects dob in form MM/DD/YYYY and ssn in form #### (last 4) + // Tries to match this owner and this document. + public bool DoesOwnerMatch(CoreMemberOwner owner, OnlineIdentityFormData document) + { + if (string.IsNullOrEmpty(owner.BirthDate) || owner.TaxId == null || owner.TaxId.Length != 11) + { + return false; + } + if (owner.TaxId.Substring(7) != document.SocialSecurityNumber) + { + return false; + } + string[] formats = { "MM/dd/yyyy", "yyyy-MM-dd" }; + var dobEntered = DateTime.ParseExact(document.DateOfBirth, formats, new CultureInfo("en-US"), DateTimeStyles.None); + var dobDesired = DateTime.ParseExact(owner.BirthDate, formats, new CultureInfo("en-US"), DateTimeStyles.None); + return dobEntered == dobDesired; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreService.cs new file mode 100644 index 0000000..6d64945 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreService.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Serilog; +using Newtonsoft.Json; +using Landmark.OLA.Models.Core; +using LCU.Library.Internal.LcuHttpClient.HttpRequest; + +namespace Landmark.OLA.Application.Services +{ + public class CoreService : ICoreService + { + private readonly IGetRequest _getRequest; + + public CoreService(IGetRequest getRequest) + { + _getRequest = getRequest; + } + + public async Task GetIndividual(string individualId) + { + try + { + var uri = new Uri($"core/individuals?userId={individualId}"); + var responseString = await _getRequest.StringResponseAsync(uri); + return JsonConvert.DeserializeObject(responseString); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to get Individual for IndividualId: {individualId} in {GetType().Name}"); + return null; + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreSettings.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreSettings.cs new file mode 100644 index 0000000..dca22f1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreSettings.cs @@ -0,0 +1,9 @@ + +namespace Landmark.OLA.Application.Services +{ + public class CoreSettings + { + public string EndPoint { get; set; } + public int Timeout { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/EmailTemplateService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/EmailTemplateService.cs new file mode 100644 index 0000000..e458e16 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/EmailTemplateService.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Serilog; + +namespace Landmark.OLA.Application.Services.Email +{ + public class EmailTemplateService : IEmailTemplateService + { + private readonly string _embeddedLogoBase64; + + public EmailTemplateService(IWebHostEnvironment webHostEnvironment) + { + _embeddedLogoBase64 = LoadEmbeddedLogo(webHostEnvironment); + } + + private static string LoadEmbeddedLogo(IWebHostEnvironment webHostEnvironment) + { + if (webHostEnvironment == null) + { + return string.Empty; + } + + try + { + var logoPath = Path.Combine(webHostEnvironment.ContentRootPath, "Files", "Logo.png"); + if (!File.Exists(logoPath)) + { + Log.Warning("Logo not found at {LogoPath}. Email will fall back to remote image.", logoPath); + return string.Empty; + } + + var bytes = File.ReadAllBytes(logoPath); + return Convert.ToBase64String(bytes); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to embed logo from file."); + return string.Empty; + } + } + + public string BuildBrandedBody(string contentHtml, string redBannerMessage = "") + { + var topLogoTag = $""; + var bottomLogoTag = $"\"Landmark"; + + redBannerMessage ??= string.Empty; + var bannerHtml = string.IsNullOrWhiteSpace(redBannerMessage) + ? string.Empty + : + "
" + + "" + + redBannerMessage + + "

" + + "
"; + + var requestBody = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + topLogoTag + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + bannerHtml + + contentHtml + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
 
 " + + "
" + + "
" + + bottomLogoTag + + "
" + + "
" + + "
" + + " 
 
" + + "" + + ""; + + return requestBody; + } + } +} + diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/IEmailTemplateService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/IEmailTemplateService.cs new file mode 100644 index 0000000..c1d9947 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/IEmailTemplateService.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Hosting; + +namespace Landmark.OLA.Application.Services.Email +{ + public interface IEmailTemplateService + { + string BuildBrandedBody(string contentHtml, string redBannerMessage = ""); + } +} + diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Exceptions/CoreUnavailableException.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Exceptions/CoreUnavailableException.cs new file mode 100644 index 0000000..e3224b4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Exceptions/CoreUnavailableException.cs @@ -0,0 +1,12 @@ +using System; +using System.Net.Http; + +namespace Landmark.OLA.Application.Services.Exceptions +{ + public class CoreUnavailableException : Exception + { + public CoreUnavailableException(HttpRequestException hre): base(hre.Message, hre) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/ICoreService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/ICoreService.cs new file mode 100644 index 0000000..bbb5848 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/ICoreService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Landmark.OLA.Models.Core; + +namespace Landmark.OLA.Application.Services +{ + public interface ICoreService + { + Task GetIndividual(string individualId); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/IPdfService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/IPdfService.cs new file mode 100644 index 0000000..cd9ff6a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/IPdfService.cs @@ -0,0 +1,8 @@ + +namespace Landmark.OLA.Application.Services +{ + public interface IPdfService + { + byte[] GetFileBytes(string path); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/INotificationsApiService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/INotificationsApiService.cs new file mode 100644 index 0000000..521a350 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/INotificationsApiService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using LCU.Library.Internal.NotificationsModels.Models; + +namespace Landmark.OLA.Application.Services.MicroApiServices +{ + public interface INotificationsApiService + { + Task SendEmailAsync(EmailRequest request); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/NotificationsApiService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/NotificationsApiService.cs new file mode 100644 index 0000000..d0c61cb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/NotificationsApiService.cs @@ -0,0 +1,32 @@ +using Serilog; +using System; +using System.Threading.Tasks; +using Landmark.OLA.Application.DataAccess; +using LCU.Library.Internal.NotificationsModels.Models; + +namespace Landmark.OLA.Application.Services.MicroApiServices +{ + public class NotificationsApiService : INotificationsApiService + { + private readonly INotificationDao _notificationsApiDao; + + public NotificationsApiService(INotificationDao notificationsApiDao) + { + _notificationsApiDao = notificationsApiDao; + } + + public async Task SendEmailAsync(EmailRequest request) + { + try + { + await _notificationsApiDao.SendEmailAsync(request); + } + catch (Exception exception) + { + var error = $"Failure during SendEmailAsync. Exception Message: {exception.Message}"; + Log.Error(error); + throw new Exception(error); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/PdfService.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/PdfService.cs new file mode 100644 index 0000000..ac0f5ed --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/PdfService.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; + +namespace Landmark.OLA.Application.Services +{ + public class PdfService : IPdfService + { + public byte[] GetFileBytes(string path) + { + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + return File.ReadAllBytes(path); + } + + throw new ArgumentException($"Path {path} must exist."); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Startup.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Startup.cs new file mode 100644 index 0000000..865abef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Startup.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Converters; +using Serilog; +using CRIF.ACTion.Common.Parsers; +using CRIF.ACTion.Configuration; +using CRIF.ACTion.Decisioning; +using CRIF.ACTion.Loan; +using CRIF.ACTion.Loan.Individuals; +using CRIF.ACTion.Services.Crif; +using Landmark.OLA.Application.Common.Domain; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Application.Configuration; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Application.DataAccessLayer; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.OnlineBanking; +using Landmark.OLA.Models.Vendor; +using OnlineBankingAuth.Communication; +using OnlineBankingAuth.Parsers; +using OnlineBankingAuth.Services; +using OnlineBankingAuth.Tokens; +using LCU.Library.Internal.Logging; +using LCU.Library.Internal.Logging.Middleware; +using LCU.Library.Internal.Logging.Models; +using LCU.Library.Internal.LcuHttpClient; +using Landmark.OLA.Application.Services.MicroApiServices; +using Landmark.OLA.Application.Services.Email; + +namespace Landmark.OLA.Application +{ + public class Startup(IConfiguration configuration) + { + public IConfiguration Configuration { get; } = configuration; + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + var appConfig = new ApplicationSettings(); + Configuration.GetSection("Primary").Bind(appConfig); + + services.AddSingleton(appConfig); + + var loggerConfiguration = new LoggingConfiguration(appConfig.AppName, + appConfig.SeqKey, + appConfig.SeqUrl); + var lcuLogging = new LcuLogging(loggerConfiguration); + lcuLogging.UseLcuLogging(); + + Log.Information($"{appConfig.AppName} Starting Up"); + + services.AddLCUHttpServices(); + + services.AddCors(options => + { + options.AddPolicy("DevCors", b => b + .SetIsOriginAllowed(_ => true) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials()); + + options.AddPolicy("ProdCors", b => b + .WithOrigins( + "https://applydev.landmarkcu.com", + "https://applyqa.landmarkcu.com", + "https://apply.landmarkcu.com", + "https://qa-online-loan-app.apps.azure.lcu-internal.com", + "https://uat-online-loan-app.apps.azure.lcu-internal.com", + "https://prd-online-loan-app.apps.azure.lcu-internal.com", + "https://qa-apps.landmarkcu.com", + "https://uat-apps.landmarkcu.com", + "https://apps.landmarkcu.com") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials()); + }); + + services.AddControllers().AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(new StringEnumConverter())); + + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => false; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); + + //TODO #245: Disable in production? + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Landmark Loan API", Version = "v1" }); + // For this to work, I also had to check the generate xml box in build options. + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); + }); + + + services.AddScoped(); + services.AddScoped(); + + services.Configure(options => + Configuration.GetSection("ApplicationStorage").Bind(options)); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped, AddIndividualResponseParser>(); + services.AddScoped, LoanApplicationResponseParser>(); + services.AddScoped, DecisioningStateResponseParser>(); + services.AddScoped, GetApplicationProductsResponseParser>(); + services.AddScoped>, ConfigurationRequestResponseParser>(); + services.AddScoped>, ProductDetailsResponseParser>(); + services.AddScoped, CustomFieldRequestResponseParser>(); + services.AddScoped, DealershipResponseParser>(); + + services.AddSingleton(); + + // Add USPS scopes + services.AddScoped(); + services.AddScoped(); + + services.AddScoped, USPS.Parsers.AddressValidationResponseParser>(); + services.Configure(options => Configuration.GetSection("ApplicationControl").Bind(options)); + + services.Configure(options => + Configuration.GetSection("CrifSettings").Bind(options)); + + services.Configure(options => + Configuration.GetSection("BlobStorageSettings").Bind(options)); + + services.Configure(options => + Configuration.GetSection("BlobStorageConnectionSettings").Bind(options)); + + services.Configure(options => + Configuration.GetSection("DealershipEmailSettings").Bind(options)); + + services.AddSingleton(); + + // Set up USPS + services.Configure(options => + Configuration.GetSection("UspsSettings").Bind(options)); + + // Setup up Email + services.Configure(options => + Configuration.GetSection("NotificationsApiConfiguration").Bind(options)); + services.AddScoped(); + services.AddSingleton(); + + // Setup up Core + var coreSettings = new CoreSettings(); + Configuration.GetSection("CoreSettings").Bind(coreSettings); + services.AddSingleton(coreSettings); + services.AddScoped(); + services.AddScoped(); + + // Set up SAML IdP scopes + services.AddScoped(); + services.AddScoped, SAMLParser>(); + + // Set up SAML IdP configuration + services.Configure(options => + { + Configuration.GetSection("SecuritySettings").Bind(options); + }); + + // Setup DI service + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + //Set up Authentication + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + // AzureAD config + .AddAzureAdBearer(options => Configuration.Bind("AzureActiveDirectory", options)); + + services.AddAuthorization(); + services.AddDistributedMemoryCache(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ApplicationSettings appSettings) + { + Log.Information("Configuring..."); + + var policyCollection = new HeaderPolicyCollection() + .AddDefaultSecurityHeaders() + .AddCustomHeader("Cache-Control", "no-cache") + .AddCustomHeader("Arr-Disable-Session-Affinity", "true"); + + app.UseCookiePolicy(new CookiePolicyOptions + { + CheckConsentNeeded = context => false, + MinimumSameSitePolicy = SameSiteMode.None + }); + + + app.UseSecurityHeaders(policyCollection); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Landmark Loan API")); + + app.UseMiddleware(); + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest; + }); + } + else + { + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false) + app.UseCors("DevCors"); + else + app.UseCors("ProdCors"); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Status.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Status.cs new file mode 100644 index 0000000..54c714c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Status.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; + +namespace Landmark.OLA.Application +{ + public interface IStatus + { + string Application { get; } + DateTime CurrentDate { get; } + string BuildVersion { get; } + DateTime BuildDate { get; } + string Environment { get; } + } + + public class Status : IStatus + { + public string Application => _env.ApplicationName; + public DateTime CurrentDate => DateTime.Now; + public string BuildVersion => Assembly.GetExecutingAssembly().GetName().Version.ToString(); + public DateTime BuildDate => AssemblyBuildDate(); + public string Environment => _env.EnvironmentName; + + private readonly IWebHostEnvironment _env; + + public Status(IWebHostEnvironment env) + { + _env = env; + } + + public static DateTime AssemblyBuildDate() + { + //https://stackoverflow.com/a/804310/266594 + //TODO #246: Look up this value at start up to avoid hitting the filesystem repeatedly + Assembly assembly = Assembly.GetExecutingAssembly(); + System.IO.FileInfo fileInfo = new(assembly.Location); + DateTime lastModified = fileInfo.LastWriteTime; + + return lastModified; + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/appsettings.json b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/appsettings.json new file mode 100644 index 0000000..3113c64 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/appsettings.json @@ -0,0 +1,158 @@ +{ + "Primary": { + "AppName": "Loan API", + "SeqUrl": "http://localhost:5080/", + "SeqKey": "VALUE-IN-KEYVAULT", + "LogFilePath": "serilog.log" + }, + "AzureActiveDirectory": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "landmarkcu.com", + "TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499", + /** the azure id of the backend **/ + "ClientId": "f8173aad-fccb-4226-8c6a-963693956ad4", + /** the azure id of the front end **/ + "ValidAudience": "59d54f39-3ff9-4166-8b16-254c00416754" + }, + "ApplicationControl": { + "NotifyLogging": true, + "HomePageUrl": "http://localhost:4200", + "ImportantTermsPdfFilePath": "./Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf", + "WhatYouShouldKnowPdfFilePath": "./Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf" + }, + "AllowedHosts": "*", + "UspsSettings": { + "Username": "VALUE-IN-KEYVAULT", + "Password": "No Password", + "EndpointUrl": "http://production.shippingapis.com/ShippingAPI.dll?" + }, + "ApplicationStorage": { + "DatabaseName": "LoanApplications", + "UserName": "loan-app-dvl", + "Host": "loan-app-dvl.documents.azure.com", + "Port": 10255, + "Password": "VALUE-IN-KEYVAULT" + }, + "CrifSettings": { + "ApiSettings": { + "Username": "VALUE-IN-KEYVAULT", + "InterfaceId": 600002, + "OrgCode": "LandmarkCU", + "OrgId": 4, + "EndpointUrl": "https://www-lc3cst.myappro.com/ACS/ConnectorService.svc/ConnectorService" + }, + "SourceChannels": { + "DefaultSourceChannelId": 2399724, + "CreditCardSourceChannelId": 2399734 + }, + "ProductIds": { + "CC_CashBack": 132, + "CC_Rewards": 133, + "CC_Student": 134, + "CC_Secure": 136, + "EQ_HELOC_Flex": 139, + "EQ_Home_Equity_Fixed": 148, + "EQ_Mtg_Refi_Express": 149, + "EQ_Mfg_Home_And_Land": 146, + "V_Vehicle": 125, + "V_VehiclePreapproval": 126, + "M_Boat": 129, + "P_Powersport": 131, + "T_TrailerMotorhome": 130, + "S_Signature": 140 + }, + "ServicingChannels": { + "DefaultServicingChannel": 1698432 + }, + "CustomFieldMappings": { + "LoanTypeToPromoId": { + "132": "-114", + "133": "-114", + "134": "-114", + "136": "-114", + "139": "-118", + "148": "-118", + "149": "-118", + "146": "-118", + "125": "-117", + "126": "-117", + "129": "-117", + "131": "-117", + "130": "-117", + "140": "-115" + }, + "CustomFieldID": { + "LicensePlateNumber": "-76", + "CountyTitled": "-96", + "MunicipalityType": "-173", + "MunicipalityWhereKept": "-174" + }, + "MunicipalityTypeCode": { + "CITY": "402", + "TOWN": "403", + "VILLAGE": "404" + } + }, + "FormatCategoryMappings": { + "Categories": { + "Stip Verification": "-4", + "Loan Application": "-3", + "Vehicle Value": "-2", + "Proof of Income": "-1", + "Attachment": "1", + "Appraisal": "12", + "Flood": "13", + "Title": "14", + "eDocs": "15", + "eCon": "16" + }, + "DocumentToCategory": { + "Proof of Income": "Proof of Income", + "Boat Title": "Title", + "Trailer Title": "Title", + "Copy of the homeowners insurance": "Attachment", + "Proof of being a student": "Attachment", + "Copy of first mortgage balance statement": "Attachment", + "DNR Registration Card": "Attachment", + "Purchase Contract": "Attachment", + "Manufactured Home Title / Purchase Contract": "Attachment" + } + } + }, + "SecuritySettings": { + "PrivateCertAzureThumbprint": "VALUE-IN-KEYVAULT", + "PublicCertAzureThumbprint": "VALUE-IN-KEYVAULT", + "TokenSecret": "VALUE-IN-KEYVAULT", + "SAMLSuccessRedirectRoute": "http://localhost:4200/select-loan?referrer=NCR", + "SAMLFailureRedirectRoute": "http://localhost:4200/error?referrer=NCR", + "DigitalInsightPostTokenURI": "https://diapis.stg1.digitalinsight.com/v1/oauth/token", + "DigitalInsightConsumerKey": "VALUE-IN-KEYVAULT", + "DigitalInsightConsumerSecret": "VALUE-IN-KEYVAULT", + "DigitalInsightFIID": "03456" + }, + "NotificationsApiConfiguration": { + "Url": "http://localhost:50353", + "SendEmail": "/api/Email", + "ErrorRecipients": [ "ITDevelopment@landmarkcu.com" ], + "RetryCount": 5 + }, + "CoreSettings": { + "EndPoint": "http://localhost:44355/", + "Timeout": 60 + }, + "BlobStorageSettings": { + "MaxFileSizeInMB": "10.0", + "AllowedFileTypes": [ "gif", "jpg", "jpeg", "png", "pdf" ], + "FileLimit": "5" + }, + "BlobStorageConnectionSettings": { + "AccountName": "devloanappsa", + "AccountKey": "VALUE-IN-KEYVAULT" + }, + "DealershipEmailSettings": { + "RedBannerMessage": "This is from the QA test environment.", + "Recipients": [ "ITDevelopment@landmarkcu.com" ], + "ActionLink": "https://actiona6t.criflending.com/Core/Application/AppSecurity.aspx?orgCode=landmarkcu&resID=&resTypeID=-995&appID=", + "EmailAPIUrl": "https://localhost:50353/api/Email" + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/wwwroot/.gitkeep b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/wwwroot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/AddressValidationControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/AddressValidationControllerTest.cs new file mode 100644 index 0000000..540a1a0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/AddressValidationControllerTest.cs @@ -0,0 +1,57 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Moq; +using Newtonsoft.Json; +using Landmark.OLA.Models.Addresses; + +namespace Landmark.OLA.Integration.Tests +{ + public class AddressValidationControllerTest : OlaIntegrationTest + { + public Address testResult; + + public AddressValidationControllerTest() : base() + { + testResult = new Address() + { + Address1 = "111 South First Street", + City = "Milwaukee", + State = Models.Addresses.Enums.StateOption.WI, + PostalCode = "53207", + Country = Models.Addresses.Enums.CountryOption.UnitedStates + }; + mockUspsVendorClient.Setup(repo => repo.CompareAddress(It.IsAny
())) + .Returns(Task.FromResult(testResult)); + } + + [Fact] + public async Task CanValidateAddress() + { + // Arrange + var requestAddress = new Address + { + Address1 = "111 South First Street", + City = "Milwaukee", + State = Models.Addresses.Enums.StateOption.WI, + PostalCode = "53207", + Country = Models.Addresses.Enums.CountryOption.UnitedStates + }; + + // Act + var request = new StringContent(JsonConvert.SerializeObject(requestAddress), Encoding.UTF8, + "application/json"); + var httpResponse = await _client.PostAsync("/api/AddressValidation/", request); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var resultAddress = JsonConvert.DeserializeObject
(stringResponse); + Assert.NotNull(resultAddress.Address1); + Assert.Equal(testResult.Address1, resultAddress.Address1); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CoreAccessServiceTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CoreAccessServiceTest.cs new file mode 100644 index 0000000..21bd890 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CoreAccessServiceTest.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using Xunit; +using Landmark.OLA.Application.Controllers; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Models.Core; + +namespace Landmark.OLA.Integration.Tests +{ + public class CoreAccessServiceTest + { + private readonly CoreAccessService coreAccessService; + private readonly List forms; + private readonly List owners; + + public CoreAccessServiceTest() + { + coreAccessService = new CoreAccessService(); + forms = + [ + new OnlineIdentityFormData() + { + DateOfBirth = "2009-01-01", + SocialSecurityNumber = "9999" + }, + new OnlineIdentityFormData() + { + DateOfBirth = "01/01/2009", + SocialSecurityNumber = "9999" + }, + new OnlineIdentityFormData() + { + DateOfBirth = "01/01/09", + SocialSecurityNumber = "7777" + } + ]; + owners = + [ + new CoreMemberOwner() + { + Address = "address", + BirthDate = "2009-01-01", + DriverLicense = "F620-3888-0442-88", + IndividualId = 99, + Name = "John Tester", + TaxId = "999-99-9999", + Type = "Primary" + }, + new CoreMemberOwner() + { + Address = "address", + BirthDate = "2006-01-01", + DriverLicense = "F620-3999-0442-99", + IndividualId = 100, + Name = "Jane Tester", + TaxId = "999-99-8888", + Type = "Secondary" + } + ]; + } + + [Fact] + public void FindMatchingOwner_Bad_Args() + { + var result = coreAccessService.FindMatchingOwner(null, null); + Assert.Null(result); + + var document = new OnlineIdentityFormData() + { + DateOfBirth = "asdfasd", + SocialSecurityNumber = "asdf" + }; + result = coreAccessService.FindMatchingOwner(document, null); + Assert.Null(result); + + var ownersEmpty = new List() { }; + result = coreAccessService.FindMatchingOwner(document, ownersEmpty); + Assert.Null(result); + + result = coreAccessService.FindMatchingOwner(null, owners); + Assert.Null(result); + } + + [Fact] + public void FindMatchingOwnerBasic() + { + var result = coreAccessService.FindMatchingOwner(forms[0], owners); + + Assert.NotNull(result); + Assert.Equal(owners[0].IndividualId, result.IndividualId); + + result = coreAccessService.FindMatchingOwner(forms[1], owners); + + Assert.NotNull(result); + Assert.Equal(owners[0].IndividualId, result.IndividualId); + + result = coreAccessService.FindMatchingOwner(forms[2], owners); + + Assert.Null(result); + } + + [Fact] + public void DoesOwnerMatchBasic() + { + Assert.True(coreAccessService.DoesOwnerMatch(owners[0], forms[0])); + Assert.True(coreAccessService.DoesOwnerMatch(owners[0], forms[1])); + Assert.False(coreAccessService.DoesOwnerMatch(owners[0], forms[2])); + Assert.False(coreAccessService.DoesOwnerMatch(owners[1], forms[0])); + Assert.False(coreAccessService.DoesOwnerMatch(owners[1], forms[1])); + Assert.False(coreAccessService.DoesOwnerMatch(owners[1], forms[2])); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CustomWebApplicationFactory.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..135dbc1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CustomWebApplicationFactory.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Landmark.OLA.Application; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Integration.Tests +{ + public class CustomWebApplicationFactory : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + var contentRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Landmark.OLA.Application")); + + builder.UseEnvironment("Development"); + builder.UseContentRoot(contentRoot); + builder.ConfigureAppConfiguration((context, config) => + { + config.Sources.Clear(); + config.SetBasePath(contentRoot); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); + config.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddEnvironmentVariables(); + }); + + builder.ConfigureServices(services => + { + var mockRepo = new Mock(); + mockRepo.Setup(repo => repo.GetBranchConfig()).Returns(new BranchConfig()); + services.AddScoped(_ => mockRepo.Object); + }); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/DecisionControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/DecisionControllerTest.cs new file mode 100644 index 0000000..16f2f75 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/DecisionControllerTest.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Moq; +using Newtonsoft.Json; +using Xunit; +using CRIF.ACTion.Services.Crif; +using Landmark.OLA.Models.Decisioning; +using Landmark.OLA.Models.Decisioning.Enums; + +namespace Landmark.OLA.Integration.Tests +{ + public class DecisionControllerTest : OlaIntegrationTest + { + public DecisionControllerTest() : base() + { + var decisionResult = new DecisioningDetails() + { + DecisioningState = DecisioningState.Ready, + DecisioningResult = DecisioningResult.AcceptanceStatus_Provisional + }; + + mockVendorClient.Setup(repo => repo.GetDecisionStatus("555")).Throws(new Error13Exception("Test Failure")); + mockVendorClient.Setup(repo => repo.GetDecisionStatus("444")).ReturnsAsync(decisionResult); + } + + [Fact] + public async Task GetDecisionSpecialError13() + { + // The endpoint or route of the controller action. + var httpResponse = await _client.GetAsync("/api/LoanApplication/555/DecisioningDetails"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var status = JsonConvert.DeserializeObject(stringResponse); + Assert.Equal(DecisioningState.Unknown, status.DecisioningState); + Assert.Equal(DecisioningResult.ProcessStatus_Incomplete, status.DecisioningResult); + } + + [Fact] + public async Task GetDecisionSuccess() + { + // The endpoint or route of the controller action. + var httpResponse = await _client.GetAsync("/api/LoanApplication/444/DecisioningDetails"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var status = JsonConvert.DeserializeObject(stringResponse); + Assert.Equal(DecisioningState.Ready, status.DecisioningState); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/IndividualControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/IndividualControllerTest.cs new file mode 100644 index 0000000..275b052 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/IndividualControllerTest.cs @@ -0,0 +1,32 @@ + +namespace Landmark.OLA.Integration.Tests +{ + public class IndividualControllerTest : OlaIntegrationTest + { + + + public IndividualControllerTest() : base() + { + + } + /* + [Fact] + public async Task CanGetIndividual() + { + // Arrange + + // Act + var httpResponse = await _client.GetAsync("/api/Individual/555555"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(stringResponse); + Assert.NotNull(result); + Assert.Equal(555555, result.IndividualId); + } + */ + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/Landmark.OLA.Integration.Tests.csproj b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/Landmark.OLA.Integration.Tests.csproj new file mode 100644 index 0000000..738eeb6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/Landmark.OLA.Integration.Tests.csproj @@ -0,0 +1,19 @@ + + + false + net8.0 + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/LoanApplicationControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/LoanApplicationControllerTest.cs new file mode 100644 index 0000000..302cdf2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/LoanApplicationControllerTest.cs @@ -0,0 +1,70 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Moq; +using Newtonsoft.Json; +using Xunit; +using CRIF.ACTion.Services.Landmark.DistributionChannel; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Integration.Tests +{ + public class LoanApplicationControllerTest : OlaIntegrationTest + { + public LoanApplicationControllerTest() : base() + { + } + + [Fact] + public async Task SubmitExisting() + { + // Arrange + // mock: loanApplicationStorage.GetContainerById(cosmosId); + // mock: _tokenService.GetAuthenticatedDataFromCookie(HttpContext); + // mock: loanApplicationStorage.UpdateLoanEnvelop(loanContainer.Id, LoanStatus.Submitted, new LoanError(LoanErrorCode.No_Error, "No_Error"), application.Id); + // mock: loanApplicationStorage.DeleteApplication(cosmosId); + var mockFoundContainer = new LoanContainer + { + LoanApplication = new MockApplication().LoanApplication, + Status = LoanStatus.In_Progress, + Id = "cosmos-123" + }; + var mockLoanFile = new LoanApplicationFile(mockFoundContainer.Id, Guid.NewGuid(), "document.pdf", "Section", "Section-1", "Mock Document"); + mockFoundContainer.LoanApplication.Files.Add(mockLoanFile); + var mockResult = new MockApplication().LoanApplication; + mockResult.Id = "123456789"; + mockVendorClient + .Setup(repo => repo.CreateApplicationAndGetId(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Id); + mockVendorClient + .Setup(repo => repo.AddFile(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + mockLoanApplicationDataAccess.Setup(repo => repo.GetContainerById(It.IsAny())) + .Returns(mockFoundContainer); + mockLoanApplicationDataAccess.Setup(repo => repo.UpdateLoanId(It.IsAny(), It.IsAny())); + mockLoanApplicationDataAccess.Setup(repo => repo.UpdateError(It.IsAny(), It.IsAny())); + mockLoanApplicationDataAccess.Setup(repo => repo.UpdateStatus(It.IsAny(), It.IsAny())); + mockLoanApplicationDataAccess.Setup(repo => repo.UpdateContainer(It.IsAny())); + mockLoanApplicationDataAccess.Setup(repo => repo.DeleteApplication(It.IsAny())); + mockBlobStorageAccess.Setup(repo => repo.GetBlobAsBase64(It.IsAny())) + .Returns("dGhpcy1pcy1hLW1vY2tlZC1ibG9i"); + mockBlobStorageAccess.Setup(repo => repo.DeleteBlob(It.IsAny())) + .Returns(true); + + // Act + var request = new StringContent("", Encoding.UTF8, "application/json"); + var httpResponse = await _client.PostAsync("/api/LoanApplication/Submit/555-444-333", request); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var resultApplication = JsonConvert.DeserializeObject(stringResponse); + Assert.NotNull(resultApplication); + Assert.Equal("{\"loanApplicationId\":\"123456789\"}", stringResponse); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/OlaIntegrationTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/OlaIntegrationTest.cs new file mode 100644 index 0000000..414cd3b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/OlaIntegrationTest.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using Serilog; +using Xunit; +using CRIF.ACTion.Services.Crif; +using Landmark.OLA.Application; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Application.DataAccessLayer; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Models.Vendor; +using OnlineBankingAuth.Tokens; + +namespace Landmark.OLA.Integration.Tests +{ + public class OlaIntegrationTest : IClassFixture> + { + protected readonly HttpClient _client; + protected readonly Mock mockBranchDataAccess; + protected readonly Mock mockConfigurationAccess; + protected readonly Mock mockVendorClient; + protected readonly Mock mockLoanApplicationDataAccess; + protected readonly Mock mockBlobStorageAccess; + protected readonly Mock mockUspsVendorClient; + protected readonly Mock mockTokenService; + protected readonly Mock mockCoreService; + + public OlaIntegrationTest() + { + mockBranchDataAccess = new Mock(); + mockConfigurationAccess = new Mock(); + mockVendorClient = new Mock(); + mockLoanApplicationDataAccess = new Mock(); + mockBlobStorageAccess = new Mock(); + mockUspsVendorClient = new Mock(); + mockTokenService = new Mock(); + mockCoreService = new Mock(); + + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + var contentRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Landmark.OLA.Application")); + + var host = new HostBuilder() + .ConfigureAppConfiguration((context, config) => + { + config.Sources.Clear(); + config.SetBasePath(contentRoot); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); + config.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddEnvironmentVariables(); + }) + .ConfigureWebHost(web => + { + web.UseContentRoot(contentRoot); + web.UseEnvironment("Development"); + web.UseStartup(); + web.UseTestServer(); + web.ConfigureTestServices(s => + { + s.AddSingleton(mockBranchDataAccess.Object); + s.AddSingleton(mockConfigurationAccess.Object); + s.AddSingleton(mockVendorClient.Object); + s.AddSingleton(mockLoanApplicationDataAccess.Object); + s.AddSingleton(mockBlobStorageAccess.Object); + s.AddSingleton(mockUspsVendorClient.Object); + s.AddSingleton(mockTokenService.Object); + s.AddSingleton(mockCoreService.Object); + }); + }) + .UseSerilog((ctx, services, lc) => lc + .ReadFrom.Configuration(ctx.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext()) + .Build(); + + host.Start(); + + var server = host.GetTestServer(); + _client = server.CreateClient(); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/SAMLAssertionControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/SAMLAssertionControllerTest.cs new file mode 100644 index 0000000..1d0efd2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/SAMLAssertionControllerTest.cs @@ -0,0 +1,45 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using Xunit; +using Landmark.OLA.Models.Addresses; + +namespace Landmark.OLA.Integration.Tests +{ + public class SAMLAssertionControllerTest : OlaIntegrationTest + { + public Address testResult; + + public SAMLAssertionControllerTest() : base() + { + testResult = new Address() + { + Address1 = "111 South First Street", + City = "Milwaukee", + State = Models.Addresses.Enums.StateOption.WI, + PostalCode = "53207", + Country = Models.Addresses.Enums.CountryOption.UnitedStates + }; + mockUspsVendorClient.Setup(repo => repo.CompareAddress(It.IsAny
())) + .Returns(Task.FromResult(testResult)); + } + + [Fact] + public async Task CanInitAnonymousSession() + { + // Arrange + + // Act + HttpResponseMessage httpResponse = await _client.GetAsync("/api/SAMLAssertionConsumer/InitSession"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + + Assert.Equal("", stringResponse); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/StatusControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/StatusControllerTest.cs new file mode 100644 index 0000000..04cbb33 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/StatusControllerTest.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; +using Landmark.OLA.Application; + +namespace Landmark.OLA.Integration.Tests +{ + public class StatusControllerTest : OlaIntegrationTest + { + + public StatusControllerTest(): base() + { + } + + [Fact] + public async Task CanGetStatus() + { + // The endpoint or route of the controller action. + var httpResponse = await _client.GetAsync("/api/status/api/status"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var status = JsonConvert.DeserializeObject(stringResponse); + Assert.Equal("1.0.0.0", status.BuildVersion); + } +/* + [Fact] + public async Task CanGetRoot() + { + // The endpoint or route of the controller action. + var httpResponse = await _client.GetAsync("/"); + + // Must be successful. + Assert.Equal(HttpStatusCode.Redirect, httpResponse.StatusCode); + + } + */ + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/UISettingsControllerTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/UISettingsControllerTest.cs new file mode 100644 index 0000000..7175636 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/UISettingsControllerTest.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; +using Landmark.OLA.Application.Common.Domain; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Integration.Tests +{ + + public class UISettingsControllerTest : OlaIntegrationTest + { + private readonly BranchConfig _branchConfig; + private readonly BranchNameConfig _branchNameConfig; + private readonly StoredSettings _storedSettings; + + public UISettingsControllerTest() : base() + { + _branchConfig = new BranchConfig + { + Id = "012345678901234567890123456789012345" + }; + _branchNameConfig = new BranchNameConfig + { + Id = "912345678901234567890123456789012345" + }; + _storedSettings = new StoredSettings + { + Id = "123456789012345678901234567890123456" + }; + + mockBranchDataAccess.Setup(repo => repo.GetBranchConfig()).Returns(_branchConfig); + mockBranchDataAccess.Setup(repo => repo.GetBranchNameConfig()).Returns(_branchNameConfig); + mockConfigurationAccess.Setup(repo => repo.GetStoredSettings()).Returns(_storedSettings); + } + + [Fact] + public async Task CanGetSettings() + { + // The endpoint or route of the controller action. + var httpResponse = await _client.GetAsync("/api/UISettings"); + + // Must be successful. + httpResponse.EnsureSuccessStatusCode(); + + // Deserialize and examine results. + var stringResponse = await httpResponse.Content.ReadAsStringAsync(); + var settings = JsonConvert.DeserializeObject(stringResponse); + Assert.NotNull(settings.BranchMapping); + Assert.NotNull(settings.BranchNameMapping); + Assert.Equal(_branchConfig.Id, settings.BranchMapping.Id); + Assert.Equal(_branchNameConfig.Id, settings.BranchNameMapping.Id); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/additional_test_args.runsettings b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/additional_test_args.runsettings new file mode 100644 index 0000000..c6fe146 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/additional_test_args.runsettings @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + .*microsoft.* + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Logging/Landmark.OLA.Logging.csproj b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Logging/Landmark.OLA.Logging.csproj new file mode 100644 index 0000000..6dc95df --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Logging/Landmark.OLA.Logging.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.1 + + + + + + + + + + + diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Address.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Address.cs new file mode 100644 index 0000000..4aa71fd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Address.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses.Enums; + +namespace Landmark.OLA.Models.Addresses +{ + public class Address : IAddress + { + [Required] + [StringLength(40)] + public string Address1 { get; set; } + + [StringLength(40)] + public string Address2 { get; set; } + + [Required] + [StringLength(100)] + public string City { get; set; } + + [Required] + //Nullable so that there is no default value + public StateOption? State { get; set; } + + [Required] + [StringLength(5, MinimumLength = 5)] + public string PostalCode { get; set; } + + [StringLength(4, MinimumLength = 4)] + public string PostalCodeExt { get; set; } + + [Required] + //Nullable so that there is no default value. + public CountryOption? Country { get; set; } + + public Guid Id { get; private set; } + + public Address() + { + Id = Guid.NewGuid(); + } + + public Address(Guid id) + { + Id = id; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/CountryOption.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/CountryOption.cs new file mode 100644 index 0000000..f6a8a48 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/CountryOption.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models.Addresses.Enums +{ + public enum CountryOption + { + //only value allowed by CRIF + UnitedStates, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/ResidenceTypeOption.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/ResidenceTypeOption.cs new file mode 100644 index 0000000..e2b9927 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/ResidenceTypeOption.cs @@ -0,0 +1,9 @@ +namespace Landmark.OLA.Models.Addresses.Enums +{ + public enum ResidenceTypeOption + { + Own, + Rent, + Other, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/StateOption.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/StateOption.cs new file mode 100644 index 0000000..de03bc0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/StateOption.cs @@ -0,0 +1,71 @@ +namespace Landmark.OLA.Models.Addresses.Enums +{ + /// + /// StateEnum values from CRIF's commonSchema.xsd + /// + public enum StateOption + { + AA, + AE, + AK, + AL, + AP, + AR, + AS, + AZ, + CA, + CO, + CT, + DC, + DE, + FL, + //FM, //Present in XSD but not available in CRIF's v_RptGeoCode_v001 table + GA, + GU, + HI, + IA, + ID, + IL, + IN, + KS, + KY, + LA, + MA, + MD, + ME, + //MH, //Present in XSD but not available in CRIF's v_RptGeoCode_v001 table + MI, + MN, + MO, + MP, + MS, + MT, + NC, + ND, + NE, + NH, + NJ, + NM, + NV, + NY, + OH, + OK, + OR, + PA, + PR, + PW, + RI, + SC, + SD, + TN, + TX, + UT, + VA, + VI, + VT, + WA, + WI, + WV, + WY, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/IAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/IAddress.cs new file mode 100644 index 0000000..6124866 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/IAddress.cs @@ -0,0 +1,21 @@ +using Landmark.OLA.Models.Addresses.Enums; + +namespace Landmark.OLA.Models.Addresses +{ + public interface IAddress : IReferenceable + { + string Address1 { get; set; } + + string Address2 { get; set; } + + string City { get; set; } + + StateOption? State { get; set; } + + string PostalCode { get; set; } + + string PostalCodeExt { get; set; } + + CountryOption? Country { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/PropertyLot.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/PropertyLot.cs new file mode 100644 index 0000000..7296844 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/PropertyLot.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models.Addresses +{ + public class PropertyLot : Address + { + [Required] + public bool? IsRented { get; set; } + + [Range(minimum: 0, maximum: 1000000000)] + public decimal? RentalAmt { get; set; } + + [Range(minimum: 0, maximum: 1000000000)] + public decimal? AnnualPropertyTax { get; set; } + + //TODO #399 + // public LienPositionEnum lienPositionID { get; set; } + + public PropertyLot() + : base() + { + } + + public PropertyLot(Guid id) + : base(id) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Validators/AddressValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Validators/AddressValidator.cs new file mode 100644 index 0000000..d305b0c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Validators/AddressValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; + +namespace Landmark.OLA.Models.Addresses.Validators +{ + public class AddressValidator : AbstractValidator
+ { + public AddressValidator() + { + RuleFor(x => x.Address1).NotNull(); + RuleFor(x => x.City).NotNull(); + RuleFor(x => x.State).NotNull(); + RuleFor(x => x.PostalCode).NotNull(); + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ApplicationControlConfiguration.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ApplicationControlConfiguration.cs new file mode 100644 index 0000000..5c04d1a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ApplicationControlConfiguration.cs @@ -0,0 +1,10 @@ +namespace Landmark.OLA.Models +{ + public class ApplicationControlConfiguration + { + public bool NotifyLogging { get; set; } + public string HomePageUrl { get; set; } + public string ImportantTermsPdfFilePath { get; set; } + public string WhatYouShouldKnowPdfFilePath { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/LienPositionType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/LienPositionType.cs new file mode 100644 index 0000000..ac60122 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/LienPositionType.cs @@ -0,0 +1,11 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum LienPositionType + { + First, + Second, + Third, + Fourth, + Fifth + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/ManufacturedHomeType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/ManufacturedHomeType.cs new file mode 100644 index 0000000..326bdfd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/ManufacturedHomeType.cs @@ -0,0 +1,10 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum ManufacturedHomeType + { + Single, + Double, + Triple, + Quad, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/NewUsed.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/NewUsed.cs new file mode 100644 index 0000000..559cfe1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/NewUsed.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum NewUsed + { + New, + Used, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/PropertyUse.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/PropertyUse.cs new file mode 100644 index 0000000..cd3c93e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/PropertyUse.cs @@ -0,0 +1,14 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + /// + /// OwnerOccupiedTypeEnum in CRIF's commonSchema.xsd + /// + public enum PropertyUse + { + OwnerOccupied, + SecondHome, + Rental, + VacationHome, + Vacant + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/RealEstateType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/RealEstateType.cs new file mode 100644 index 0000000..a1f8e2b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/RealEstateType.cs @@ -0,0 +1,19 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + /// + /// Derived from PropertySubTypeEnum in CRIF's commonSchema.xsd + /// + public enum RealEstateType + { + Family2_4, //1-4 Family + OneUnit, + TwoUnit, + ThreeUnit, + FourUnit, + Condo, + CoOp, + Other, + SingleFamily, + Townhouse + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/UsageType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/UsageType.cs new file mode 100644 index 0000000..67ecf2f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/UsageType.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum UsageType + { + Personal, + Business, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleSeller.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleSeller.cs new file mode 100644 index 0000000..3f9f50f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleSeller.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum VehicleSeller + { + Dealer, + Private_Party, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleType.cs new file mode 100644 index 0000000..c144cfb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleType.cs @@ -0,0 +1,14 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + public enum VehicleType + { + Automobile, + Motorcycle, + Boat, + ATV, + PWC, + RecreationalVehicle, //This is *not* a motorhome/camper, but is instead things like golf carts. + Snowmobile, + TrailerMotorHome, //This *does* include a traditional RV motorohome. + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleValuationType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleValuationType.cs new file mode 100644 index 0000000..2c80ef9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleValuationType.cs @@ -0,0 +1,14 @@ +namespace Landmark.OLA.Models.Assets.Enums +{ + //CRIF's ValueTypeEnum from commonSchema.xsd + public enum VehicleValuationType + { + Invoice, + Loan, + MSRP, + Retail, + TradeIn, + Wholesale, + Manual, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/LienPosition.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/LienPosition.cs new file mode 100644 index 0000000..87c32ef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/LienPosition.cs @@ -0,0 +1,14 @@ + +namespace Landmark.OLA.Models.Assets +{ + public class LienPosition : OlaModel + { + public bool HasFirstMortgage { get; set; } + + public bool HasSecondMortgage { get; set; } + + public bool PayoffFirstMortgage { get; set; } + + public bool PayoffSecondMortgage { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/ManufacturedHome.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/ManufacturedHome.cs new file mode 100644 index 0000000..1a48547 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/ManufacturedHome.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Assets.Enums; + +namespace Landmark.OLA.Models.Assets +{ + public class ManufacturedHome : RealEstateBase + { + [Required] + public ManufacturedHomeType? Type { get; set; } + + [Required] + public PropertyLot Lot { get; set; } + + //Slightly arbitrary values that should be safe + [Range(0, 1000000000)] + public decimal? EstimatedLandValuation { get; set; } + + [Required] + [Range(minimum: 0, maximum: 1000000000)] + public decimal EstimatedHomeValuation { get; set; } + + public ManufacturedHome(Guid? manufacturedHomeId = null) + :base(manufacturedHomeId) + { + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstate.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstate.cs new file mode 100644 index 0000000..9c31e69 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstate.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Assets.Enums; + +namespace Landmark.OLA.Models.Assets +{ + public class RealEstate : RealEstateBase + { + public Address RealEstateAddress { get; set; } + + [Required] + public RealEstateType? Type { get; set; } + + public RealEstate(Guid? realEstateId = null) + : base(realEstateBaseId: realEstateId) + { + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstateBase.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstateBase.cs new file mode 100644 index 0000000..0cab59f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstateBase.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Assets.Enums; + +namespace Landmark.OLA.Models.Assets +{ + public class RealEstateBase : OlaModel + { + //Slightly arbitrary values that should be safe + [Range(0, 1000000000)] + public decimal InitialEstimatedValue { get; set; } + + [Required] + public PropertyUse? Use { get; set; } + + public bool? HasSpa { get; set; } + + public bool? HasPool { get; set; } + + public bool? HasBasement { get; set; } + + public bool? HasCentralAir { get; set; } + + public bool? HasGarage { get; set; } + + public bool? InFloodZone { get; set; } + + public bool? InEquity { get; set; } + + public bool? IsPropertyHeldInTrust { get; set; } + + public bool IsCollateral { get; set; } = true; + + public bool IsTradeIn { get; set; } + + public LienPosition LienPosition { get; set; } + + [Required] + public bool? CurrentlyListedForSale { get; set; } + + [Required] + public bool? CurrentlyBeingRemodeledOrImproved { get; set; } + + public RealEstateBase(Guid? realEstateBaseId = null) + { + Id = realEstateBaseId.HasValue ? realEstateBaseId.Value.ToString() : null; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Vehicle.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Vehicle.cs new file mode 100644 index 0000000..eef76de --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Vehicle.cs @@ -0,0 +1,123 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Assets.Enums; + +namespace Landmark.OLA.Models.Assets +{ + public class Vehicle : OlaModel + { + //This will either hold an ToString'ed int or a 36-char long GUID + [StringLength(36)] + public string ValuationId { get; private set; } + + [Required] + public VehicleValuationType? ValuationType { get; set; } + + [Required] + public NewUsed? Condition { get; set; } + + public VehicleSeller? Seller { get; set; } + + [Required] + public VehicleType? Type { get; set; } + + public UsageType? Usage { get; set; } + + public bool? HasClearTitle { get; set; } + + [Required] + [Range(minimum: 0, maximum: 1000000000)] + public decimal? InitialEstimatedValue { get; set; } + + public bool? IsForBusinessUse { get; set; } + + public bool? IsOwnerOperator { get; set; } + + [Required] + //Slightly arbitrary values that should be safe + [Range(1900, 3000)] + public int? Year { get; set; } + + //Slightly arbitrary values that should be safe + [Range(0, 1000000000)] + public int? Mileage { get; set; } + + [Required] + [StringLength(100)] + public string Make { get; set; } + + [Required] + [StringLength(100)] + public string Model { get; set; } + + [StringLength(20)] + public string Color { get; set; } + + [StringLength(400)] + public string Body { get; set; } + + [StringLength(100)] + public string Series { get; set; } + + [StringLength(100)] + public string Style { get; set; } + + [StringLength(400)] + public string Trim { get; set; } + + [StringLength(60)] + public string VIN { get; set; } + + [StringLength(8)] + public string LicensePlateNumber { get; set; } + + public string StateTitled { get; set; } + + [StringLength(20)] + public string CountyTitled { get; set; } + + [StringLength(120)] + public string MunicipalityType { get; set; } + + [StringLength(120)] + public string MunicipalityWhereKept { get; set; } + + public string Lien { get; set; } + + [StringLength(120)] + public string DealerName { get; set; } + + [StringLength(180)] + public string DealerPhone { get; set; } + + public Address BoatAddress { get; set; } + + [Range(0, 1000000000000000)] + public decimal? BoatLength { get; set; } + + [Range(1900, 3000)] + public int? TrailerYear { get; set; } + + [StringLength(60)] + public string TrailerMake { get; set; } + + public string TrailerType { get; set; } + + [Range(1900, 3000)] + public int? MotorYear { get; set; } + + [StringLength(60)] + public string MotorMake { get; set; } + + [StringLength(200)] + public string MotorModel { get; set; } + + public int? MotorType { get; set; } + + public Vehicle(Guid? valuationId = null, Guid? vehicleId = null, Guid? sellerId = null) + { + ValuationId = valuationId.HasValue ? valuationId.ToString() : Guid.NewGuid().ToString(); + Id = vehicleId.HasValue ? vehicleId.ToString() : null; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ClientError.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ClientError.cs new file mode 100644 index 0000000..254b738 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ClientError.cs @@ -0,0 +1,10 @@ + +namespace Landmark.OLA.Models +{ + public class ClientError + { + public string Message { get; set; } + public string Name { get; set; } + public string StackTrace { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreAddress.cs new file mode 100644 index 0000000..623defe --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreAddress.cs @@ -0,0 +1,24 @@ + +namespace Landmark.OLA.Models.Core +{ + public class CoreAddress + { + public int AddressId { get; set; } + public string Line1 { get; set; } + public string Line2 { get; set; } + public string Line3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string ZipCode { get; set; } + public string Country { get; set; } + public string CountyName { get; set; } + public string AddressType { get; set; } + public bool IsPrimary { get; set; } + public bool IsInternational { get; set; } + public int AddressVerification { get; set; } + public string Occupancy { get; set; } + public System.DateTime StartDate { get; set; } + public System.DateTime EndDate { get; set; } + public bool UspsVerified { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreIndividual.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreIndividual.cs new file mode 100644 index 0000000..0442a7b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreIndividual.cs @@ -0,0 +1,40 @@ + +namespace Landmark.OLA.Models.Core +{ + public class CoreIndividual + { + public int IndividualId { get; set; } + public string IndividualType { get; set; } + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string NameSuffix { get; set; } + public string TaxId { get; set; } + public string Gender { get; set; } + public System.DateTime BirthDate { get; set; } + public int Branch { get; set; } + public string DriversLicense { get; set; } + public string DriversLicenseState { get; set; } + public string Ident { get; set; } + public string PreferredContact { get; set; } + public string MaritalStatus { get; set; } + public bool IsUSCitizen { get; set; } + public int PrimaryAddressId { get; set; } + public bool IsEmployee { get; set; } + public int EmployeeNumber { get; set; } + public string CompanyName { get; set; } + public string EmployerName { get; set; } + public int ApplicationCountLast90Days { get; set; } + public int ApplicationsDeclinedLast90Days { get; set; } + public int ApplicationsApprovedLast90Days { get; set; } + public List Address { get; set; } + public string Email1 { get; set; } + public string Email2 { get; set; } + public List Phone { get; set; } + public string Status { get; set; } + public bool IsHomeOwner { get; set; } + public bool IsHeadOfHouseHold { get; set; } + public string Language { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreMemberOwner.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreMemberOwner.cs new file mode 100644 index 0000000..c85e1c8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreMemberOwner.cs @@ -0,0 +1,14 @@ + +namespace Landmark.OLA.Models.Core +{ + public class CoreMemberOwner + { + public int IndividualId { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Address { get; set; } + public string TaxId { get; set; } + public string DriverLicense { get; set; } + public string BirthDate { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CorePhone.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CorePhone.cs new file mode 100644 index 0000000..6412ca1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CorePhone.cs @@ -0,0 +1,14 @@ + +namespace Landmark.OLA.Models.Core +{ + public class CorePhone + { + public string PhoneKey { get; set; } + public bool IsPrimaryForType { get; set; } + public bool IsInternational { get; set; } + public string CountryCode { get; set; } + public string AreaCode { get; set; } + public string PhoneNumber { get; set; } + public string Extension { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/MemberMeta.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/MemberMeta.cs new file mode 100644 index 0000000..cfbb0a8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/MemberMeta.cs @@ -0,0 +1,10 @@ + +namespace Landmark.OLA.Models.Core +{ + public class MemberMeta + { + public bool IsMember { get; set; } + public bool IsIndividual { get; set; } + public bool IsTwoFactor { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/DecisioningDetails.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/DecisioningDetails.cs new file mode 100644 index 0000000..6f6194a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/DecisioningDetails.cs @@ -0,0 +1,36 @@ +using Landmark.OLA.Models.Decisioning.Enums; + +namespace Landmark.OLA.Models.Decisioning +{ + + //TODO: At some point the LoanApplication class should probably have this class as a property. Might need to wait for parsing. + public class DecisioningDetails + { + public DecisioningState DecisioningState { get; set; } + public DecisioningResult? DecisioningResult { get; set; } + + public ICollection Stipulations { get; private set; } + + public DecisioningDetails(ICollection stipulations = null) + { + Stipulations = stipulations ?? new List(); + } + + public override bool Equals(object obj) + { + if (obj.GetType() != this.GetType()) + return false; + + var compare = (DecisioningDetails)obj; + + return DecisioningState.Equals(compare.DecisioningState) + && DecisioningResult.Equals(compare.DecisioningResult) + && Stipulations.SequenceEqual(compare.Stipulations); + } + + public override int GetHashCode() + { + return HashCode.Combine(DecisioningState, DecisioningResult, Stipulations); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningResult.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningResult.cs new file mode 100644 index 0000000..9835a76 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningResult.cs @@ -0,0 +1,163 @@ +namespace Landmark.OLA.Models.Decisioning.Enums +{ + /// + /// StatusEnum values from CRIF's commonSchema.xsd + /// + public enum DecisioningResult + { + NoneSelected, + DecisionStatus_Approved, + DecisionStatus_Counteroffer, + DecisionStatus_Review, + DecisionStatus_Declined, + DecisionStatus_Cancelled, + DecisionStatus_Withdrawn, + DecisionStatus_Aprvd_RejectedbyApplicant, + DecisionStatus_CounterofferRejectedbyApplicant, + WorkflowStatus_DataAcquisition, + WorkflowStatus_LoanProcessing, + WorkflowStatus_BookingFulfillment, + ProcessStatus_Incomplete, + ProcessStatus_Complete, + ProcessStatus_MissingInformation, + ResourceStatus_MissingInformation, + ResourceStatus_RequiredInformation, + WorkflowStatus_Decisioning, + RiskManagerServiceRequestStatus_PendingHistory, + RiskManagerServiceRequestStatus_HistoryComplete, + RiskManagerServiceRequestStatus_PendingEvaluation, + RiskManagerServiceRequestStatus_EvaluationComplete, + RiskManagerServiceRequestStatus_ReplacedbyRiskManager_HistoryRequest, + StipulationStatus_Open, + StipulationStatus_Waive, + TitleExceptionStatus_Open, + TitleExceptionStatus_Satisfy, + TitleExceptionStatus_Waive, + StipulationStatus_Satisfied, + DisbursementStatus_Pending, + DisbursementStatus_Completed, + DisbursementStatus_Voided, + DisbursementStatus_Deleted, + ApplicationStatus_Pending, + ApplicationStatus_Complete, + MasterApplicationStatus_Signed, + MasterApplicationStatus_NotSigned, + LoanDocumentStatus_PendingGeneration, + LoanDocumentStatus_Generated, + DecisionStatus_Pending, + MasterApplicationStatus_Expired, + MasterApplicationStatus_Cancelled, + ApplicationStatus_Cancelled, + ApplicationExtractStatus_Pending, + ApplicationExtractStatus_Completed, + RiskManagerServiceRequestStatus_PendingDecisioning, + RiskManagerServiceRequestStatus_DecisioningComplete, + LoanDocumentStatus_Viewed, + LoanDocumentStatus_Error, + ContractStatus_Contracted, + ContractStatus_Funded, + ContractStatus_Unfunded, + ContractStatus_ReturntoChannel, + RiskManagerServiceRequestStatus_CompletewithError, + CollateralStatus_Active, + CollateralStatus_Withdrawn, + RecommendedStatus_Approved, + RecommendedStatus_Counteroffer, + RecommendedStatus_Declined, + RiskManagerServiceRequestStatus_NoDiscreteMatch, + RiskManagerServiceRequestStatus_ProcesswithoutMatch, + FileStatus_Deleted, + StipulationStatus_Ignore, + RuleStatus_Pass, + RuleStatus_Fail, + RuleStatus_Overriden, + ContractReviewStatus_Accept, + ContractReviewStatus_Counteroffer, + ContractReviewStatus_ReturntoChannel, + DecisionStatus_Incomplete, + IncomeStatus_Included, + IncomeStatus_Excluded, + DisclosureLetterStatus_Pending, + DisclosureLetterStatus_Printed, + DisclosureLetterStatus_Suppressed, + DisclosureLetterStatus_Failed, + NotificationStatus_Pending, + NotificationStatus_Completed, + NotificationStatus_Failed, + NotificationStatus_Previewed, + AcceptanceStatus_Provisional, + AcceptanceStatus_Current, + AcceptanceStatus_Rejected, + AcceptanceStatus_Cancelled, + AcceptanceStatus_Withdrawn, + SettlementServiceRequestStatus_Submitted, + SettlementServiceRequestStatus_CompletedReceived, + SettlementServiceRequestStatus_Cancelled, + SettlementServiceRequestStatus_Reviewed, + SettlementServiceRequestStatus_Failed, + SettlementServiceRequestStatus_CancellationRequest, + SettlementServiceRequestStatus_Accepted, + SettlementServiceRequestStatus_Incomplete, + SettlementServiceRequestStatus_Pending, + SettlementServiceRequestStatus_NoHit, + SettlementServiceRequestStatus_NoHitPendingResearch, + DisbursTrans_Printed, + DisbursTrans_Reprinted, + Contract_Unfunded, + History_Pending, + History_NoneRequested, + History_Error, + History_NoRecord, + History_Success, + LoanDoc_Processing, + LoanDoc_Cancelled, + HistoryStatus_Pending, + HistoryStatus_NoneRequested, + HistoryStatus_Error, + HistoryStatus_NoRecord, + HistoryStatus_Success, + LoanDocumentStatus_Processing, + LoanDocumentStatus_Cancelled, + LoanDocumentStatus_NonApplicable, + NotificationStatus_NonApplicable, + ChannelStatisticStatus_Entered, + ChannelStatisticStatus_Approved, + ChannelStatisticStatus_Other, + ChannelStatisticStatus_Counteroffer, + ChannelStatisticStatus_Declined, + ChannelStatisticStatus_Review, + ChannelStatisticStatus_Booked, + PrescreenStatus_PrescreenPass, + PrescreenStatus_PrescreenFail, + RiskManagerServiceRequestStatus_PrescreenCancel, + RiskManagerServiceRequestStatus_ChexSystemsCancel, + ACHExported, + ApplicantAcceptance, + ProcessStatus_Failed, + DocInterfaceStatus_Pending, + DocInterfaceStatus_Completed, + DocInterfaceStatus_Failed, + AFPApplicationReceived, + AFPApplicationDecisioned, + AFPApplicationFunded, + AFPApplicationWithdrawn, + IncompleteApplicationStatus_Pending, + IncompleteApplicationStatus_Cancelled, + eDocsRescanRequested, + eDocsReturnToDealerRequested, + eDocsImported, + eDocsPendingImport, + eDocsArchived, + eDocsReturnedToDealer, + eDocsRescanRequestFailed, + eDocsReturnToDealerRequestFailed, + eDocsPurged, + EContractingStatus_Verified, + EContractingStatus_VerificationError, + EContractingStatus_ContractImported, + EContractingStatus_EContractingError, + EContractingStatus_EContractingCancelled, + MultiOffer, + MultiOfferAccepted, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningState.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningState.cs new file mode 100644 index 0000000..983c201 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningState.cs @@ -0,0 +1,11 @@ +namespace Landmark.OLA.Models.Decisioning.Enums +{ + public enum DecisioningState + { + NotStarted, + Unknown, + Requested, + Ready, + MissingRequiredInformation + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Hmda.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Hmda.cs new file mode 100644 index 0000000..6d6fdf4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Hmda.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.HomeMortgageDisclosureAct +{ + public class Hmda + { + public ICollection Races { get; set; } + + [StringLength(100)] + public string AmericanIndianAlaskaNativeTribe { get; set; } + + [StringLength(100)] + public string OtherAsianRace { get; set; } + + [StringLength(100)] + public string OtherPacificIslanderRace { get; set; } + + public ICollection Ethnicities { get; set; } + + [StringLength(100)] + public string OtherHispanicLatinoEthnicity { get; set; } + + public List Sexes { get; set; } + + public bool? HmdaNoticeDisclosed { get; set; } + + public Hmda() + { + Races = []; + Ethnicities = []; + Sexes = []; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Validators/HmdaValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Validators/HmdaValidator.cs new file mode 100644 index 0000000..caf4d20 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Validators/HmdaValidator.cs @@ -0,0 +1,32 @@ +using FluentValidation; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.HomeMortgageDisclosureAct.Validators +{ + public class HmdaValidator : AbstractValidator + { + public HmdaValidator() + { + RuleFor(x => x.Races).NotEmpty() + .WithMessage($"No race information was provided. Set {Race.NotProvided} if none will be provided."); + RuleFor(x => x.Races).Must(races => races.Contains(Race.AmericanIndianAlaskaNative)) + .When(x => !string.IsNullOrWhiteSpace(x.AmericanIndianAlaskaNativeTribe)) + .WithMessage($"Include {Race.AmericanIndianAlaskaNative} in Races when a value is set in AmericanIndianAlaskaNativeTribe."); + RuleFor(x => x.Races).Must(races => races.Contains(Race.OtherAsian)) + .When(x => !string.IsNullOrWhiteSpace(x.OtherAsianRace)) + .WithMessage($"Include {Race.OtherAsian} in Races when a value is set in OtherAsianRace."); + RuleFor(x => x.Races).Must(races => races.Contains(Race.OtherPacificIslander)) + .When(x => !string.IsNullOrWhiteSpace(x.OtherPacificIslanderRace)) + .WithMessage($"Include {Race.OtherPacificIslander} in Races when a value is set in OtherPacificIslanderRace."); + + RuleFor(x => x.Ethnicities).NotEmpty() + .WithMessage($"No ethnicity information was provided. Set {Ethnicity.NotProvided} if none will be provided."); + RuleFor(x => x.Ethnicities).Must(ethnicities => ethnicities.Contains(Ethnicity.OtherHispanicLatino)) + .When(x => !string.IsNullOrWhiteSpace(x.OtherHispanicLatinoEthnicity)) + .WithMessage($"Include {Ethnicity.OtherHispanicLatino} in Ethnicities when a value is set in OtherHispanicLatinoEthnicity."); + + RuleFor(x => x.Sexes).NotEmpty() + .WithMessage($"No sex information was provided. Set {Sex.NotProvided} if none will be provided."); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/IReferenceable.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/IReferenceable.cs new file mode 100644 index 0000000..f6ada53 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/IReferenceable.cs @@ -0,0 +1,8 @@ + +namespace Landmark.OLA.Models +{ + public interface IReferenceable + { + Guid Id { get; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/CoApplicant.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/CoApplicant.cs new file mode 100644 index 0000000..793e786 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/CoApplicant.cs @@ -0,0 +1,20 @@ + +namespace Landmark.OLA.Models.Individuals +{ + public class CoApplicant : Individual + { + public bool IsMarriedToPrimaryBorrower { get; set; } + + public CoApplicant() + : base() + { + + } + + public CoApplicant(Guid id) + : base(id) + { + + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Employer.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Employer.cs new file mode 100644 index 0000000..a51ee68 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Employer.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.Individuals +{ + public class Employer : OlaModel + { + [Required] + [StringLength(60, MinimumLength = 1)] + //Due to downstream CRIF issues, applications with employer names containing special characters will be denied + [RegularExpression(@"[a-zA-Z0-9 ]*")] + public string Name { get; set; } + + [Required] + //Nullable so that there is no default value + public bool? CurrentEmployer { get; set; } // "isCurrentFL" + + [Required] + //Nullable so that there is no default value + public EmploymentTypeOption EmploymentType { get; set; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string Title { get; set; } + + [Required] + public Income Income { get; set; } + + //[Required] + //The current front end offers an upper limit of 10 years, 11 months. + [Range(0, 130)] + public int MonthsEmployed { get; set; } + + //[Required] + //Nullable so that there is no default value + public DateTime? StartDate { get; set; } + + // Required if CurrentEmployer == false + //Nullable so that there is no default value + public DateTime? EndDate { get; set; } + + [MaxLength(180)] + public string WorkPhone { get; set; } + + [Required] + //Nullable so that there is no default value + public LanguageOption? Language; + + public Employer(Guid? employerId = null) + { + Id = employerId.HasValue ? employerId.Value.ToString() : null; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/EmploymentTypeOption.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/EmploymentTypeOption.cs new file mode 100644 index 0000000..fd4bd17 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/EmploymentTypeOption.cs @@ -0,0 +1,13 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + public enum EmploymentTypeOption + { + NoneSelected, + Contract, + FullTime, + PartTime, + SelfEmployed, + Retired, + Temporary + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Ethnicity.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Ethnicity.cs new file mode 100644 index 0000000..596967e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Ethnicity.cs @@ -0,0 +1,16 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + /// + /// EthnicityEnum from CRIF's commonSchema.xsd trimmed down to HMDA form options + /// + public enum Ethnicity + { + HispanicOrLatino, + NotHispanicOrLatino, + Mexican, + PuertoRican, + Cuban, + OtherHispanicLatino, + NotProvided + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Frequency.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Frequency.cs new file mode 100644 index 0000000..a2c8b2c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Frequency.cs @@ -0,0 +1,13 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + public enum Frequency + { + Annually, + SemiAnnually, + Monthly, + SemiMonthly, + TwoWeeks, + Weekly, + Hourly + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/LanguageOption.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/LanguageOption.cs new file mode 100644 index 0000000..e5f36c3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/LanguageOption.cs @@ -0,0 +1,9 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + public enum LanguageOption + { + //A language value is required by the CRIF API + //TODO #229: Accept other values here? + English, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/PhoneType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/PhoneType.cs new file mode 100644 index 0000000..62fb057 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/PhoneType.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + public enum PhoneType + { + Mobile, + Home, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Race.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Race.cs new file mode 100644 index 0000000..4d0a815 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Race.cs @@ -0,0 +1,26 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + /// + /// RaceEnum from CRIF's commonSchema.xsd trimmed down to HMDA form options + /// + public enum Race + { + AmericanIndianAlaskaNative, + Asian, + NativeHawaiian, + BlackOrAfricanAmerican, + White, + AsianIndian, + Chinese, + Filipino, + Japanese, + Korean, + Vietnamese, + OtherAsian, + Hawaiian, + GuamanianChamorro, + Samoan, + OtherPacificIslander, + NotProvided + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Sex.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Sex.cs new file mode 100644 index 0000000..6ac6ba7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Sex.cs @@ -0,0 +1,12 @@ +namespace Landmark.OLA.Models.Individuals.Enums +{ + /// + /// Values from HMDA form + /// + public enum Sex + { + Female, + Male, + NotProvided, + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Suffix.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Suffix.cs new file mode 100644 index 0000000..9dbb070 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Suffix.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Models.Individuals.Enums +{ + public enum Suffix + { + II, + III, + IV, + V, + Jr, + Sr + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/SuffixMap.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/SuffixMap.cs new file mode 100644 index 0000000..1621098 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/SuffixMap.cs @@ -0,0 +1,45 @@ + +namespace Landmark.OLA.Models.Individuals.Enums +{ + + public class SuffixType + { + public string CD { get; set; } + public string Name { get; set; } + public Suffix SuffixEnum { get; set; } + public SuffixType(string cd, string name, Suffix suffix) + { + SuffixEnum = suffix; + CD = cd; + Name = name; + } + } + + public sealed class SuffixMap + { + public SuffixMap() {} + + public static readonly SuffixType II = new("2", "II", Suffix.II); + public static readonly SuffixType III = new("3", "III", Suffix.III); + public static readonly SuffixType IV = new("4", "IV", Suffix.IV); + public static readonly SuffixType V = new("5", "V", Suffix.V); + public static readonly SuffixType Jr = new("J", "Jr.", Suffix.Jr); + public static readonly SuffixType Sr = new("S", "Sr.", Suffix.Sr); + + public static readonly SuffixType[] map = [II, III, IV, V, Jr, Sr]; + + public static SuffixType GetSuffixType(Suffix? suffix) + { + SuffixType match = null; + foreach (SuffixType suff in map) + { + if (suff.SuffixEnum == suffix) + { + match = suff; + } + } + + return match; + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/HistoryIndividual.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/HistoryIndividual.cs new file mode 100644 index 0000000..dac8e08 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/HistoryIndividual.cs @@ -0,0 +1,101 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Residences; + +namespace Landmark.OLA.Models.Individuals +{ + public class HistoryIndividual : IIndividualHistory + { + public Guid Id { get; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string FirstName { get; set; } + + [StringLength(1, MinimumLength = 0)] + [RegularExpression(@"[a-zA-Z]")] + public string MiddleInitial { get; set; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string LastName { get; set; } + + public Suffix? Suffix { get; set; } + + [Required] + public CurrentResidenceAddress Address { get; set; } + + public Address MailingAddress { get; set; } + + [Required] + //Nullable so that there is no default value + public LanguageOption? Language { get; set; } + + //A value is required for decisioning. May require input from user in the future. + [StringLength(60)] + public string CustomerNumber { get; set; } = "0"; + + [Required] + //Nullable so that there is no default value + public DateTime? DOB { get; set; } + + [Required] + [StringLength(9, MinimumLength = 9)] + [RegularExpression(@"[0-9]+")] + public string SSN { get; set; } + + [Required] + [StringLength(180, MinimumLength = 10)] + //CRIF accepts up to 180 characters. + //Dashes, periods, and parentheses are accepted but ignored. + //Numbers, letters, pound signs, and spaces are kept. + public string Phone { get; set; } + + [Required] + //Nullable so that there is no default value + public PhoneType? PhoneType { get; set; } + + [StringLength(180, MinimumLength = 10)] + public string SecondaryPhone { get; set; } + + public PhoneType? SecondaryPhoneType { get; set; } + + [Required] //Required by Landmark + [StringLength(180, MinimumLength = 3)] + [RegularExpression(@".+@.+")] + //Given that a user could enter a fake address, + //this is not exhaustive check for a valid address form + public string Email { get; set; } + + [StringLength(180)] + [RegularExpression(@".+@.+")] + public string PrimaryApplicantEmail { get; set; } + + [StringLength(180)] + [RegularExpression(@".+@.+")] + public string CoApplicantEmail { get; set; } + + //TODO #228: Enforce time requirement of total MonthsResided across Address (on IndividualBase) and PreviousAddresses + public ICollection PreviousAddresses { get; set; } + + public ICollection Employers { get; set; } + + public ICollection OtherIncomes { get; set; } + + public bool? IsLandmarkEmployee { get; set; } + + public HistoryIndividual() + { + Id = Guid.NewGuid(); + } + + public HistoryIndividual(Guid id) + { + Id = id; + } + + [Required] + public bool HasOptedOutTCPA { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IApplicant.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IApplicant.cs new file mode 100644 index 0000000..6fc0cd8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IApplicant.cs @@ -0,0 +1,17 @@ +using Landmark.OLA.Models.HomeMortgageDisclosureAct; + +namespace Landmark.OLA.Models.Individuals +{ + public interface IApplicant : IIndividualHistory + { + bool? IsMarried { get; set; } + + MailableSpouse NonApplicantSpouse { get; set; } + + HistoryIndividual DetailedNonApplicantSpouse { get; set; } + + Hmda HmdaValues { get; set; } + + bool HasAnyNonApplicantSpouse(); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/ICoApplicant.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/ICoApplicant.cs new file mode 100644 index 0000000..63010d7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/ICoApplicant.cs @@ -0,0 +1,7 @@ +namespace Landmark.OLA.Models.Individuals +{ + public interface ICoApplicant : IApplicant + { + bool IsMarriedToPrimaryBorrower { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IIndividualHistory.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IIndividualHistory.cs new file mode 100644 index 0000000..54393b9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IIndividualHistory.cs @@ -0,0 +1,49 @@ +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Residences; + +namespace Landmark.OLA.Models.Individuals +{ + public interface IIndividualHistory : IReferenceable + { + string FirstName { get; set; } + + string MiddleInitial { get; set; } + + string LastName { get; set; } + + Suffix? Suffix { get; set; } + + string SSN { get; set; } + + DateTime? DOB { get; set; } + + string Phone { get; set; } + + PhoneType? PhoneType { get; set; } + + string SecondaryPhone { get; set; } + + PhoneType? SecondaryPhoneType { get; set; } + + string Email { get; set; } + + LanguageOption? Language { get; set; } + + string CustomerNumber { get; set; } + + CurrentResidenceAddress Address { get; set; } + + ICollection PreviousAddresses { get; set; } + + Address MailingAddress { get; set; } + + ICollection Employers { get; set; } + + ICollection OtherIncomes { get; set; } + + bool? IsLandmarkEmployee { get; set; } + + bool HasOptedOutTCPA { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/INonApplicant.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/INonApplicant.cs new file mode 100644 index 0000000..fda4037 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/INonApplicant.cs @@ -0,0 +1,24 @@ +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.Individuals +{ + public interface INonApplicant : IReferenceable + { + string FirstName { get; set; } + + string MiddleInitial { get; set; } + + string LastName { get; set; } + + Address MailingAddress { get; set; } + + string SSN { get; set; } + + string Email { get; set; } + + LanguageOption? Language { get; set; } + + bool HasOptedOutTCPA { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Income.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Income.cs new file mode 100644 index 0000000..f04a6d0 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Income.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Individuals.Enums; + + +namespace Landmark.OLA.Models.Individuals +{ + public class Income + { + [Required] + //Nullable so that there is no default value + public IncomeType? IncomeTypeCD { get; set; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string SourceName { get; set; } + + // required based on type? + //Nullable so that there is no default value + public AlimonyStatus? AlimonyStatus { get; set; } + + [Required] + [Range(minimum: 0, maximum: 1000000000)] + public decimal? AmountPerPeriod { get; set; } + + [Required] + //Nullable so that there is no default value + public Frequency? Frequency { get; set; } + + [Range(0, 168)] + public int? HoursPerWeek { get; set; } + + [Required] + // Must be Excluded when part of a previous employer + //Nullable so that there is no default value + public IncomeStatus? IncomeStatus { get; set; } + + } + + public enum IncomeStatus + { + IncomeStatus_Excluded, + IncomeStatus_Included + } + + public enum IncomeType + { + Alimony, + Bonuses, + ChildSupport, + RetirementDisability, + Dividends, + Commissions, + Other, + Spouse, + RentalProperty, + Interest, + SocialSecurity, + IndividualAccount, + Salary + } + + public enum AlimonyStatus + { + CourtOrdered, + OralUnderstanding, + Written + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IncomeLimits.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IncomeLimits.cs new file mode 100644 index 0000000..f3d5adc --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IncomeLimits.cs @@ -0,0 +1,16 @@ +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.Individuals +{ + public class IncomeLimits + { + private static Limit HourLimit => new() { min = 0, max = 999 }; + private static Limit AnnualLimit => new() { min = 0, max = 1000000000 }; + + public static Dictionary Limits => new() + { + { Frequency.Hourly, HourLimit }, + { Frequency.Annually, AnnualLimit }, + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Individual.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Individual.cs new file mode 100644 index 0000000..d4e1ffa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Individual.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; + +namespace Landmark.OLA.Models.Individuals +{ + public class Individual : HistoryIndividual, IApplicant + { + [Required] + //Nullable so that there is no default value + public bool? IsMarried { get; set; } + + public MailableSpouse NonApplicantSpouse { get; set; } + + public HistoryIndividual DetailedNonApplicantSpouse { get; set; } + + public Hmda HmdaValues { get; set; } + + public Individual() + : base() + { + HmdaValues = new Hmda(); + } + + public Individual(Guid id) + : base(id) + { + HmdaValues = new Hmda(); + } + + public Individual( Hmda hmda) + : base() + { + HmdaValues = hmda; + } + + public bool HasAnyNonApplicantSpouse() + { + var hasNonApplicantSpouse = NonApplicantSpouse is not null; + var hasDetailedNonApplicantSpouse = DetailedNonApplicantSpouse is not null; + + return hasNonApplicantSpouse || hasDetailedNonApplicantSpouse; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/MailableSpouse.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/MailableSpouse.cs new file mode 100644 index 0000000..d240875 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/MailableSpouse.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.Individuals +{ + public class MailableSpouse : INonApplicant + { + public Guid Id { get; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string FirstName { get; set; } + + [StringLength(1, MinimumLength = 0)] + [RegularExpression(@"[a-zA-Z]")] + public string MiddleInitial { get; set; } + + [Required] + [StringLength(60, MinimumLength = 1)] + public string LastName { get; set; } + + [Required] + public Address MailingAddress { get; set; } + + [StringLength(9, MinimumLength = 9)] + [RegularExpression(@"[0-9]+")] + public string SSN { get; set; } + + [StringLength(180, MinimumLength = 3)] + [RegularExpression(@".+@.+")] + public string Email { get; set; } + + [Required] + //Nullable so that there is no default value + public LanguageOption? Language { get; set; } + + public MailableSpouse() + { + Id = Guid.NewGuid(); + } + + public MailableSpouse(Guid id) + { + Id = id; + } + + [Required] + public bool HasOptedOutTCPA { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/ApplicantValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/ApplicantValidator.cs new file mode 100644 index 0000000..fddc29d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/ApplicantValidator.cs @@ -0,0 +1,26 @@ +using FluentValidation; + +namespace Landmark.OLA.Models.Individuals.Validators +{ + class ApplicantValidator : AbstractValidator + { + public ApplicantValidator() + { + RuleForEach(x => x.Employers).SetValidator(new EmployerValidator()); + + When(x => x.DetailedNonApplicantSpouse is not null, () => + { + RuleForEach(x => x.DetailedNonApplicantSpouse.Employers).SetValidator(new EmployerValidator()); + }); + + When(x => x.NonApplicantSpouse is not null, () => + { + //An individual can only have a single spouse entry + RuleFor(x => x.DetailedNonApplicantSpouse).Null(); + }); + + RuleFor(x => x.SecondaryPhoneType).NotNull().When(x => x.SecondaryPhone?.Length > 0) + .WithMessage("SecondaryPhoneType must be set when a SecondaryPhone value is present."); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/EmployerValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/EmployerValidator.cs new file mode 100644 index 0000000..84f0aef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/EmployerValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Models.Individuals.Validators +{ + class EmployerValidator : AbstractValidator + { + public EmployerValidator() + { + When(x => x.Income.Frequency == Frequency.Hourly, () => + { + RuleFor(x => x.Income.HoursPerWeek).NotNull().WithMessage("Hourly employment must state hours per week."); + }); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Landmark.OLA.Models.csproj b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Landmark.OLA.Models.csproj new file mode 100644 index 0000000..7044ca4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Landmark.OLA.Models.csproj @@ -0,0 +1,20 @@ + + + false + enable + net8.0 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Limit.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Limit.cs new file mode 100644 index 0000000..15a0168 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Limit.cs @@ -0,0 +1,8 @@ +namespace Landmark.OLA.Models +{ + public struct Limit + { + public decimal min; + public decimal max; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchConfig.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchConfig.cs new file mode 100644 index 0000000..1d68a9a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchConfig.cs @@ -0,0 +1,32 @@ +using MongoDB.Bson.Serialization.Attributes; +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models.Loan +{ + + public class BranchData + { + [Required] + [StringLength(5, MinimumLength = 5, ErrorMessage = "Zipcodes must be 5 digits")] + public string Zipcode { get; set; } + + [Required] + [Range(minimum: 1, maximum: 99999999)] + public int ChannelId { get; set; } + + public BranchData(string zip, int channel) + { + this.Zipcode = zip; + this.ChannelId = channel; + } + } + + [BsonIgnoreExtraElements] + public class BranchConfig : OlaModel + { + public DateTime Date { get; set; } + + [Required] + public List BranchCodesByZip { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchMapping.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchMapping.cs new file mode 100644 index 0000000..9577f77 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchMapping.cs @@ -0,0 +1,485 @@ + + +namespace Landmark.OLA.Models.Loan +{ + public static class BranchMapping + { + + public static Dictionary BranchCodesByZip = new Dictionary + { + + {"53001", 1561599}, + {"53002", 1561599}, + {"53003", 1561245}, + {"53004", 1561591}, + {"53005", 1561255}, + {"53006", 1561657}, + {"53007", 1561255}, + {"53008", 1561255}, + {"53010", 1561599}, + {"53011", 1561599}, + {"53012", 1561591}, + {"53013", 1561591}, + {"53014", 1561657}, + {"53015", 1561241}, + {"53016", 1561665}, + {"53017", 1561664}, + {"53018", 1561267}, + {"53019", 1561599}, + {"53020", 1561599}, + {"53021", 1561604}, + {"53022", 1561288}, + {"53023", 1561288}, + {"53024", 1561591}, + {"53026", 1561591}, + {"53027", 1561662}, + {"53029", 1561267}, + {"53031", 1561599}, + {"53032", 1561657}, + {"53033", 1561664}, + {"53034", 1561665}, + {"53035", 1561657}, + {"53036", 1561284}, + {"53037", 1561604}, + {"53038", 1561284}, + {"53039", 1561665}, + {"53040", 1561599}, + {"53042", 1561599}, + {"53044", 1561599}, + {"53045", 1561588}, + {"53046", 1561265}, + {"53047", 1561284}, + {"53048", 1561662}, + {"53049", 1561657}, + {"53050", 1561657}, + {"53051", 1561288}, + {"53052", 1561288}, + {"53056", 1561267}, + {"53057", 1561599}, + {"53058", 1561267}, + {"53059", 1561662}, + {"53060", 1561599}, + {"53061", 1561599}, + {"53062", 1561599}, + {"53063", 1561599}, + {"53064", 1561267}, + {"53065", 1561657}, + {"53066", 1561245}, + {"53069", 1561267}, + {"53070", 1561591}, + {"53072", 1561262}, + {"53073", 1561599}, + {"53074", 1561591}, + {"53075", 1561599}, + {"53076", 1561664}, + {"53078", 1561662}, + {"53079", 1561599}, + {"53080", 1561591}, + {"53081", 1561591}, + {"53082", 1561604}, + {"53083", 1561599}, + {"53085", 1561599}, + {"53086", 1561662}, + {"53088", 1561657}, + {"53089", 1561265}, + {"53090", 1561599}, + {"53091", 1561657}, + {"53092", 1561591}, + {"53093", 1561599}, + {"53094", 1561284}, + {"53095", 1561604}, + {"53096", 1561599}, + {"53097", 1561591}, + {"53098", 1561284}, + {"53099", 1561662}, + {"53101", 1561631}, + {"53102", 1561631}, + {"53103", 2081492}, + {"53104", 1561624}, + {"53105", 1561631}, + {"53108", 1561598}, + {"53109", 1561631}, + {"53110", 1561616}, + {"53114", 1561631}, + {"53115", 1561631}, + {"53118", 1561245}, + {"53119", 1561245}, + {"53120", 1561631}, + {"53121", 1561631}, + {"53122", 1561588}, + {"53125", 1561631}, + {"53126", 1561624}, + {"53127", 1561262}, + {"53128", 1561631}, + {"53129", 1561289}, + {"53130", 1561289}, + {"53132", 1561289}, + {"53137", 1561284}, + {"53138", 1561661}, + {"53139", 1561631}, + {"53140", 1561639}, + {"53141", 1561639}, + {"53142", 1561639}, + {"53143", 1561639}, + {"53144", 1561639}, + {"53146", 1561243}, + {"53147", 1561631}, + {"53148", 1561631}, + {"53149", 2081492}, + {"53150", 2081492}, + {"53151", 1561243}, + {"53152", 1561631}, + {"53153", 1561262}, + {"53154", 1561598}, + {"53156", 1561245}, + {"53157", 1561631}, + {"53158", 1561639}, + {"53159", 1561631}, + {"53167", 1561631}, + {"53168", 1561631}, + {"53170", 1561631}, + {"53171", 1561639}, + {"53172", 1561598}, + {"53176", 1561631}, + {"53177", 1561624}, + {"53178", 1561245}, + {"53179", 1561631}, + {"53181", 1561631}, + {"53182", 1561624}, + {"53183", 1561267}, + {"53184", 1561631}, + {"53185", 1561631}, + {"53186", 1561262}, + {"53187", 1561262}, + {"53188", 1561262}, + {"53189", 1561262}, + {"53190", 1561245}, + {"53191", 1561631}, + {"53192", 1561631}, + {"53194", 1561624}, + {"53195", 1561631}, + {"53199", 1561631}, + {"53201", 1561241}, + {"53202", 1561615}, + {"53203", 1561241}, + {"53204", 1561241}, + {"53205", 1561241}, + {"53206", 1561241}, + {"53207", 1561615}, + {"53208", 1561241}, + {"53209", 1561255}, + {"53210", 1561241}, + {"53211", 1561615}, + {"53212", 1561241}, + {"53213", 1561241}, + {"53214", 1561587}, + {"53215", 1561283}, + {"53216", 1561241}, + {"53217", 1561255}, + {"53218", 1561255}, + {"53219", 1561241}, + {"53220", 1561283}, + {"53221", 1561283}, + {"53222", 1561255}, + {"53223", 1561255}, + {"53224", 1561255}, + {"53225", 1561255}, + {"53226", 1561255}, + {"53227", 1561587}, + {"53228", 1561587}, + {"53233", 1561241}, + {"53234", 1561241}, + {"53235", 1561616}, + {"53237", 1561283}, + {"53259", 1561615}, + {"53263", 1561255}, + {"53267", 1561241}, + {"53268", 1561615}, + {"53274", 1561283}, + {"53278", 1561241}, + {"53288", 1561615}, + {"53290", 1561241}, + {"53293", 1561241}, + {"53295", 1561241}, + {"53401", 1561639}, + {"53402", 1561285}, + {"53403", 1561639}, + {"53404", 1561639}, + {"53405", 1561285}, + {"53406", 1561624}, + {"53407", 1561639}, + {"53408", 1561624}, + {"53490", 1561285}, + {"53501", 1561661}, + {"53502", 1561661}, + {"53503", 1561661}, + {"53505", 1561631}, + {"53506", 1561661}, + {"53507", 1561661}, + {"53508", 1561661}, + {"53511", 1561661}, + {"53512", 1561661}, + {"53515", 1561661}, + {"53517", 1561661}, + {"53520", 1561661}, + {"53521", 1561661}, + {"53522", 1561661}, + {"53523", 1561284}, + {"53525", 1561661}, + {"53526", 1561661}, + {"53527", 1561661}, + {"53528", 1561661}, + {"53529", 1561661}, + {"53531", 1561661}, + {"53532", 1561661}, + {"53533", 1561661}, + {"53534", 1561661}, + {"53535", 1561661}, + {"53536", 1561661}, + {"53537", 1561661}, + {"53538", 1561284}, + {"53542", 1561661}, + {"53543", 1561661}, + {"53544", 1561661}, + {"53545", 1561661}, + {"53546", 1561631}, + {"53547", 1561661}, + {"53548", 1561661}, + {"53549", 1561284}, + {"53550", 1561661}, + {"53551", 1561284}, + {"53553", 1561661}, + {"53555", 1561661}, + {"53557", 1561665}, + {"53558", 1561661}, + {"53559", 1561660}, + {"53560", 1561661}, + {"53562", 1561661}, + {"53563", 1561284}, + {"53565", 1561661}, + {"53566", 1561661}, + {"53570", 1561661}, + {"53571", 1561661}, + {"53572", 1561661}, + {"53574", 1561661}, + {"53575", 1561661}, + {"53576", 1561661}, + {"53579", 1561660}, + {"53580", 1561661}, + {"53582", 1561661}, + {"53585", 1561631}, + {"53589", 1561661}, + {"53590", 1561661}, + {"53593", 1561661}, + {"53594", 1561284}, + {"53595", 1561661}, + {"53596", 1561661}, + {"53597", 1561661}, + {"53598", 1561661}, + {"53701", 1561661}, + {"53702", 1561661}, + {"53703", 1561661}, + {"53704", 1561661}, + {"53705", 1561661}, + {"53706", 1561661}, + {"53707", 1561661}, + {"53708", 1561661}, + {"53711", 1561661}, + {"53713", 1561661}, + {"53714", 1561661}, + {"53715", 1561661}, + {"53716", 1561661}, + {"53717", 1561661}, + {"53718", 1561661}, + {"53719", 1561661}, + {"53725", 1561661}, + {"53726", 1561661}, + {"53744", 1561661}, + {"53774", 1561661}, + {"53777", 1561661}, + {"53778", 1561661}, + {"53779", 1561661}, + {"53782", 1561661}, + {"53783", 1561661}, + {"53784", 1561661}, + {"53785", 1561661}, + {"53786", 1561661}, + {"53788", 1561661}, + {"53789", 1561661}, + {"53790", 1561661}, + {"53791", 1561661}, + {"53792", 1561661}, + {"53793", 1561661}, + {"53794", 1561661}, + {"53901", 1561660}, + {"53911", 1561661}, + {"53916", 1561649}, + {"53919", 1561649}, + {"53920", 1561660}, + {"53922", 1561657}, + {"53923", 1561660}, + {"53925", 1561660}, + {"53926", 1561660}, + {"53928", 1561660}, + {"53930", 1561660}, + {"53931", 1561649}, + {"53932", 1561660}, + {"53933", 1561649}, + {"53935", 1561660}, + {"53939", 1561649}, + {"53946", 1561649}, + {"53947", 1561649}, + {"53949", 1561660}, + {"53952", 1561660}, + {"53953", 1561660}, + {"53954", 1561660}, + {"53955", 1561660}, + {"53956", 1561660}, + {"53957", 1561649}, + {"53960", 1561660}, + {"53963", 1561657}, + {"53964", 1561660}, + {"53965", 1561660}, + {"53969", 1561661}, + {"54106", 1561657}, + {"54110", 1561599}, + {"54113", 1561657}, + {"54115", 1561657}, + {"54123", 1561657}, + {"54126", 1561599}, + {"54129", 1561657}, + {"54130", 1561657}, + {"54131", 1561657}, + {"54136", 1561657}, + {"54140", 1561657}, + {"54152", 1561657}, + {"54155", 1561657}, + {"54160", 1561599}, + {"54162", 1561657}, + {"54165", 1561657}, + {"54169", 1561657}, + {"54170", 1561657}, + {"54173", 1561657}, + {"54180", 1561657}, + {"54207", 1561599}, + {"54208", 1561599}, + {"54214", 1561599}, + {"54215", 1561599}, + {"54220", 1561599}, + {"54221", 1561599}, + {"54227", 1561599}, + {"54228", 1561599}, + {"54229", 1561599}, + {"54230", 1561599}, + {"54232", 1561599}, + {"54240", 1561599}, + {"54241", 1561599}, + {"54245", 1561599}, + {"54247", 1561599}, + {"54301", 1561657}, + {"54302", 1561599}, + {"54303", 1561657}, + {"54304", 1561657}, + {"54305", 1561657}, + {"54306", 1561657}, + {"54307", 1561657}, + {"54308", 1561657}, + {"54311", 1561599}, + {"54313", 1561657}, + {"54324", 1561657}, + {"54344", 1561657}, + {"54901", 1561657}, + {"54902", 1561657}, + {"54903", 1561657}, + {"54904", 1561657}, + {"54906", 1561657}, + {"54911", 1561657}, + {"54912", 1561657}, + {"54913", 1561657}, + {"54914", 1561657}, + {"54915", 1561657}, + {"54919", 1561657}, + {"54922", 1561657}, + {"54923", 1561615}, + {"54927", 1561657}, + {"54931", 1561657}, + {"54932", 1561657}, + {"54934", 1561649}, + {"54935", 1561657}, + {"54936", 1561657}, + {"54937", 1561657}, + {"54941", 1561649}, + {"54942", 1561657}, + {"54944", 1561657}, + {"54947", 1561657}, + {"54952", 1561657}, + {"54956", 1561657}, + {"54957", 1561657}, + {"54960", 1561649}, + {"54963", 1561657}, + {"54964", 1561657}, + {"54968", 1561649}, + {"54971", 1561649}, + {"54974", 1561657}, + {"54979", 1561657}, + {"54980", 1561649}, + {"54985", 1561657}, + {"54986", 1561657}, + {"60001", 1561631}, + {"60002", 1561624}, + {"60010", 1561631}, + {"60011", 1561631}, + {"60012", 1561631}, + {"60013", 1561631}, + {"60014", 1561631}, + {"60015", 1561639}, + {"60020", 1561631}, + {"60021", 1561631}, + {"60030", 1561631}, + {"60031", 1561639}, + {"60033", 1561631}, + {"60034", 1561631}, + {"60035", 1561639}, + {"60037", 1561639}, + {"60039", 1561631}, + {"60040", 1561639}, + {"60041", 1561631}, + {"60042", 1561631}, + {"60044", 1561639}, + {"60045", 1561639}, + {"60046", 1561631}, + {"60047", 1561631}, + {"60048", 1561639}, + {"60049", 1561631}, + {"60050", 1561631}, + {"60051", 1561631}, + {"60060", 1561639}, + {"60061", 1561639}, + {"60064", 1561639}, + {"60069", 1561639}, + {"60071", 1561631}, + {"60072", 1561631}, + {"60073", 1561631}, + {"60075", 1561639}, + {"60079", 1561639}, + {"60081", 1561631}, + {"60083", 1561639}, + {"60084", 1561631}, + {"60085", 1561639}, + {"60086", 1561631}, + {"60087", 1561639}, + {"60088", 1561639}, + {"60089", 1561639}, + {"60092", 1561631}, + {"60096", 1561639}, + {"60097", 1561631}, + {"60098", 1561631}, + {"60099", 1561639}, + {"60102", 1561631}, + {"60142", 1561631}, + {"60152", 1561631}, + {"60156", 1561631}, + {"60180", 1561631} + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchNameConfig.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchNameConfig.cs new file mode 100644 index 0000000..c5fdb69 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchNameConfig.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace Landmark.OLA.Models.Loan +{ + + public class BranchNameData + { + [Required] + [Range(minimum: 1, maximum: 99999999)] + public int ChannelId { get; set; } + + [Required] + [RegularExpression("^[a-zA-Z ]*$")] + [StringLength(150, ErrorMessage = "Branch Names must be less than 150 characters")] + public string BranchName { get; set; } + } + + [BsonIgnoreExtraElements] + public class BranchNameConfig : OlaModel + { + public DateTime Date { get; set; } + + [Required] + public List BranchNamesByChannelId { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DealershipAuthRequest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DealershipAuthRequest.cs new file mode 100644 index 0000000..72c995b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DealershipAuthRequest.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace Landmark.OLA.Models.Loan +{ + [BsonIgnoreExtraElements] + public class DealershipAuthRequest + { + [Required] + public string DealershipCode { get; set; } + + public string DealershipName { get; set; } + + [Required] + public string DealershipPassword { get; set; } + + [Required] + public string ApplicantLastName { get; set; } + + [Required] + public string ApplicationId { get; set; } + + public DealershipAuthRequest(string dealershipCode, string dealershipName, string dealershipPassword, string applicantLastName, string applicationId) + { + this.DealershipCode = dealershipCode; + this.DealershipName = dealershipName; + this.DealershipPassword = dealershipPassword; + this.ApplicantLastName = applicantLastName; + this.ApplicationId = applicationId; + } + + public static bool ValidateAuthRequest(DealershipAuthRequest dealershipRequest, DealershipAuthRequest actionResponse) + { + return dealershipRequest?.Equals(actionResponse) ?? false; + } + + public override bool Equals(Object authRequest) + { + if (authRequest is not DealershipAuthRequest request) + { + return false; + } + + return string.Equals(DealershipCode, request.DealershipCode, StringComparison.Ordinal) && + string.Equals(DealershipPassword, request.DealershipPassword, StringComparison.Ordinal) && + string.Equals(ApplicantLastName, request.ApplicantLastName, StringComparison.OrdinalIgnoreCase) && + string.Equals(ApplicationId, request.ApplicationId, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return HashCode.Combine( + DealershipCode?.ToUpperInvariant(), + DealershipPassword, + ApplicantLastName?.ToUpperInvariant(), + ApplicationId?.ToUpperInvariant() + ); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DocumentUploadFromLoanApplication.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DocumentUploadFromLoanApplication.cs new file mode 100644 index 0000000..21e068d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DocumentUploadFromLoanApplication.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; + +namespace Landmark.OLA.Models.Loan +{ + public class DocumentUploadFromLoanApplication + { + public IFormFile File { get; set; } + public string Section { get; set; } + public string SectionId { get; set; } + public string Description { get; set; } + public static bool IsNullOrEmpty(DocumentUploadFromLoanApplication file) + { + if(file == null || file.File == null || String.IsNullOrEmpty(file.SectionId) || String.IsNullOrEmpty(file.Description)) + { + return true; + } + + if(file.File.Length == 0 || String.IsNullOrEmpty(file.File.FileName)) + { + return true; + } + + return false; + } + + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanErrorCode.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanErrorCode.cs new file mode 100644 index 0000000..a8b686c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanErrorCode.cs @@ -0,0 +1,11 @@ +namespace Landmark.OLA.Models.Loan.Enums +{ + public enum LoanErrorCode + { + No_Error, + Error_Updating, + Error_Submitting, + Error_Deleting + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanStatus.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanStatus.cs new file mode 100644 index 0000000..9ade0b1 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanStatus.cs @@ -0,0 +1,13 @@ +namespace Landmark.OLA.Models.Loan.Enums +{ + public enum LoanStatus + { + Application_Error, + Vendor_Error, + In_Progress, + Deleted, + Submitted, + Decisioned + } + +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanType.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanType.cs new file mode 100644 index 0000000..411382a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanType.cs @@ -0,0 +1,29 @@ +namespace Landmark.OLA.Models.Loan.Enums +{ + public enum LoanType + { + CC_CashBack, + CC_Rewards, + CC_Student, + CC_Secure, + EQ_HELOC_Flex, + EQ_Home_Equity_Fixed, + EQ_Mtg_Refi_Express, + EQ_Mfg_Home_With_or_Without_Land, + V_Vehicle, + V_VehiclePreapproval, + M_Boat, + P_Powersport, + T_TrailerMotorhome, + S_Signature, + S_Special + } + + public enum HMDALoanType + { + EQ_HELOC_Flex, + EQ_Home_Equity_Fixed, + EQ_Mtg_Refi_Express, + EQ_Mfg_Home_With_or_Without_Land + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Filter.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Filter.cs new file mode 100644 index 0000000..7c3946f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Filter.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Models.Loan +{ + public class Filter + { + public int Count { get; set; } + public bool SortDesc { get; set; } + public string SortField { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public FilterField[] FilterField { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/FilterField.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/FilterField.cs new file mode 100644 index 0000000..4b25c8b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/FilterField.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Models.Loan +{ + public class FilterField + { + public string Field { get; set; } + public string FilterValue { get; set; } + public bool Equal { get; set; } + public bool NotEqual { get; set; } + public bool GreaterThan { get; set; } + public bool LessThan { get; set; } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplication.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplication.cs new file mode 100644 index 0000000..cb6a0aa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplication.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Loan.Enums; +using MongoDB.Bson.Serialization.Attributes; + +namespace Landmark.OLA.Models.Loan +{ + [BsonIgnoreExtraElements] + public class LoanApplication : OlaModel + { + [Required] + //Nullable so that there is no default value + public LoanType? LoanType { get; set; } + + [Required] + //One Billion is an arbitrary limit but I'm pretty confidant that it will be an acceptable upper limit for a long time. + [Range(minimum: 0, maximum: 1000000000)] + //Nullable so that there is no default value + public decimal? RequestedAmount { get; set; } + + //Required for Personal Signature loans. + [StringLength(100)] + public string PurposeOfLoanSignature { get; set; } + + public DateTime Date { get; private set; } + + [Range(minimum: 0, maximum: 1000000000)] + //Required for certain loan types. Checked by FluentValidation. + public int? TermLengthInMonths { get; set; } + + //these could be ICollection if needed, would impact builders + public RealEstate RealEstate { get; set; } + public Vehicle Vehicle { get; set; } + public ManufacturedHome ManufacturedHome { get; set; } + public Guid SecuredCardCollateralId { get; } + + [Required] + //Nullable so that there is no default value + //Values must match loan type. Checked by FluentValidation. + public LoanPurpose.Purpose? Purpose { get; set; } + + [Required] + public Individual PrimaryBorrower { get; set; } + + public CoApplicant CoApplicant { get; set; } + + public string ModFeeAccount { get; set; } + + //3500 is maximum crif note length + [StringLength(3500)] + [RegularExpression(@"[\w\d\s\.\,\&\n\r\@\-\;\'\!]*")] + public string Note { get; set; } + + public string PrimaryContactMethod { get; set; } + public string PromoCode { get; set; } + public string LoanTitle { get; set; } + [StringLength(3450)] + public string ClientMessage { get; set; } + public DateTime DisclosureDate { get; set; } + public string IPAddress { get; set; } + + public IList Files { get; set; } + + public LoanApplication() + { + Date = DateTime.Now; + SecuredCardCollateralId = Guid.NewGuid(); + Files = []; + } + + public LoanApplication(DateTime? loanDate = null, Guid? securedCardCollateralId = null, string loanId = null) + { + Date = loanDate ?? DateTime.Now; + SecuredCardCollateralId = securedCardCollateralId ?? Guid.NewGuid(); + Id = loanId ?? null; + Files = []; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplicationFile.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplicationFile.cs new file mode 100644 index 0000000..f4b5131 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplicationFile.cs @@ -0,0 +1,42 @@ + +namespace Landmark.OLA.Models.Loan +{ + public class LoanApplicationFile + { + public string CosmosLoanId { get; set; } + public Guid FileId { get; set; } + public string Name { get; set; } + public string Section { get; set; } + public string SectionId { get; set; } + public string Description { get; set; } + + public LoanApplicationFile(string cosmosLoanId, Guid fileId, string name, string section, string sectionId, string description) + { + CosmosLoanId = cosmosLoanId; + FileId = fileId; + Name = name; + Section = section; + SectionId = sectionId; + Description = description; + } + + public string GetBlobPath() + { + return $"{CosmosLoanId}/{FileId}"; + } + + public Dictionary GetMetadata() + { + var metadata = new Dictionary + { + { "Name", Name }, + { "Section", Section }, + { "SectionId", SectionId.ToString() }, + { "Description", Description }, + { "CosmosLoanId", CosmosLoanId } + }; + + return metadata; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanContainer.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanContainer.cs new file mode 100644 index 0000000..748fb86 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanContainer.cs @@ -0,0 +1,34 @@ +using Landmark.OLA.Models.Loan.Enums; +using MongoDB.Bson.Serialization.Attributes; + +namespace Landmark.OLA.Models.Loan +{ + [BsonIgnoreExtraElements] + public class LoanContainer : OlaModel + { + public LoanApplication LoanApplication { get; set; } + public LoanType LoanType { get; set; } + public LoanPage Page { get; set; } + public LoanStatus Status { get; set; } + public LoanError Error { get; set; } + public DateTime Date { get; private set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Branch { get; set; } + public string LoanId { get; set; } + public string MemberId { get; set; } + public string IndividualId { get; set; } + + public LoanContainer() + { + Date = DateTime.UtcNow; //allows us to have control over date format + } + + public LoanContainer(LoanApplication loanApplication, DateTime? loanDate = null, LoanStatus status = LoanStatus.In_Progress) + { + LoanApplication = loanApplication ?? null; + Date = loanDate ?? DateTime.UtcNow; + Status = status; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanError.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanError.cs new file mode 100644 index 0000000..e54c8bd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanError.cs @@ -0,0 +1,15 @@ +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models.Loan { + public class LoanError + { + public LoanErrorCode code; + public string message; + + public LoanError(LoanErrorCode code, string message) + { + this.code = code; + this.message = message; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanLimits.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanLimits.cs new file mode 100644 index 0000000..b2f6d81 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanLimits.cs @@ -0,0 +1,36 @@ +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models.Loan +{ + public class LoanLimits + { + private static Limit EqLimit => new() { min = 10000, max = 250000 }; + private static Limit EqHighLimit => new() { min = 10000, max = 500000 }; + private static Limit VLimit => new() { min = 250, max = 250000 }; + private static Limit MfgHome => new() { min = 250, max = 250000 }; + private static Limit SLimit => new() { min = 250, max = 25000 }; + private static Limit SpecLimit => new() { min = 250, max = 10000 }; + + public static Dictionary Limits => new() + { + { LoanType.EQ_HELOC_Flex, EqHighLimit }, + { LoanType.EQ_Mtg_Refi_Express, EqLimit }, + { LoanType.EQ_Home_Equity_Fixed, EqHighLimit }, + { LoanType.EQ_Mfg_Home_With_or_Without_Land, MfgHome }, + { LoanType.V_Vehicle, VLimit }, + { LoanType.V_VehiclePreapproval, VLimit }, + { LoanType.M_Boat, VLimit }, + { LoanType.P_Powersport, VLimit }, + { LoanType.T_TrailerMotorhome, MfgHome }, //same limits as mfHome + { LoanType.S_Signature, SLimit }, + { LoanType.S_Special, SpecLimit }, + }; + + public static bool IsValidAmountForType(LoanType loanType, decimal requestedAmount) + { + var limit = Limits[loanType]; + + return requestedAmount >= limit.min && requestedAmount <= limit.max; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPage.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPage.cs new file mode 100644 index 0000000..eae54a9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPage.cs @@ -0,0 +1,18 @@ +namespace Landmark.OLA.Models.Loan +{ + public class LoanPage + { + public bool hasPersonal; + public bool hasAddress; + public bool hasEmployment; + public bool hasMemberInfo; + public bool hasSpouseInfo; + public bool hasTattleSpouseInfo; + public bool hasResidenceInfo; + public bool hasVehicleCollateral; + public bool hasHMDA; + public bool hasDisclosure; + public bool hasCollateralInfo; + public bool hasNote; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPurpose.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPurpose.cs new file mode 100644 index 0000000..7cdbbf8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPurpose.cs @@ -0,0 +1,109 @@ +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models.Loan +{ + + public static class LoanPurpose + { + //Acceptable values taken from CRIF database + /* + select p.shortname, p.name, p.productid, lp.name, lp.[description], + lp.loanpurposeid, lp.isactivefl from v_RptProduct_v001 as p + inner join v_RptProductLoanPurpose_v001 as plp + on plp.productid = p.productid + inner join v_RptLoanPurpose_v001 as lp + on lp.loanpurposeid = plp.loanpurposeid + order by p.productid + */ + private static readonly List _ccPurposes = new List + { Purpose.Other, Purpose.Consolidation, Purpose.Reestablish_Credit}; + + private static readonly List _eqPurposes = new List + { Purpose.Home_Improvement, Purpose.Home_Purchase, Purpose.Home_Refinance, + Purpose.Other, Purpose.Cash_Out_Refinance }; + + private static readonly List _mfgHomePurposes = new List + { Purpose.Home_Improvement, Purpose.Purchase_Manufactured_Home, Purpose.Home_Refinance, + Purpose.Other, Purpose.Cash_Out_Refinance }; + + private static readonly List _vPurposes = new List + { Purpose.Purchase, Purpose.Cash_Out_Refinance, Purpose.Refinance_Vehicle }; + + private static readonly List _mPurposes = new List + { Purpose.Purchase, Purpose.Refinance_Vehicle }; + + private static readonly List _pPurposes = new List + { Purpose.Purchase, Purpose.Refinance_Vehicle }; + + private static readonly List _tPurposes = new List + { Purpose.Purchase, Purpose.Refinance_Vehicle }; + + private static readonly List _sPurposes = new List + { Purpose.Other, Purpose.Consolidation}; + + + public static Dictionary> TypePurposes => new Dictionary> + { + { LoanType.CC_CashBack, _ccPurposes }, + { LoanType.CC_Rewards, _ccPurposes }, + { LoanType.CC_Student, _ccPurposes }, + { LoanType.CC_Secure, _ccPurposes }, + { LoanType.EQ_HELOC_Flex, _eqPurposes }, + { LoanType.EQ_Mtg_Refi_Express, _eqPurposes }, + { LoanType.EQ_Home_Equity_Fixed, _eqPurposes }, + { LoanType.EQ_Mfg_Home_With_or_Without_Land, _mfgHomePurposes }, + { LoanType.V_Vehicle, _vPurposes }, + { LoanType.V_VehiclePreapproval, _vPurposes }, + { LoanType.M_Boat, _mPurposes }, + { LoanType.P_Powersport, _pPurposes }, + { LoanType.T_TrailerMotorhome, _tPurposes }, + { LoanType.S_Signature, _sPurposes }, + { LoanType.S_Special, _sPurposes }, + }; + + public static bool IsValidPurposeForType(LoanType? type, Purpose? purpose) + { + if (!type.HasValue) + throw new ArgumentException("LoanType cannot be null"); + + if (!purpose.HasValue) + throw new ArgumentException("Purpose cannot be null"); + + return IsValidPurposeForType(type.Value, purpose.Value); + } + + public static bool IsValidPurposeForType(LoanType type, Purpose purpose) + { + return TypePurposes[type].Contains(purpose); + } + + + //Enum names and values based on CRIF's v_RptProductLoanPurpose_v001 table + //joined with v_RptProductLoanPurpose_v001 + //to find purposes that are active and in use + /* + select distinct lp.loanpurposeid, lp.name,lp.[description] + from v_RptLoanPurpose_v001 as lp + inner join v_RptProductLoanPurpose_v001 as plp + on lp.loanpurposeid = plp.loanpurposeid + where isactivefl = 1 + */ + public enum Purpose + { + Home_Improvement = 3, + Home_Purchase = 4, + Home_Refinance = 5, + Purchase_Boat = 8, + Purchase = 12, + Other = 149, + Refinance = 141, + Dental_Expense = 142, + Medical_Expense = 143, + Cash_Out_Refinance = 144, + Refinance_Vehicle = 146, + Consolidation = 148, + Reestablish_Credit = 150, + Purchase_Manufactured_Home = 151, + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/MockApplication.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/MockApplication.cs new file mode 100644 index 0000000..ca0d008 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/MockApplication.cs @@ -0,0 +1,155 @@ +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Residences; + +namespace Landmark.OLA.Models.Loan +{ + public class MockApplication + { + public readonly LoanApplication LoanApplication = + new(DateTime.Parse("2018-02-21T16:52:47.62"), new Guid("9ed5a50f-279c-49b0-b5b4-d5b614e42501")) + { + LoanType = LoanType.CC_Secure, + RequestedAmount = 1320, + PrimaryContactMethod = "1234567890", + Purpose = LoanPurpose.Purpose.Other, + PrimaryBorrower = new Individual(new Guid("8066fa2a-16ba-4148-b28c-2926ba263c5f")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("bf7e502a-9830-43c6-b603-1a32294c7e1a")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 24, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + MailingAddress = new Address(new Guid("f84db562-36a0-4039-9ce5-346a2ba6899b")) + { + Address1 = "321 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151" + }, + Employers = + [ + new Employer(employerId: new Guid("eee4efa3-327f-4ce5-9e8b-8c3e7a865fff")) + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + MonthsEmployed = 60, + StartDate = new DateTime(2017, 01, 01), + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ] + }, + CoApplicant = new CoApplicant(new Guid("ba99037f-df92-46d4-a477-ffecea1f4837")) + { + FirstName = "John", + LastName = "Smith", + DOB = new DateTime(2018, 7, 25), + SSN = "111221111", + Phone = "5555550983", + PhoneType = PhoneType.Mobile, + Email = "john.smith@example.org", + IsMarried = true, + IsMarriedToPrimaryBorrower = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("ae795a5f-1ea3-4186-b4b8-e9f9b81e3059")) + { + Address1 = "339 1st St", + Address2 = "#76", + City = "Milwaukee", + State = StateOption.IL, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 12, + MonthlyPayment = 100, + }, + PreviousAddresses = + [ + new ResidenceAddress(new Guid("e7e881f0-d371-4371-8022-cce4f700e6d6")) + { + Address1 = "339 1st St", + Address2 = "#80", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 28, + }, + ], + Employers = + [ + new Employer(employerId: new Guid("e8436884-0e6c-4144-8de5-1f44c7936a23")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Chocolate Teapot Manfuacturing Inc", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Lead Teapot Assembler", + WorkPhone = "5556783241", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + HoursPerWeek = 24, + AmountPerPeriod = 10, + Frequency = Frequency.Hourly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + new Employer(employerId: new Guid("58d25f44-1260-428d-ac2c-d8804b06ed66")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Vanilla Teapot Manfuacturing Coop", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Junior Teapot Assembler", + WorkPhone = "5559572318", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + HoursPerWeek = 32, + AmountPerPeriod = 50, + Frequency = Frequency.Hourly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + ], + }, + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/TermLengths.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/TermLengths.cs new file mode 100644 index 0000000..4c11a60 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/TermLengths.cs @@ -0,0 +1,223 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models.Loan +{ + public static class TermLengths + { + //All lengths are in months. + public class UiTermOption + { + [Required] + public int value; + + [Required] + public string desc; + } + + public static List AvailableTermOptions = + [ + new UiTermOption {value = 12, desc = "12 months / 1 year"}, + new UiTermOption {value = 24, desc = "24 months / 2 years"}, + new UiTermOption {value = 36, desc = "36 months / 3 years"}, + new UiTermOption {value = 48, desc = "48 months / 4 years"}, + new UiTermOption {value = 60, desc = "60 months / 5 years"}, + new UiTermOption {value = 63, desc = "63 months"}, + new UiTermOption {value = 72, desc = "72 months / 6 years"}, + new UiTermOption {value = 75, desc = "75 months"}, + new UiTermOption {value = 84, desc = "84 months / 7 years"}, + new UiTermOption {value = 96, desc = "96 months / 8 years"}, + new UiTermOption {value = 120, desc = "120 months / 10 years"}, + new UiTermOption {value = 144, desc = "144 months / 12 years"}, + new UiTermOption {value = 180, desc = "180 months / 15 years"}, + new UiTermOption {value = 240, desc = "240 months / 20 years"} + ]; + + public static List AvailableTermOptionsWithoutYears = + [ + new UiTermOption {value = 12, desc = "12 months"}, + new UiTermOption {value = 24, desc = "24 months"}, + new UiTermOption {value = 36, desc = "36 months"}, + new UiTermOption {value = 48, desc = "48 months"}, + new UiTermOption {value = 60, desc = "60 months"}, + new UiTermOption {value = 63, desc = "63 months"}, + new UiTermOption {value = 72, desc = "72 months"}, + new UiTermOption {value = 75, desc = "75 months"}, + new UiTermOption {value = 84, desc = "84 months"}, + new UiTermOption {value = 96, desc = "96 months"}, + new UiTermOption {value = 120, desc = "120 months"}, + new UiTermOption {value = 144, desc = "144 months"}, + new UiTermOption {value = 180, desc = "180 months"}, + new UiTermOption {value = 240, desc = "240 months"} + ]; + + public static Dictionary> BoatLoansByAmount = new() + { + //Loan amount can be greater than or equal to the amount listed here. + {0, + new List + { + 48 + } + }, + {10000, + new List + { + 48, + 63, + 72, + 84 + } + }, + {20000, + new List + { + 48, + 63, + 72, + 84, + 120, + 180, + } + }, + }; + + public static Dictionary> MfdHomeAndRvByAmount = new() + { + //Loan amount can be greater than or equal to the amount listed here. + {0, + new List + { + 48 + } + }, + {10000, + new List + { + 48, + 60, + 72, + 84 + } + }, + {20000, + new List + { + 48, + 60, + 72, + 84, + 120, + 180 + } + }, + }; + + public static List Special = + [ + 36 + ]; + + public static List HomeEquity = + [ + 60, + 84, + 120, + 180, + 240, + ]; + + public static List MortgageRefiExpress = + [ + 96, + 120, + 144, + ]; + + public static List VehiclePreApproval = + [ + 36, + 48, + 63, + 75, + ]; + + public static Dictionary> TermsByYear = new() + { + {2023, + new List + { + 36, + 48, + 63, + 75, + 84 + } + }, + {2022, + new List + { + 36, + 48, + 63, + 75 + } + }, + {2021, + new List + { + 36, + 48, + 63 + } + }, + {2020, + new List + { + 36, + 48 + } + } + }; + + public static Dictionary> PowersportByAmount = new() + { + //Loan amount can be greater than or equal to the amount listed here. + {0, + new List + { + 36, + 48, + 63 + } + }, + {10000, + new List + { + 36, + 48, + 63, + 72 + } + } + }; + + public static Dictionary> SignatureByAmount = new() + { + //Loan amount can be greater than or equal to the amount listed here. + {0, + new List + { + 12, + 24, + 36 + } + }, + {5000, + new List + { + 48 + } + }, + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/BranchConfigValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/BranchConfigValidator.cs new file mode 100644 index 0000000..039f331 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/BranchConfigValidator.cs @@ -0,0 +1,33 @@ +using FluentValidation; + +namespace Landmark.OLA.Models.Loan.Validators +{ + public class BranchConfigValidator : AbstractValidator + { + public BranchConfigValidator() { + RuleFor(x => x.BranchCodesByZip).Must((x, branchCodes) => NoZipcodeDuplicates(branchCodes)) + .WithMessage("You cannot have duplicate zipcodes."); + } + + private static bool NoZipcodeDuplicates(List branchData) + { + var matchFound = false; + foreach (var eachMapping in branchData) + { + int matchCount = 0; + foreach (var otherMapping in branchData) + { + if( otherMapping.Zipcode == eachMapping.Zipcode) + { + matchCount++; + } + } + if( matchCount > 1 ) + { + matchFound = true; + } + } + return !matchFound; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/LoanApplicationValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/LoanApplicationValidator.cs new file mode 100644 index 0000000..a6453ff --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/LoanApplicationValidator.cs @@ -0,0 +1,283 @@ +using FluentValidation; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.HomeMortgageDisclosureAct.Validators; +using Landmark.OLA.Models.Individuals.Validators; +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models.Loan.Validators +{ + public class LoanApplicationValidator : AbstractValidator + { + public LoanApplicationValidator() + { + RuleFor(x => x.PrimaryBorrower).SetValidator(new ApplicantValidator()); + + //when primary is married and in a state requiring spouse information + When(x => + (x.PrimaryBorrower.IsMarried ?? false) && + (x.PrimaryBorrower.Address?.State.HasValue ?? false) && + ValidationConstants.StatesRequiringSpouseInformation.Contains(x.PrimaryBorrower.Address.State.Value), + () => + { + //Primary must have spouse data if not married to coapplicant + RuleFor(x => x.PrimaryBorrower.HasAnyNonApplicantSpouse()).Equal(true) + .Unless(x => x.CoApplicant?.IsMarriedToPrimaryBorrower ?? false) + .WithMessage("PrimaryBorrower must include spouse information due to marriage in a community property state."); + }); + + + //when coapp is married and in a state requiring spouse information + When(x => + (x.CoApplicant?.IsMarried ?? false) && + (x.CoApplicant?.Address?.State.HasValue ?? false) && + ValidationConstants.StatesRequiringSpouseInformation.Contains(x.CoApplicant.Address.State.Value), + () => + { + //coapp must have spouse data if not married to coapplicant + RuleFor(x => x.CoApplicant.HasAnyNonApplicantSpouse()).Equal(true) + .Unless(x => x.CoApplicant?.IsMarriedToPrimaryBorrower ?? false) + .WithMessage("CoApplicant must include spouse information due to marriage in a community property state."); + }); + + + Unless(x => x.CoApplicant is null, () => + { + RuleFor(x => x.CoApplicant).SetValidator(new ApplicantValidator()); + + When(x => x.CoApplicant.IsMarriedToPrimaryBorrower, () => + { + //If the two applicants are married there should be no other spouse information + RuleFor(x => x.PrimaryBorrower.NonApplicantSpouse).Null(); + RuleFor(x => x.PrimaryBorrower.DetailedNonApplicantSpouse).Null(); + + RuleFor(x => x.CoApplicant.NonApplicantSpouse).Null(); + RuleFor(x => x.CoApplicant.DetailedNonApplicantSpouse).Null(); + }); + }); + + When(x => (x.PrimaryBorrower?.IsMarried ?? false) == false, () => + { + RuleFor(x => x.PrimaryBorrower.HasAnyNonApplicantSpouse()).Equal(false) + .WithMessage("PrimaryBorrower is set as not married but includes spouse information."); + }); + + When(x => IsHmdaLoan(x.LoanType), () => + { + RuleFor(x => x.PrimaryBorrower.HmdaValues).SetValidator(new HmdaValidator()); + + RuleFor(x => x.CoApplicant.HmdaValues).SetValidator(new HmdaValidator()) + .Unless(x => x.CoApplicant is null); + }); + + When(x => IsVehicleRequired(x.LoanType), () => + { + RuleFor(x => x.Vehicle).NotNull(); + + RuleFor(x => x).SetValidator(new VehicleValidator()); + }); + + When(x => IsRealEstateRequired(x.LoanType), () => + { + RuleFor(x => x.RealEstate).NotNull(); + }); + + When(x => IsTermRequired(x.LoanType), () => + { + RuleFor(x => x.TermLengthInMonths).NotNull(); + RuleFor(x => x.TermLengthInMonths).Must((x, termLength) => IsValidTermLength(x) == true) + .WithMessage("TermLengthInMonths is not set to a valid value for this loan type."); + }); + + RuleFor(x => x.Purpose).Must((x, purpose) => LoanPurpose.IsValidPurposeForType(x.LoanType, purpose)); + + Unless(x => x.LoanType.Value == LoanType.CC_CashBack || x.LoanType.Value == LoanType.CC_Rewards || x.LoanType.Value == LoanType.CC_Secure + || x.LoanType.Value == LoanType.CC_Student, () => { + RuleFor(x => x.RequestedAmount).Must((x, requestedAmount) => LoanLimits.IsValidAmountForType(x.LoanType.Value, requestedAmount.Value)) + .WithMessage("The requested loan amount is outside the limits for this product type."); + }); + + + //They actually can, but for now that means something went wrong on the frontend. + //To allow vehicle details on preapprovals, simply remove this validation and everything should likely work. + RuleFor(x => x.Vehicle).Null().When(x => x.LoanType == LoanType.V_VehiclePreapproval) + .WithMessage("Vehicle PreApproval loans cannot include vehicle details"); + + When(x => IsClientMessageRequired(x.LoanType), () => + { + RuleFor(x => x.ClientMessage).NotNull(); + RuleFor(x => x.ClientMessage.Trim()).Must((x, clientMessage) => clientMessage.Length >= 5) + .WithMessage("Applications for this type of loan must include a client message"); + }); + } + + private bool IsValidTermLength(LoanApplication loanApplication) + { + if ((!loanApplication.LoanType.HasValue) || (!loanApplication.TermLengthInMonths.HasValue)) + return false; + + List validTermLengths = this.GetValidTermLengthsForLoanType(loanApplication); + + return validTermLengths.Contains(loanApplication.TermLengthInMonths.Value); + } + + private List GetValidTermLengthsForLoanType(LoanApplication loanApplication) + { + return loanApplication.LoanType.Value switch + { + LoanType.EQ_Mtg_Refi_Express => TermLengths.MortgageRefiExpress, + LoanType.EQ_Home_Equity_Fixed => TermLengths.HomeEquity, + LoanType.V_VehiclePreapproval => TermLengths.VehiclePreApproval, + LoanType.V_Vehicle => GetVehicleTermList(loanApplication.Vehicle), + LoanType.EQ_Mfg_Home_With_or_Without_Land or LoanType.T_TrailerMotorhome => GetMfdHomeAndRvTermList(loanApplication.RequestedAmount.Value), + LoanType.M_Boat => GetBoatTermList(loanApplication.RequestedAmount.Value), + LoanType.P_Powersport => GetPowersportTermList(loanApplication.Vehicle.InitialEstimatedValue.Value), + LoanType.S_Signature => GetSignatureTermList(loanApplication.RequestedAmount.Value), + LoanType.S_Special => GetSpecialTermList(loanApplication.RequestedAmount.Value), + LoanType.CC_CashBack => [], + LoanType.CC_Rewards => [], + LoanType.CC_Student => [], + LoanType.CC_Secure => [], + LoanType.EQ_HELOC_Flex => [], + _ => [], + }; + } + + private static List GetSignatureTermList(decimal loanAmount) + { + var applicableKeys = TermLengths.SignatureByAmount.Keys.Where(x => loanAmount >= x); + var allowedTerms = new List(); + foreach (var key in applicableKeys) + { + allowedTerms.AddRange(TermLengths.SignatureByAmount[key]); + } + return allowedTerms; + } + + + private static List GetSpecialTermList(decimal loanAmount) + { + var allowedTerms = new List + { + 36 + }; + return allowedTerms; + } + + private static List GetMfdHomeAndRvTermList(decimal loanAmount) + { + var availableTermLengths = new List(); + + foreach (var amount in TermLengths.MfdHomeAndRvByAmount.Keys) + { + if (loanAmount >= amount) + { + availableTermLengths.AddRange(TermLengths.MfdHomeAndRvByAmount[amount]); + } + } + + return availableTermLengths; + } + + public static List GetVehicleTermList(Vehicle vehicle) + { + if ((vehicle is null) || (!vehicle.Condition.HasValue) || (!vehicle.Year.HasValue)) + return []; + + var yearList = TermLengths.TermsByYear; + + int lookupYear = vehicle.Year.Value; + + if (yearList.TryGetValue(lookupYear, out List value)) + { + return value; + } + else if (lookupYear >= yearList.First().Key) + { + return yearList.First().Value; + } + else + { + return yearList.Last().Value; + } + + } + + private static List GetBoatTermList(decimal loanAmount) + { + var applicableKeys = TermLengths.BoatLoansByAmount.Keys.Where(x => loanAmount >= x); + + var allowedTerms = new List(); + + foreach (var key in applicableKeys) + { + allowedTerms.AddRange(TermLengths.BoatLoansByAmount[key]); + } + + return allowedTerms; + } + + private static List GetPowersportTermList(decimal loanAmount) + { + var applicableKeys = TermLengths.PowersportByAmount.Keys.Where(x => loanAmount >= x); + + var allowedTerms = new List(); + + foreach (var key in applicableKeys) + { + allowedTerms.AddRange(TermLengths.PowersportByAmount[key]); + } + + return allowedTerms; + } + + public static bool IsTermRequired(LoanType? loanType) + { + if (!loanType.HasValue) + { + return false; + } + + return !ValidationConstants.LoansNotRequiringTerm.Contains(loanType.Value); + } + + private static bool IsVehicleRequired(LoanType? loanType) + { + if (!loanType.HasValue) + { + return false; + } + + return ValidationConstants.LoansRequiringVehicle.Contains(loanType.Value); + } + + private static bool IsRealEstateRequired(LoanType? loanType) + { + if (!loanType.HasValue) + { + return false; + } + + return ValidationConstants.LoansRequiringRealEstate.Contains(loanType.Value); + } + + private static bool IsHmdaLoan(LoanType? loanType) + { + if (!loanType.HasValue) + { + return false; + } + + return ValidationConstants.HmdaLoans.Contains(loanType.Value); + } + + private static bool IsClientMessageRequired(LoanType? loanType) + { + if (!loanType.HasValue) + { + return false; + } + + return loanType.Value == LoanType.S_Special; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/VehicleValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/VehicleValidator.cs new file mode 100644 index 0000000..bb81bc5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/VehicleValidator.cs @@ -0,0 +1,80 @@ +using FluentValidation; +using Landmark.OLA.Models.Assets.Enums; +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models.Loan.Validators +{ + class VehicleValidator : AbstractValidator + { + private readonly Dictionary> _VehiclesToLoans = new() + { + { VehicleType.Automobile, + new List + { + LoanType.V_Vehicle, + LoanType.V_VehiclePreapproval, + } + }, + { VehicleType.Motorcycle, + new List + { + LoanType.V_Vehicle, + LoanType.V_VehiclePreapproval, + } + }, + { VehicleType.Boat, + new List + { + LoanType.M_Boat, + } + }, + { VehicleType.ATV, + new List + { + LoanType.P_Powersport, + } + }, + { VehicleType.PWC, + new List + { + LoanType.P_Powersport, + } + }, + { VehicleType.RecreationalVehicle, + new List + { + LoanType.P_Powersport, + } + }, + { VehicleType.Snowmobile, + new List + { + LoanType.P_Powersport, + } + }, + { VehicleType.TrailerMotorHome, + new List + { + LoanType.T_TrailerMotorhome, + } + }, + }; + + + public VehicleValidator() + { + RuleFor(x => x.Vehicle.Type).Must((x, vehicleType) => IsAllowedVehicleTypeForLoan(x.LoanType.Value, vehicleType.Value)) + .WithMessage($"Vehicle type is not allowed for this loan type."); + } + + private bool IsAllowedVehicleTypeForLoan(LoanType loanType, VehicleType vehicleType) + { + if (_VehiclesToLoans.TryGetValue(vehicleType, out var loanTypeList)) + { + return loanTypeList.Contains(loanType); + } + + return false; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ModelValidator.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ModelValidator.cs new file mode 100644 index 0000000..93fbbbb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ModelValidator.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models +{ + public static class ModelValidator + { + public static bool TryValidate(Object data) + { + try + { + Validate(data); + return true; + } + catch(Exception) + { + return false; + } + + } + + //TODO #231: This will not validate properties of the object passed in - a valid Individual with an invalid Address will be deemed valid. + //Controllers do validate all properties. + public static void Validate(Object data) + { + //https://stackoverflow.com/a/3783328/266594 + var validationResults = new List(); //Can use this to inform user of exact problems + var vc = new ValidationContext(data, null, null); + var isValid = Validator.TryValidateObject(data, vc, validationResults, true); + + if (!isValid) + { + string validationProblems = String.Join('\n', validationResults); + throw new ArgumentException($"{data.GetType()} data is invalid: \n {validationProblems}"); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OlaModel.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OlaModel.cs new file mode 100644 index 0000000..47f4767 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OlaModel.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models +{ + public class OlaModel + { + //This will either hold an ToString'ed int or a 36-char long GUID + [StringLength(36)] + public string Id { get; set; } + + public Guid GetOrCreateGuid() + { + if (Guid.TryParse(Id, out Guid parsedGuid)) + { + return parsedGuid; + } + else + { + var newGuid = Guid.NewGuid(); + Id = newGuid.ToString(); + return newGuid; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthTokenResponse.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthTokenResponse.cs new file mode 100644 index 0000000..7c0954e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthTokenResponse.cs @@ -0,0 +1,33 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class AuthTokenResponse + { + public AuthTokenStatus Status { get; set; } + public AuthToken Token { get; set; } + } + + public class AuthTokenStatus + { + public string StatusMessage { get; set; } + public AuthTokenError ErrorInfo { get; set; } + } + + public class AuthTokenError + { + public string ErrorType { get; set; } + public string ErrorCode { get; set; } + public string ErrorMessage { get; set; } + } + + public class AuthToken + { + public string DiFiid { get; set; } + public string DiFicustomer { get; set; } + public string DiMemberNumber { get; set; } + public string AccessToken { get; set; } + public string ExpiresIn { get; set; } + public string RefreshToken { get; set; } + public string RefreshTokenExpiresIn { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthenticationStatement.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthenticationStatement.cs new file mode 100644 index 0000000..0822e82 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthenticationStatement.cs @@ -0,0 +1,9 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class AuthenticationStatement + { + public string SessionIndex { get; set; } + public string AuthContextClassRef { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/Constants.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/Constants.cs new file mode 100644 index 0000000..1c6d920 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/Constants.cs @@ -0,0 +1,17 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class Constants + { + public static readonly string TokenPayload = "TokenPayload"; + public static readonly string TokenAuthorization = "TokenAuthorization"; + public static readonly string TokenAuthorizationString = "Member"; + public static readonly string TokenNonAuthorizationString = "None"; + public static readonly string MemberID = "MemberID"; + public static readonly string IndividualID = "IndividualID"; + public static readonly string FI_ID = "GroupID"; + public static readonly string IsTwoFactor = "IsTwoFactor"; + public static readonly string InProgressLoanId = "InProgressLoanId"; + public static readonly string TokenPrefix = "Bearer "; // space is intentional + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/DISAMLAssertion.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/DISAMLAssertion.cs new file mode 100644 index 0000000..59c91c2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/DISAMLAssertion.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class DISAMLAssertion + { + public string NameID { get; set; } + public string Recipient { get; set; } + public SAMLConditions Conditions { get; set; } + public AuthenticationStatement AuthnStatement { get; set; } + public string MemberID { get; set; } + public string FI_ID { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingFormData.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingFormData.cs new file mode 100644 index 0000000..bb25c31 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingFormData.cs @@ -0,0 +1,13 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class OnlineBankingFormData + { + public string Username { get; set; } + public string Password { get; set; } + public string DI_TID { get; set; } + public string DI_FIID { get; set; } + public string GrantType = "password"; + public string Authorization { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingToken.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingToken.cs new file mode 100644 index 0000000..7bc0bde --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingToken.cs @@ -0,0 +1,22 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class OnlineBankingToken + { + public string MemberID { get; set; } + public string IndividualID { get; set; } + public string FI_ID { get; set; } + public string LoanId { get; set; } + public bool IsTwoFactor { get; set; } + + public OnlineBankingToken(DISAMLAssertion assertion, string loanId, bool isTwoFactor) + { + this.MemberID = assertion?.MemberID; + this.FI_ID = assertion.FI_ID; + this.LoanId = loanId; + this.IsTwoFactor = isTwoFactor; + } + + public OnlineBankingToken() { } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/SAMLConditions.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/SAMLConditions.cs new file mode 100644 index 0000000..c0ff0c8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/SAMLConditions.cs @@ -0,0 +1,10 @@ + +namespace Landmark.OLA.Models.OnlineBanking +{ + public class SAMLConditions + { + public DateTime NotBefore { get; set; } + public DateTime NotAfter { get; set; } + public string Audience { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/CurrentResidenceAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/CurrentResidenceAddress.cs new file mode 100644 index 0000000..2ba9895 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/CurrentResidenceAddress.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models.Residences +{ + public class CurrentResidenceAddress : ResidenceAddress, ICurrentResidenceAddress + { + [Required] + //Slightly arbitrary values that should be safe + [Range(0, 1000000000)] + //Nullable so that there is no default value + public decimal? MonthlyPayment { get; set; } + + public CurrentResidenceAddress() + : base() + { + } + + public CurrentResidenceAddress(Guid id) + : base(id) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ICurrentResidenceAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ICurrentResidenceAddress.cs new file mode 100644 index 0000000..67e89ba --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ICurrentResidenceAddress.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Landmark.OLA.Models.Residences +{ + public interface ICurrentResidenceAddress : IResidenceAddress + { + //Ten Billion is an arbitrary limit but I'm pretty confidant that it will be an acceptable upper limit for a long time. + [Range(minimum: 0, maximum: 10000000000)] + decimal? MonthlyPayment { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/IResidenceAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/IResidenceAddress.cs new file mode 100644 index 0000000..0c1bf54 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/IResidenceAddress.cs @@ -0,0 +1,12 @@ +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; + +namespace Landmark.OLA.Models.Residences +{ + public interface IResidenceAddress : IAddress + { + int? MonthsResided { get; set; } + + ResidenceTypeOption? ResidenceType { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ResidenceAddress.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ResidenceAddress.cs new file mode 100644 index 0000000..7eead83 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ResidenceAddress.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; + +namespace Landmark.OLA.Models.Residences +{ + public class ResidenceAddress : Address, IResidenceAddress + { + [Required] + //TODO #227: Should MonthsResided be at least 1? What if you just moved yesterday? + //The current front end offers an upper limit of 10 years, 11 months. + [Range(0, 130)] + //Nullable so that there is no default value. Could change if a value of at least 1 is required + public int? MonthsResided { get; set; } + + [Required] + //Nullable so that there is no default value + public ResidenceTypeOption? ResidenceType { get; set; } + + public ResidenceAddress() + : base() + { + } + + public ResidenceAddress(Guid id) + : base(id) + { + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ValidationConstants.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ValidationConstants.cs new file mode 100644 index 0000000..40cb866 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ValidationConstants.cs @@ -0,0 +1,43 @@ +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Loan.Enums; + +namespace Landmark.OLA.Models +{ + public static class ValidationConstants + { + public static readonly List StatesRequiringSpouseInformation = new List + { + //This is not an exhaustive list of community property states + StateOption.WI, + }; + + public static readonly List HmdaLoans = new List + { + LoanType.EQ_HELOC_Flex, + LoanType.EQ_Home_Equity_Fixed, + LoanType.EQ_Mtg_Refi_Express, + LoanType.EQ_Mfg_Home_With_or_Without_Land, + }; + + public static readonly List LoansNotRequiringTerm = new List + { + LoanType.CC_CashBack, + LoanType.CC_Rewards, + LoanType.CC_Student, + LoanType.CC_Secure, + LoanType.EQ_HELOC_Flex, + }; + + public static readonly List LoansRequiringVehicle = new List + { + LoanType.V_Vehicle, + }; + + public static readonly List LoansRequiringRealEstate = new List + { + LoanType.EQ_HELOC_Flex, + LoanType.EQ_Home_Equity_Fixed, + LoanType.EQ_Mtg_Refi_Express, + }; + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Vendor/IUspsVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Vendor/IUspsVendorClient.cs new file mode 100644 index 0000000..bacb624 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Vendor/IUspsVendorClient.cs @@ -0,0 +1,9 @@ +using Landmark.OLA.Models.Addresses; + +namespace Landmark.OLA.Models.Vendor +{ + public interface IUspsVendorClient + { + Task
CompareAddress(Address address); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Addresses/Validators/AddressValidatorTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Addresses/Validators/AddressValidatorTest.cs new file mode 100644 index 0000000..b53ec09 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Addresses/Validators/AddressValidatorTest.cs @@ -0,0 +1,55 @@ +using System; +using Xunit; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Addresses.Validators; + +namespace Landmark.OLA.Tests.Addresses.Validators +{ + public class AddressValidatorTest + { + [Fact] + public void TestWithNull() + { + Address address = null; + Assert.Throws(() => new AddressValidator().Validate(address).IsValid); + } + + [Theory] + [InlineData(false, null, null, null, null, null, null, null)] + [InlineData(true, "address1", "address2", "city", "postalCode", "ext", "WI", "UnitedStates")] + [InlineData(true, "address1", null, "city", "postalCode", null, "WI", null)] + [InlineData(false, null, null, "city", "postalCode", null, "WI", null)] + [InlineData(false, "address1", null, null, "postalCode", null, "WI", null)] + [InlineData(false, "address1", null, "city", null, null, "WI", null)] + [InlineData(false, "address1", null, "city", "postalCode", null, null, null)] + [InlineData(false, "address1", null, "city", null, null, null, null)] + public void TestCombinationsWithValidStateAndCountry(bool expected, string address1, string address2, string city, string postalCode, string postalCodeExt, string state, string country) + { + StateOption? stateOptionToUse = null; + if(!string.IsNullOrEmpty(state)) { + Enum.TryParse(state, out StateOption stateOption); + stateOptionToUse = stateOption; + } + + CountryOption? countryOptionToUse = null; + if (!string.IsNullOrEmpty(country)) + { + Enum.TryParse(state, out CountryOption countryOption); + countryOptionToUse = countryOption; + } + + Address address = new() { + Address1 = address1, + Address2 = address2, + City = city, + State = stateOptionToUse, + PostalCode = postalCode, + Country = countryOptionToUse, + PostalCodeExt = postalCodeExt + }; + + Assert.Equal(expected, new AddressValidator().Validate(address).IsValid); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/AddressValidationTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/AddressValidationTest.cs new file mode 100644 index 0000000..2a23ca9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/AddressValidationTest.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Moq; +using Landmark.OLA.Models.Vendor; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Application.Controllers; + +namespace Landmark.OLA.Tests.Controllers +{ + public class AddressValidationTest + { + // TEST: public async Task Post([FromBody] Address address) + [Fact] + public async void Address_Validate_OK() + { + var mockUspsClient = new Mock(); + + var postError = new Models.ClientError() { Message = "Error Jim Robinson" }; + var postAddress = new Address() { + Address1 = "111 S 1st st", + City = "Milwaukee", + State = Models.Addresses.Enums.StateOption.WI, + PostalCode = "53207" + }; + // arrange + var controller = new AddressValidationController(mockUspsClient.Object); + + // act + var result = await controller.Post(postAddress); + var okResult = result as OkObjectResult; + + // assert + Assert.NotNull(okResult); + Assert.Equal(200, okResult.StatusCode); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/LoggingTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/LoggingTest.cs new file mode 100644 index 0000000..6be73a3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/LoggingTest.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Landmark.OLA.Application.Controllers; + +namespace Landmark.OLA.Tests.Controllers +{ + public class LoggingTest + { + [Fact] + public void Logging_Error_OK() + { + + var postError = new Models.ClientError() { Message = "Error Jim Robinson" }; + // arrange + var controller = new LoggingController(); + + // act + var result = controller.Post(postError); + var okResult = result as OkResult; + + // assert + Assert.NotNull(okResult); + Assert.Equal(200, okResult.StatusCode); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/UiSettingsTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/UiSettingsTest.cs new file mode 100644 index 0000000..2699ff9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/UiSettingsTest.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Moq; +using Landmark.OLA.Application.Common.Domain; +using Landmark.OLA.Application.Controllers; +using Landmark.OLA.Application.DataAccess; +using Landmark.OLA.Models.Loan; + +namespace Landmark.OLA.Tests.Controllers +{ + public class UiSettingsTest + { + [Fact] + public void UiSettings_Returns_OK() + { + var _branchConfig = new BranchConfig + { + Id = "012345678901234567890123456789012345" + }; + var mockRepo = new Mock(); + mockRepo.Setup(repo => repo.GetBranchConfig()).Returns(_branchConfig); + var mockRepo2 = new Mock(); + + // arrange + var controller = new UiSettingsController(new UiSettings(), mockRepo.Object, mockRepo2.Object); + + // act + var result = controller.Get(); + var okResult = result as OkObjectResult; + + // assert + Assert.NotNull(okResult); + Assert.Equal(200, okResult.StatusCode); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Landmark.OLA.Tests.csproj b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Landmark.OLA.Tests.csproj new file mode 100644 index 0000000..575788a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Landmark.OLA.Tests.csproj @@ -0,0 +1,27 @@ + + + false + net8.0 + false + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Enums/LoanTypeTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Enums/LoanTypeTest.cs new file mode 100644 index 0000000..f5234b5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Enums/LoanTypeTest.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Loan.Validators; + +namespace Landmark.OLA.Tests.Loan.Enums +{ + //Check that all Loan Types are included in various settings + + public class LoanTypeTest + { + private static List GetLoanTypes() + { + return [.. Enum.GetValues(typeof(LoanType)) + .Cast() + .Where(e => !e.ToString().StartsWith("CC_"))]; + } + + [Fact] + public void TermLengthCheck() + { + _ = new LoanApplicationValidator(); + + foreach (var loanType in GetLoanTypes()) + { + switch (loanType) + { + case LoanType.EQ_Mtg_Refi_Express: + Assert.NotEmpty(TermLengths.MortgageRefiExpress); + break; + case LoanType.EQ_Home_Equity_Fixed: + Assert.NotEmpty(TermLengths.HomeEquity); + break; + case LoanType.V_Vehicle: + case LoanType.V_VehiclePreapproval: + Assert.NotEmpty(TermLengths.TermsByYear); + break; + case LoanType.EQ_Mfg_Home_With_or_Without_Land: + case LoanType.T_TrailerMotorhome: + Assert.NotEmpty(TermLengths.MfdHomeAndRvByAmount); + break; + case LoanType.M_Boat: + Assert.NotEmpty(TermLengths.BoatLoansByAmount); + break; + case LoanType.P_Powersport: + Assert.NotEmpty(TermLengths.PowersportByAmount); + break; + case LoanType.S_Signature: + Assert.NotEmpty(TermLengths.SignatureByAmount); + break; + case LoanType.S_Special: + Assert.NotEmpty(TermLengths.Special); + break; + default: + Assert.False(LoanApplicationValidator.IsTermRequired(loanType), + $"{loanType} does not have any term lengths or is not excluded from the test."); + break; + + } + } + } + + [Fact] + public void PurposeCheck() + { + foreach (var loanType in GetLoanTypes()) + { + Assert.True(LoanPurpose.TypePurposes.ContainsKey(loanType), $"{loanType} does not have any set purposes."); + } + } + + [Fact] + public void LimitCheck() + { + foreach (var loanType in GetLoanTypes()) + { + Assert.True(LoanLimits.Limits.ContainsKey(loanType), $"{loanType} does not have a set limit."); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/BranchConfigValidatorTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/BranchConfigValidatorTest.cs new file mode 100644 index 0000000..668ade5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/BranchConfigValidatorTest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Xunit; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Validators; + +namespace Landmark.OLA.Tests.Loan.Validators +{ + public class BranchConfigValidatorTest + { + + [Fact] + public void NoDuplicateZipCodesTest() + { + var validator = new BranchConfigValidator(); + + var badConfig = new BranchConfig + { + BranchCodesByZip = [ + new BranchData("53207", 1234567), + new BranchData("53207", 1234437) + ] + }; + + var goodConfig = new BranchConfig + { + BranchCodesByZip = [ + new BranchData("53207", 1234567), + new BranchData("53209", 1234437) + ] + }; + + var singleConfig = new BranchConfig + { + BranchCodesByZip = [ + new BranchData("53209", 1234437) + ] + }; + + Assert.True(validator.Validate(singleConfig).IsValid); + Assert.True(validator.Validate(goodConfig).IsValid); + Assert.False(validator.Validate(badConfig).IsValid); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorSpouseTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorSpouseTest.cs new file mode 100644 index 0000000..c3bd5a4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorSpouseTest.cs @@ -0,0 +1,217 @@ +using System; +using Xunit; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Loan.Validators; +using Landmark.OLA.Models.Residences; + +namespace Landmark.OLA.Tests.Loan.Validators +{ + public class LoanApplicationValidatorSpouseTest + { + private readonly LoanApplication _LoanApplication = new(DateTime.Parse("2018-02-21T16:52:47.62")) + { + LoanType = LoanType.CC_CashBack, + RequestedAmount = 6000m, + PrimaryContactMethod = "1234567890", + Purpose = LoanPurpose.Purpose.Other, + PrimaryBorrower = new Individual(id: new Guid("8066fa2a-16ba-4148-b28c-2926ba263c5f")) + { + FirstName = "Jane", + MiddleInitial = "A", + LastName = "Doe", + DOB = new DateTime(2018, 7, 9), + Phone = "1235555555", + PhoneType = PhoneType.Home, + SSN = "000090001", + Email = "test@example.com", + IsMarried = false, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("bf7e502a-9830-43c6-b603-1a32294c7e1a")) + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151", + MonthsResided = 12, + ResidenceType = ResidenceTypeOption.Own, + MonthlyPayment = 100, + }, + MailingAddress = new Address(new Guid("f84db562-36a0-4039-9ce5-346a2ba6899b")) + { + Address1 = "321 Main St", + City = "New Berlin", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53151" + }, + Employers = + [ + new Employer(employerId: new Guid("eee4efa3-327f-4ce5-9e8b-8c3e7a865fff")) + { + Name = "College Pro Painters", + Title = "tester", + WorkPhone = "9992245555", + MonthsEmployed = 60, + StartDate = new DateTime(2017, 01, 01), + CurrentEmployer = true, + EmploymentType = EmploymentTypeOption.FullTime, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 5000.0m, + Frequency = Frequency.Annually, + IncomeStatus = IncomeStatus.IncomeStatus_Included + } + } + ] + }, + }; + + private readonly CoApplicant _CoApplicant = new(new Guid("ba99037f-df92-46d4-a477-ffecea1f4837")) + { + FirstName = "John", + LastName = "Smith", + DOB = new DateTime(2018, 7, 25), + SSN = "111221111", + Phone = "5555550983", + PhoneType = PhoneType.Mobile, + Email = "john.smith@example.org", + IsMarried = true, + Language = LanguageOption.English, + Address = new CurrentResidenceAddress(new Guid("ae795a5f-1ea3-4186-b4b8-e9f9b81e3059")) + { + Address1 = "339 1st St", + Address2 = "#76", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 12, + MonthlyPayment = 100, + }, + PreviousAddresses = + [ + new ResidenceAddress(new Guid("e7e881f0-d371-4371-8022-cce4f700e6d6")) + { + Address1 = "339 1st St", + Address2 = "#80", + City = "Milwaukee", + State = StateOption.WI, + Country = CountryOption.UnitedStates, + PostalCode = "53202", + ResidenceType = ResidenceTypeOption.Rent, + MonthsResided = 6, + }, + ], + Employers = + [ + new Employer(employerId: new Guid("e8436884-0e6c-4144-8de5-1f44c7936a23")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Chocolate Teapot Manfuacturing Inc", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Lead Teapot Assembler", + WorkPhone = "5556783241", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + new Employer(employerId: new Guid("58d25f44-1260-428d-ac2c-d8804b06ed66")) + { + StartDate = new DateTime(2015, 01, 01), + Name = "Vanilla Teapot Manfuacturing Coop", + EmploymentType = EmploymentTypeOption.PartTime, + Title = "Junior Teapot Assembler", + WorkPhone = "5559572318", + CurrentEmployer = true, + Language = LanguageOption.English, + Income = new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Work", + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }, + }, + ], + }; + + [Fact] + public void Unmarried() + { + var validator = new LoanApplicationValidator(); + + var result = validator.Validate(_LoanApplication); + Assert.True(result.IsValid); + } + + [Fact] + public void MarriedWithoutSpouseDataInWisconsin() + { + var validator = new LoanApplicationValidator(); + + var marriedWithoutSpouse = _LoanApplication; + marriedWithoutSpouse.CoApplicant = _CoApplicant; + + var result = validator.Validate(marriedWithoutSpouse); + Assert.False(result.IsValid); + } + + [Fact] + public void CoappMarriedToPrimary() + { + var validator = new LoanApplicationValidator(); + + var marriedToPrimary = _LoanApplication; + marriedToPrimary.CoApplicant = _CoApplicant; + marriedToPrimary.CoApplicant.IsMarriedToPrimaryBorrower = true; + + var result = validator.Validate(marriedToPrimary); + Assert.True(result.IsValid); + } + + [Fact] + public void CoappHasNonappSpouseInWisconsin() + { + var validator = new LoanApplicationValidator(); + + var coappHasNonappSpouse = _LoanApplication; + coappHasNonappSpouse.CoApplicant = _CoApplicant; + coappHasNonappSpouse.CoApplicant.NonApplicantSpouse = new MailableSpouse + { + + }; + + var result = validator.Validate(coappHasNonappSpouse); + Assert.True(result.IsValid); + } + + [Fact] + public void MarriedCoappWithoutSpouseDataInIllinois() + { + var validator = new LoanApplicationValidator(); + + var ilCoappMarriedWithoutSpouse = _LoanApplication; + ilCoappMarriedWithoutSpouse.CoApplicant = _CoApplicant; + ilCoappMarriedWithoutSpouse.CoApplicant.Address.State = StateOption.IL; + + var result = validator.Validate(ilCoappMarriedWithoutSpouse); + Assert.True(result.IsValid); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorTest.cs new file mode 100644 index 0000000..d77f05e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorTest.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using Landmark.OLA.Models.Assets; +using Landmark.OLA.Models.Assets.Enums; +using Landmark.OLA.Models.HomeMortgageDisclosureAct; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models.Loan.Enums; +using Landmark.OLA.Models.Loan.Validators; + +namespace Landmark.OLA.Tests.Loan.Validators +{ + public class LoanApplicationValidatorTest + { + + private List validTestApplications = [ + new LoanApplication + { + LoanType = LoanType.CC_CashBack, + PrimaryContactMethod = "1234567890", + Purpose = LoanPurpose.Purpose.Other, + RequestedAmount = 5000, + PrimaryBorrower = new Individual(), + }, + new LoanApplication + { + LoanType = LoanType.EQ_Mtg_Refi_Express, + PrimaryContactMethod = "1234567890", + Purpose = LoanPurpose.Purpose.Home_Refinance, + TermLengthInMonths = 144, + RequestedAmount = 10000, + PrimaryBorrower = new Individual(new Hmda + { + Ethnicities = [Ethnicity.NotProvided], + Races = [Race.NotProvided], + Sexes = [Sex.NotProvided], + }), + RealEstate = new RealEstate + { + //This is valid from a business perspective. Model Validation would flag the empty object. + } + } + ]; + + private List invalidTestApplications = [ + + new LoanApplication + { + LoanType = LoanType.V_Vehicle, + Purpose = LoanPurpose.Purpose.Purchase, + TermLengthInMonths = 36, + RequestedAmount = 5000, + PrimaryBorrower = new Individual(), + Vehicle = new Vehicle + { + Type = VehicleType.Automobile, + } + }, + new LoanApplication + { + LoanType = LoanType.EQ_Mtg_Refi_Express, + Purpose = LoanPurpose.Purpose.Home_Refinance, + TermLengthInMonths = 144, + RequestedAmount = 5000, + PrimaryBorrower = new Individual(), + } + ]; + + [Fact] + public void IsValidTermLengthTest() + { + var validator = new LoanApplicationValidator(); + + using (StreamReader file = File.OpenText("TestData" + System.IO.Path.DirectorySeparatorChar + + "json" + System.IO.Path.DirectorySeparatorChar + "TestValidatorLoans.json")) + using (JsonTextReader reader = new JsonTextReader(file)) + { + JObject o2 = (JObject)JToken.ReadFrom(reader); + JArray a = (JArray)o2["validLoans"]; + IList validTestApps = a.ToObject>(); + foreach (var loan in validTestApps) + { + Assert.True(validator.Validate(loan).IsValid); + } + } + + foreach ( var loan in validTestApplications ) + { + Assert.True(validator.Validate(loan).IsValid); + } + + foreach (var loan in invalidTestApplications) + { + Assert.False(validator.Validate(loan).IsValid); + } + + } + + [Fact] + public void GetSignatureTermValidityTest() + { + + } + + [Fact] + public void GetMfdHomeAndRvTermValidityTest() + { + + } + + [Fact] + public void GetVehicleTermValidityTest() + { + _ = new LoanApplicationValidator(); + + using StreamReader file = File.OpenText("TestData" + Path.DirectorySeparatorChar + + "json" + Path.DirectorySeparatorChar + "TestVehicleTermValidity.json"); + using JsonTextReader reader = new(file); + JObject allData = (JObject)JToken.ReadFrom(reader); + JArray vehicles = (JArray)allData["vehicles"]; + IList validTestVehicles = vehicles.ToObject>(); + foreach (var vehicle in validTestVehicles) + { + List termList = LoanApplicationValidator.GetVehicleTermList(vehicle); + Assert.NotNull(termList); + Assert.NotEmpty(termList); + } + } + + [Fact] + public void GetBoatTermValidityTest() + { + + } + + [Fact] + public void GetPowersportTermValidityTest() + { + + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/ModelValidatorTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/ModelValidatorTest.cs new file mode 100644 index 0000000..9c26007 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/ModelValidatorTest.cs @@ -0,0 +1,128 @@ +using System; +using System.Reflection; +using System.Linq; +using System.Collections.Generic; +using Xunit; +using Landmark.OLA.Tests.TestData; +using Landmark.OLA.Models.Loan; +using Landmark.OLA.Models; + +namespace Landmark.OLA.Tests +{ + public class ModelValidatorTest + { + + [Theory] + [MemberData(nameof(TestIndividuals.GetValidTestIndividuals), MemberType = typeof(TestIndividuals))] + [MemberData(nameof(TestAddresses.GetValidTestAddresses), MemberType = typeof(TestAddresses))] + [MemberData(nameof(TestEmployers.GetValidTestEmployers), MemberType = typeof(TestEmployers))] + public void ExpectValidModelsTest(object testData) + { + ModelValidator.Validate(testData); + } + + [Theory] + [MemberData(nameof(TestIndividuals.GetInvalidTestIndividuals), MemberType = typeof(TestIndividuals))] + [MemberData(nameof(TestAddresses.GetInvalidTestAddresses), MemberType = typeof(TestAddresses))] + [MemberData(nameof(TestEmployers.GetInvalidTestEmployers), MemberType = typeof(TestEmployers))] + public void ExpectInvalidModelsTest(object testData) + { + Assert.Throws(() => ModelValidator.Validate(testData)); + } + + [Fact] + public void VerifyDataAnnotations() + { + string nspace = "Landmark.OLA.Models"; + + //LoanApplication is in the namespace we want to look into + Assembly modelAssembly = typeof(LoanApplication).GetTypeInfo().Assembly; + + var models = modelAssembly.GetTypes() + .Where(t => t.IsClass == true && string.Equals(t.Namespace, nspace, StringComparison.Ordinal)) + .ToArray(); + + var missingValidation = new List(); + + foreach(var model in models) + { + var fields = model.GetProperties() + .Where(x => !x.PropertyType.Namespace.StartsWith("Landmark") + && x.PropertyType != typeof(bool) + && x.PropertyType != typeof(Guid) + && x.PropertyType != typeof(Enum)); + + foreach (var field in fields) + { + var hasSizeValidation = false; + + var fieldType = field.PropertyType; + + if (fieldType.IsGenericType) + { + if (fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + fieldType = Nullable.GetUnderlyingType(fieldType); + } + else + { + fieldType = fieldType.GetGenericArguments()[0]; + } + } + + if (fieldType.BaseType == typeof(Enum) + || fieldType == typeof(bool) + || fieldType == typeof(Guid) + || fieldType == typeof(DateTime) + || fieldType.IsClass) + { + //enum, bool, guid are restricted to specific sizes or values + //DateTime seems safe? + //Landmark classes will already have their validation checked + hasSizeValidation = true; + } + + if(field.SetMethod?.IsPrivate ?? false) + { + //If inbound data ins't accepted we won't require validation + hasSizeValidation = true; + } + + if (!hasSizeValidation) + { + foreach (var attribute in field.GetCustomAttributes() + .Where(x => x.TypeId.ToString().StartsWith("System.ComponentModel.DataAnnotations"))) + { + var sizeValidationAttributeTypes = new List + { + typeof(System.ComponentModel.DataAnnotations.MaxLengthAttribute), + typeof(System.ComponentModel.DataAnnotations.RangeAttribute), + typeof(System.ComponentModel.DataAnnotations.StringLengthAttribute), + }; + + if (sizeValidationAttributeTypes.Contains(attribute.GetType())) + { + hasSizeValidation = true; + } + } + } + + if(!hasSizeValidation) + { + missingValidation.Add($"{model.Name} : {field.Name}"); + } + } + } + + Assert.Empty(missingValidation); + } + + private T GetAttributeFrom(object instance, string propertyName) where T : Attribute + { + var attrType = typeof(T); + var property = instance.GetType().GetProperty(propertyName); + return (T)property.GetCustomAttributes(attrType, false).First(); + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Services/CoreAccessServiceTest.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Services/CoreAccessServiceTest.cs new file mode 100644 index 0000000..3d3680b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Services/CoreAccessServiceTest.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using Xunit; +using Landmark.OLA.Application.Services; +using Landmark.OLA.Models.Core; + +namespace Landmark.OLA.Tests.Services +{ + public class CoreAccessServiceTest + { + + [Fact] + public void FindOwners_VariableResponses() + { + var service = new CoreAccessService(); + + // arrange + var owners = new List() + { + new() + { + Name = "John Doe", + BirthDate = "2000-01-01", + TaxId = "111-22-3333" + }, + new() + { + Name = "Jane Doe", + BirthDate = null, + TaxId = "111-22-4444" + }, + new() + { + Name = "Janus Doe", + BirthDate = "1995-08-08", + TaxId = null + }, + new() + { + Name = "Tester Doe", + BirthDate = "1995-09-08", + TaxId = "123-45-6789" + } + }; + + var find1 = new Application.Controllers.OnlineIdentityFormData() { + DateOfBirth = "09/08/1995", + SocialSecurityNumber = "6789" + }; + var noFind1 = new Application.Controllers.OnlineIdentityFormData() { + DateOfBirth = "09/08/1995", + SocialSecurityNumber = "3333" + }; + var noFind2 = new Application.Controllers.OnlineIdentityFormData() { + DateOfBirth = "01/01/2000", + SocialSecurityNumber = "6789" + }; + + // act + var found1 = service.FindMatchingOwner(find1, owners); + var notFound1 = service.FindMatchingOwner(noFind1, owners); + var notFound2 = service.FindMatchingOwner(noFind2, owners); + + // assert + Assert.NotNull(found1); + Assert.Null(notFound1); + Assert.Null(notFound2); + Assert.Equal("Tester Doe", found1.Name); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestAddresses.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestAddresses.cs new file mode 100644 index 0000000..bf34ee7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestAddresses.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Landmark.OLA.Models.Addresses.Enums; +using Landmark.OLA.Models.Residences; + +namespace Landmark.OLA.Tests.TestData +{ + public class TestAddresses + { + public static CurrentResidenceAddress GetValidAddress() + { + return new CurrentResidenceAddress + { + Address1 = "123 Main St", + City = "New Berlin", + State = StateOption.WI, + PostalCode = "53151", + Country = CountryOption.UnitedStates, + ResidenceType = ResidenceTypeOption.Own, + MonthsResided = 18, + MonthlyPayment = 100, + }; + } + + public static IEnumerable GetValidTestAddresses() + { + yield return new Object[] { GetValidAddress() }; + } + + public static IEnumerable GetInvalidTestAddresses() + { + var address1Empty = GetValidAddress(); + address1Empty.Address1 = " "; + yield return new Object[] { address1Empty }; + + var address1TooLong = GetValidAddress(); + address1TooLong.Address1 = new string('x', 45); //45 char string + yield return new Object[] { address1TooLong }; + + var address2TooLong = GetValidAddress(); + address2TooLong.Address2 = new string('x', 45); //45 char string + yield return new Object[] { address2TooLong }; + + var cityTooLong = GetValidAddress(); + cityTooLong.City = new string('x', 105); //105 char string + yield return new Object[] { cityTooLong }; + + var postalCodeTooLong = GetValidAddress(); + postalCodeTooLong.PostalCode = new string('x', 10); //10 char string + yield return new Object[] { postalCodeTooLong }; + + var missingState = GetValidAddress(); + missingState.State = null; + yield return new Object[] { missingState }; + + var missingAddress1 = GetValidAddress(); + missingAddress1.Address1 = null; + yield return new Object[] { missingAddress1 }; + + var missingCity = GetValidAddress(); + missingCity.City = null; + yield return new Object[] { missingCity }; + + var missingPostalCode = GetValidAddress(); + missingPostalCode.PostalCode = null; + yield return new Object[] { missingPostalCode }; + + var missingResidenceType = GetValidAddress(); + missingResidenceType.ResidenceType = null; + yield return new Object[] { missingResidenceType }; + + var missingMonthsResided = GetValidAddress(); + missingMonthsResided.MonthsResided = null; + yield return new Object[] { missingMonthsResided }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestEmployers.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestEmployers.cs new file mode 100644 index 0000000..6dd1abb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestEmployers.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Tests.TestData +{ + public class TestEmployers + { + private static Employer GetValidEmployer() + { + return new Employer + { + Name = "Test Employer", + Title = "QA", + WorkPhone = "9992245555", + Income = TestIncomes.GetValidEmploymentIncome(), + CurrentEmployer = false, + EmploymentType = EmploymentTypeOption.PartTime, + StartDate = new DateTime(2017, 10, 1), + }; + } + + public static IEnumerable GetValidTestEmployers() + { + yield return new Object[] { GetValidEmployer() }; + } + + public static IEnumerable GetInvalidTestEmployers() + { + var missingName = GetValidEmployer(); + missingName.Name = null; + yield return new Object[] { missingName }; + + var invalidEmployerName = GetValidEmployer(); + invalidEmployerName.Name+="."; + yield return new Object[] { invalidEmployerName }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIncomes.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIncomes.cs new file mode 100644 index 0000000..f8d2cc6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIncomes.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; + +namespace Landmark.OLA.Tests.TestData +{ + public class TestIncomes + { + private static Income GetValidIncome() + { + return new Income + { + IncomeTypeCD = IncomeType.Salary, + SourceName = "Salary", + AmountPerPeriod = 1000, + Frequency = Frequency.Monthly, + IncomeStatus = IncomeStatus.IncomeStatus_Included + }; + } + + public static Income GetValidEmploymentIncome() + { + return GetValidIncome(); + } + + public static IEnumerable GetValidTestIncomes() + { + yield return new Object[] { GetValidIncome() }; + } + + public static IEnumerable GetInvalidTestIncomes() + { + var missingSourceName = GetValidIncome(); + missingSourceName.SourceName = null; + yield return new Object[] { missingSourceName }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIndividuals.cs b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIndividuals.cs new file mode 100644 index 0000000..8c24dc6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIndividuals.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Landmark.OLA.Models.Individuals; +using Landmark.OLA.Models.Individuals.Enums; +using Landmark.OLA.Tests.TestData; + +namespace Landmark.OLA.Tests +{ + public class TestIndividuals + { + + private static Individual GetValidIndividual() + { + return new Individual + { + FirstName = "First", + LastName = "Last", + MiddleInitial = "M", + DOB = DateTime.Now, + SSN = "123456789", + Phone = "5551234567", + PhoneType = PhoneType.Home, + Email = "example@test.com", + IsMarried = true, + Address = TestAddresses.GetValidAddress(), + Language = LanguageOption.English, + }; + } + + public static IEnumerable GetValidTestIndividuals() + { + yield return new Object[] { GetValidIndividual() }; + + //Different but valid phone formatting + var phoneFormatting = GetValidIndividual(); + phoneFormatting.Phone = "+1 (555) 123-4567 x123"; + yield return new Object[] { phoneFormatting }; + } + + public static IEnumerable GetInvalidTestIndividuals() + { + var missingEmail = GetValidIndividual(); + missingEmail.Email = null; + yield return new Object[] { missingEmail }; + + var missingDob = GetValidIndividual(); + missingDob.DOB = null; + yield return new Object[] { missingDob }; + + var missingAddress = GetValidIndividual(); + missingAddress.Address = null; + yield return new Object[] { missingAddress }; + + var badMI = GetValidIndividual(); + badMI.MiddleInitial = "Name"; + yield return new Object[] { badMI }; + + var missingPhoneType = GetValidIndividual(); + missingPhoneType.PhoneType = null; + yield return new Object[] { missingPhoneType }; + + var invalidSssnChars = GetValidIndividual(); + invalidSssnChars.SSN = "123-45-6789"; + yield return new Object[] { invalidSssnChars }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestValidatorLoans.json b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestValidatorLoans.json new file mode 100644 index 0000000..01c5eb4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestValidatorLoans.json @@ -0,0 +1,121 @@ + +{ + "validLoans": [ + { + "loanType": "V_Vehicle", + "note": "testing 2", + "primaryBorrower": { + "language": "English", + "firstName": "Joe", + "middleInitial": null, + "lastName": "TEST", + "dob": "01/01/1990", + "ssn": "123456789", + "phone": "1112223333", + "phoneType": "Home", + "secondaryPhone": null, + "secondaryPhoneType": null, + "email": "test@test.test", + "isMarried": false, + "address": { + "address1": "123 Main St", + "address2": "", + "city": "Milwaukee", + "state": "WI", + "postalCode": "53202", + "postalCodeExt": null, + "country": "UnitedStates", + "residenceType": "Own", + "monthsResided": 101, + "monthlyPayment": "500" + }, + "previousAddresses": null, + "otherIncomes": [ + { + "incomeTypeCD": "Other", + "sourceName": "IRA", + "frequency": "Annually", + "amountPerPeriod": "99999", + "incomeStatus": "IncomeStatus_Included" + } + ], + "hasOptedOutTCPA": false, + "customerNumber": "0", + "employers": [] + }, + "purpose": "Purchase", + "requestedAmount": "5000", + "termLengthInMonths": "75", + "vehicle": { + "valuationType": "Manual", + "condition": "New", + "seller": "Dealer", + "type": "Automobile", + "year": "2030", + "mileage": "10", + "make": "Chevy", + "model": "Caprice classic", + "initialEstimatedValue": "50000" + } + }, + { + "LoanType": "CC_CashBack", + "Purpose": "Other", + "RequestedAmount": "5000", + "PrimaryBorrower": { + } + }, + { + "LoanType": "V_Vehicle", + "Purpose": "Purchase", + "TermLengthInMonths": "36", + "RequestedAmount": "5000", + "PrimaryBorrower": {}, + "Vehicle": { + "Year": 0, + "Condition": "New", + "Type": "Automobile" + } + }, + { + "LoanType": "P_Powersport", + "Purpose": "Purchase", + "TermLengthInMonths": "72", + "RequestedAmount": "8000", + "PrimaryBorrower": {}, + "Vehicle": { + "ValuationType": "Manual", + "Condition": "Used", + "Type": "Snowmobile", + "Year": "2019", + "Make": "SKIDOO", + "Model": "MXZ", + "InitialEstimatedValue": "10500" + } + }, + { + "LoanType": "V_Vehicle", + "Purpose": "Purchase", + "TermLengthInMonths": "36", + "RequestedAmount": "5000", + "PrimaryBorrower": {}, + "Vehicle": { + "Year": 9999, + "Condition": "New", + "Type": "Automobile" + } + }, + { + "LoanType": "V_Vehicle", + "Purpose": "Purchase", + "TermLengthInMonths": "36", + "RequestedAmount": "5000", + "PrimaryBorrower": {}, + "Vehicle": { + "Year": 2010, + "Condition": "New", + "Type": "Automobile" + } + } + ] +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestVehicleTermValidity.json b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestVehicleTermValidity.json new file mode 100644 index 0000000..1bdc9af --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestVehicleTermValidity.json @@ -0,0 +1,45 @@ +{ + "vehicles": [ + { + "Year": 0, + "Condition": "New", + "Type": "Automobile" + }, + { + "Year": 9999, + "Condition": "New", + "Type": "Automobile" + }, + { + "Year": 2009, + "Condition": "New", + "Type": "Automobile" + }, + { + "Year": 2010, + "Condition": "New", + "Type": "Automobile" + }, + { + "Year": 2018, + "Condition": "New", + "Type": "Automobile" + }, + { + "Year": 2019, + "Condition": "New", + "Type": "Automobile" + }, + { + "valuationType": "Manual", + "condition": "New", + "seller": "Dealer", + "type": "Automobile", + "year": "2030", + "mileage": "10", + "make": "Chevy", + "model": "Caprice classic", + "initialEstimatedValue": "50000" + } + ] +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/additional_test_args.runsettings b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/additional_test_args.runsettings new file mode 100644 index 0000000..c6fe146 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/additional_test_args.runsettings @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + .*microsoft.* + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.sln b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.sln new file mode 100644 index 0000000..5ba2ef5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/Landmark.OLA.sln @@ -0,0 +1,86 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Landmark.OLA.Application", "Landmark.OLA.Application\Landmark.OLA.Application.csproj", "{18FE7B90-2D2E-483E-99EB-577648D8A47A}" + ProjectSection(ProjectDependencies) = postProject + {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7} = {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7} + {CBC0628B-00EC-473C-A898-3745CD258761} = {CBC0628B-00EC-473C-A898-3745CD258761} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Landmark.OLA.Models", "Landmark.OLA.Models\Landmark.OLA.Models.csproj", "{1A10FFB0-11EF-4D55-A73B-957F61F2E70E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Landmark.OLA.Tests", "Landmark.OLA.Tests\Landmark.OLA.Tests.csproj", "{C982CE65-72F9-46A9-8465-24BC1774CD7F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CRIF.ACTion", "CRIF.ACTion\CRIF.ACTion.csproj", "{FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CRIF.ACTion.Tests", "CRIF.ACTion.Tests\CRIF.ACTion.Tests.csproj", "{A7A004FA-F16F-4E25-9513-32D8B8E368D8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "USPS", "USPS\USPS.csproj", "{CBC0628B-00EC-473C-A898-3745CD258761}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "USPS.Tests", "USPS.Tests\USPS.Tests.csproj", "{D5CA85F2-8A40-4522-9545-BA09643C0F66}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnlineBankingAuth", "OnlineBankingAuth\OnlineBankingAuth.csproj", "{F7A788F3-9170-4299-995D-6812CEAD69C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnlineBankingAuth.Tests", "OnlineBankingAuth.Tests\OnlineBankingAuth.Tests.csproj", "{CDFF56B2-EEEA-4197-9D21-9AE405D7CB55}" + ProjectSection(ProjectDependencies) = postProject + {18FE7B90-2D2E-483E-99EB-577648D8A47A} = {18FE7B90-2D2E-483E-99EB-577648D8A47A} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Landmark.OLA.Integration.Tests", "Landmark.OLA.Integration.Tests\Landmark.OLA.Integration.Tests.csproj", "{89D8F8D3-52E1-42D2-A72F-064C2583BFAA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {18FE7B90-2D2E-483E-99EB-577648D8A47A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18FE7B90-2D2E-483E-99EB-577648D8A47A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18FE7B90-2D2E-483E-99EB-577648D8A47A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18FE7B90-2D2E-483E-99EB-577648D8A47A}.Release|Any CPU.Build.0 = Release|Any CPU + {1A10FFB0-11EF-4D55-A73B-957F61F2E70E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A10FFB0-11EF-4D55-A73B-957F61F2E70E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A10FFB0-11EF-4D55-A73B-957F61F2E70E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A10FFB0-11EF-4D55-A73B-957F61F2E70E}.Release|Any CPU.Build.0 = Release|Any CPU + {C982CE65-72F9-46A9-8465-24BC1774CD7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C982CE65-72F9-46A9-8465-24BC1774CD7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C982CE65-72F9-46A9-8465-24BC1774CD7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C982CE65-72F9-46A9-8465-24BC1774CD7F}.Release|Any CPU.Build.0 = Release|Any CPU + {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAE6BB74-6E40-4BC1-8A67-2D6A4D9FB9C7}.Release|Any CPU.Build.0 = Release|Any CPU + {A7A004FA-F16F-4E25-9513-32D8B8E368D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7A004FA-F16F-4E25-9513-32D8B8E368D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7A004FA-F16F-4E25-9513-32D8B8E368D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7A004FA-F16F-4E25-9513-32D8B8E368D8}.Release|Any CPU.Build.0 = Release|Any CPU + {CBC0628B-00EC-473C-A898-3745CD258761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBC0628B-00EC-473C-A898-3745CD258761}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBC0628B-00EC-473C-A898-3745CD258761}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBC0628B-00EC-473C-A898-3745CD258761}.Release|Any CPU.Build.0 = Release|Any CPU + {D5CA85F2-8A40-4522-9545-BA09643C0F66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5CA85F2-8A40-4522-9545-BA09643C0F66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5CA85F2-8A40-4522-9545-BA09643C0F66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5CA85F2-8A40-4522-9545-BA09643C0F66}.Release|Any CPU.Build.0 = Release|Any CPU + {F7A788F3-9170-4299-995D-6812CEAD69C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7A788F3-9170-4299-995D-6812CEAD69C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7A788F3-9170-4299-995D-6812CEAD69C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7A788F3-9170-4299-995D-6812CEAD69C9}.Release|Any CPU.Build.0 = Release|Any CPU + {CDFF56B2-EEEA-4197-9D21-9AE405D7CB55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDFF56B2-EEEA-4197-9D21-9AE405D7CB55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDFF56B2-EEEA-4197-9D21-9AE405D7CB55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDFF56B2-EEEA-4197-9D21-9AE405D7CB55}.Release|Any CPU.Build.0 = Release|Any CPU + {89D8F8D3-52E1-42D2-A72F-064C2583BFAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89D8F8D3-52E1-42D2-A72F-064C2583BFAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89D8F8D3-52E1-42D2-A72F-064C2583BFAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89D8F8D3-52E1-42D2-A72F-064C2583BFAA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DDBE87C6-A8DF-4767-A77D-FA5115D2C031} + EndGlobalSection +EndGlobal diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/AuthenticationParserTest.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/AuthenticationParserTest.cs new file mode 100644 index 0000000..5d4cbb3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/AuthenticationParserTest.cs @@ -0,0 +1,24 @@ +using System.Xml; +using Xunit; +using OnlineBankingAuth.Parsers; + +namespace OnlineBankingAuth.Tests +{ + public class AuthenticationParserTest + { + [Fact] + public void AuthenticationParse() + { + XmlDocument doc = new XmlDocument(); + doc.Load("ExampleXML" + System.IO.Path.DirectorySeparatorChar + "DecryptedAssertion.xml"); + + XmlElement readAssertion = doc.DocumentElement; + + var parser = new SAMLParser(); + var parsedAssertion = parser.ParseAssertion(readAssertion); + + Assert.Equal("DI0019", parsedAssertion.FI_ID); + Assert.Equal("123456", parsedAssertion.MemberID); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/CookieCrytorTest.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/CookieCrytorTest.cs new file mode 100644 index 0000000..fa40325 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/CookieCrytorTest.cs @@ -0,0 +1,21 @@ +using Xunit; +using Landmark.OLA.Application.Common.Security; + +namespace OnlineBankingAuth.Tests +{ + public class CookieCrytorTest + { + [Fact] + public void EncryptAndDecrypt() + { + var sampleUnencryptedString = "This string will be encrypted and decrypted in this test"; + var secret = "A secret to serve as a key in our symmetric encryption"; + + var encrypted = CookieCryptor.EncryptJwt(sampleUnencryptedString, secret); + Assert.NotEqual(sampleUnencryptedString, encrypted); + + var decrypted = CookieCryptor.DecryptJwt(encrypted, secret); + Assert.Equal(sampleUnencryptedString, decrypted); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/ExampleXML/DecryptedAssertion.xml b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/ExampleXML/DecryptedAssertion.xml new file mode 100644 index 0000000..bd8b206 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/ExampleXML/DecryptedAssertion.xml @@ -0,0 +1,99 @@ + + pfd.qal1.digitalinsight.com + + + + + + + + + + + IH1Nf8hwUP2FPf9ozez8Fsx9HEE5uBggOXuOISfvdgk= + + + + pCpwRldAMg3IUHyusRLznzFoEeHbGlYKyZl9vyi6hwL/z+afqN9kRUTgtQwnwnwk7mUZmhdInP4P + MGUvKm2LFFu6BQ7iWHOqE93ZJ9gwbJbj+VnKsKxeepNcH/vDamTx/iTJ4lR3pKzISJTOvllND72s + 6LTvCpfTs6aAVv7qJDrJTkMUNOPVzy1YELy+T68GoZ25rJ1byNOSJkN9cdHgfqgI43p86eVYuKiK + lTDXWpyxyfuV24AgDbUP8xy8f57DtlRvyThZ92iS5L5zA9DpY+w44DhPyOCm9TxMKOJO1KaDKbbb + dCL+IKHuPaBoqY62FaV1HjGx6ZyEdu5rgcGhSQ== + + + + + MIIGejCCBWKgAwIBAgIQBuighc3hzI6Q8zikuBZQvzANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG + EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5EaWdpQ2VydCBTSEEyIFNlY3Vy + ZSBTZXJ2ZXIgQ0EwHhcNMTgwODE3MDAwMDAwWhcNMTkxMDE2MTIwMDAwWjCBszELMAkGA1UEBhMC + VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExGTAXBgNVBAcTEFdlc3RsYWtlIFZpbGxhZ2UxJDAiBgNV + BAoTG0RpZ2l0YWwgSW5zaWdodCBDb3Jwb3JhdGlvbjEjMCEGA1UECxMaUG93ZXJlZCBCeSBEaWdp + dGFsIEluc2lnaHQxKTAnBgNVBAMTIHBmZC1zaWduLnFhbDEuZGlnaXRhbGluc2lnaHQuY29tMIIB + IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtff3NhEQw2GeTGoI97G28t7/plzLCBe9kBay + q9knqD2OC0yOrfT65MGmHjq8PrGZGR7xIgeP282li+M71ITPjs+tny+njCBv53GdRW+5DqREteJS + syUS5ydXZPcuHBCFS6g/lEvojsbOeeQoCMiMfS2qfC81HdVqpsiQiQMC5ksbGKAdFPHd1K/tx0OX + IYsQRmUWU4GN6sSO8+uYfE/dyC4fYGd/0LSVkwYGZubTVK5e84plaNOYbC5rSHOiyAJTMvXaU8PP + 2oobp3vYDRxIsvj/vPZtzSJqSYahXSXK+qP3yOcNfx9BlWQagkkEg6sKRJfHFCR0JngnSojdwTOR + TQIDAQABo4IC7TCCAukwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeNRji0LOHG2eIwHQYDVR0OBBYE + FM0cZ8MrIy+kauc6Fu+Y+jnSzLKhMCsGA1UdEQQkMCKCIHBmZC1zaWduLnFhbDEuZGlnaXRhbGlu + c2lnaHQuY29tMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw + awYDVR0fBGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5j + cmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTItZzYuY3JsMEwGA1Ud + IARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv + bS9DUFMwCAYGZ4EMAQICMHwGCCsGAQUFBwEBBHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Au + ZGlnaWNlcnQuY29tMEYGCCsGAQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln + aUNlcnRTSEEyU2VjdXJlU2VydmVyQ0EuY3J0MAkGA1UdEwQCMAAwggEFBgorBgEEAdZ5AgQCBIH2 + BIHzAPEAdwC72d+8H4pxtZOUI5eqkntHOFeVCqtS6BqQlmQ2jh7RhQAAAWVFMDGaAAAEAwBIMEYC + IQDkZdJn7l+5cuCvTc1IBhWbPGRnBlzlF0idM5vW+6hJ5QIhAILyNNA0R/CknRpy1C/gwBq0bTwx + a8943O1cl0LqhMFeAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16ggw8AAAFlRTAxfwAA + BAMARzBFAiEAt0eDmAR+709rXPYcui9OMmMF25uzitbdRNaepZD2cfICIFLmOF+ZH6rFB/bke6Ql + zX4Abq8lEfT845V/CAWnenXoMA0GCSqGSIb3DQEBCwUAA4IBAQB+wiMTrhyBaFalIeUlTAQaskMe + CXCJ5N7ORozblg2Dz97Vs/CIOod2ucwKFhr/n7Fh+K5ch3huEh8pupqqI3TIMvS2fWHkKmZV1tWt + b2X6B6aRW8bmkJea+Hc03hO7QruGw3kESSzfYO8OrgFd5Aww1+oTATmzsPnl19EZfbK2ustmeDS/ + 0E1eENN5uAZDus4NqQZkWDlVcABtXcKXORzayiH4zU3Ghh59tYSZ6uzHnyDG7vA4QJENxdYHL8ed + 1qdODv6a7rJhPXSOwH7gNpINRTNe0SJQPgtgzQiBIgpR079+05vJjwqF2nXyVvZqI1YKPNQLjdKc + coyQJC0B0qmB + + + + + + tff3NhEQw2GeTGoI97G28t7/plzLCBe9kBayq9knqD2OC0yOrfT65MGmHjq8PrGZGR7xIgeP282l + i+M71ITPjs+tny+njCBv53GdRW+5DqREteJSsyUS5ydXZPcuHBCFS6g/lEvojsbOeeQoCMiMfS2q + fC81HdVqpsiQiQMC5ksbGKAdFPHd1K/tx0OXIYsQRmUWU4GN6sSO8+uYfE/dyC4fYGd/0LSVkwYG + ZubTVK5e84plaNOYbC5rSHOiyAJTMvXaU8PP2oobp3vYDRxIsvj/vPZtzSJqSYahXSXK+qP3yOcN + fx9BlWQagkkEg6sKRJfHFCR0JngnSojdwTORTQ== + + AQAB + + + + + + tpvcerts1 + + + + + + + landmarkcu.digitalinsight.test + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified + + + + + 123456 + + + DI0019 + + + https://www.iqa019.com/tob/live/usp-core/app/launch?code=ping + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/OnlineBankingAuth.Tests.csproj b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/OnlineBankingAuth.Tests.csproj new file mode 100644 index 0000000..a136e29 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/OnlineBankingAuth.Tests.csproj @@ -0,0 +1,26 @@ + + + false + net8.0 + false + + + + + + + + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/TokenServiceTest.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/TokenServiceTest.cs new file mode 100644 index 0000000..b21a9f6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/TokenServiceTest.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using OnlineBankingAuth.Configuration; +using OnlineBankingAuth.Tokens; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Models.OnlineBanking; + +namespace OnlineBankingAuth.Tests +{ + public class TokenServiceTest + { + public static SecuritySettings _settings = new SecuritySettings + { + TokenSecret = "Some fake secret for crypt" + }; + + //public static IOptions _options = IOptions(_settings); + public static IOptions _options = Options.Create(_settings); + + TokenService _tokenService = new TokenService(_options); + + [Fact] + public void ShouldGenerateTokenAndRetrieveTokenFromCookie() + { + var assertion = new DISAMLAssertion + { + MemberID = "123456", + FI_ID = "7890" + }; + var obToken = new OnlineBankingToken(assertion, null, false); + var token = _tokenService.CreateOnlineBankingJWT(obToken); + + var cookies = new Dictionary + { + { Constants.TokenPayload, token }, + { Constants.TokenAuthorization, "Role=" + Constants.TokenAuthorizationString } + }; + + var cookieMock = new Mock(); + cookieMock.Setup(c => c[It.IsAny()]) + .Returns((string key) => cookies.TryGetValue(key, out var v) ? v : null); + cookieMock.Setup(c => c.ContainsKey(It.IsAny())) + .Returns((string key) => cookies.ContainsKey(key)); + cookieMock.Setup(c => c.TryGetValue(It.IsAny(), out It.Ref.IsAny)) + .Returns((string key, out string value) => + { + var ok = cookies.TryGetValue(key, out var v); + value = v; + return ok; + }); + + var context = new DefaultHttpContext(); + context.Request.Cookies = cookieMock.Object; + + var retrievedAssertion = _tokenService.GetAuthenticatedDataFromCookie(context); + + Assert.Equal(assertion.FI_ID, retrievedAssertion.FI_ID); + Assert.Equal(assertion.MemberID, retrievedAssertion.MemberID); + } + + [Fact] + public void ShouldRenewCookie() + { + var assertion = new DISAMLAssertion + { + MemberID = "123456", + FI_ID = "7890" + }; + + var obToken = new OnlineBankingToken(assertion, null, false); + var token = _tokenService.CreateOnlineBankingJWT(obToken); + + var cookies = new Dictionary + { + { Constants.TokenPayload, token }, + { Constants.TokenAuthorization, "Role=" + Constants.TokenAuthorizationString } + }; + + var cookieMock = new Mock(); + cookieMock.Setup(c => c[It.IsAny()]) + .Returns((string key) => cookies.TryGetValue(key, out var v) ? v : null); + cookieMock.Setup(c => c.ContainsKey(It.IsAny())) + .Returns((string key) => cookies.ContainsKey(key)); + cookieMock.Setup(c => c.TryGetValue(It.IsAny(), out It.Ref.IsAny)) + .Returns((string key, out string value) => + { + var ok = cookies.TryGetValue(key, out var v); + value = v; + return ok; + }); + + var context = new DefaultHttpContext(); + context.Request.Cookies = cookieMock.Object; + + var renewedSession = _tokenService.RenewOnlineBankingJWT(context); + + var tokenHandler = new JwtSecurityTokenHandler(); + + var firstEncryptedToken = CookieCryptor.DecryptJwt(token[Constants.TokenPrefix.Length..], _settings.TokenSecret); + var firstToken = tokenHandler.ReadJwtToken(firstEncryptedToken); + + var secondEncryptedToken = CookieCryptor.DecryptJwt(renewedSession[Constants.TokenPrefix.Length..], _settings.TokenSecret); + var secondToken = tokenHandler.ReadJwtToken(secondEncryptedToken); + + Assert.Equal(firstToken.Payload[Constants.MemberID], secondToken.Payload[Constants.MemberID]); + Assert.Equal(firstToken.Payload[Constants.FI_ID], secondToken.Payload[Constants.FI_ID]); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/additional_test_args.runsettings b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/additional_test_args.runsettings new file mode 100644 index 0000000..c6fe146 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/additional_test_args.runsettings @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + .*microsoft.* + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Configuration/SecuritySettings.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Configuration/SecuritySettings.cs new file mode 100644 index 0000000..b06eb54 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Configuration/SecuritySettings.cs @@ -0,0 +1,16 @@ + +namespace OnlineBankingAuth.Configuration +{ + public class SecuritySettings + { + public string PrivateCertAzureThumbprint { get; set; } + public string PublicCertAzureThumbprint { get; set; } + public string TokenSecret { get; set; } + public string SAMLSuccessRedirectRoute { get; set; } + public string SAMLFailureRedirectRoute { get; set; } + public string DigitalInsightPostTokenURI { get; set; } + public string DigitalInsightConsumerKey { get; set; } + public string DigitalInsightConsumerSecret { get; set; } + public string DigitalInsightFIID { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/OnlineBankingAuth.csproj b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/OnlineBankingAuth.csproj new file mode 100644 index 0000000..a82615f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/OnlineBankingAuth.csproj @@ -0,0 +1,23 @@ + + + + false + enable + net8.0 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/AuthTokenParser.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/AuthTokenParser.cs new file mode 100644 index 0000000..8635cb8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/AuthTokenParser.cs @@ -0,0 +1,74 @@ +using Landmark.OLA.Models.OnlineBanking; +using System.Security.Authentication; +using System.Xml.Linq; + +namespace OnlineBankingAuth.Parsers +{ + public class AuthTokenParser : IAuthTokenParser + { + + public AuthTokenResponse ParseAuthTokenResponse(XElement xElement) + { + try + { + var accessToken = (string)xElement.Element("access_token"); + if (String.IsNullOrEmpty(accessToken)) + { + return ParseErroneousAuthTokenResponse(xElement); + } + var authToken = new AuthToken + { + + DiFiid = (string)xElement.Element("di_fiid"), + DiFicustomer = (string)xElement.Element("di_ficustomer"), + DiMemberNumber = (string)xElement.Element("di_member_number"), + AccessToken = (string)xElement.Element("access_token"), + ExpiresIn = (string)xElement.Element("expires_in"), + RefreshToken = (string)xElement.Element("refresh_token"), + RefreshTokenExpiresIn = (string)xElement.Element("refresh_token_expires_in") + }; + + var authTokenResponse = new AuthTokenResponse + { + Token = authToken, + Status = null + }; + + return authTokenResponse; + } + catch (Exception e) + { + throw new AuthenticationException($"Could not parse response from IdP vendor. {e.Message}, {e.StackTrace}"); + } + + } + + private AuthTokenResponse ParseErroneousAuthTokenResponse(XElement xElement) + { + var messageElement = (XElement)xElement.FirstNode; + var errorElement = (XElement)xElement.FirstNode.NextNode; + + var errorType = (XElement)errorElement.FirstNode; + var errorCode = (XElement)errorElement.FirstNode.NextNode; + var errorMessage = (XElement)errorElement.FirstNode.NextNode.NextNode; + + var error = new AuthTokenError + { + ErrorType = errorType.Value, + ErrorCode = errorCode.Value, + ErrorMessage = errorMessage.Value + }; + + var authTokenStatus = new AuthTokenStatus + { + StatusMessage = messageElement.Value, + ErrorInfo = error + }; + + return new AuthTokenResponse + { + Status = authTokenStatus + }; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/IAuthTokenParser.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/IAuthTokenParser.cs new file mode 100644 index 0000000..9b07750 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/IAuthTokenParser.cs @@ -0,0 +1,10 @@ +using Landmark.OLA.Models.OnlineBanking; +using System.Xml.Linq; + +namespace OnlineBankingAuth.Parsers +{ + public interface IAuthTokenParser + { + AuthTokenResponse ParseAuthTokenResponse(XElement xElement); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/ISAMLParser.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/ISAMLParser.cs new file mode 100644 index 0000000..0b1477e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/ISAMLParser.cs @@ -0,0 +1,12 @@ +using Landmark.OLA.Models.OnlineBanking; +using System.Xml; + +namespace OnlineBankingAuth.Parsers +{ + public interface ISAMLParser + { + DISAMLAssertion ParseAssertion(XmlElement xmlElement); + SAMLConditions ParseConditions(XmlElement xmlElement); + AuthenticationStatement ParseAuthnStatement(XmlElement xmlElement); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLMessaging.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLMessaging.cs new file mode 100644 index 0000000..03b1efa --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLMessaging.cs @@ -0,0 +1,8 @@ + +namespace OnlineBankingAuth.Parsers +{ + public class SAMLMessaging + { + public string SAMLResponse { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLParser.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLParser.cs new file mode 100644 index 0000000..4450180 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLParser.cs @@ -0,0 +1,70 @@ +using Landmark.OLA.Models.OnlineBanking; +using System.Xml; + +namespace OnlineBankingAuth.Parsers +{ + public class SAMLParser : ISAMLParser + { + public string SAML_NAMESPACE = "saml:"; + + public DISAMLAssertion ParseAssertion(XmlElement xmlElement) + { + try + { + + var assertion = new DISAMLAssertion + { + NameID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "NameID")[0].InnerText, + Recipient = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "SubjectConfirmationData")[0].Attributes[1].InnerText, + MemberID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AttributeValue")[0].InnerText, + FI_ID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AttributeValue")[1].InnerText, + AuthnStatement = ParseAuthnStatement(xmlElement), + Conditions = ParseConditions(xmlElement), + + }; + + return assertion; + } + catch + { + throw new FormatException("SAML Response is in an unexpected layout."); + } + } + + public AuthenticationStatement ParseAuthnStatement(XmlElement xmlElement) + { + try + { + var authnStatement = new AuthenticationStatement + { + SessionIndex = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AuthnStatement")[0].Attributes[1].InnerText, + AuthContextClassRef = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AuthnContextClassRef")[0].InnerText + }; + return authnStatement; + } + catch + { + throw new FormatException("AuthnStatement is in an unexpected layout."); + } + } + + public SAMLConditions ParseConditions(XmlElement xmlElement) + { + try + { + var conditions = new SAMLConditions + { + Audience = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Audience")[0].InnerText, + NotBefore = DateTime.Parse(xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Conditions")[0].Attributes[0].InnerText), + NotAfter = DateTime.Parse(xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Conditions")[0].Attributes[1].InnerText), + }; + return conditions; + } + catch + { + throw new FormatException("SAML Conditions is in an unexpected layout"); + } + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightService.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightService.cs new file mode 100644 index 0000000..60020da --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightService.cs @@ -0,0 +1,84 @@ +using System.Text; +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using Landmark.OLA.Models.OnlineBanking; +using OnlineBankingAuth.Configuration; +using OnlineBankingAuth.Parsers; + +namespace OnlineBankingAuth.Services +{ + public class DigitalInsightService : IDigitalInsightService + { + private readonly SecuritySettings _settings; + private readonly IAuthTokenParser _parser; + + public DigitalInsightService( + IAuthTokenParser parser, + IOptions settings) + { + _parser = parser; + _settings = settings.Value; + } + + public OnlineBankingFormData PopulateOnlineBankingDataFromConfig(OnlineBankingFormData data) + { + data.Authorization = "Basic " + + Convert.ToBase64String( + Encoding.UTF8.GetBytes( + _settings.DigitalInsightConsumerKey + ":" + _settings.DigitalInsightConsumerSecret + ) + ); + data.DI_FIID = _settings.DigitalInsightFIID; + data.DI_TID = (Guid.NewGuid()).ToString(); + + + return data; + } + + private IEnumerable> BuildKeyValuePair(OnlineBankingFormData data) + { + return new Dictionary + { + {"grant_type", data.GrantType }, + {"username", data.Username }, + {"password", data.Password } + }; + } + + public async Task LoginUser(OnlineBankingFormData data) + { + try + { + using (var hc = new HttpClient()) + { + var formData = BuildKeyValuePair(data); + using (var content = new FormUrlEncodedContent(formData)) + { + content.Headers.Clear(); + content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + HttpContent httpContent = new StringContent(await content.ReadAsStringAsync(), Encoding.UTF8, "application/x-www-form-urlencoded"); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, _settings.DigitalInsightPostTokenURI); + httpRequest.Content = httpContent; + + httpRequest.Headers.Add("Authorization", data.Authorization); + httpRequest.Headers.Add("di_fiid", data.DI_FIID); + httpRequest.Headers.Add("di_tid", data.DI_TID); + + var response = hc.SendAsync(httpRequest); + var xdoc = XDocument.Parse(await response.Result.Content.ReadAsStringAsync()); + var xel = XElement.Parse(xdoc.ToString()); + + var authTokenResponse = _parser.ParseAuthTokenResponse(xel); + return authTokenResponse; + } + } + + } + catch (Exception) + { + throw; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightVendorClient.cs new file mode 100644 index 0000000..f102f81 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightVendorClient.cs @@ -0,0 +1,143 @@ + +using System.Security.Cryptography; +using System.Security.Authentication; +using System.Xml; +using System.Text; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Options; +using OnlineBankingAuth.Configuration; +using OnlineBankingAuth.Services; +using OnlineBankingAuth.Parsers; +using Landmark.OLA.Models.OnlineBanking; + +namespace OnlineBankingAuth.Communication +{ + public class DigitalInsightVendorClient : IDigitalInsightVendorClient + { + private readonly ISAMLParser _samlParser; + private readonly SecuritySettings _settings; + private readonly IDigitalInsightService _serivce; + + public DigitalInsightVendorClient( + ISAMLParser samlParser, + IOptions settings, + IDigitalInsightService service) + { + _samlParser = samlParser; + _settings = settings.Value; + _serivce = service; + } + + + public bool ValidateDIPublicKey(XmlElement x509) + { + var vendorTrustedCert = GetPublicKeyFromStore(); + + var innerText = x509.InnerText; + byte[] payloadCertBytes = Encoding.Default.GetBytes(innerText); + + var payloadCert = new X509Certificate2(payloadCertBytes); + + return vendorTrustedCert.GetPublicKeyString().Equals(payloadCert.GetPublicKeyString()); + // @TODO: Assert other equalities besides the "publicKeyString"? + } + + public DISAMLAssertion GetSAMLPayload(XmlDocument xmlDocument) + { + var lmcuPrivateCertificate = GetPrivateKeyFromStore(); + + // The "EncryptedAsserion" block is the only acceptable block we can use to construct SAML2.Saml20EncryptedAssertion + var encryptedXml = (XmlElement)xmlDocument.GetElementsByTagName("saml:EncryptedAssertion")[0]; + var encryptedXmlDoc = new XmlDocument(); + encryptedXmlDoc.LoadXml(encryptedXml.OuterXml); + + // SAML2 library does the decrypting work + var privateKeyAlgorithm = (RSA)lmcuPrivateCertificate.GetRSAPrivateKey(); + var encryptedAssertion = new SAML2.Saml20EncryptedAssertion(privateKeyAlgorithm, encryptedXmlDoc); + encryptedAssertion.Decrypt(); + + // The decrypted document should be signed by DI. The Vendor Public Key is used to assert that this is the case. + var vendorPublicKey = GetPublicKeyFromStore().GetRSAPublicKey(); + var checkSignature = SAML2.Utils.XmlSignatureUtils.CheckSignature(encryptedAssertion.Assertion, vendorPublicKey); + + if (!checkSignature) + { + throw new AuthenticationException("Signature does not match therefore the decrypted payload cannot be accepted."); + } + + var decryptedAssertion = _samlParser.ParseAssertion(encryptedAssertion.Assertion.DocumentElement); + return decryptedAssertion; + } + + public bool ValidateTimeStamp(DISAMLAssertion assertion) + { + return 0 <= DateTime.Compare(assertion.Conditions.NotAfter, DateTime.Now); + } + + public string GetSuccessRedirectUrl() + { + return _settings.SAMLSuccessRedirectRoute; + } + + public string GetFailureRedirectUrl(string message) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(message); + var encodedMessage = System.Convert.ToBase64String(plainTextBytes); + return _settings.SAMLFailureRedirectRoute + "&msg=" + encodedMessage; + } + + private X509Certificate2 GetPrivateKeyFromStore() + { + // FOR TESTING LOCAL #1345. Note: the second argument is the password for the secure cert file + //return new X509Certificate2("Controllers" + Path.DirectorySeparatorChar + "dev.pfx", "Nvisia", X509KeyStorageFlags.Exportable); + + ///* + var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + certStore.Open(OpenFlags.ReadOnly); + var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _settings.PrivateCertAzureThumbprint, false); + + X509Certificate2 lmcuPrivateCertificate; + + if (certCollection.Count == 0) + { + throw new AuthenticationException("Could not GET private key from PrivateCertAzureThumbprint"); + } + + // Should only have 1 cert with that thumbprint + lmcuPrivateCertificate = certCollection[0]; + + return lmcuPrivateCertificate; + //*/ + } + + private X509Certificate2 GetPublicKeyFromStore() + { + // FOR TESTING LOCAL #1345 + //return new X509Certificate2("Controllers" + Path.DirectorySeparatorChar + "pfd-sign.qal1signingcert2018"); + + ///* + var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + certStore.Open(OpenFlags.ReadOnly); + var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _settings.PublicCertAzureThumbprint, false); + + X509Certificate2 vendorTrustedCert; + + if (certCollection.Count == 0) + { + throw new AuthenticationException("Could not GET public key from PublicCertAzureThumbprint"); + } + + // Should only have 1 cert with that thumbprint + vendorTrustedCert = certCollection[0]; + + return vendorTrustedCert; + //*/ + } + + public async Task AuthenticateOnlineBankingAccount(OnlineBankingFormData data) + { + var onlineBankingData = _serivce.PopulateOnlineBankingDataFromConfig(data); + return await _serivce.LoginUser(onlineBankingData); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightService.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightService.cs new file mode 100644 index 0000000..54f6985 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightService.cs @@ -0,0 +1,10 @@ +using Landmark.OLA.Models.OnlineBanking; + +namespace OnlineBankingAuth.Services +{ + public interface IDigitalInsightService + { + Task LoginUser(OnlineBankingFormData data); + OnlineBankingFormData PopulateOnlineBankingDataFromConfig(OnlineBankingFormData data); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightVendorClient.cs new file mode 100644 index 0000000..64615c6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightVendorClient.cs @@ -0,0 +1,16 @@ +using System.Xml; +using Landmark.OLA.Models.OnlineBanking; + +namespace OnlineBankingAuth.Services +{ + public interface IDigitalInsightVendorClient + { + bool ValidateDIPublicKey(XmlElement x509); + DISAMLAssertion GetSAMLPayload(XmlDocument xmlDocument); + bool ValidateTimeStamp(DISAMLAssertion assertion); + string GetSuccessRedirectUrl(); + string GetFailureRedirectUrl(string message); + Task AuthenticateOnlineBankingAccount(OnlineBankingFormData data); + }; +} + diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/CookieCryptor.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/CookieCryptor.cs new file mode 100644 index 0000000..ae86c69 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/CookieCryptor.cs @@ -0,0 +1,82 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Landmark.OLA.Application.Common.Security +{ + public static class CookieCryptor + { + private static byte[] RandomBytes(int len) + { + var b = new byte[len]; + RandomNumberGenerator.Fill(b); + return b; + } + + private static byte[] DeriveKey(string pass, byte[] salt, int keyBytes = 32, int iterations = 100_000) + => new Rfc2898DeriveBytes(pass, salt, iterations, HashAlgorithmName.SHA256).GetBytes(keyBytes); + + + public static string EncryptJwt(string jwt, string key) + { + var salt = RandomBytes(32); + + using var aes = Aes.Create(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + aes.GenerateIV(); + aes.Key = DeriveKey(key, salt, 32); + + using var ms = new MemoryStream(); + + ms.Write(salt, 0, salt.Length); + ms.Write(aes.IV, 0, aes.IV.Length); + + using (var crypto = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write)) + using (var writer = new StreamWriter(crypto, Encoding.UTF8)) + { + writer.Write(jwt); + writer.Flush(); + crypto.FlushFinalBlock(); + } + + return Convert.ToBase64String(ms.ToArray()); + } + + public static string DecryptJwt(string cipherText, string passPhrase) + { + var all = Convert.FromBase64String(cipherText); + + var saltLen = 32; + var ivLen = 16; + + if (all.Length < saltLen + ivLen) + throw new CryptographicException("Ciphertext too short."); + + var salt = new byte[saltLen]; + Buffer.BlockCopy(all, 0, salt, 0, saltLen); + + var iv = new byte[ivLen]; + Buffer.BlockCopy(all, saltLen, iv, 0, ivLen); + + var cipher = new byte[all.Length - saltLen - ivLen]; + Buffer.BlockCopy(all, saltLen + ivLen, cipher, 0, cipher.Length); + + using var aes = Aes.Create(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = DeriveKey(passPhrase, salt, 32); + aes.IV = iv; + + using var ms = new MemoryStream(cipher); + using var crypto = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read); + using var reader = new StreamReader(crypto, Encoding.UTF8); + + return reader.ReadToEnd(); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/ITokenService.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/ITokenService.cs new file mode 100644 index 0000000..d9ad952 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/ITokenService.cs @@ -0,0 +1,16 @@ +using Landmark.OLA.Models.Core; +using Landmark.OLA.Models.OnlineBanking; +using Microsoft.AspNetCore.Http; + +namespace OnlineBankingAuth.Tokens +{ + public interface ITokenService + { + string CreateOnlineBankingJWT(OnlineBankingToken payload); + string RenewOnlineBankingJWT(HttpContext context); + string RenewAuthorization(HttpContext context); + OnlineBankingToken GetAuthenticatedDataFromCookie(HttpContext context); + OnlineBankingToken GetAuthenticatedUserDataFromJwt(string jwt); + string CreateAuthMetaToken(MemberMeta meta); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/TokenService.cs b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/TokenService.cs new file mode 100644 index 0000000..9e10d84 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/TokenService.cs @@ -0,0 +1,168 @@ +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Authentication; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Landmark.OLA.Application.Common.Security; +using Landmark.OLA.Models.Core; +using Landmark.OLA.Models.OnlineBanking; +using OnlineBankingAuth.Configuration; + +namespace OnlineBankingAuth.Tokens +{ + public class TokenService : ITokenService + { + private readonly SecuritySettings _settings; + + public TokenService( + IOptions settings) + { + _settings = settings.Value; + } + + public string CreateOnlineBankingJWT(OnlineBankingToken payload) + { + var handler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + + var claims = new List + { + new(Constants.MemberID, payload.MemberID ?? string.Empty), + new(Constants.IndividualID, payload.IndividualID ?? string.Empty), + new(Constants.FI_ID, payload.FI_ID ?? string.Empty), + new(Constants.InProgressLoanId, payload.LoanId ?? string.Empty), + new(Constants.IsTwoFactor, payload.IsTwoFactor.ToString()) + }; + + var jwt = new JwtSecurityToken( + claims: claims, + expires: DateTime.UtcNow.AddMinutes(15) + ); + + var token = handler.WriteToken(jwt); + var encrypted = CookieCryptor.EncryptJwt(token, _settings.TokenSecret); + return Constants.TokenPrefix + encrypted; + } + + public string RenewOnlineBankingJWT(HttpContext context) + { + var cookies = context.Request.Cookies; + string cookieValue = null; + + if (cookies[Constants.TokenPayload] != null) + { + var jwtCookie = cookies[Constants.TokenPayload][Constants.TokenPrefix.Length..]; + var jwt = CookieCryptor.DecryptJwt(jwtCookie, _settings.TokenSecret); + + if (!IsTokenReadable(jwt)) + { + throw new AuthenticationException("Could not parse token."); + } + + var prevToken = ReadJwtSecurityToken(jwt); + cookieValue = Constants.TokenPrefix + GenerateEncodedToken(prevToken); + } + + return cookieValue; + } + + public string RenewAuthorization(HttpContext context) + { + var cookies = context.Request.Cookies; + string cookieValue = null; + + if (cookies[Constants.TokenAuthorization] != null) + { + cookieValue = cookies[Constants.TokenAuthorization]; + } + + return cookieValue; + } + + public OnlineBankingToken GetAuthenticatedDataFromCookie(HttpContext context) + { + var cookies = context.Request.Cookies; + + if (!string.IsNullOrEmpty(cookies[Constants.TokenPayload])) + { + return GetAuthenticatedUserDataFromJwt(cookies[Constants.TokenPayload][Constants.TokenPrefix.Length..]); + } + + return null; + } + + private string GenerateEncodedToken(JwtSecurityToken jwtToken) + { + var tokenHandler = new JwtSecurityTokenHandler { }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(jwtToken.Claims), + Expires = DateTime.UtcNow.AddMinutes(15) + }; + + var token = tokenHandler.CreateEncodedJwt(tokenDescriptor); + var encryptedToken = CookieCryptor.EncryptJwt(token, _settings.TokenSecret); + + return encryptedToken; + } + + private static bool TryGet(JwtSecurityToken jwt, string key, out string? value) + { + var claim = jwt.Claims.FirstOrDefault(c => c.Type == key) + ?? jwt.Claims.FirstOrDefault(c => string.Equals(c.Type, key, StringComparison.OrdinalIgnoreCase)); + if (claim is not null) { value = claim.Value; return true; } + + foreach (var kv in jwt.Payload) + if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase)) + { + value = Convert.ToString(kv.Value, CultureInfo.InvariantCulture); + return true; + } + + value = null; + return false; + } + + public OnlineBankingToken GetAuthenticatedUserDataFromJwt(string encryptedJwt) + { + var jwt = CookieCryptor.DecryptJwt(encryptedJwt, _settings.TokenSecret); + var token = ReadJwtSecurityToken(jwt); + + var data = new OnlineBankingToken(); + + if (!TryGet(token, Constants.MemberID, out var memberId)) return null; + if (!TryGet(token, Constants.FI_ID, out var fiId)) return null; + + data.MemberID = memberId!; + data.FI_ID = fiId!; + + if (TryGet(token, Constants.IsTwoFactor, out var twoFa)) + data.IsTwoFactor = bool.TryParse(twoFa, out var b) && b; + + if (TryGet(token, Constants.IndividualID, out var indId)) + data.IndividualID = indId; + + return data; + } + + private static JwtSecurityToken ReadJwtSecurityToken(string jwt) + { + return new JwtSecurityTokenHandler().ReadJwtToken(jwt); + } + + private static bool IsTokenReadable(string jwt) + { + return new JwtSecurityTokenHandler().CanReadToken(jwt); + } + + public string CreateAuthMetaToken(MemberMeta meta) + { + var json = JsonConvert.SerializeObject(meta); + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(json); + return Convert.ToBase64String(plainTextBytes); + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/README.md b/LCU.API.Internal.OnlineLoanApp/README.md new file mode 100644 index 0000000..812e433 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/README.md @@ -0,0 +1,170 @@ +# Introduction +TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project. + +# Getting Started +TODO: Guide users through getting your code up and running on their own system. In this section you can talk about: +1. Installation process +2. Software dependencies +3. Latest releases +4. API references + +# Build and Test +TODO: Describe and show how to build your code and run the tests. + +# Contribute +TODO: Explain how other users and developers can contribute to make your code better. + +If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files: +- [ASP.NET Core](https://github.com/aspnet/Home) +- [Visual Studio Code](https://github.com/Microsoft/vscode) +- [Chakra Core](https://github.com/Microsoft/ChakraCore) +======= +# Landmark Online Loan Application Backend + +The Landmark OLA backend provides a RESTful API facade over the CRIF ACTion SOAP API. The backend is built to abstract away the vendor sepcific calls and could be adapted to use alternative loan application / decisioning vendors. + +This project targets .NET 8.0. +Unit tests are built with xUnit. + +Swagger OpenAPI documentation is generated by [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) and is available at `/swagger`. + +## Suggested IDE +Visual Studio 2017 Community Edition + +## Logging +.NET has [7 logging levels](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=aspnetcore-2.1): + +| Name | Level | +| :----------- | ----: | +| Trace | 0 | +| Debug | 1 | +| Information | 2 | +| Warning | 3 | +| Error | 4 | +| Critical | 5 | +| None | 6 | + +`Information` levels logs will contain URLs requested by the client, which could contain Loan Application IDs and may (but should not) contain PII. + +Logging is set to `Debug` for the `Development` environment and to `Error` for the `Production` environment via the appsettings.*ENVIRONMENT*.json files. +CRIF ACTion API calls are logged by a WCF behavior (`CRIF.ACTion.Communication.WcfInspector`) at the `Debug` level. Logs can be enabled in Azure using the [filesystem Application Logging feature](https://docs.microsoft.com/en-us/azure/app-service/web-sites-enable-diagnostic-log#enablediag). Note that this feature will turn off after 12 hours to prevent any acceidental logging of PII. + + +## Contributing style/naming conventions + +* All private fields in a class should be prefixed with an underscore (_) and camel case. + Ex: ```private VendorConfiguration _vendorConfiguration``` +* All public fields in a class should be PascalCase. + Ex: ```public Task SendMessageToCrif(){...}``` +* All local variables should be camelCase. + Ex: ```var formatString = "This string is formatted" ``` + +## Adding new endpoints and vendor calls + +1. Update/create models in `Landmark.OLA.Models`. +* As the amount of models grow over time these may require additional organization. +* Use [`System.ComponentModel.DataAnnotations`](https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx) attributes to set data validation for the controller. Note that display attributes may not apply since this project is not using MVC views. +3. Update IVendorClient and implementations with any new methods or parameters. +4. Create required objects in the `CRIF.ACTion` project. +* 'ApiCalls' represent individual API calls made to CRIF, and should extend the `ApiCall` class. +* Other objects, such as `MainSections` or AppDetails classes can be created to represent XML sections or elements. The `IBuildable` interface offers a common way to return `XElement` content. +* Store product IDs, custom field IDs, and other CRIF data in `CrifConstants.cs`. +5. Update/create required controllers in the `Controllers` directory of `Landmark.OLA.Application`. +* Controllers should inherit OlaControllerBase and have an `[ApiController]` attribute at the class level. +* Controllers should have a constructor that accepts `IVendorClient vendorClient, ILogger<*ClassName*> logger` parameters. These items will be injected by .Net Core's Service Configuration, as set in `Startup.cs`'s `ConfigureServices` method. +* Controller methods should: + * be `async` + * return `Task` + * log exceptions without logging PII, but return consumer-friendly errors + * contain no vendor-specific code + * have `ProducesResponseType` attributes, per Swashbuckle's [Explicit Responses](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#explicit-responses) instructions + * `[ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)]` for validation errors (checked [automatically by ASP.NET 2.1](https://docs.microsoft.com/en-us/aspnet/core/web-api/index?view=aspnetcore-2.1#automatic-http-400-responses)) + * `[ProducesResponseType(500)]` for uncaught exceptions, if applicable + * Call `[FluentValidation](https://fluentvalidation.net/)` for business-level model validation. +6. Create additional LoanType enum values as required. + * Loan limits, purposes, and term lengths may need to be updated with new types. The `SettingsCheck` tests will verify that all types either have values or are explicitly excluded. +7. **Create or update tests as required.** + + +## Validation +Field level validation is handled on the model level with the [`System.ComponentModel.DataAnnotations`](https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx) attributes. It's important to consider the default value of types and how this may impact validation. For example, *bool* defaults to *false*, so if you do not want to assume *false* on a null value instead of *bool* use *Nullable* / *bool?*. + +Variable length value-type fields (that is, strings or decimals but not Guids or enums) should generally have length or range validation. These validation limits may be derived from CRIF's database field definitions or perhaps by business logic. Validation requirements are checked on all classes in *Landmark.OLA.Models* via reflection by the *VerifyDataAnnotations()* unit test. + +Business level validation, such as checking a valid HMDA state, is handled with the `[FluentValidation](https://fluentvalidation.net/)` library which should be called within the controllers. + +## Areas To Enhance +1. Add additional test coverage for the CRIF API. +*A soapui test suite exists in the landmark folder. The suite must be ran manually and the results have to be analyzed upon completion. +*At some point we want to create an automated test suite that is ran everytime we build the api. The results should be automatically analyzed. This will allow us to monitor any behavior changes on CRIFS end. +2. Add run time validation on CRIFS response xml. CRIF currently validates all request xml. +*See xml validation in unit tests to get started +3. Hide status/build info controller in production +*Prevents external entites from checking prod server status +*Can use env.IsProduction() check for validating environment. Inject environment object into controller. +4. Establish a way to identify when CRIF is down +5. Add error handling and reporting when CRIF is down +6. Add automated testing for custom CRIF Fields +*CRIF has custom fields for specific loan types. +*If we submit a loan to CRIF with an invalid custom field, the submission is rejected. +*If we submit a loan to CRIF with a disabled field, the submission is accepted but nobody will be able to view it in CRIF. (Big Risk) +*We should setup regular automated tests to validate that the custom fields still exist AND are enabled. API function exists to check fields. +7. Modify the IIS App Pool Settings for faster site loading +*Currently the back-end takes some extra time to load if IIS has taken it out of memory due to lack of activity. Likely can remove time-outs and either remove recycling or change to always overnight. +*https://weblogs.asp.net/owscott/why-is-the-iis-default-app-pool-recycle-set-to-1740-minutes + +## Running in Docker + +Use the provided `Dockerfile` to build and run the API in a container. + +```bash +# Build the image from the repository root +docker build -t loanwizard-api . + +# Run the container and expose the HTTP port +docker run --rm -p 8080:8080 \ + -e ASPNETCORE_ENVIRONMENT=Development \ + -e Vault= \ + -e ClientId= \ + -e ClientSecret= \ + loanwizard-api +``` + +The application binds to port 8080 inside the container. Override configuration and secrets by providing environment variables (for example `Primary__SeqUrl`, `ApplicationStorage__Host`, etc.). When using Azure Key Vault, ensure that `Vault`, `ClientId`, and `ClientSecret` correspond to a principal that has access to the configured vault. If you do not need Key Vault integration for local testing, set `ASPNETCORE_ENVIRONMENT=Development` and provide configuration values directly through environment variables or JSON files mounted into the container. + +##Azure Active Directory Security + +We are using v1.0 (ADAL). + +To secure additional endpoints you only need to include an annotation in the controller: [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +Either on the entire controller, or on an endpoint by endpoint basis + +Resources: +https://docs.microsoft.com/en-us/azure/active-directory/develop/index +https://docs.microsoft.com/en-us/azure/active-directory/develop/web-api +https://docs.microsoft.com/en-us/azure/active-directory/develop/web-app +https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison + +##Cosmos DB Time To Live Index (TTL) +The time to live index is used to automatically expire loans in the db based on an expiration time in seconds. TTL goes off of the last modified time. + +Steps for setting up TTL +1. Download a mongodb client. Recommendation is Robo 3T (Robomongo). +2. Connect to the db using the client. +3. Open the shell and run the following command: db.ApplicationsDvl.createIndex({"_ts":1}, {expireAfterSeconds: 14400}) +*db.ApplicationsDvl denotes the database and collection +*createIndex({"_ts":1} indicates an index is being applied to the _ts field +*{expireAfterSeconds: 14400} indicates to expire the document after 14400 seconds (4 hours) +4. Verify the index was created by viewing the index section. + +Notes +1. When connecting using a client, make sure the ssl authentication method is self-signed cert. +2. Robo 3T will delete the index when you edit it in the ui. Always use the shell. +3. _ts is a Cosmos DB-specific field and is not accessible from MongoDB clients. It is a reserved (system) property that contains the timestamp of the document's last modification. +4. The azure portal also has a built in TTL setting. We are not relying on this for TTL management. + +Resources +https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-robomongo +https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-indexing +https://docs.microsoft.com/en-us/azure/cosmos-db/time-to-live +https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-time-to-live diff --git a/LCU.API.Internal.OnlineLoanApp/USPS.Tests/AddressValidationTest.cs b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/AddressValidationTest.cs new file mode 100644 index 0000000..6c45c7f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/AddressValidationTest.cs @@ -0,0 +1,82 @@ +using System; +using System.Xml; +using System.Xml.Linq; +using Xunit; +using USPS.ApiCalls; +using USPS.Configuration; +using USPS.Parsers; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; + +namespace USPS.Tests +{ + public class AddressValidationTest + { + private readonly IResponseParser
AddressParser; + + + public AddressValidationTest() + { + AddressParser = new AddressValidationResponseParser(); + } + + private readonly Address address = new Address() + { + Address1 = "109 N Hancock St", + Address2 = "Apt 2", + City = "Madison", + State = StateOption.WI, + PostalCode = "53703", + PostalCodeExt = "2466", + Country = CountryOption.UnitedStates + }; + + [Fact] + public void ErrorResponseParse() + { + XmlDocument doc = new XmlDocument(); + doc.Load("ExampleXML" + System.IO.Path.DirectorySeparatorChar + "USPSAddressError.xml"); + + XDocument response; + + using (XmlNodeReader reader = new XmlNodeReader(doc)) + { + response = XDocument.Load(reader); + } + + Exception ex = Assert.Throws(() => AddressParser.Parse(response.Root)); + + Assert.Equal("Attempt to parse address that is in error.", ex.Message); + } + + [Fact] + public void SuccessResponseParse() + { + XmlDocument doc = new XmlDocument(); + doc.Load("ExampleXML" + System.IO.Path.DirectorySeparatorChar + "USPSAddressSuccess.xml"); + + XDocument response; + + using (XmlNodeReader reader = new XmlNodeReader(doc)) + { + response = XDocument.Load(reader); + } + + var parsedAddress = AddressParser.Parse(response.Root); + + Assert.Equal("MADISON", parsedAddress.City); + } + + [Fact] + public void AddressValidationBuild() + { + UspsSettings settings = new UspsSettings(); + settings.Username = "TestUser"; + settings.EndpointUrl = "http://production.shippingapis.com/ShippingAPI?"; + AddressValidationRequestBuilder api = new AddressValidationRequestBuilder(settings, address); + string expectedURI = "http://production.shippingapis.com/ShippingAPI?API=Verify&XML=%3cAddressValidateRequest+USERID%3d%22TestUser%22%3e%3cAddress%3e%3cAddress1%3eApt+2%3c%2fAddress1%3e%3cAddress2%3e109+N+Hancock+St%3c%2fAddress2%3e%3cCity%3eMadison%3c%2fCity%3e%3cState%3eWI%3c%2fState%3e%3cZip5%3e53703%3c%2fZip5%3e%3cZip4+%2f%3e%3c%2fAddress%3e%3c%2fAddressValidateRequest%3e"; + Assert.Equal(expectedURI, api.BuildGET()); + } + + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressError.xml b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressError.xml new file mode 100644 index 0000000..bedfeba --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressError.xml @@ -0,0 +1,12 @@ + + +
+ + -2147219403 + clsAMS + Multiple addresses were found for the information you entered, and no default exists. + + + +
+
\ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressSuccess.xml b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressSuccess.xml new file mode 100644 index 0000000..818ba77 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressSuccess.xml @@ -0,0 +1,11 @@ + + +
+ APT 2 + 109 N HANCOCK ST + MADISON + WI + 53703 + 2466 +
+
\ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/USPS.Tests/USPS.Tests.csproj b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/USPS.Tests.csproj new file mode 100644 index 0000000..a49f444 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/USPS.Tests.csproj @@ -0,0 +1,35 @@ + + + false + net8.0 + false + + + 7.2 + + + 7.2 + + + + + + + + + + + + + + + + + + Always + + + Always + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/USPS.Tests/additional_test_args.runsettings b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/additional_test_args.runsettings new file mode 100644 index 0000000..c6fe146 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS.Tests/additional_test_args.runsettings @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + .*microsoft.* + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Configuration/UspsSettings.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Configuration/UspsSettings.cs new file mode 100644 index 0000000..e2e54e5 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Configuration/UspsSettings.cs @@ -0,0 +1,10 @@ + +namespace USPS.Configuration +{ + public class UspsSettings + { + public string Username { get; set; } + public string Password { get; set; } + public string EndpointUrl { get; set; } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Requests/AddressValidationRequestBuilder.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Requests/AddressValidationRequestBuilder.cs new file mode 100644 index 0000000..a16a3c6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Requests/AddressValidationRequestBuilder.cs @@ -0,0 +1,56 @@ +using Landmark.OLA.Models.Addresses; +using System.Text; +using System.Web; +using System.Xml; +using USPS.Configuration; + +namespace USPS.ApiCalls +{ + public class AddressValidationRequestBuilder : IRequestBuildable + { + private readonly UspsSettings _settings; + private readonly Address _address; + + public AddressValidationRequestBuilder(UspsSettings settings, Address address) + { + _settings = settings; + _address = address; + } + + public virtual string BuildAddressXML() + { + var sb = new StringBuilder(); + var sw = new StringWriter(sb); + var xml = new XmlTextWriter(sw); + + xml.WriteStartElement("AddressValidateRequest"); + xml.WriteAttributeString("USERID", _settings.Username); + xml.WriteStartElement("Address"); + // USPS considers unit numbers (ex: APT 204) to be Address1. Our app's models would consider these to be Address2. DO NOT CHANGE + xml.WriteElementString("Address1", _address.Address2); + xml.WriteElementString("Address2", _address.Address1); + xml.WriteElementString("City", _address.City); + xml.WriteElementString("State", _address.State.ToString()); + xml.WriteElementString("Zip5", _address.PostalCode); + xml.WriteElementString("Zip4", ""); + xml.WriteEndElement(); + xml.WriteEndElement(); + xml.Close(); + + var query = HttpUtility.ParseQueryString(string.Empty); + query["API"] = "Verify"; + query["XML"] = sb.ToString(); + + return query.ToString(); + } + + public string BuildGET() + { + var xml = BuildAddressXML(); + + var message = _settings.EndpointUrl + xml; + + return message; + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Requests/IRequestBuildable.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Requests/IRequestBuildable.cs new file mode 100644 index 0000000..d78e64c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Requests/IRequestBuildable.cs @@ -0,0 +1,8 @@ + +namespace USPS.ApiCalls +{ + public interface IRequestBuildable + { + string BuildAddressXML(); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Responses/AddressValidationResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Responses/AddressValidationResponseParser.cs new file mode 100644 index 0000000..c5157b7 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Responses/AddressValidationResponseParser.cs @@ -0,0 +1,68 @@ +using System.Xml.Linq; +using Landmark.OLA.Models.Addresses; +using Landmark.OLA.Models.Addresses.Enums; + +namespace USPS.Parsers +{ + public class AddressValidationResponseParser : IResponseParser
+ { + + public bool CheckForError(XElement xdoc) + { + var elementAddress = xdoc.Element("Address"); + if (elementAddress == null) + { + return true; + } + var error = elementAddress.Element("Error"); + return (error != null); + } + + public Address Parse(XElement xdoc) + { + if( xdoc == null ) + { + throw new ArgumentNullException("Address xml is null"); + } + + var elementAddress = xdoc.Element("Address"); + if (elementAddress == null || elementAddress.Element("Error") != null) + { + throw new FormatException("Attempt to parse address that is in error."); + } + + try + { + var compareAddress = new Address + { + // USPS considers unit numbers (ex: APT 204) to be Address1. Our app's models would consider these to be Address2. DO NOT CHANGE + Address2 = (string)elementAddress.Element("Address1"), + Address1 = (string)elementAddress.Element("Address2"), + City = (string)elementAddress.Element("City"), + State = MakeStateOptionEnum((string)elementAddress.Element("State")), + PostalCode = (string)elementAddress.Element("Zip5"), + PostalCodeExt = (string)elementAddress.Element("Zip4"), + Country = CountryOption.UnitedStates + }; + + return compareAddress; + } + catch + { + throw new FormatException("USPS XML is in an unexpected layout."); + } + } + + public static StateOption MakeStateOptionEnum(string state) + { + if (Enum.TryParse(state, out StateOption parsedState)) + { + return parsedState; + } + else + { + throw new FormatException("USPS XML contains an unexpected 'State'"); + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Responses/IResponseParser.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Responses/IResponseParser.cs new file mode 100644 index 0000000..0b5835a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Responses/IResponseParser.cs @@ -0,0 +1,10 @@ +using System.Xml.Linq; + +namespace USPS.Parsers +{ + public interface IResponseParser + { + T Parse(XElement xElement); + bool CheckForError(XElement xElement); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Services/IUspsService.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Services/IUspsService.cs new file mode 100644 index 0000000..5370d8a --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Services/IUspsService.cs @@ -0,0 +1,9 @@ +using System.Xml.Linq; + +namespace USPS.Communication +{ + public interface IUspsService + { + Task SendMessage(string request); + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsService.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsService.cs new file mode 100644 index 0000000..88692ff --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsService.cs @@ -0,0 +1,46 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using Serilog; +using USPS.Configuration; + +namespace USPS.Communication +{ + public class UspsService : IUspsService + { + private readonly UspsSettings _settings; + + public UspsService( + IOptions settings) + { + + _settings = settings.Value; + } + + public async Task SendMessage(string request) + { + try + { + var hc = new HttpClient(); + var result = await hc.GetAsync(request); + + if (result.IsSuccessStatusCode) + { + var resultString = await result.Content.ReadAsStringAsync(); + var xdoc = XDocument.Parse(resultString); + XElement xel = XElement.Parse(xdoc.ToString()); + return xel; + } + else + { + throw new Exception("Bad status code from USPS"); + } + + } + catch (Exception e) + { + Log.Debug("{exception}", e.Message); + throw; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsVendorClient.cs b/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsVendorClient.cs new file mode 100644 index 0000000..7b1738e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsVendorClient.cs @@ -0,0 +1,62 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using Serilog; +using USPS.Parsers; +using USPS.ApiCalls; +using USPS.Configuration; +using Landmark.OLA.Models; +using Landmark.OLA.Models.Vendor; +using Landmark.OLA.Models.Addresses; + +namespace USPS.Communication +{ + public class UspsVendorClient : IUspsVendorClient + { + private readonly IUspsService _uspsService; + private readonly IResponseParser
_addressParser; + + private readonly UspsSettings _settings; + + public UspsVendorClient(IUspsService uspsService, + IResponseParser
addressParser, + IOptions settings) + { + _uspsService = uspsService; + _addressParser = addressParser; + _settings = settings.Value; + } + + + public async Task
CompareAddress(Address address) + { + + ModelValidator.Validate(address); + + AddressValidationRequestBuilder api = new(_settings, address); + + string createXml = api.BuildGET(); + + XElement response = await _uspsService.SendMessage(createXml); + + try + { + if (_addressParser.CheckForError(response) ) + { + throw new ArgumentException("USPS rejected address"); + } + + return _addressParser.Parse(response); + } + catch (ArgumentException) + { + // Do not log; this is thrown when a bad address is submitted and it just spams the logs. + throw; + } + catch (Exception) + { + Log.Fatal("Error parsing response."); + throw; + } + } + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/USPS/USPS.csproj b/LCU.API.Internal.OnlineLoanApp/USPS/USPS.csproj new file mode 100644 index 0000000..2a922ef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/USPS/USPS.csproj @@ -0,0 +1,20 @@ + + + + false + enable + net8.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 new file mode 100644 index 0000000..8b1994d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 @@ -0,0 +1 @@ +cinst -y kind diff --git a/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/kind.cluster.config b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/kind.cluster.config new file mode 100644 index 0000000..b33f889 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/kind.cluster.config @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane + extraPortMappings: + - containerPort: 30000 + hostPort: 80 diff --git a/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 new file mode 100644 index 0000000..747b49f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 @@ -0,0 +1,17 @@ +kind create cluster --config kind.cluster.config --wait 300s + +kubectl create namespace landmarkcu + +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +helm repo update + +helm install my-release ingress-nginx/ingress-nginx ` + --set controller.service.type=LoadBalancer ` + --set controller.service.nodePorts.http=30000 ` + --set controller.service.nodePorts.https=30001 + +docker build -t local/online-loan-app-api:1.0 .. + +kind load docker-image local/online-loan-app-api:1.0 + +kubectl apply -k ..\_deploy\overlays\local diff --git a/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/teardown.ps1 b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/teardown.ps1 new file mode 100644 index 0000000..64866e8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/_kube_local_testing/teardown.ps1 @@ -0,0 +1 @@ +kind delete cluster diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml new file mode 100644 index 0000000..81d7d6c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml @@ -0,0 +1,55 @@ +parameters: +- name: target_environment + displayName: Target Environment + type: string + default: qa + values: + - qa + - uat + - prd + +- name: k8s_namespace + displayName: K8s Namespace to deploy to + type: string + default: landmarkcu + values: + - landmarkcu + +variables: +- template: ./config-files/${{ parameters.target_environment }}-pvars.yaml +- name: source_branch_name + value: $[replace(replace(variables['Build.SourceBranch'], 'refs/heads/', ''), '/', '-')] +- name: docker_container_repository_name + value: landmarkcu/lcu.api.internal.onlineloanappapi + +resources: + repositories: + - repository: azdo_pipeline_templates + type: git + name: Infrastructure Platform/azdo-pipeline-templates + ref: main + +stages: +# DOCKER BUILD AND PUSH +- template: docker-build-push.yaml@azdo_pipeline_templates + parameters: + azdo_agent_pool: $(pvar_azdo_agent_pool) + docker_registry_service_connection: $(pvar_docker_registry_service_connection) + docker_container_repository_name: $(docker_container_repository_name) + dockerfile_path: ./Dockerfile + docker_image_tags: | + build-$(Build.BuildId) + $(source_branch_name) + +# K8S DEPLOY +- template: k8s-deploy.yaml@azdo_pipeline_templates + parameters: + dependsOn: CI + condition: or(eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'main')) + azdo_agent_pool: $(pvar_azdo_agent_pool) + kubectl_version: $(pvar_kubectl_version) + k8s_service_connection: $(pvar_k8s_service_connection) + k8s_namespace: ${{ parameters.k8s_namespace }} + k8s_kustomization_path: ./kustomize/overlays/${{ parameters.target_environment }} + docker_registry_service_connection: $(pvar_docker_registry_service_connection) + docker_image_to_deploy: $(docker_container_repository_name):build-$(Build.BuildId) diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml new file mode 100644 index 0000000..6a7b5a9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml @@ -0,0 +1,11 @@ +trigger: + branches: + include: + - '*' + exclude: + - 'main' + - 'develop' + +extends: + template: azure-k8s-pipeline.yaml + \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml new file mode 100644 index 0000000..8e66ed4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml @@ -0,0 +1,10 @@ +trigger: + branches: + include: + - 'main' + +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: prd + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml new file mode 100644 index 0000000..30ce939 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml @@ -0,0 +1,10 @@ +trigger: + branches: + include: + - 'develop' + +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: qa + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml new file mode 100644 index 0000000..e33f5d3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml @@ -0,0 +1,10 @@ +trigger: + branches: + include: + - 'main' + +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: uat + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml new file mode 100644 index 0000000..9b650cd --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-prd-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-prd-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml new file mode 100644 index 0000000..dfc1bfb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-qa-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-qa-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem diff --git a/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml new file mode 100644 index 0000000..10d7ce3 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-uat-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-uat-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem diff --git a/LCU.API.Internal.OnlineLoanApp/azure-pipelines.yml b/LCU.API.Internal.OnlineLoanApp/azure-pipelines.yml new file mode 100644 index 0000000..deb9fa8 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/azure-pipelines.yml @@ -0,0 +1,45 @@ +# ASP.NET Core + +name: 'LoanAppAPI.$(Build.SourceBranchName).$(Date:yyyyMMdd)$(Rev:.r)' +trigger: + branches: + include: + - '*' + +pool: + name: LCUOnPrem + +variables: + buildConfiguration: 'Release' + +steps: +- task: DotNetCoreCLI@2 + inputs: + command: 'restore' + feedsToUse: 'select' + vstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7' + +- script: dotnet build --configuration $(buildConfiguration) + displayName: 'dotnet build $(buildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(Parameters.TestProjects)' + arguments: '--configuration $(BuildConfiguration) --collect "Code coverage" --settings additional_test_args.runsettings' + +- task: DotNetCoreCLI@2 + displayName: 'dotnet publish $(buildConfiguration)' + inputs: + command: publish + publishWebProjects: false + arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory) --framework netcoreapp2.1' + zipAfterPublish: true + +- task: PublishBuildArtifacts@1 + condition: and (succeeded(), or( eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'release'), eq(variables['Build.SourceBranchName'], 'master')) ) + displayName: 'Publish Artifact: dist_$(Build.SourceBranchName)' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + ArtifactName: 'dist_$(Build.SourceBranchName)' diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/base/Deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/base/Deployment.yaml new file mode 100644 index 0000000..776ed2d --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/base/Deployment.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app-api +spec: + replicas: 1 + selector: + matchLabels: + app: online-loan-app-api + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: online-loan-app-api + spec: + volumes: [] + containers: + - name: online-loan-app-api + env: [] + volumeMounts: [] + image: online-loan-app-api + resources: + limits: + memory: "1536Mi" + requests: + memory: "1536Mi" + cpu: "100m" + ports: + - containerPort: 8080 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /api/status/api/status + port: 8080 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /api/status/api/status + port: 8080 \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/base/Service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/base/Service.yaml new file mode 100644 index 0000000..ca3c72f --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/base/Service.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: online-loan-app-api +spec: + selector: + app: online-loan-app-api + ports: + - port: 8080 + targetPort: 8080 \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/base/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..f9d7476 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/base/kustomization.yaml @@ -0,0 +1,9 @@ +resources: + - Deployment.yaml + - Service.yaml + - secrets-provider-class.yaml + - virtual-service.yaml + +images: +- name: online-loan-app-api + newName: landmarkcu/lcu.api.internal.onlineloanappapi \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/base/secrets-provider-class.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/base/secrets-provider-class.yaml new file mode 100644 index 0000000..390d915 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/base/secrets-provider-class.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: online-loan-app-api-csi-key-vault-secrets-provider +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + tenantId: 63cd1554-2783-4a86-ae50-b9e6c21c9499 \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/base/virtual-service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/base/virtual-service.yaml new file mode 100644 index 0000000..5deedeb --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/base/virtual-service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app-api +spec: + hosts: [] + gateways: + - istio-system/ingress-gateway + http: + - name: online-loan-app-api + match: + - uri: + prefix: / + route: + - destination: + host: online-loan-app-api + port: + number: 8080 diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/kustomization.yaml new file mode 100644 index 0000000..0ae1552 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: Deployment + path: set_secrets_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider.yaml diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml new file mode 100644 index 0000000..4c629d9 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml @@ -0,0 +1,89 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: online-loan-app-api-secrets + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: normal-online-loan-app-api-csi-key-vault-secrets-provider + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-primary-seq-key.json + readOnly: true + subPath: online-loan-app-api-primary-seq-key + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-usps-settings-username.json + readOnly: true + subPath: online-loan-app-api-usps-settings-username + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-application-storage-password.json + readOnly: true + subPath: online-loan-app-api-application-storage-password + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-crif-settings-api-settings-username.json + readOnly: true + subPath: online-loan-app-api-crif-settings-api-settings-username + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-security-settings-private-cert-azure-thumbprint.json + readOnly: true + subPath: online-loan-app-api-security-settings-private-cert-azure-thumbprint + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-security-settings-public-cert-azure-thumbprint.json + readOnly: true + subPath: online-loan-app-api-security-settings-public-cert-azure-thumbprint + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-security-settings-token-secret.json + readOnly: true + subPath: online-loan-app-api-security-settings-token-secret + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-key.json + readOnly: true + subPath: online-loan-app-api-security-settings-digital-insight-consumer-key + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-secret.json + readOnly: true + subPath: online-loan-app-api-security-settings-digital-insight-consumer-secret + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: online-loan-app-api-secrets + mountPath: /vault/secrets/online-loan-app-api-blob-storage-connection-settings-account-key.json + readOnly: true + subPath: online-loan-app-api-blob-storage-connection-settings-account-key \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_provider.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_provider.yaml new file mode 100644 index 0000000..7c9e6c6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_provider.yaml @@ -0,0 +1,44 @@ +- op: add + path: /spec/parameters/objects + value: | + array: + - | + objectName: online-loan-app-api-primary-seq-key + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-usps-settings-username + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-application-storage-password + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-crif-settings-api-settings-username + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-security-settings-private-cert-azure-thumbprint + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-security-settings-public-cert-azure-thumbprint + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-security-settings-token-secret + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-security-settings-digital-insight-consumer-key + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-security-settings-digital-insight-consumer-secret + objectType: secret + objectVersion: "" + - | + objectName: online-loan-app-api-blob-storage-connection-settings-account-key + objectType: secret + objectVersion: "" \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/appsettings/appsettings.json b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/appsettings/appsettings.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/appsettings/appsettings.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/kustomization.yaml new file mode 100644 index 0000000..c67b641 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/kustomization.yaml @@ -0,0 +1,40 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +#Test istio process +namePrefix: normal- + +configMapGenerator: + - name: online-loan-app-api-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + +replacements: + - source: + kind: Deployment + fieldPath: metadata.name + targets: + - select: + kind: Deployment + fieldPaths: + - spec.selector.matchLabels.app + - spec.template.metadata.labels.app + - spec.template.spec.containers.*.name + + - select: + kind: Service + fieldPaths: + - spec.selector.app + + - select: + kind: VirtualService + fieldPaths: + - spec.http.*.route.*.destination.host + - spec.http.*.name diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/set_appsettings_deployment.yaml new file mode 100644 index 0000000..4a4fd1e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-appsettings + configMap: + name: online-loan-app-api-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-appsettings + mountPath: /app/config/workload-appsettings diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/appsettings/appsettings.json b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/appsettings/appsettings.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/appsettings/appsettings.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/kustomization.yaml new file mode 100644 index 0000000..c67b641 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/kustomization.yaml @@ -0,0 +1,40 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +#Test istio process +namePrefix: normal- + +configMapGenerator: + - name: online-loan-app-api-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + +replacements: + - source: + kind: Deployment + fieldPath: metadata.name + targets: + - select: + kind: Deployment + fieldPaths: + - spec.selector.matchLabels.app + - spec.template.metadata.labels.app + - spec.template.spec.containers.*.name + + - select: + kind: Service + fieldPaths: + - spec.selector.app + + - select: + kind: VirtualService + fieldPaths: + - spec.http.*.route.*.destination.host + - spec.http.*.name diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/set_appsettings_deployment.yaml new file mode 100644 index 0000000..4a4fd1e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-appsettings + configMap: + name: online-loan-app-api-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-appsettings + mountPath: /app/config/workload-appsettings diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml new file mode 100644 index 0000000..0bf2a36 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml + - set_secrets_provider_class.yaml +images: +- name: online-loan-app-api + newName: landmarkcu/lcu.api.internal.onlineloanappapi-onlineloanappapi diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..eaa9416 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app-api +spec: + template: + spec: + containers: + - name: online-loan-app-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_secrets_provider_class.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..9300701 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: online-loan-app-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + keyvaultName: kv-lz-apps-qa-uscz diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml new file mode 100644 index 0000000..fea4470 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: online-loan-app-api-configmap +data: + primary.json: | + { + "Primary": { + "SeqUrl": "https://qa-seq.lcu-internal.com", + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://qa-api.lcu-internal.com" + ] + } + } \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml new file mode 100644 index 0000000..2243ec2 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app-api +spec: + hosts: + - online-loan-app-api.apps.azure.lcu-internal.com diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/appsettings/appsettings.json b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/appsettings/appsettings.json new file mode 100644 index 0000000..55850ef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/appsettings/appsettings.json @@ -0,0 +1,67 @@ +{ + "Primary": { + "SeqUrl": "https://seq.lcu-internal.com/" + }, + "ApplicationControl": { + "HomePageUrl": "https://prd-online-loan-app.apps.azure.lcu-internal.com" + }, + "ApplicationStorage": { + "UserName": "loan-app-prod", + "Host": "loan-app-prod.documents.azure.com" + }, + "CrifSettings": { + "ApiSettings": { + "EndpointUrl": "https://www-lc3cs.myappro.com/ACS/ConnectorService.svc/ConnectorService" + }, + "CustomFieldMappings": { + "LoanTypeToPromoId": { + "132": "-114", + "133": "-114", + "134": "-114", + "136": "-114", + "139": "-118", + "148": "-118", + "149": "-118", + "146": "-118", + "125": "-117", + "126": "-117", + "129": "-117", + "131": "-117", + "130": "-117", + "140": "-115" + }, + "CustomFieldID": { + "LicensePlateNumber": "-76", + "CountyTitled": "-96", + "MunicipalityType": "-171", + "MunicipalityWhereKept": "-172" + }, + "MunicipalityTypeCode": { + "CITY": "401", + "TOWN": "402", + "VILLAGE": "403" + } + } + }, + "SecuritySettings": { + "SAMLSuccessRedirectRoute": "https://apply.landmarkcu.com/select-loan?referrer=NCR", + "SAMLFailureRedirectRoute": "https://apply.landmarkcu.com/error?referrer=NCR", + "DigitalInsightPostTokenURI": "https://diapis.digitalinsight.com/v1/oauth/token" + }, + "NotificationsApiConfiguration": { + "Url": "https://prd-notifications-api.apps.azure.lcu-internal.com", + "ErrorRecipients": [ "ITDevelopment@landmarkcu.com" ] + }, + "CoreSettings": { + "EndPoint": "https://prd-core-api-wrapper-api.apps.azure.lcu-internal.com/" + }, + "BlobStorageConnectionSettings": { + "AccountName": "prodloanappsa" + }, + "DealershipEmailSettings": { + "RedBannerMessage": "", + "Recipients": [ "dealerportal@landmarkcu.com" ], + "ActionLink": "https://actiona6.criflending.com/Core/Application/AppSecurity.aspx?orgCode=landmarkcu&resID=&resTypeID=-995&appID=", + "EmailAPIUrl": "https://notificationsapi.lcu-internal.com/api/Email" + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/kustomization.yaml new file mode 100644 index 0000000..75fab81 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_appsettings_deployment.yaml new file mode 100644 index 0000000..335b8be --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_appsettings_deployment.yaml @@ -0,0 +1,5 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: ASPNETCORE_ENVIRONMENT + value: "Production" \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_secrets_provider_class.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..816f14c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_secrets_provider_class.yaml @@ -0,0 +1,7 @@ +- op: add + path: /spec/parameters/userAssignedIdentityID + value: 9eaefc49-e5d3-48d4-a2b9-b42cf2d8ca03 + +- op: add + path: /spec/parameters/keyvaultName + value: kv-lz-apps-prod-uscz diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml new file mode 100644 index 0000000..b228fef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: online-loan-app-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml new file mode 100644 index 0000000..b74d310 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal-prod + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml new file mode 100644 index 0000000..058a669 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml @@ -0,0 +1,3 @@ +- op: add + path: /spec/hosts/- + value: prd-online-loan-app-api.apps.azure.lcu-internal.com \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/set_configmap_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/set_configmap_deployment.yaml new file mode 100644 index 0000000..bd63e29 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: online-loan-app-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/appsettings/appsettings.json b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/appsettings/appsettings.json new file mode 100644 index 0000000..c5225bf --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/appsettings/appsettings.json @@ -0,0 +1,67 @@ +{ + "Primary": { + "SeqUrl": "https://qa-seq.lcu-internal.com/" + }, + "ApplicationControl": { + "HomePageUrl": "https://qa-online-loan-app.apps.azure.lcu-internal.com" + }, + "ApplicationStorage": { + "UserName": "loan-app-dvl", + "Host": "loan-app-dvl.documents.azure.com" + }, + "CrifSettings": { + "ApiSettings": { + "EndpointUrl": "https://www-lc3cst.myappro.com/ACS/ConnectorService.svc/ConnectorService" + }, + "CustomFieldMappings": { + "LoanTypeToPromoId": { + "132": "-114", + "133": "-114", + "134": "-114", + "136": "-114", + "139": "-118", + "148": "-118", + "149": "-118", + "146": "-118", + "125": "-117", + "126": "-117", + "129": "-117", + "131": "-117", + "130": "-117", + "140": "-115" + }, + "CustomFieldID": { + "LicensePlateNumber": "-76", + "CountyTitled": "-96", + "MunicipalityType": "-173", + "MunicipalityWhereKept": "-174" + }, + "MunicipalityTypeCode": { + "CITY": "402", + "TOWN": "403", + "VILLAGE": "404" + } + } + }, + "SecuritySettings": { + "SAMLSuccessRedirectRoute": "https://applydev.landmarkcu.com/select-loan?referrer=NCR", + "SAMLFailureRedirectRoute": "https://applydev.landmarkcu.com/error?referrer=NCR", + "DigitalInsightPostTokenURI": "https://diapis.stg1.digitalinsight.com/v1/oauth/token" + }, + "NotificationsApiConfiguration": { + "Url": "https://qa-notifications-api.apps.azure.lcu-internal.com", + "ErrorRecipients": [ "ITDevelopment@landmarkcu.com" ] + }, + "CoreSettings": { + "EndPoint": "https://qa-core-api-wrapper-api.apps.azure.lcu-internal.com/" + }, + "BlobStorageConnectionSettings": { + "AccountName": "devloanappsa" + }, + "DealershipEmailSettings": { + "RedBannerMessage": "This is from the QA test environment.", + "Recipients": [ "ITDevelopment@landmarkcu.com" ], + "ActionLink": "https://actiona6t.criflending.com/Core/Application/AppSecurity.aspx?orgCode=landmarkcu&resID=&resTypeID=-995&appID=", + "EmailAPIUrl": "https://qa-notificationsapi.lcu-internal.com/api/Email" + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/kustomization.yaml new file mode 100644 index 0000000..75fab81 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_appsettings_deployment.yaml new file mode 100644 index 0000000..818dec4 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_appsettings_deployment.yaml @@ -0,0 +1,5 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: ASPNETCORE_ENVIRONMENT + value: Development \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_secrets_provider_class.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..8719a77 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_secrets_provider_class.yaml @@ -0,0 +1,7 @@ +- op: add + path: /spec/parameters/userAssignedIdentityID + value: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + +- op: add + path: /spec/parameters/keyvaultName + value: kv-lz-apps-qa-uscz diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml new file mode 100644 index 0000000..b228fef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: online-loan-app-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml new file mode 100644 index 0000000..a1e6b06 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml new file mode 100644 index 0000000..fdb0031 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml @@ -0,0 +1,3 @@ +- op: add + path: /spec/hosts/- + value: qa-online-loan-app-api.apps.azure.lcu-internal.com \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/set_configmap_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/set_configmap_deployment.yaml new file mode 100644 index 0000000..bd63e29 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: online-loan-app-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/appsettings/appsettings.json b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/appsettings/appsettings.json new file mode 100644 index 0000000..dff032c --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/appsettings/appsettings.json @@ -0,0 +1,67 @@ +{ + "Primary": { + "SeqUrl": "https://uat-seq.lcu-internal.com/" + }, + "ApplicationControl": { + "HomePageUrl": "https://uat-online-loan-app.apps.azure.lcu-internal.com" + }, + "ApplicationStorage": { + "UserName": "loan-app-qa", + "Host": "loan-app-qa.documents.azure.com" + }, + "CrifSettings": { + "ApiSettings": { + "EndpointUrl": "https://www-lc3cst.myappro.com/ACS/ConnectorService.svc/ConnectorService" + }, + "CustomFieldMappings": { + "LoanTypeToPromoId": { + "132": "-114", + "133": "-114", + "134": "-114", + "136": "-114", + "139": "-118", + "148": "-118", + "149": "-118", + "146": "-118", + "125": "-117", + "126": "-117", + "129": "-117", + "131": "-117", + "130": "-117", + "140": "-115" + }, + "CustomFieldID": { + "LicensePlateNumber": "-76", + "CountyTitled": "-96", + "MunicipalityType": "-173", + "MunicipalityWhereKept": "-174" + }, + "MunicipalityTypeCode": { + "CITY": "402", + "TOWN": "403", + "VILLAGE": "404" + } + } + }, + "SecuritySettings": { + "SAMLSuccessRedirectRoute": "https://applyqa.landmarkcu.com/select-loan?referrer=NCR", + "SAMLFailureRedirectRoute": "https://applyqa.landmarkcu.com/error?referrer=NCR", + "DigitalInsightPostTokenURI": "https://diapis.stg1.digitalinsight.com/v1/oauth/token" + }, + "NotificationsApiConfiguration": { + "Url": "https://uat-notifications-api.apps.azure.lcu-internal.com", + "ErrorRecipients": [ "ITDevelopment@landmarkcu.com" ] + }, + "CoreSettings": { + "EndPoint": "https://uat-core-api-wrapper-api.apps.azure.lcu-internal.com/" + }, + "BlobStorageConnectionSettings": { + "AccountName": "qaloanappsa" + }, + "DealershipEmailSettings": { + "RedBannerMessage": "This is from the UAT test environment.", + "Recipients": [ "ITDevelopment@landmarkcu.com", "dealerportal@landmarkcu.com" ], + "ActionLink": "https://actiona6t.criflending.com/Core/Application/AppSecurity.aspx?orgCode=landmarkcu&resID=&resTypeID=-995&appID=", + "EmailAPIUrl": "https://uat-notificationsapi.lcu-internal.com/api/Email" + } +} diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/kustomization.yaml new file mode 100644 index 0000000..75fab81 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_appsettings_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_appsettings_deployment.yaml new file mode 100644 index 0000000..4fd0533 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_appsettings_deployment.yaml @@ -0,0 +1,5 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: ASPNETCORE_ENVIRONMENT + value: "Staging" \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_secrets_provider_class.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..efe4103 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_secrets_provider_class.yaml @@ -0,0 +1,7 @@ +- op: add + path: /spec/parameters/userAssignedIdentityID + value: 64ba01ed-1ecc-4c9b-b56e-8d618f2b81ce + +- op: add + path: /spec/parameters/keyvaultName + value: kv-lz-apps-uat-uscz diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml new file mode 100644 index 0000000..b228fef --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: online-loan-app-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml new file mode 100644 index 0000000..a1e6b06 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml new file mode 100644 index 0000000..52c649e --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml @@ -0,0 +1,3 @@ +- op: add + path: /spec/hosts/- + value: uat-online-loan-app-api.apps.azure.lcu-internal.com \ No newline at end of file diff --git a/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/set_configmap_deployment.yaml b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/set_configmap_deployment.yaml new file mode 100644 index 0000000..bd63e29 --- /dev/null +++ b/LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: online-loan-app-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings