From f3cb730f3b1fed2341f2abbcea2f8db5a4992df6 Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Wed, 11 Mar 2026 11:18:25 -0500 Subject: [PATCH] first commit --- LCU.API.Internal.OnlineLoanApp/.dockerignore | 17 + LCU.API.Internal.OnlineLoanApp/.gitignore | 330 + .../CRIF.ACTion.Tests/BuilderTestBase.cs | 19 + .../CRIF.ACTion.Tests.csproj | 299 + .../Decisioning/DecisionStatusBuilderTest.cs | 33 + .../DecisioningStateResponseParserTest.cs | 28 + ...etApplicationProductsRequestBuilderTest.cs | 32 + ...etApplicationProductsResponseParserTest.cs | 60 + .../SendToDecisioningRequestBuilderTest.cs | 32 + .../AddAppFilesSection/AddAppFiles.xml | 9 + .../ExampleXML/AddIndividual-BadNamespace.xml | 29 + .../ExampleXML/AddIndividual-Simple.xml | 47 + .../ExampleXML/AddIndividualError.xml | Bin 0 -> 426 bytes .../ExampleXML/AddIndividualSuccess.xml | Bin 0 -> 4778 bytes .../AddressesSection/AddAddress.xml | 5 + .../SetAsStudentCcProduct.xml | 21 + .../AddIndividualBorrowerParty.xml | 9 + ...vidualNonSigningCoApplicantSpouseParty.xml | 9 + ...IndividualNonSigningPrimarySpouseParty.xml | 9 + .../AddIndividualPrimaryBorrowerParty.xml | 9 + .../AppPartiesSection/SetServicingChannel.xml | 3 + .../AppPartiesSection/SetSourceChannel.xml | 3 + .../ExampleXML/CreateApp/AppWithPromo1.xml | 4971 ++++++++++ .../CreateApp/CreateApplication.xml | 198 + ...eateApplicationNonSigningHistorySpouse.xml | 190 + .../CreateApp/CreateApplicationWithNote.xml | 202 + .../ExampleXML/Existing/cc_cash_back.xml | 88 + .../ExampleXML/Existing/equity_heloc.xml | 109 + .../Existing/unsecured_line_of_credit.xml | 86 + .../ExampleXML/Existing/vehicle_car_loan.xml | 112 + .../Existing/vehicle_marine_2borrowers.xml | 158 + .../GetApplicationProducts.xml | 3 + .../CashBackCC_Under-Review.xml | 977 ++ .../InvalidApplicationId.xml | 13 + .../Multiple-AppDetails.xml | 2778 ++++++ .../GetDecisionStatus/GetDecisionStatus.xml | 3 + .../HelocFlex/CreateApplication.xml | 134 + .../ExampleXML/MfgHomeAndLand/CreateApp.xml | 134 + .../CreateAppWithAnnualTaxes.xml | 134 + .../ExampleXML/OrgSection/SetOrgId.xml | 1 + .../PartiesSection/AddChannelParty.xml | 3 + .../ExampleXML/PartiesSection/AddEmployer.xml | 9 + .../PartiesSection/AddIndividual.xml | 44 + .../PartiesSection/AddOrganizationParty.xml | 3 + ...AddressContactMechanismElement-Current.xml | 4 + .../GetAddressContactMechanismElement.xml | 4 + .../GetContactMechanismElement.xml | 3 + .../GetContactMechanismsSectionElement.xml | 16 + ...mployerContactMechanismsSectionElement.xml | 5 + .../GetEthnicitiesElement-DidNotProvide.xml | 3 + .../PartiesSection/GetEthnicitiesElement.xml | 3 + .../GetIncomesSectionElement.xml | 6 + .../GetMaritalStatusElement-False.xml | 1 + .../GetMaritalStatusElement-True.xml | 1 + .../GetNoCoApplicantDetermination.xml | 6 + .../GetPartyRelationshipsSectionElement.xml | 8 + .../GetRacesElement-NotProvided.xml | 3 + .../PartiesSection/GetRacesElement.xml | 5 + .../GetSexAttributes-NotProvided.xml | 1 + .../PartiesSection/GetSexAttributes.xml | 1 + .../PartiesSection/GetSuffixElement.xml | 1 + .../PartiesSection/GetTradesElement.xml | 11 + .../SendToDecisioning/sendToDecisioning.xml | 3 + .../SuccessResponse.xml | 25 + .../ExampleXML/Signature/CreateApp.xml | Bin 0 -> 12418 bytes .../ExampleXML/Vehicle/CreateApp.xml | 128 + ...ateApplicationWithNoteAndVehicleSeller.xml | 136 + .../CreateApplicationWithVehicleSeller.xml | 132 + .../ExampleXML/Vehicle/CreateAtvApp.xml | 128 + .../ExampleXML/Vehicle/CreateBoatApp.xml | 139 + .../Vehicle/CreateMotorcycleApp.xml | 128 + .../Vehicle/CreatePreapprovalApp.xml | 110 + .../ExampleXML/Vehicle/CreatePwcApp.xml | 128 + .../Vehicle/CreateRvPowersportApp.xml | 128 + .../Vehicle/CreateSnowmobileApp.xml | 128 + .../Vehicle/CreateTrailerMotorhomeApp.xml | 128 + .../Loan/AppDetailBankcardSectionTest.cs | 35 + .../Loan/FileRequestBuilderTest.cs | 35 + .../CRIF.ACTion.Tests/Loan/HelocFlexTest.cs | 141 + .../AddIndividualResponseParserTest.cs | 121 + .../Loan/Individuals/AddressesSectionTest.cs | 40 + .../Loan/Individuals/AppPartiesSectionTest.cs | 215 + .../ExampleXML/BuildIncomeSuccess.xml | Bin 0 -> 642 bytes .../Individuals/IncomeSectionBuilderTest.cs | 50 + .../Loan/Individuals/PartiesSectionTest.cs | 451 + .../Loan/LienPositionTest.cs | 62 + .../Loan/LoanApplicationRequestBuilderTest.cs | 384 + .../Loan/MfgHomeAndLandTest.cs | 243 + .../CRIF.ACTion.Tests/Loan/OrgSectionTest.cs | 28 + .../Loan/SignatureLoanTest.cs | 99 + .../CRIF.ACTion.Tests/Loan/VehicleEtcTest.cs | 320 + .../Loan/json/TestLienPositions.json | 54 + .../CRIF.ACTion.Tests/TestCommon.cs | 254 + .../TestDistributionChannelLookupService.cs | 13 + .../CRIF.ACTion.Tests/XSDs/actionAPI.XSD | 1739 ++++ .../XSDs/actionAPIResponse.xsd | 278 + .../CRIF.ACTion.Tests/XSDs/appSchema.xsd | 3060 ++++++ .../CRIF.ACTion.Tests/XSDs/commonSchema.xsd | 8712 +++++++++++++++++ .../CRIF.ACTion.Tests/XSDs/maintSchema.xsd | 596 ++ .../CRIF.ACTion.Tests/XSDs/msgSchema.xsd | 1346 +++ .../CRIF.ACTion.Tests/XSDs/qoSchema.xsd | 3289 +++++++ .../CRIF.ACTion.Tests/XSDs/utilSchema.xsd | 42 + .../additional_test_args.runsettings | 27 + .../CRIF.ACTion/CRIF.ACTion.csproj | 40 + .../Common/Builders/BuilderBase.cs | 33 + .../Common/Builders/BuilderUtils.cs | 10 + .../Common/Builders/CrifRequestSection.cs | 44 + .../CRIF.ACTion/Common/Builders/IBuildable.cs | 9 + .../CRIF.ACTion/Common/ContactMechanisms.cs | 107 + .../CRIF.ACTion/Common/CrifNamespaces.cs | 70 + .../CRIF.ACTion/Common/Dates.cs | 16 + .../CRIF.ACTion/Common/ICrifNamespaces.cs | 19 + .../Common/Parsers/IResponseParser.cs | 9 + .../Common/Parsers/XElementExtensions.cs | 34 + .../ConfigurationRequestBuilder.cs | 24 + .../ConfigurationRequestResponseParser.cs | 78 + .../Configuration/CrifConstants.cs | 86 + .../CRIF.ACTion/Configuration/CrifSettings.cs | 106 + .../CustomFieldRequestResponseParser.cs | 222 + .../ProductDetailsResponseParser.cs | 47 + .../Decisioning/DecisionStatusBuilder.cs | 29 + .../DecisioningStateResponseParser.cs | 47 + .../GetApplicationProductsRequestBuilder.cs | 26 + .../GetApplicationProductsResponseParser.cs | 165 + .../SendToDecisioningRequestBuilder.cs | 26 + .../CRIF API Risks and Issues.md | 32 + .../CRIF.ACTion/Files/AddAppFilesSection.cs | 82 + .../CRIF.ACTion/Files/FileRequestBuilder.cs | 34 + .../CRIF.ACTion/Files/IAddAppFiles.cs | 7 + .../CRIF.ACTion/Files/IFileRequestBuilder.cs | 8 + .../Loan/AppDetailBankcardSection.cs | 14 + .../CRIF.ACTion/Loan/AppDetailHelocSection.cs | 89 + .../Loan/AppDetailSignatureSection.cs | 13 + .../CRIF.ACTion/Loan/AppDetailsSection.cs | 237 + .../Loan/AppNotesSectionBuilder.cs | 35 + .../Assets/AppDetailMfgHomeAndLandSection.cs | 23 + .../Loan/Assets/AppDetailRealEstateSection.cs | 16 + .../Loan/Assets/AppDetailVehicleSection.cs | 69 + .../Loan/Assets/IVehicleValuations.cs | 7 + .../Assets/MfgHousingAndLandAppBuilder.cs | 36 + .../Loan/Assets/MfgHousingProperty.cs | 12 + .../CRIF.ACTion/Loan/Assets/PropertyValues.cs | 32 + .../Loan/Assets/RealEstateEquityAppBuilder.cs | 27 + .../Loan/Assets/RealEstateProperty.cs | 221 + .../Loan/Assets/VehicleAppBuilder.cs | 45 + .../Loan/Assets/VehicleProperty.cs | 167 + .../Loan/Assets/VehicleValuationsSection.cs | 79 + .../CRIF.ACTion/Loan/CreditCardAppBuilder.cs | 45 + .../Loan/DealershipRequestBuilder.cs | 35 + .../Loan/DealershipResponseParser.cs | 96 + .../CRIF.ACTion/Loan/HelocFlexAppBuilder.cs | 28 + .../CRIF.ACTion/Loan/IAppDetails.cs | 7 + .../CRIF.ACTion/Loan/IAppNotes.cs | 7 + .../Loan/ILoanApplicationRequestBuilder.cs | 7 + .../CRIF.ACTion/Loan/IOrg.cs | 7 + .../CRIF.ACTion/Loan/IProperties.cs | 7 + .../CRIF.ACTion/Loan/IProperty.cs | 8 + .../AddIndividualRequestBuilder.cs | 65 + .../AddIndividualResponseParser.cs | 42 + .../Loan/Individuals/AddressesSection.cs | 68 + .../Loan/Individuals/AppPartiesSection.cs | 111 + .../Loan/Individuals/CrifResidenceType.cs | 29 + .../Loan/Individuals/IAddresses.cs | 7 + .../Loan/Individuals/IAppParties.cs | 7 + .../CRIF.ACTion/Loan/Individuals/IParties.cs | 7 + .../Loan/Individuals/IncomeSectionBuilder.cs | 36 + .../Loan/Individuals/MaritalStatus.cs | 29 + .../Loan/Individuals/PartiesSection.cs | 788 ++ .../Loan/Individuals/UserEmploymentType.cs | 38 + .../Loan/LoanApplicationRequestBuilder.cs | 384 + .../Loan/LoanApplicationResponseParser.cs | 25 + .../CRIF.ACTion/Loan/OrgSection.cs | 24 + .../CRIF.ACTion/Loan/PropertiesSection.cs | 19 + .../Loan/SavingsAccountProperty.cs | 29 + .../CRIF.ACTion/Loan/SignatureAppBuilder.cs | 21 + .../CRIF.ACTion/README.md | 9 + .../CRIF.ACTion/ServiceReference/Reference.cs | 778 ++ .../CRIF.ACTion/Services/Crif/CrifService.cs | 139 + .../Services/Crif/CrifVendorClient.cs | 265 + .../Services/Crif/Error13Exception.cs | 10 + .../CRIF.ACTion/Services/Crif/ICrifService.cs | 11 + .../Services/Crif/ICrifVendorClient.cs | 29 + .../Services/Crif/IWcfInspectorBehavior.cs | 9 + .../Services/Crif/WcfInspectorBehavior.cs | 46 + .../IDistributionChannelLookupService.cs | 7 + ...andmarkDistributionChannelLookupService.cs | 23 + .../Services/Landmark/ProductIdService.cs | 38 + .../ISourceChannelLookupService.cs | 9 + .../LandmarkSourceChannelLookupService.cs | 40 + LCU.API.Internal.OnlineLoanApp/Dockerfile | 65 + .../Common/Domain/EmailRequest.cs | 10 + .../Common/Domain/StoredSettings.cs | 15 + .../Common/Domain/UiSettings.cs | 67 + .../Security/AzureActiveDirectoryOptions.cs | 12 + .../AzureAdServiceCollectionExtensions.cs | 49 + .../Common/Security/CookieService.cs | 70 + .../Common/Security/CosmosIdService.cs | 22 + .../Common/Security/ForwardedHeaderService.cs | 26 + .../Common/Security/MemberIdService.cs | 28 + .../RenewAuthenticatedSessionMiddleware.cs | 33 + .../Configuration/ApplicationSettings.cs | 13 + .../BlobStorageConnectionSettings.cs | 9 + .../Configuration/BlobStorageSettings.cs | 13 + .../Configuration/DealershipEmailSettings.cs | 12 + .../NotificationsApiConfiguration.cs | 15 + .../AddressValidationController.cs | 73 + .../Controllers/BranchConfigController.cs | 223 + .../Controllers/DecisioningController.cs | 142 + .../Controllers/IndividualController.cs | 63 + .../Controllers/LoanApplicationController.cs | 440 + .../Controllers/LoggingController.cs | 44 + .../Controllers/OnlineIdentityFormData.cs | 8 + .../Controllers/PdfController.cs | 79 + .../SAMLAssertionConsumerController.cs | 213 + .../Controllers/StatusController.cs | 92 + .../Controllers/StorageController.cs | 577 ++ .../Controllers/UiSettingsController.cs | 165 + .../DataAccess/BlobStorageAccess.cs | 72 + .../DataAccess/BranchDataAccess.cs | 250 + .../DataAccess/ConfigurationAccess.cs | 114 + .../DataAccess/DataAccessConfiguration.cs | 13 + .../DataAccess/DataClient.cs | 45 + .../DataAccess/IBlobStorageAccess.cs | 15 + .../DataAccess/IBranchDataAccess.cs | 15 + .../DataAccess/IConfigurationAccess.cs | 12 + .../DataAccess/IDataClient.cs | 9 + .../DataAccess/ILoanApplicationDataAccess.cs | 22 + .../DataAccess/INotificationDao.cs | 10 + .../DataAccess/LoanApplicationDataAccess.cs | 426 + .../DataAccess/NotificationDao.cs | 24 + ...ERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf | Bin 0 -> 137866 bytes .../Landmark.OLA.Application/Files/Logo.png | Bin 0 -> 7592 bytes ... Equity Lines of Credit Revised 8-2022.pdf | Bin 0 -> 401602 bytes .../Landmark.OLA.Application.csproj | 65 + .../Landmark.OLA.Application.xml | 286 + .../Landmark.OLA.Application/Program.cs | 37 + .../Properties/launchSettings.json | 30 + .../Services/CoreAccessService.cs | 47 + .../Services/CoreService.cs | 34 + .../Services/CoreSettings.cs | 9 + .../Services/Email/EmailTemplateService.cs | 113 + .../Services/Email/IEmailTemplateService.cs | 10 + .../Exceptions/CoreUnavailableException.cs | 12 + .../Services/ICoreService.cs | 10 + .../Services/IPdfService.cs | 8 + .../INotificationsApiService.cs | 10 + .../NotificationsApiService.cs | 32 + .../Services/PdfService.cs | 18 + .../Landmark.OLA.Application/Startup.cs | 256 + .../Landmark.OLA.Application/Status.cs | 43 + .../Landmark.OLA.Application/appsettings.json | 158 + .../Landmark.OLA.Application/wwwroot/.gitkeep | 0 .../AddressValidationControllerTest.cs | 57 + .../CoreAccessServiceTest.cs | 112 + .../CustomWebApplicationFactory.cs | 39 + .../DecisionControllerTest.cs | 56 + .../IndividualControllerTest.cs | 32 + .../Landmark.OLA.Integration.Tests.csproj | 19 + .../LoanApplicationControllerTest.cs | 70 + .../OlaIntegrationTest.cs | 91 + .../SAMLAssertionControllerTest.cs | 45 + .../StatusControllerTest.cs | 42 + .../UISettingsControllerTest.cs | 54 + .../additional_test_args.runsettings | 27 + .../Landmark.OLA.Logging.csproj | 15 + .../Landmark.OLA.Models/Addresses/Address.cs | 46 + .../Addresses/Enums/CountryOption.cs | 8 + .../Addresses/Enums/ResidenceTypeOption.cs | 9 + .../Addresses/Enums/StateOption.cs | 71 + .../Landmark.OLA.Models/Addresses/IAddress.cs | 21 + .../Addresses/PropertyLot.cs | 29 + .../Addresses/Validators/AddressValidator.cs | 16 + .../ApplicationControlConfiguration.cs | 10 + .../Assets/Enums/LienPositionType.cs | 11 + .../Assets/Enums/ManufacturedHomeType.cs | 10 + .../Assets/Enums/NewUsed.cs | 8 + .../Assets/Enums/PropertyUse.cs | 14 + .../Assets/Enums/RealEstateType.cs | 19 + .../Assets/Enums/UsageType.cs | 8 + .../Assets/Enums/VehicleSeller.cs | 8 + .../Assets/Enums/VehicleType.cs | 14 + .../Assets/Enums/VehicleValuationType.cs | 14 + .../Assets/LienPosition.cs | 14 + .../Assets/ManufacturedHome.cs | 29 + .../Landmark.OLA.Models/Assets/RealEstate.cs | 20 + .../Assets/RealEstateBase.cs | 48 + .../Landmark.OLA.Models/Assets/Vehicle.cs | 123 + .../Landmark.OLA.Models/ClientError.cs | 10 + .../Landmark.OLA.Models/Core/CoreAddress.cs | 24 + .../Core/CoreIndividual.cs | 40 + .../Core/CoreMemberOwner.cs | 14 + .../Landmark.OLA.Models/Core/CorePhone.cs | 14 + .../Landmark.OLA.Models/Core/MemberMeta.cs | 10 + .../Decisioning/DecisioningDetails.cs | 36 + .../Decisioning/Enums/DecisioningResult.cs | 163 + .../Decisioning/Enums/DecisioningState.cs | 11 + .../HomeMortgageDisclosureAct/Hmda.cs | 35 + .../Validators/HmdaValidator.cs | 32 + .../Landmark.OLA.Models/IReferenceable.cs | 8 + .../Individuals/CoApplicant.cs | 20 + .../Individuals/Employer.cs | 54 + .../Individuals/Enums/EmploymentTypeOption.cs | 13 + .../Individuals/Enums/Ethnicity.cs | 16 + .../Individuals/Enums/Frequency.cs | 13 + .../Individuals/Enums/LanguageOption.cs | 9 + .../Individuals/Enums/PhoneType.cs | 8 + .../Individuals/Enums/Race.cs | 26 + .../Individuals/Enums/Sex.cs | 12 + .../Individuals/Enums/Suffix.cs | 13 + .../Individuals/Enums/SuffixMap.cs | 45 + .../Individuals/HistoryIndividual.cs | 101 + .../Individuals/IApplicant.cs | 17 + .../Individuals/ICoApplicant.cs | 7 + .../Individuals/IIndividualHistory.cs | 49 + .../Individuals/INonApplicant.cs | 24 + .../Landmark.OLA.Models/Individuals/Income.cs | 68 + .../Individuals/IncomeLimits.cs | 16 + .../Individuals/Individual.cs | 44 + .../Individuals/MailableSpouse.cs | 51 + .../Validators/ApplicantValidator.cs | 26 + .../Validators/EmployerValidator.cs | 16 + .../Landmark.OLA.Models.csproj | 20 + .../Landmark.OLA.Models/Limit.cs | 8 + .../Landmark.OLA.Models/Loan/BranchConfig.cs | 32 + .../Landmark.OLA.Models/Loan/BranchMapping.cs | 485 + .../Loan/BranchNameConfig.cs | 27 + .../Loan/DealershipAuthRequest.cs | 60 + .../Loan/DocumentUploadFromLoanApplication.cs | 28 + .../Loan/Enums/LoanErrorCode.cs | 11 + .../Loan/Enums/LoanStatus.cs | 13 + .../Loan/Enums/LoanType.cs | 29 + .../Landmark.OLA.Models/Loan/Filter.cs | 13 + .../Landmark.OLA.Models/Loan/FilterField.cs | 13 + .../Loan/LoanApplication.cs | 80 + .../Loan/LoanApplicationFile.cs | 42 + .../Landmark.OLA.Models/Loan/LoanContainer.cs | 34 + .../Landmark.OLA.Models/Loan/LoanError.cs | 15 + .../Landmark.OLA.Models/Loan/LoanLimits.cs | 36 + .../Landmark.OLA.Models/Loan/LoanPage.cs | 18 + .../Landmark.OLA.Models/Loan/LoanPurpose.cs | 109 + .../Loan/MockApplication.cs | 155 + .../Landmark.OLA.Models/Loan/TermLengths.cs | 223 + .../Loan/Validators/BranchConfigValidator.cs | 33 + .../Validators/LoanApplicationValidator.cs | 283 + .../Loan/Validators/VehicleValidator.cs | 80 + .../Landmark.OLA.Models/ModelValidator.cs | 37 + .../Landmark.OLA.Models/OlaModel.cs | 25 + .../OnlineBanking/AuthTokenResponse.cs | 33 + .../OnlineBanking/AuthenticationStatement.cs | 9 + .../OnlineBanking/Constants.cs | 17 + .../OnlineBanking/DISAMLAssertion.cs | 13 + .../OnlineBanking/OnlineBankingFormData.cs | 13 + .../OnlineBanking/OnlineBankingToken.cs | 22 + .../OnlineBanking/SAMLConditions.cs | 10 + .../Residences/CurrentResidenceAddress.cs | 23 + .../Residences/ICurrentResidenceAddress.cs | 11 + .../Residences/IResidenceAddress.cs | 12 + .../Residences/ResidenceAddress.cs | 30 + .../ValidationConstants.cs | 43 + .../Vendor/IUspsVendorClient.cs | 9 + .../Validators/AddressValidatorTest.cs | 55 + .../Controllers/AddressValidationTest.cs | 37 + .../Controllers/LoggingTest.cs | 26 + .../Controllers/UiSettingsTest.cs | 36 + .../Landmark.OLA.Tests.csproj | 27 + .../Loan/Enums/LoanTypeTest.cs | 84 + .../Validators/BranchConfigValidatorTest.cs | 44 + .../LoanApplicationValidatorSpouseTest.cs | 217 + .../LoanApplicationValidatorTest.cs | 147 + .../Landmark.OLA.Tests/ModelValidatorTest.cs | 128 + .../Services/CoreAccessServiceTest.cs | 70 + .../TestData/TestAddresses.cs | 77 + .../TestData/TestEmployers.cs | 40 + .../TestData/TestIncomes.cs | 39 + .../TestData/TestIndividuals.cs | 67 + .../TestData/json/TestValidatorLoans.json | 121 + .../json/TestVehicleTermValidity.json | 45 + .../additional_test_args.runsettings | 27 + .../Landmark.OLA.sln | 86 + .../AuthenticationParserTest.cs | 24 + .../CookieCrytorTest.cs | 21 + .../ExampleXML/DecryptedAssertion.xml | 99 + .../OnlineBankingAuth.Tests.csproj | 26 + .../TokenServiceTest.cs | 113 + .../additional_test_args.runsettings | 27 + .../Configuration/SecuritySettings.cs | 16 + .../OnlineBankingAuth.csproj | 23 + .../Parsers/AuthTokenParser.cs | 74 + .../Parsers/IAuthTokenParser.cs | 10 + .../OnlineBankingAuth/Parsers/ISAMLParser.cs | 12 + .../Parsers/SAMLMessaging.cs | 8 + .../OnlineBankingAuth/Parsers/SAMLParser.cs | 70 + .../Services/DigitalInsightService.cs | 84 + .../Services/DigitalInsightVendorClient.cs | 143 + .../Services/IDigitalInsightService.cs | 10 + .../Services/IDigitalInsightVendorClient.cs | 16 + .../OnlineBankingAuth/Tokens/CookieCryptor.cs | 82 + .../OnlineBankingAuth/Tokens/ITokenService.cs | 16 + .../OnlineBankingAuth/Tokens/TokenService.cs | 168 + LCU.API.Internal.OnlineLoanApp/README.md | 170 + .../USPS.Tests/AddressValidationTest.cs | 82 + .../ExampleXML/USPSAddressError.xml | 12 + .../ExampleXML/USPSAddressSuccess.xml | 11 + .../USPS.Tests/USPS.Tests.csproj | 35 + .../additional_test_args.runsettings | 27 + .../USPS/Configuration/UspsSettings.cs | 10 + .../AddressValidationRequestBuilder.cs | 56 + .../USPS/Requests/IRequestBuildable.cs | 8 + .../AddressValidationResponseParser.cs | 68 + .../USPS/Responses/IResponseParser.cs | 10 + .../USPS/Services/IUspsService.cs | 9 + .../USPS/Services/UspsService.cs | 46 + .../USPS/Services/UspsVendorClient.cs | 62 + .../USPS/USPS.csproj | 20 + .../install_dependencies.ps1 | 1 + .../_kube_local_testing/kind.cluster.config | 7 + .../_kube_local_testing/setup_and_deploy.ps1 | 17 + .../_kube_local_testing/teardown.ps1 | 1 + .../azdo_pipelines/azure-k8s-pipeline.yaml | 55 + .../azure-pipelines-feature-branches.yaml | 11 + .../azdo_pipelines/azure-pipelines-prd.yaml | 10 + .../azdo_pipelines/azure-pipelines-qa.yaml | 10 + .../azdo_pipelines/azure-pipelines-uat.yaml | 10 + .../config-files/prd-pvars.yaml | 7 + .../azdo_pipelines/config-files/qa-pvars.yaml | 7 + .../config-files/uat-pvars.yaml | 7 + .../azure-pipelines.yml | 45 + .../kustomize/base/Deployment.yaml | 46 + .../kustomize/base/Service.yaml | 11 + .../kustomize/base/kustomization.yaml | 9 + .../base/secrets-provider-class.yaml | 11 + .../kustomize/base/virtual-service.yaml | 19 + .../keyvaults/normal/kustomization.yaml | 10 + .../normal/set_secrets_deployment.yaml | 89 + .../normal/set_secrets_provider.yaml | 44 + .../normal-prod/appsettings/appsettings.json | 3 + .../workloads/normal-prod/kustomization.yaml | 40 + .../set_appsettings_deployment.yaml | 12 + .../normal/appsettings/appsettings.json | 3 + .../workloads/normal/kustomization.yaml | 40 + .../normal/set_appsettings_deployment.yaml | 12 + .../overlays/local/common/kustomization.yaml | 8 + .../common/set_appsettings_deployment.yaml | 12 + .../common/set_secrets_provider_class.yaml | 9 + .../overlays/local/kustomization.yaml | 2 + .../overlays/local/normal/configmap.yaml | 17 + .../overlays/local/normal/kustomization.yaml | 5 + .../local/normal/set_virtual_service.yaml | 8 + .../overlays/prd/appsettings/appsettings.json | 67 + .../overlays/prd/component/kustomization.yaml | 10 + .../component/set_appsettings_deployment.yaml | 5 + .../component/set_secrets_provider_class.yaml | 7 + .../kustomize/overlays/prd/kustomization.yaml | 14 + .../overlays/prd/normal/kustomization.yaml | 12 + .../prd/normal/set_virtual_service.yaml | 3 + .../prd/set_configmap_deployment.yaml | 12 + .../overlays/qa/appsettings/appsettings.json | 67 + .../overlays/qa/component/kustomization.yaml | 10 + .../component/set_appsettings_deployment.yaml | 5 + .../component/set_secrets_provider_class.yaml | 7 + .../kustomize/overlays/qa/kustomization.yaml | 14 + .../overlays/qa/normal/kustomization.yaml | 12 + .../qa/normal/set_virtual_service.yaml | 3 + .../overlays/qa/set_configmap_deployment.yaml | 12 + .../overlays/uat/appsettings/appsettings.json | 67 + .../overlays/uat/component/kustomization.yaml | 10 + .../component/set_appsettings_deployment.yaml | 5 + .../component/set_secrets_provider_class.yaml | 7 + .../kustomize/overlays/uat/kustomization.yaml | 14 + .../overlays/uat/normal/kustomization.yaml | 12 + .../uat/normal/set_virtual_service.yaml | 3 + .../uat/set_configmap_deployment.yaml | 12 + 472 files changed, 53651 insertions(+) create mode 100644 LCU.API.Internal.OnlineLoanApp/.dockerignore create mode 100644 LCU.API.Internal.OnlineLoanApp/.gitignore create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/BuilderTestBase.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/CRIF.ACTion.Tests.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisionStatusBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/DecisioningStateResponseParserTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsRequestBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/GetApplicationProductsResponseParserTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Decisioning/SendToDecisioningRequestBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddAppFilesSection/AddAppFiles.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-BadNamespace.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividual-Simple.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualError.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddIndividualSuccess.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AddressesSection/AddAddress.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppDetailsSection/SetAsStudentCcProduct.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualBorrowerParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningCoApplicantSpouseParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualNonSigningPrimarySpouseParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/AddIndividualPrimaryBorrowerParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetServicingChannel.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/AppPartiesSection/SetSourceChannel.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/AppWithPromo1.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplication.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationNonSigningHistorySpouse.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/CreateApp/CreateApplicationWithNote.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/cc_cash_back.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/equity_heloc.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/unsecured_line_of_credit.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_car_loan.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Existing/vehicle_marine_2borrowers.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationProducts/GetApplicationProducts.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/CashBackCC_Under-Review.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/InvalidApplicationId.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetApplicationsProductsParser/Multiple-AppDetails.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/GetDecisionStatus/GetDecisionStatus.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/HelocFlex/CreateApplication.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/MfgHomeAndLand/CreateAppWithAnnualTaxes.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/OrgSection/SetOrgId.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddChannelParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddEmployer.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddIndividual.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/AddOrganizationParty.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement-Current.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetAddressContactMechanismElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetContactMechanismsSectionElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEmployerContactMechanismsSectionElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement-DidNotProvide.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetEthnicitiesElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetIncomesSectionElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-False.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetMaritalStatusElement-True.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetNoCoApplicantDetermination.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetPartyRelationshipsSectionElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement-NotProvided.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetRacesElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes-NotProvided.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSexAttributes.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetSuffixElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/PartiesSection/GetTradesElement.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioning/sendToDecisioning.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/SendToDecisioningParser/SuccessResponse.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Signature/CreateApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithNoteAndVehicleSeller.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateApplicationWithVehicleSeller.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateAtvApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateBoatApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateMotorcycleApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePreapprovalApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreatePwcApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateRvPowersportApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateSnowmobileApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/ExampleXML/Vehicle/CreateTrailerMotorhomeApp.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/AppDetailBankcardSectionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/FileRequestBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/HelocFlexTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddIndividualResponseParserTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AddressesSectionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/AppPartiesSectionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/ExampleXML/BuildIncomeSuccess.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/IncomeSectionBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/Individuals/PartiesSectionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LienPositionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/LoanApplicationRequestBuilderTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/MfgHomeAndLandTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/OrgSectionTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/SignatureLoanTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/VehicleEtcTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/Loan/json/TestLienPositions.json create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestCommon.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/TestDistributionChannelLookupService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPI.XSD create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/actionAPIResponse.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/appSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/commonSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/maintSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/msgSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/qoSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/XSDs/utilSchema.xsd create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion.Tests/additional_test_args.runsettings create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/CRIF.ACTion.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderBase.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/BuilderUtils.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/CrifRequestSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Builders/IBuildable.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ContactMechanisms.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/CrifNamespaces.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Dates.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/ICrifNamespaces.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/IResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Common/Parsers/XElementExtensions.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ConfigurationRequestResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifConstants.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CrifSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/CustomFieldRequestResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Configuration/ProductDetailsResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisionStatusBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/DecisioningStateResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/GetApplicationProductsResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Decisioning/SendToDecisioningRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Documentation/CRIF API Risks and Issues.md create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/AddAppFilesSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/FileRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IAddAppFiles.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Files/IFileRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailBankcardSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailHelocSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailSignatureSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppDetailsSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/AppNotesSectionBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailMfgHomeAndLandSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailRealEstateSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/AppDetailVehicleSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/IVehicleValuations.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingAndLandAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/MfgHousingProperty.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/PropertyValues.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateEquityAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/RealEstateProperty.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleProperty.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Assets/VehicleValuationsSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/CreditCardAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/DealershipResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/HelocFlexAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppDetails.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IAppNotes.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/ILoanApplicationRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IOrg.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperties.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/IProperty.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddIndividualResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AddressesSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/AppPartiesSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/CrifResidenceType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAddresses.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IAppParties.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IParties.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/IncomeSectionBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/MaritalStatus.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/PartiesSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/Individuals/UserEmploymentType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/LoanApplicationResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/OrgSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/PropertiesSection.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SavingsAccountProperty.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Loan/SignatureAppBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/README.md create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/ServiceReference/Reference.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/CrifVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/Error13Exception.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/ICrifVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/IWcfInspectorBehavior.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Crif/WcfInspectorBehavior.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/IDistributionChannelLookupService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/DistributionChannel/LandmarkDistributionChannelLookupService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/ProductIdService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/ISourceChannelLookupService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/CRIF.ACTion/Services/Landmark/SourceChannel/LandmarkSourceChannelLookupService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Dockerfile create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/EmailRequest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/StoredSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Domain/UiSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureActiveDirectoryOptions.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/AzureAdServiceCollectionExtensions.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CookieService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/CosmosIdService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/ForwardedHeaderService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/MemberIdService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Common/Security/RenewAuthenticatedSessionMiddleware.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/ApplicationSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageConnectionSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/BlobStorageSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/DealershipEmailSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Configuration/NotificationsApiConfiguration.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/AddressValidationController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/BranchConfigController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/DecisioningController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/IndividualController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoanApplicationController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/LoggingController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/OnlineIdentityFormData.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/PdfController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/SAMLAssertionConsumerController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StatusController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/StorageController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Controllers/UiSettingsController.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BlobStorageAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/BranchDataAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ConfigurationAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataAccessConfiguration.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/DataClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBlobStorageAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IBranchDataAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IConfigurationAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/IDataClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/ILoanApplicationDataAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/INotificationDao.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/LoanApplicationDataAccess.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/DataAccess/NotificationDao.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/Logo.png create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Landmark.OLA.Application.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Program.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Properties/launchSettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreAccessService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/CoreSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/EmailTemplateService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Email/IEmailTemplateService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/Exceptions/CoreUnavailableException.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/ICoreService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/IPdfService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/INotificationsApiService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/MicroApiServices/NotificationsApiService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Services/PdfService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Startup.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/Status.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Application/wwwroot/.gitkeep create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/AddressValidationControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CoreAccessServiceTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/CustomWebApplicationFactory.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/DecisionControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/IndividualControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/Landmark.OLA.Integration.Tests.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/LoanApplicationControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/OlaIntegrationTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/SAMLAssertionControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/StatusControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/UISettingsControllerTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Integration.Tests/additional_test_args.runsettings create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Logging/Landmark.OLA.Logging.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Address.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/CountryOption.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/ResidenceTypeOption.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Enums/StateOption.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/IAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/PropertyLot.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Addresses/Validators/AddressValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ApplicationControlConfiguration.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/LienPositionType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/ManufacturedHomeType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/NewUsed.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/PropertyUse.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/RealEstateType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/UsageType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleSeller.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Enums/VehicleValuationType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/LienPosition.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/ManufacturedHome.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstate.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/RealEstateBase.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Assets/Vehicle.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ClientError.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreIndividual.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CoreMemberOwner.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/CorePhone.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Core/MemberMeta.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/DecisioningDetails.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningResult.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Decisioning/Enums/DecisioningState.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Hmda.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/HomeMortgageDisclosureAct/Validators/HmdaValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/IReferenceable.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/CoApplicant.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Employer.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/EmploymentTypeOption.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Ethnicity.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Frequency.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/LanguageOption.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/PhoneType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Race.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Sex.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/Suffix.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Enums/SuffixMap.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/HistoryIndividual.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IApplicant.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/ICoApplicant.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IIndividualHistory.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/INonApplicant.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Income.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/IncomeLimits.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Individual.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/MailableSpouse.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/ApplicantValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Individuals/Validators/EmployerValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Landmark.OLA.Models.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Limit.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchConfig.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchMapping.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/BranchNameConfig.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DealershipAuthRequest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/DocumentUploadFromLoanApplication.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanErrorCode.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanStatus.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Enums/LoanType.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Filter.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/FilterField.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplication.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanApplicationFile.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanContainer.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanError.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanLimits.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPage.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/LoanPurpose.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/MockApplication.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/TermLengths.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/BranchConfigValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/LoanApplicationValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Loan/Validators/VehicleValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ModelValidator.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OlaModel.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthTokenResponse.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/AuthenticationStatement.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/Constants.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/DISAMLAssertion.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingFormData.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/OnlineBankingToken.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/OnlineBanking/SAMLConditions.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/CurrentResidenceAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ICurrentResidenceAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/IResidenceAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Residences/ResidenceAddress.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/ValidationConstants.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Models/Vendor/IUspsVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Addresses/Validators/AddressValidatorTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/AddressValidationTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/LoggingTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Controllers/UiSettingsTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Landmark.OLA.Tests.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Enums/LoanTypeTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/BranchConfigValidatorTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorSpouseTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Loan/Validators/LoanApplicationValidatorTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/ModelValidatorTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/Services/CoreAccessServiceTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestAddresses.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestEmployers.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIncomes.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/TestIndividuals.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestValidatorLoans.json create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/TestData/json/TestVehicleTermValidity.json create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.Tests/additional_test_args.runsettings create mode 100644 LCU.API.Internal.OnlineLoanApp/Landmark.OLA.sln create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/AuthenticationParserTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/CookieCrytorTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/ExampleXML/DecryptedAssertion.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/OnlineBankingAuth.Tests.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/TokenServiceTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth.Tests/additional_test_args.runsettings create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Configuration/SecuritySettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/OnlineBankingAuth.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/AuthTokenParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/IAuthTokenParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/ISAMLParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLMessaging.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Parsers/SAMLParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/DigitalInsightVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Services/IDigitalInsightVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/CookieCryptor.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/ITokenService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/OnlineBankingAuth/Tokens/TokenService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/README.md create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS.Tests/AddressValidationTest.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressError.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS.Tests/ExampleXML/USPSAddressSuccess.xml create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS.Tests/USPS.Tests.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS.Tests/additional_test_args.runsettings create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Configuration/UspsSettings.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Requests/AddressValidationRequestBuilder.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Requests/IRequestBuildable.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Responses/AddressValidationResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Responses/IResponseParser.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Services/IUspsService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsService.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/Services/UspsVendorClient.cs create mode 100644 LCU.API.Internal.OnlineLoanApp/USPS/USPS.csproj create mode 100644 LCU.API.Internal.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 create mode 100644 LCU.API.Internal.OnlineLoanApp/_kube_local_testing/kind.cluster.config create mode 100644 LCU.API.Internal.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 create mode 100644 LCU.API.Internal.OnlineLoanApp/_kube_local_testing/teardown.ps1 create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/azure-pipelines.yml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/base/Deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/base/Service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/base/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/base/secrets-provider-class.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/base/virtual-service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/keyvaults/normal/set_secrets_provider.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/appsettings/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal-prod/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/appsettings/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/components/workloads/normal/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/common/set_secrets_provider_class.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/appsettings/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/component/set_secrets_provider_class.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/prd/set_configmap_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/appsettings/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/component/set_secrets_provider_class.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/qa/set_configmap_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/appsettings/appsettings.json create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_appsettings_deployment.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/component/set_secrets_provider_class.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml create mode 100644 LCU.API.Internal.OnlineLoanApp/kustomize/overlays/uat/set_configmap_deployment.yaml 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 0000000000000000000000000000000000000000..010023e5fc1e4f05eafad10f4b29556b3f4bbbfc GIT binary patch literal 426 zcmaixy$ZrW5QM)rg70wH*ePi&BnCuK1PcpaAR56yG?GN|>DAfO7_^aPx!l~$>`xx| zh=d(A83iY%tXbl?vZthaqZ4|BO2>^Oxp)tK@`bI!zHO&q8e%&6W%uDI$jAG!wP+eB3<-;qkyPrS`&+1XzG$`eV1X81dz_4t^M(BzwcbGGk0N-Hffq0 z80K>BS-!K*{QCJyZX}VpjO0$Hax53pk%~M>UsA~yyRT(a>iAUTyG&#Zx=gsYDM#{B zUdV=!OAo7q|9Uv}fKN~Eu{(aEA=LyYhjNJh8CD1PGssOL|1%_JdPnYk$Z}~@&|L>w zHDxQ%>~L|C8lEcG4DyEh%eBGkLCRy*$p3$WlOxzPS9?#N?tHp?MR2TCUlzZ8ngkY1 zu!u7ywhQ+;k{gO3e3-y@Hp*RU6TOVbLm7Z_ ztXOS3Qj0Vuc72`|57!ZNIs>O~>gxfdkFnE--z)fSq{TVW9>#}@j2j^aPnXXH_%f45 zs(%qPi@&AsA_7vZJ7~u|4|VTpJ`Plm9cjoekU3Bc2U3+TJl2A=sx+~B*stPlAbYZ} z|MswZfc-9ZZa~?U_GSx>ALU9Ofk|KG#cD_qH6qL08Dr6>w1DWo*DG_C*1eNY z0rDXxp*-oN??-?y zuSo04iboggzlmw<(>NW`a$|tL8shX6ar*;To1?LqWL7ZB*lSvd$pm**T>FSS6OB8# z?qaV6sutp}ipZwy)>*LBoi0H7Uzjm(tq!hz@3QwF!Pch9*ason$37@oPG7 zCbvLfRuX0Fa`NqC%*7-YbMJhKH?ELVZDc~G*);*D6qeIlH*$_DGSwYxLmZ3Mcnto+ zC!9Bk>Hk!-;(5#*@VjsxQOn{Y9$oA6iB+*spJiDYYT@TIrzL*^^dphJVlMxS&SEpP zfu6O8)j+K%#0~25ocD;iuT-F-DS6Dbca==3)&(6oi6gh^e{@Rqt;Y$WlyZZ z)+@-aVa90ccLN=A3%gBxLavIIHEWoelE^ir=G4MzeU6Fa22-2S;iFn)CJxuu z3FtYkbJ8iEcoX4GTzt+as0J~s`~#m7fr6}^rAJ#@+zRugd->zS+g6no1D*vi&S#1=)uLjVf~2X{3jT-fjg(kA+$>|kNTKBe?Dk0rUK^b^LxN5 zEaUOycYsw~Vi6VA|5z3G +
+ +
+ \ 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 0000000000000000000000000000000000000000..53dfbd3fec99ea6daae783c6d7c6b4b495087270 GIT binary patch literal 12418 zcmeHO*-{%v6zx~1D*r(FIV?h~D#sO>#oz!|5RRR^2`xs21XvQb$;T%-r?0Min;8vY zh%4oq(lAT+-M79y{PXuS(=u%{F@1AsE==7t%)Xg3A5GVc%y|0rXYo+3Vt1o zrwxX9uBP^1Q-;bcYjo$?%cpDC@;A#`UmRZG*meT=eoA3d0 zNUm3Lb&7W@IOg%BhY?jfhn&$O`Q{-x6nmk^jUcNR)^bKTe**SVBH2)br{Jvz`qx2c z73XgJx8!4hM4`&R7?O8+wKo}90P--qDk zG4g@(QbFw9MD$nzPollDdSO(GqimjaJzNH01ZTcwlxMxFeC^n$tmekHnu#f+x3l7& zm?O*_VkWKVm8s)?C&P+~Cmy|x3OH+Q2ljhv;bm2XX~bl;ePPG1K^x(bj`kxqbMFy& zAKKYV8FUZ9X9vds9EO}0WsB4Yxmsb3&9w_x2>dek!-Y$mM1W&AkX-BkzRqtRW&pN$GTFU8drM3lma$WKsbh4_7snnWW z)`EF{>9EIHahBA+9i8pKrnpv=I<3f7VqK}nzB0eS7vI5u525)3TrXHpKKV~IC-u?) zS#w*6J|i4m>z{QSYk0?)b{C&%1rTHv82d%Ix?sHQ;%H;$Dz0VCv~JfKD>$=C;p%A_ zKd!A-Y~@nHbqjvIff3An#ko&?>@Kmate2OgfOTY%f26Vb+wpSsaf!6wZ6(#!w`(i6 zx4WX6AkLJ`zx4e4K9YK1{>(T^uvVd`T-(T`6@uneMi%x3b`Ymn*Dwzs+B`f#K9*WC zts5Mj3Qz0J*>j~6$%jLb8=PHRd8Rxt zQeI+~KR&MG(^=9pkyuY^*}o+f(sNb%V=1tjqRiN(NMTCndV)G_fa-_q;Tv0tQxcph zmDk;BuX9_^bx@)A(5<3;7##w+l^?tJ~*yXGi(bIrTU< z>o&e6Jv&+*?cKMSvT|VDmOgO|A=RnM=1qp&E&xYYT5q5Yp^T8(p)HbRDhp~ew%Syd zoL9Ds;`%KzV;fXaW~@r{X4E8 z0=ojT5^<0TWrQ)iICsql>t~$n`uX%o;uasHj1?CkYP&2YCsyv# z6Uf3zID3d)+mKjRm)r3!k{Vj6IZKk=$SP{>q6)vrH)i!dHQP(Qt+JB8zF#VwbnP6j z5gwyE=-Y6(`{R3z@Zjk7XEm+f@^_I4rzqF>(Jo(BjJA34Y?i!JyOs!51n;geofW#+ zre5s(8a*r9UyA4Xl79)AusUEaY6ISVCuNmC88QJm7%?Z11`F0X}W;id_7jrP9^VjUqg{#(9~Go)A(T+T1MP;og_flo$VeFW`Rr5%fiw zXL(sxjMVWi5?)Lp>_C5%yl8VDk+TNPvGZKP?u0w@>LtQYWdk`m z>}JGry^n3AwC73rK3KBl`P~9%S>O}%1m99=jJU1UG14_`%wxQJjjT0iV<5l9pq(-L zF^eQ^K^JN$NOdNZ%BgQN!aU_bNK6FGA9$hasw2;*6^6f1vD?7`R zuij$r!d}qV{L(F}YH`%?+s89xUGBzo&F}CJ?#j4c>lhXtLr6J~zesn|;@G_J+XYVi Ip9as(zkE1^JOBUy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e8c548390f63498d6dac996b48bdc89e6b6883c5 GIT binary patch literal 642 zcmZva%}&EG5QM)o67LXk2igmVs!)}}0SKJaB{<+$(`eaN?&uc4$QIkUbR#i7OI)s zhLg(0Qe7{vcj6WCYZ<38V=rcGSe9{<;9ksBtCBC9NjpWm_r#?pnaM*XWvA73($*ZS z`x+EiVM>j=EM&72=i1eG(A7y>Tsx)hr^f%yQ-&E7a_6{_w%veXues%X$LDjzmT^*# i!aClzvobgJi}9MsKo08rHNDL(+WYC^{_7s=#ry(dSZ$U7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dcd66abb59fe28348af90a27857a7cd2da2246dd GIT binary patch literal 137866 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

*swFN=$OKyKKs%6;T_Am|hu+%^7}_q~q{ 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%0Dt(- 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 UC6>`~^1W42UQMp#zUlM-0o995)Bpeg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0f990e745f5ffddd7790867b95df5b81067991a1 GIT binary patch literal 401602 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 SD4 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

465@EeuFg>%O0*v^O|vIXPE9fkC!1U>N@P9 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 Sf`?vFMq`04G&Y586#)Rt+CDr0 literal 0 HcmV?d00001 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