diff --git a/LCU.WebApps.External.OnlineLoanApp/.editorconfig b/LCU.WebApps.External.OnlineLoanApp/.editorconfig new file mode 100644 index 0000000..a61d1fd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/.editorconfig @@ -0,0 +1,37 @@ +# This file doesn't account for some differences: +# 1.VS Code VS Webstorm spaces inside imports. This can be fixed on the Webstorm side with: +# settings -> editor -> code style -> typescript -> spaces -> within -> ES6 import/export braces +# 2. Webstorm places a carriage return on {}, even for simply functions, adjust with: +# settings -> editor -> code style -> typescript -> wrapping and spacing -> simple blocks in one line +# settings -> editor -> code style -> typescript -> wrapping and spacing -> simple methods in one line + +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +# EditorConfig is awesome: https://EditorConfig.org + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py,ts}] +charset = utf-8 + +# Indentation override for all JS under lib directory +[lib/**.js] +indent_style = space +indent_size = 2 + +# Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 diff --git a/LCU.WebApps.External.OnlineLoanApp/.gitignore b/LCU.WebApps.External.OnlineLoanApp/.gitignore new file mode 100644 index 0000000..284150e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/.gitignore @@ -0,0 +1,412 @@ +## 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 +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# 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 +nunit-*.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/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.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 + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# 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 +# NuGet Symbol Packages +*.snupkg +# 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 +*.appxbundle +*.appxupload + +# 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 +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# 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/ + +# CodeRush personal settings +.cr/personal + +# 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/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +======= +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +/test-results +/debug.log + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +# Patches +*.patch + +# the version file shouldn't be saved +src/environments/version.ts +/.vs/slnx.sqlite +/.vs diff --git a/LCU.WebApps.External.OnlineLoanApp/CHANGELOG.md b/LCU.WebApps.External.OnlineLoanApp/CHANGELOG.md new file mode 100644 index 0000000..ec4e9f3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/CHANGELOG.md @@ -0,0 +1,838 @@ +# Change Log + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +# [4.11.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.10.0...v4.11.0) (2021-04-13) + + +### Bug Fixes + +* **CreditApplicationService:** Fix note concatenation ([55dc603](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/55dc603)) + + +### Features + +* **HelocRateModPhaseTwo:** Hide additional fields on initial and collateral pages ([424067b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/424067b)) + + + + +# [4.10.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.9.0...v4.10.0) (2021-03-02) + + +### Bug Fixes + +* **HelocRateMod:** Re hide fields that were left behind in change to hidden logic ([c09ec5b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c09ec5b)) + + +### Features + +* **LoanType:** Add HELOCRateMod Loan Type ([1c264f4](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1c264f4)) + + + + +## [4.9.0](https://landmarkcu.visualstudio.com///compare/v4.8.0...v4.9.0) (2020-11-16) + + +### Features + +* **employment-info:** Add BSA message to income page ([088c0a2](https://landmarkcu.visualstudio.com///commit/088c0a2831848a17b8b5c4efd0fe1bc9e8df70eb)) + + +### Bug Fixes + +* **employment-info:** Move BSA message to translate ([55eb2b1](https://landmarkcu.visualstudio.com///commit/55eb2b14fb49ba98435cafa0f72a2971cddb6ee9)) + + +## [4.8.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.8.0...v4.8.1) (2020-10-08) + +### Enhancements + +* **HmdaUI:** Replace DoesNotWishToFurnish option with NotProvided ([914ad21b]((https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/914ad21b))) +* **Hmda:** Update Behaviour for Hmda Form. Only send "NotProvided" if it is the only option selected. (Api) + + + +# [4.8.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.1...v4.8.0) (2020-08-14) + +### Features + +* **collateralInfoPage:** Add validators to require Wisconsin for HELOC fixed and flex Collaterals ([9719280](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9719280)) +* **equityMemberInfoPage:** Update Equity Member Info Page Getting Started Message For HELOC Fixed a ([0c2ac8e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0c2ac8e)) + + +# [4.7.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.1...v4.7.0) (2020-06-19) + +### Bug Fixes +* **BranchNameMap:** Pull BranchNameMap from CRIF based on environment ([1373805](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1373805)) +* **Options:** Update Select-Options to include special Loan Types ([248970e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/248970e)) + +### Features + +* **LoanPurposeOptions:** add 'Refinance Cash-Out' to LoanPurposeOptions for Home Equity and HELOC loans ([adbaaec](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/adbaaec)) + + +# [4.6.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.1...v4.6.0) (2020-06-12) + + +### Bug Fixes + +* **Covid19:** Removed duplicate text from covid loan ([94b5037](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/94b5037)) + +### Features + +* **BranchOptions:** Add 'Brookfield West' 'Greenfield' update 'Brookfield' to 'Brookfield East' ([682564d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/682564d)) +* **Covid19:** Update to the topmost message for covid 19 loans ([723be00](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/723be00)) + + +# [4.5.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.1...v4.5.0) (2020-04-15) + + +### Bug Fixes + +* **SpecialLoan:** Standardize client message field name ([16ea351](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/16ea351)) + + +### Features + +* **COVID-19:** Add COVID-19 loan type ([eb466db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/eb466db)) +* **Covid19:** ui updates for covid 19 loan enhancements ([5183fb6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5183fb6)) + + + + +# [4.4.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.1...v4.4.0) (2020-03-31) + + +### Features + +* **COVID-19:** Add COVID-19 loan type ([eb466db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/eb466db)) + + + + +## [4.3.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.3.0...v4.3.1) (2020-03-16) + + +### Bug Fixes + +* **Schumer:** update, start refactor ([45aa896](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/45aa896)) + + + + +# [4.3.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.2.1...v4.3.0) (2020-01-10) + + +### Bug Fixes + +* **Test:** Fix broken unit test ([1880541](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1880541)) + + +### Features + +* **Logging:** Log to on-premise seq via proxy ([9b768a5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9b768a5)) + + + + +## [4.2.2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.2.1...v4.2.2) (2019-11-26) + +### Features + +* **RefiExpress:** Add 8 year term to mortgage refi express loan. + + + +## [4.2.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v4.2.0...v4.2.1) (2019-10-25) + + +### Bug Fixes + +* **Environment:** Update prod environment flags ([4a466ab](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4a466ab)) + + + + +# [4.2.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v4.2.0) (2019-10-14) + + +### Bug Fixes + +* **CoreTranslate:** Parse core/apex zipcode into zipcode and extension ([915a0cf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/915a0cf)) +* **SchumerBoxes:** Update erroneous schumer box text ([a4f4bc7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a4f4bc7)) + + + + +## 3.6.1 (2019-07-12) + + +### Bug Fixes + +* **AddressValidation:** Improve css to support variable screen widths ([23dc937](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/23dc937)), closes [#877](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/877) +* **AddressValidation:** Wrap addrss validation headers ([c6bc17a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c6bc17a)) +* **Admin:** Fixed unit tests for snackbar ([96f2d68](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/96f2d68)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **AdminExport:** Add member id to admin export ([1fbf058](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1fbf058)), closes [#1048](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1048) +* **AdminExport:** Add memberid to export ([bf36aad](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bf36aad)) +* **AppResume:** Do not pre-fill member information when resuming a loan ([766bf4b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/766bf4b)), closes [#1388](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1388) +* **AppStatus:** Add card body to status to support ie11 ([ada0c8a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ada0c8a)), closes [#1248](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1248) +* **AppStatus:** Modify the system config page to better reflect the app status ([94634e1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/94634e1)), closes [#1235](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1235) +* **AppWorkflow:** Allow users to begin existing loans and then switch into a new loan ([cde14b2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/cde14b2)), closes [#1029](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1029) +* **BranchMapping:** Check for null branch mapping when finding channel Id ([f1fa734](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f1fa734)), closes [#1053](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1053) +* **CookieTests:** Stop mocking cookies for normal tests. Use spys. ([09e58d5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/09e58d5)), closes [#1095](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1095) +* **DecisionPage:** Increase the time the application will wait for a decision to 40 seconds. ([02798e0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/02798e0)), closes [#971](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/971) +* **Employers:** Set all employers as current moving forward ([dd843b8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/dd843b8)), closes [#1149](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1149) +* **Environment:** Lock test unless users have test role in AD ([8e56bde](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8e56bde)) +* **ErrorDisplay:** Have error display component wait for translations to be ready. ([27941af](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/27941af)) +* **ErrorHandling:** Handle errors for member verificaiton and loading existing loans ([b178afb](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b178afb)), closes [#1044](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1044) +* **ErrorReporting:** USe a simplifier to try and relive the circular object references. ([46355c9](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/46355c9)) +* **LoanTranslator:** Use correct ID when translating a loan. ([240055f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/240055f)) +* **LoginFlow:** Add ability to add verification and core loading per environment ([ed1da05](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ed1da05)) +* **MemberLogin:** App startup was attempting to load existing member applications ([b8e1ae5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b8e1ae5)), closes [#876](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/876) [#882](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/882) +* **MemberLogin:** Remove return to existing loan link for 3.0 ([63dbb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/63dbb8b)) +* **MemberVerification:** Allow member to continue when they can't verify ([c2c7e7e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c2c7e7e)), closes [#1393](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1393) +* **MemberVerification:** Dont show the verification page when a user is an authenticated non member ([56f738b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/56f738b)), closes [#1090](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1090) +* **MockApiModel:** Fix model to correctly populate loan application ([7322112](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7322112)), closes [#1128](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1128) +* **PersonalInfo:** Reset co-applicant has-same-address flag on co-applicant status change ([22bb115](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/22bb115)), closes [#767](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/767) [#709](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/709) +* **Processing:** Report all processing discrepencies to bugsnag for debugging ([f0e6138](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f0e6138)) +* **ProcessingPage:** Update unit tests, expand wait time ([f9f59fa](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f9f59fa)) +* **SpouseDisclosure:** Show disclosure for coapplicant spouse ([5563bf4](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5563bf4)), closes [#1050](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1050) +* **Submit:** Use loan id directly from adjusted web service ([0c728c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0c728c6)) +* **SummaryComments:** Fix allowance of special characters, prevent bad form submissions ([fe19e39](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fe19e39)), closes [#1172](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1172) +* **TattleSpouse:** Update tattle spouse copy to be in orange box, move to bottom. ([23e2dcf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/23e2dcf)), closes [#890](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/890) +* **Tests:** Fix broken unit test ([e8afd47](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e8afd47)) +* **Timeout:** Change timeout info text depending on the preceding page ([c94e68f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c94e68f)), closes [#880](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/880) +* **Timeout:** Forward user to landmarkcu.com when timing out completed application ([c924ed5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c924ed5)), closes [#967](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/967) +* **TimeOutComponent:** Dont reset credit application when one doesnt exist ([5e59d24](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5e59d24)), closes [#1176](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1176) + + +### Features + +* **Admin:** Refactor admin to support better error handling ([ce5d790](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ce5d790)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **Admin:** UI for customer redirect when app is turned off. UI for admins to turn app off. ([870b0c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/870b0c6)), closes [#1142](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1142) +* **AdminSystem:** Display server errors in snack bar prompt ([db3ce19](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/db3ce19)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **AppLoader:** Display loan icon next to existing loan list ([3c0c305](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3c0c305)), closes [#1125](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1125) +* **AppStatus:** Added a new decision status for tracking in admin ([efbb683](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/efbb683)), closes [#1133](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1133) +* **BoatTerms:** Update minimum term length for boats to 63 months ([d3cfcec](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d3cfcec)), closes [#883](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/883) +* **CookieDisclosure:** Additional text for cookie disclosure ([bd4f297](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bd4f297)), closes [#963](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/963) +* **CoreAccess:** Change to all cookie based security, no direct ID manipulation ([607487c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/607487c)) +* **CoreAccess:** Prefill member info, get individual id ([091adae](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/091adae)), closes [#799](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/799) +* **CoreIntegration:** Replace mock services with real restful calls ([c40d42e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c40d42e)), closes [#1162](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1162) +* **EmploymentStatus:** Removed retired status from dropdown ([62f3159](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62f3159)), closes [#1180](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1180) +* **ErrorHandling:** Report errors to logging service instead of bugsnag ([785400c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/785400c)), closes [#1179](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1179) +* **MemberLogin:** Catch login status and throw appropriate error messages ([d002541](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d002541)), closes [#1129](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1129) +* **MemberLogin:** Disable member login for 3.0 release ([758ca0b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/758ca0b)), closes [#931](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/931) +* **MemberLogin:** Route user to lmcu login when they click the existing app link ([6b28315](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/6b28315)), closes [#1037](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1037) +* **MemberVerification:** Front end implementation of core work. Includes verification & continue of ([fc57b12](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fc57b12)), closes [#766](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/766) +* **Options:** Added test coverage to option classes ([636eb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/636eb8b)), closes [#1218](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1218) +* **Prefill:** Prefill member primary address ([7f2c57d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7f2c57d)) +* **ReviewWording:** Allow review wording to be based on loan type ([ff2fb4a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ff2fb4a)) +* **StatusToolTip:** Added tooltip to status column in admin page ([62058dd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62058dd)), closes [#1163](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1163) + + + + +# [4.1.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v4.1.0) (2019-09-04) + +### Bug Fixes + +* **CoreTranslate:** Parse core/apex zipcode into zipcode and extension ([915a0cf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/915a0cf)) + + +# [4.0.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v4.0.0) (2019-07-26) + + +### Features + +* **AppWorkflow:** Allow users to begin existing loans and then switch into a new loan ([cde14b2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/cde14b2)), closes [#1029](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1029) +* **LoanTranslator:** Use correct ID when translating a loan. ([240055f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/240055f)) +* **LoginFlow:** Add ability to add verification and core loading per environment ([ed1da05](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ed1da05)) +* **MemberLogin:** App startup was attempting to load existing member applications ([b8e1ae5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b8e1ae5)), closes [#876](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/876) [#882](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/882) +* **MemberLogin:** Remove return to existing loan link for 3.0 ([63dbb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/63dbb8b)) +* **MemberVerification:** Allow member to continue when they can't verify ([c2c7e7e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c2c7e7e)), closes [#1393](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1393) +* **MemberVerification:** Dont show the verification page when a user is an authenticated non member ([56f738b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/56f738b)), closes [#1090](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1090) +* **Admin:** Refactor admin to support better error handling ([ce5d790](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ce5d790)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **Admin:** UI for customer redirect when app is turned off. UI for admins to turn app off. ([870b0c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/870b0c6)), closes [#1142](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1142) +* **AdminSystem:** Display server errors in snack bar prompt ([db3ce19](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/db3ce19)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **AppLoader:** Display loan icon next to existing loan list ([3c0c305](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3c0c305)), closes [#1125](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1125) +* **AppStatus:** Added a new decision status for tracking in admin ([efbb683](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/efbb683)), closes [#1133](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1133) +* **BoatTerms:** Update minimum term length for boats to 63 months ([d3cfcec](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d3cfcec)), closes [#883](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/883) +* **CookieDisclosure:** Additional text for cookie disclosure ([bd4f297](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bd4f297)), closes [#963](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/963) +* **CoreAccess:** Change to all cookie based security, no direct ID manipulation ([607487c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/607487c)) +* **CoreAccess:** Prefill member info, get individual id ([091adae](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/091adae)), closes [#799](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/799) +* **CoreIntegration:** Replace mock services with real restful calls ([c40d42e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c40d42e)), closes [#1162](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1162) +* **EmploymentStatus:** Removed retired status from dropdown ([62f3159](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62f3159)), closes [#1180](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1180) +* **ErrorHandling:** Report errors to logging service instead of bugsnag ([785400c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/785400c)), closes [#1179](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1179) +* **MemberLogin:** Route user to lmcu login when they click the existing app link ([6b28315](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/6b28315)), closes [#1037](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1037) +* **MemberVerification:** Front end implementation of core work. Includes verification & continue of ([fc57b12](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fc57b12)), closes [#766](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/766) +* **Options:** Added test coverage to option classes ([636eb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/636eb8b)), closes [#1218](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1218) +* **Prefill:** Prefill member primary address ([7f2c57d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7f2c57d)) +* **ReviewWording:** Allow review wording to be based on loan type ([ff2fb4a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ff2fb4a)) +* **StatusToolTip:** Added tooltip to status column in admin page ([62058dd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62058dd)), closes [#1163](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1163) + + +# [3.6.1](2019-07-12) + +### Other + +* **Processing:** Display different messages for for credit cards. +* **SchumerBoxes:** Update text of schumer boxes + + +# [3.6.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.5.1...v3.6.0) (2019-04-29) + + +### Bug Fixes + +* **Admin:** Fixed unit tests for snackbar ([96f2d68](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/96f2d68)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **Employers:** Set all employers as current moving forward ([dd843b8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/dd843b8)), closes [#1149](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1149) +* **Environment:** Lock test unless users have test role in AD ([8e56bde](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8e56bde)), no ticket +* **ErrorHandling:** Handle errors for member verification and loading existing loans ([b178afb](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b178afb)), closes [#1044](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1044) +* **MemberVerification:** Dont show the verification page when a user is an authenticated non member ([56f738b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/56f738b)), closes [#1090](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1090) +* **Submit:** Use loan id directly from adjusted web service ([0c728c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0c728c6)), no ticket +* **SummaryComments:** Fix allowance of special characters, prevent bad form submissions ([fe19e39](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fe19e39)), closes [#1172](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1172) +* **TimeOutComponent:** Dont reset credit application when one doesnt exist ([5e59d24](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5e59d24)), closes [#1176](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1176) +* **USPS:** Better handle USPS errors when users enter nonsense addresses, closes 1157 + +### Features + +* **Admin:** Refactor admin to support better error handling ([ce5d790](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ce5d790)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **Admin:** UI for customer redirect when app is turned off. UI for admins to turn app off. ([870b0c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/870b0c6)), closes [#1142](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1142) +* **AdminSystem:** Display server errors in snack bar prompt ([db3ce19](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/db3ce19)), closes [#871](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/871) +* **AppLoader:** Display loan icon next to existing loan list ([3c0c305](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3c0c305)), closes [#1125](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1125) +* **AppStatus:** Added a new decision status for tracking in admin ([efbb683](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/efbb683)), closes [#1133](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1133) +* **EmploymentStatus:** Removed retired status from dropdown ([62f3159](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62f3159)), closes [#1180](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1180) +* **MemberLogin:** Catch login status and throw appropriate error messages ([d002541](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d002541)), closes [#1129](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1129) +* **MemberLogin:** Route user to lmcu login when they click the existing app link ([6b28315](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/6b28315)), closes [#1037](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1037) +* **Options:** Added test coverage to option classes ([636eb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/636eb8b)), closes [#1218](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1218) +* **StatusToolTip:** Added tooltip to status column in admin page ([62058dd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/62058dd)), closes [#1163](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1163) +* **ApiRoot:** Further change API root to redirect to client, fixing both Azure and lost consumers, closes #1156 + +### Other + +* **MemberLogin:** Disable member login, pre-fill and other 4.0 code. + + +# [3.5.0-Release](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.5.0-Release) (2019-04-10) + + +### Bug Fixes + +* **AdminExport:** Add memberid to export ([bf36aad](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bf36aad)) + + +### Features + +* **MemberVerification:** Front end implementation of core work. Includes verification & continue of ([fc57b12](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fc57b12)), closes [#766](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/766) + + + + +# [3.5.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.5.0...v3.5.1) (2019-04-15) + + +### Bug Fixes + +* **LoanAdmin:** All users to submit loans marked VENDOR_ERROR to CRIF, closes #1131 + + +# [3.5.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.5.0) (2019-04-10) + + +### Bug Fixes + +* **AddressValidation:** Wrap addrss validation headers ([c6bc17a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c6bc17a)), closes #1067 +* **AdminExport:** Add member id to admin export ([1fbf058](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1fbf058)), closes [#1048](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1048) +* **BranchMapping:** Check for null branch mapping when finding channel Id ([f1fa734](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f1fa734)), closes [#1053](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1053) +* **PersonalInfo:** Reset co-applicant has-same-address flag on co-applicant status change ([22bb115](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/22bb115)), closes [#767](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/767) [#709](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/709), #993 +* **SpouseDisclosure:** Show disclosure for coapplicant spouse ([5563bf4](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5563bf4)), closes [#1050](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1050) +* **TattleSpouse:** Update tattle spouse copy to be in orange box, move to bottom. ([23e2dcf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/23e2dcf)), closes [#890](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/890) + +### Features + +* **LoanStorage:** Cache mongo client instance, add more tests., closes #767, #993 +* **Azure:** move the api status call to root so azure sees it, closes #752 + + +# [3.4.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.4.0) (2019-04-03) + + +### Bug Fixes + +* **ProcessingPage:** Update unit tests, expand wait time ([f9f59fa](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f9f59fa)) +* **Timeout:** Forward user to landmarkcu.com when timing out completed application ([c924ed5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c924ed5)), closes [#967](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/967) +* **BranchMapping:** Handle double submission errors in IE 11, closes #992 +* **CRIF:** Handle Error 13 as a 'try again' error, closes #710 + +### Features + +* **CookieDisclosure:** Additional text for cookie disclosure ([bd4f297](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bd4f297)), closes [#963](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/963) +* **ApiStatus:** Report API status as root route for Azure, closes #752 +* **MongoDB:** Cache Cosmos/Mongo connection for improved performance + + +# [3.3.2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.4.0) (2019-03-28) + + +### Bug Fixes +* **Processing:** Report all processing discrepencies to bugsnag for debugging ([f0e6138](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/f0e6138)) + + + +# [3.3.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.4.0) (2019-03-27) + + +### Bug Fixes + +* **DecisionPage:** Increase the time the application will wait for a decision to 40 seconds. ([02798e0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/02798e0)), closes [#971](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/971) + + +# [3.3.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.3.0) (2019-03-21) + + +### Bug Fixes + +* **MemberLogin:** Remove return to existing loan link for 3.0 ([63dbb8b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/63dbb8b)) +* **TattleSpouse:** Update tattle spouse copy to be in orange box, move to bottom. ([23e2dcf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/23e2dcf)), closes [#890](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/890) + +### Features + +* **MemberLogin:** Disable member login for 3.0 release ([758ca0b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/758ca0b)), closes [#931](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/931) +* **BoatTerms:** Update minimum term length for boats to 63 months ([d3cfcec](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d3cfcec)), closes [#883](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/883) + + +## [3.2.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.2.0...v3.2.1) (2019-03-20) + + +### Bug Fixes + +* **AddressValidation:** Improve css to support variable screen widths ([23dc937](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/23dc937)), closes [#877](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/877) +* **MemberLogin:** App startup was attempting to load existing member applications ([b8e1ae5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b8e1ae5)), closes [#876](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/876) [#882](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/882) +* **Timeout:** Change timeout info text depending on the preceding page ([c94e68f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c94e68f)), closes [#880](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/880) + + + + +# [3.2.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.0.0...v3.2.0) (2019-03-20) + + +### Bug Fixes + +* **BranchConfig:** Last updated date was not shown on branch admin screen. ([17298e1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/17298e1)), closes [#833](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/833) +* **BranchExport:** Remove quotes from branch export, add more testing. ([e74d8af](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e74d8af)), closes [#872](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/872) +* **LoanAPI:** Remove forced exception test, allowing loans to complete. closes #873, backend. +* **MemberLogin:** App startup was attempting to load existing member, closes #883, addresses #876 + +### Features + +* **Admin:** Add error with start date and end date format ([1a25207](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1a25207)), closes [#878](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/878) +* **TattleSpouse:** Add additional tattle spouse text inside orange box ([8010fb8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8010fb8)), closes [#874](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/874) + + + + +# [3.1.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v3.0.0...v3.1.0) (2019-03-19) + + +### Bug Fixes + +* **BranchConfig:** Last updated date was not shown on branch admin screen. ([17298e1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/17298e1)), closes [#833](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/833) + + + +# [3.0.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.10.0...v3.0.0) (2019-03-18) + + +### Bug Fixes + +* **Admin:** Save filter settings between pages ([ad8efc5](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ad8efc5)), closes[#1412] +* **Admin:** Sort fields as case insensitive ([72ac98e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/72ac98e)), closes[#1411] +* **AdminDropDownLists:** Fixed lists in IE ([dde4448](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/dde4448)), closes[#1396] +* **Alimony:** Pass alimony information correctly to wizard service, fix tests. ([8721e6c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8721e6c)), no ticket +* **AppLoader:** Fix crash on loading of empty list of existing loans ([a170c65](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/170c65)), no ticket +* **AppRouting:** Use ternary operators instead of functions for canactivate ([ec5852c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ec5852c)), closes[#1465] +* **BranchMapping:** Map 53037 to westbend branch ([e3de1fe](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e3de1fe)), closes[#1407] +* **CoApplicantSpouse:** Fixes a broken unit test ([ca68c74](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ca68c74)), no ticket +* **CollateralInfoPage:** Patch occupancy type using enum instead of string ([c2fe840](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c2fe840)), closes[#1196] +* **DisclosureDate:** Dont initialize valuealize value ([fb5a9f6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fb5a9f6)), closes[#1406] +* **Security:** Send withcredentials in more requests ([10c5674](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/10c5674)), no ticket +* **HeaderStatus:** Remove loantype from header when navigating back to select loan page ([de709e7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/de709e7)), closes[#1362] +* **LandingPage:** Changed language on vehicle selection and headers ([8a2bacd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8a2bacd)), closes[#1363] +* **NameSuffix:** Deleted an important line of code in the last merge ([060021f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/060021f)), no ticket +* **PersonalInfo:** Add correct icons to Middle initial and suffix display ([b496880](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b496880)), closes[#1341] +* **Processing:** Rewriting processing page to avoid out of order errors ([be1bf7f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/be1bf7f)), closes [#1365](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1365) +* **ProcessingPage:** Capital S on last line of status ([a7087af](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a7087af)), closes[#1340] +* **RouteGuards:** Activate route guards ([59e9416](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/59e9416)), closes[1332] +* **SchumerBoxes:** Update Schumer Box rates for reward, student and cash back ([d9fc111](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d9fc111)), no ticket +* **Security:** Only all users with admin role to see admin screen, test role for qa access. ([183073b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/183073b)), closes [#1329](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1329) +* **Security:** Request from microsoft graph if specific user is in admin group ([260922a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/260922a)), closes [#1329](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1329) +* **Summary:** Format Other Income amount as dollars ([9190f6c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9190f6c)), closes [#1335](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1335) +* **SummaryCollateral:** Fix collateral typo ([e508cb4](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e508cb4)), closes[#1187] +* **SummaryPageButton:** Change disabled color to grey ([c5e4e4f](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c5e4e4f)), closes[#1388] +* **UrlParsing:** Strip off extra characters when parsing urls ([1c057bd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1c057bd)), closes [#1323](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1323) +* **VehicleTranslator:** Use credit app purpose of loan for boats, rvs, powersports ([d4f12f7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d4f12f7)), closes[#1401] +* **AmountValidation:** Added amount validation on monthly payment and vehicle collateral ([739f0eb](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/739f0eb)), closes #1342 + +### Features + +* **AddressValidation:** Increased header line height to support all browsers ([4e48711](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4e48711)), closes #1370 +* **AddressVerification:** Move button location for better ux ([2c7adba](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/2c7adba)), closes #1370 +* **Admin:** New admin landing page for choosing features ([4e27e73](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4e27e73)), closes #1065 +* **AdminExport:** Updated csv export with additional fields ([19899c8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/19899c8)), closes #1419 +* **AdminHome:** Added home navigation for admin pages ([15fa86b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/15fa86b)), closes #1449 +* **AdminPage:** Allow user input of date field ([eb79236](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/eb79236)), closes #1416 +* **AdminSystem:** Export table into csv ([fc75b4e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fc75b4e)), closes #1231 +* **AdminSystem:** Save vendor loan id as part of the json model. Display in admin system. ([eb5d0d2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/eb5d0d2)), closes #1170 +* **AdminSystem:** Tooltip for search field ([022d998](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/022d998)), closes #1414 +* **AdminTimeout:** Redirect users to admin home when timed out ([16d7418](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/16d7418)), closes #1418 +* **API:** Update api urls to custom domain names ([3a53dc6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3a53dc6)), closes [#1402](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1402) [#1378](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1378) +* **AppNote:** Capture datetime of when user completes disclosure page ([10cddd7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/10cddd7)), closes #1406 +* **BranchMapping:** Allow download and upload of branch mappings with error checking ([d5581db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d5581db)), closes [#1443](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1443) +* **CookieDisclosure:** Added cookie disclosure to first page of application ([a2aac4d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a2aac4d)), closes #1404 +* **CopyRightYear:** Use year function to get current year for copyright ([39872c2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/39872c2)), closes #1346 +* **Disclosure:** Change text of disclosure box ([594e307](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/594e307)), closes #1417 +* **e2eTests:** Added e2e test coverage to new landing pages ([4c29727](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4c29727)), no ticket. +* **Income:** Allow multiple other and employer incomes. ([dc7947d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/dc7947d)), closes [#565](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/565), closes #1181 +* **LoanTerms:** Updated vehicle loan terms ([84b3776](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/84b3776)), closes #1397 +* **MemberLogin:** Cookie auth, saml end point, UX for members ([609bc7e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/609bc7e)), closes #1379 +* **MultipleEmployers:** Added ability to add up to three employment incomes ([4be1d4b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4be1d4b)), closes [#1181](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1181) +* **NameHeaders:** Format applicant name as title case when shown back to the user ([5491f19](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5491f19)), closes #1427 +* **Notes:** Force user to select primary contact method ([27934e6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/27934e6)), closes #1305 +* **PageTitles:** Change page titles to sentence style ([b1326ea](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b1326ea)), closes #1428 +* **PartialAppLoader:** Allows saved applications to be loaded and continued seemlessly ([39f446d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/39f446d)), closes [#1198](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1198) +* **PromoCode:** Add promo code field to the summary page ([a9caadf](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a9caadf)), closes #1334 +* **SalaryAmount:** Changed minimum salary amount to 0 dollars ([cfed9d2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/cfed9d2)), closes #1343 +* **SaveAndContinue:** Adjusts API calls to match new save => update => submit flow ([20098a7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/20098a7)), closes [#1368](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1368) +* **Security:** Limit access to admin pages to specific AD group, limit DEV/QA to both groups. ([e981887](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e981887)), closes #1325 +* **SuffixSelect:** Add choose one select to suffix select ([dc05c6a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/dc05c6a)), closes #1422, #1341 +* **TimeoutPage:** Timeout page will now route back to admin if user was at admin ([796e48e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/796e48e)), closes #1451 +* **Translator:** Append timestamp to en.json file to prevent caching ([daeb36e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/daeb36e)), closes #1424 +* **Translator:** Convert error messages to translator. ([922a64c](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/922a64c)), closes #1290 +* **VisaTrademark:** Added registered trademark to visa text ([e57bd91](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e57bd91)), closes #1389 + + +### Performance Improvements + +* **AdminModule:** Create unique admin module for the admin system ([a261206](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a261206)), no ticket +* **MemberComponent:** Added unit tests to the member components and services ([d1bd1f9](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d1bd1f9)), no ticket + + +### Reverts + +* **CosmosIdInSession:** Roll back previous changes ([8862ca0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8862ca0)), closes [#1372](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1372) + + +# [2.1.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.10.0...v2.1.2) (2019-02-13) + + +### Bug Fixes + +* **PersonalInfoForm:** Add default value to suffix ([06d8cf7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/06d8cf7)), closes [#1422](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1422) +* **Admin:** Update list of zip codes and branch mappings + + +# [2.1.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.10.0...v2.1.0) (2019-02-11) + + +### Bug Fixes + +* **SchumerBoxes:** Update Schumer Box rates for reward, student and cash back ([d9fc111](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d9fc111)) + + +### Features + +* **Environment:** Up API urls in environment files to new domain names ([6385803](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/6385803)), closes [#1402](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1402) + + +# [2.0.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.10.0...v2.0.0) (2019-01-11) + + +### Bug Fixes + +* **CollateralPage:** Adds a max value validator to the "Estimated Value" field ([e32ae31](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e32ae31)), closes [#1173](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1173) + +### Features + +* **LoanTerms:** Updated 2019 loan terms for vehicles ([87a7a50](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/87a7a50)) + + +# [1.9.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.8.2...v1.9.0) (2019-01-07) + + +### Bug Fixes + +* **AddressValidationView:** updates the UX for Address Validation piece ([edc581a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/edc581a)), closes [#1242](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1242) +* **LoanPurpose:** Correct display the default loan purpose selection for heloc loans ([e94a646](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e94a646)), closes [#1324](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1324) +* **MemberInfoComponent:** Display error message with matching field label ([c87c1c8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c87c1c8)) +* **NumbersWithDecimals:** Numbers with decimals not 'unmasked' correctly ([ed163c6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ed163c6)), closes [#1240](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1240) + + +### Features + +* **AuthenticationGuard:** Configures The Auth guard to authenticate non-prod environments ([d4e0705](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d4e0705)), closes [#1217](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1217) +* **CurrencyFormatter:** Eliminate decimals when not needed ([41bee05](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/41bee05)) +* **EmploymentInfoPage:** Defaults the number of months employed to "0" ([c422bd4](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c422bd4)), closes [#1195](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1195) +* **PersonalInfo:** New suffix field on personal info pages ([d8e6c96](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d8e6c96)) + + +## [1.8.2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.8.1...v1.8.2) (2018-12-12) + +### Bug Fixes + +* **LienPayoffFont:** Updates the font for "Payoff" prompt on CollateralPage. ([7c2a27b8]) +(https://landmarkcu.visualstudio.com/_git/LoanWizard/commit/7c2a27b8d4c2bb28460682092da3e2532475f003), closes [#1194] + + + +## [1.8.1](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.8.0...v1.8.1) (2018-12-10) + +### Bug Fixes + +* **VehicleValutation:** Changes the messaging for vehicle valuation. ([826dcca1]), closes [#1227] + + +# [1.8.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.7.0...v1.8.0) (2018-12-04) + + +### Bug Fixes + +* **Headers:** Remove feature header, causing browser problems. ([b97c278](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b97c278)) +* **SummaryPage:** Full info spouse sections of the summary page did not have a title ("Your spouse's ([5ef5e58](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/5ef5e58)), closes [#1205](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1205) +* **SaveOnlyValidIndividuals:** The backend will validate Individual's and reject the save if they d ([af01465](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/af01465)), closes [#1202](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1202) +* **CORS:** Move CORS from Azure to Code, no ticket. + +### Features + +* **ShowSavingProgress:** Upon saving, the UI will disable the "NEXT" button and add a 'inprogress' ([8484bda](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8484bda)), closes [#1199](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1199) + + +# [1.7.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.4.0...v1.7.0) (2018-12-03) + + +### Bug Fixes + +* **AddressValidation:** Hides the compare address view when address group is invalid ([16fbb07](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/16fbb07)), closes [#1165](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1165) +* **EquityTranslator:** Allow null values in equity/vehicle translator ([b4c44db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b4c44db)), no ticket. +* **FontAndAddress:** Fixed a font class. Fixed 'saving' real estate verified address ([7d16084](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7d16084)), closes [#1194](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1194) +* **Header:** Don't link header to main site, update vehicle loan type header ([d3379fd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d3379fd)), closes [#1131](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1131) [#1132](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1132) +* **I18N:** Fix error messages to allow custom error messages ([c6f73ad](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c6f73ad)), closes #1149 +* **LoanTermYearCalc:** Grab present year loan terms for future years ([0155a93](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0155a93)), closes #1096 +* **PenTest:** Update web.config with security headers, remove location.href reference. ([2fd35c9](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/2fd35c9)), closes #1176, #1177 +* **ProcessingPage:** Changed the bugsnag error on decision page timeout to a notification in bugsnag ([d6457d8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d6457d8)), closes [#1150](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1150) +* **Spouses:** 2 fixes, spouse info needs to disappear when we change to co-applicant spouse, 'same r ([9f7cf7d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9f7cf7d)), closes [#1141](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1141) +* **VehicleSummaryTemplate:** hide vehicle condition for the vehicle types that dont ask for conditio ([1f7b495](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1f7b495)), closes [#1155](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1155), #1172 +* **Waypoints:** fix mobile way point messaging ([7505116](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7505116)), closes [#1127](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1127) +* **Branch-Zip:** Update zip/branch mapping, closes #1174 + + +### Features + +* **AddressValidation:** Adds address validation functionality, injects it to all address input form ([a605680](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a605680)), closes [#1178](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1178) [#1180](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1180) [#1168](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1168) +* **AddressValidation:** Adds address validation story to UI ([8f743b2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8f743b2)), closes [#84](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/84) +* **AdminPage:** A new admin page was created for landmark employees to review loans ([0f0e4ea](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0f0e4ea)), closes #17 +* **bugsnag:** add error to bugsnag for decisioning ([da3a3df](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/da3a3df)), no ticket +* **CommonGroups:** Only alpha characters in the name fields ([1a90415](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1a90415)), closes [#1110](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1110) +* **employment-income:** limits on hourly and annual income ([4165165](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4165165)), closes #1160 +* **environment:** use before send hook to stop bug snag reports ([d362192](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d362192)), no ticket +* **LienPosition:** Added lien position functionality to the front end ([e309377](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e309377)), closes [#1026](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1026), #1025 +* **LienPositionManufacturedHome:** Added logic on front end for lien position for manufactured home ([3886202](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3886202)), closes [#1153](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1153) +* **LoanSubmission:** Save progress of loan applications. Submit via cosmos db ([b81fda2](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b81fda2)), closes #17 +* **LoanTerms:** Signature and preapproval support up to and year truncation ([cec6f5b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/cec6f5b)) +* **LoanTerms:** Up to appended on shortest loan term ([7e71e66](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7e71e66)) +* **MemberComponentVehicleComponent:** Dropdowns disabled when no valid options exist, updated e2e t ([183b841](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/183b841)), closes [#1125](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1125) +* **MetaData:** Add loan meta data into loan container ([4fbe780](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/4fbe780)), closes #1170 +* **PersonalInfoForms:** Add custom message for alpha only name fields ([78486ae](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/78486ae)), closes [#1110](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1110) +* **robots:** disable bots from crawling site ([c16fdb0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/c16fdb0)), closes #1152 +* **SECURITY:** Upgrade IIS headers in web.config per pentest ([8912a59](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/8912a59)), closes #1176, #1177 +* **Translate:** Allow over-riding individual error messages, pull messages from Translate file ([000e025](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/000e025)), closes [#1149](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1149) +* **vehicle-collateral:** Vehicle Seller Conditional Show Hide ([ba00d24](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ba00d24)), closes #1082 +* **KeyVault:** Update API to use Azure Key Vault to read API keys and other secrets, closes #1041 + + +### Performance Improvements + +* **bugsnag:** move bug snag api key ([ce0bb7b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/ce0bb7b)), closes #1041 + + + +# 1.6.1(2018-11-12) + + +### Bug Fixes + +* **SchumerBoxes:** Update Schumer Box rates for reward, student and cash back + + + +# [1.6.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.4.0...v1.6.0) (2018-11-12) + + +### Bug Fixes + +* **LoanTermYearCalc:** Grab present year loan terms for future years ([0155a93](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/0155a93)), closes #1147 + +### Features + +* **Translate:** Allow over-riding individual error messages, pull messages from Translate file ([000e025](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/000e025)), closes [#1149](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1149) +* **CRIF:** Equity Loan without HMDA information for non-signing spouse (co-applicant to CRIF), closes #1140 + + + +# [1.5.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.4.0...v1.5.0) (2018-11-08) + + +### Bug Fixes + +* **Header:** Don't link header to main site, update vehicle loan type header ([d3379fd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d3379fd)), closes [#1131](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1131) [#1132](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1132) +* **Spouses:** Spouse removed when changed to co-applicant spouse ([9f7cf7d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9f7cf7d)), closes [#1141](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1141) +* **Spouses:** Residence is same was not properly required ([9f7cf7d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/9f7cf7d)), closes [#1141](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1141) +* **Waypoints:** fix mobile way point messaging ([7505116](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7505116)), closes [#1127](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1127) +* **TermValidator:** Support the entire range of vehicle years (0 - 9999) without failing, closes #1135 +* **PowersportLoanTermValidator:** Use estimated amount for loanterm calculations on powersports, closes #1136 +* **UX:** Show inverted correct and error field icons (x/checkmark), closes #1103 + +### Features + +* **CommonGroups:** Only alpha characters in the name fields ([1a90415](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1a90415)), closes [#1110](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1110) +* **LoanTerms:** Signature and preapproval support up to and year truncation ([cec6f5b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/cec6f5b)), closes #1098 +* **LoanTerms:** Up to appended on shortest loan term ([7e71e66](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7e71e66)), closes #1098 +* **PersonalInfoForms:** Add custom message for alpha only name fields ([78486ae](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/78486ae)), closes [#1110](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1110) +* **vehicle-collateral:** Vehicle Seller Conditional Show Hide ([3e1820a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3e1820a)), closes #1082 + + +# [1.4.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.3.0...v1.4.0) (2018-11-07) [Patch] + +### Bug Fixes +* **Header:** Fix bug where mobile header displays [object object] instead of loan type, closes #1127 +* **CRIF:** Update vehicle seller wording in app note, closes #1128 +* **Setting:** Update minimum amount for boat and manufactured terms in loan term settings, closes #1130 + + +# [1.4.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.3.0...v1.4.0) (2018-11-06) + + +### Bug Fixes + +* **ApiTranslator:** Apply TCPA disclosure individually ([b9da0ec](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b9da0ec)), closes [#1092](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1092) +* **ApiTranslator:** Unmask the other income and child support consistently with income ([da22a7e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/da22a7e)), closes [#1080](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1080) +* **Build:** Fix the build based on injecting a non DI service ([32af4e6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/32af4e6)) +* **SpouseInfoSaving:** Spouse Information was getting overwritten by a 'save' call in AWService ([a59d69e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/a59d69e)), closes [#1102](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1102), #1124 +* **SpouseTypes:** When spouse-related input changes, some previously entered info should be nullifie ([b0a6f8e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b0a6f8e)), closes [#1090](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1090) [#1124](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1124) +* **VehicleCollateralTerms:** When 'termOptions' change in local state we must ensure that the value ([b9f2eba](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b9f2eba)), closes [#1096](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1096), closed #1096, #1097 + + +### Features + +* **Email_format:** NG's default email validator did not match the regex pattern that CRIF requires ([447438e](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/447438e)), closes [#1115](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1115) +* **HMDA_Gender:** Gender doesn't have to be Male or Female, it can be both ([fd4d7a7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/fd4d7a7)), closes [#1116](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1116) +* **VehicleTerms:** Vehicle terms were refactored and updated accross all vehicle types ([7c2770d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7c2770d)), closes #1096, #1097 +* **CRIF** Vehicle seller added as an additional application note, closes #1082 +* **CRIF** Uppercase user entered fields, closes #1087 +* **CRIF** Send all dates in correct time zone, closes #1109 + + + + +# [1.3.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.2.0...v1.3.0) (2018-11-02) + + +### Bug Fixes + +* **PowerSports:** Update powersports labels, also display correct vehicle type on summary screen. ([80907fc](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/80907fc)), Closes #1099 +* **residencesummary:** Adjust previous address heading on summary page ([3bb0c52](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3bb0c52)), closes [#1107](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1107) +* **SpouseButtons:** Format spouse buttons to match other buttons ([bade806](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/bade806)), closes [#1104](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1104) +* **spouselabel:** Uppercase all spouse labels used on buttons for page navigation ([b5b3ea9](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/b5b3ea9)), closes [bug#1106](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1106) +* **VehicleCollateral:** "Term of Loan" was not patching correctly. ([1de237d](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1de237d)), closes [#1117](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1117) +* **VehicleCollateral:** "Term of Loan" was not patching correctly. ([115e508](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/115e508)), closes [#1117](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1117) + + +### Features + +* **Labels:** Support showing separate labels for loan type in header and copy ([1f6c279](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1f6c279)), closes #1100 +* **SummaryPage:** Add loan term to summary page ([2778fdd](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/2778fdd)), closes [#1101](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1101) + + + + +# [1.2.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.0.10...v1.2.0) (2018-10-30) + + +### Bug Fixes + +* **ConfigurationService:** Change the service to not load from the server more than once. ([66645db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/66645db)) +* **decisionpage:** fix loan terms for powersports ([7541bce](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/7541bce)) +* **decisionpage:** use config service for url instead of hardcoded ([aea7278](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/aea7278)) +* **MonthsResidedEmployed:** Fix the maximum months resided and employed to be < 131 per crif max ([1adb412](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1adb412)) +* **vehiclecollateral:** refactor vc component ([e06d3b8](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/e06d3b8)) +* **Labels:** Fix two appliant -> applicant labels. + +### Features + +* **CRIF** Send HMDA loan flag + + + +# [1.1.0](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.0.10...v1.1.0) (2018-10-29) + + +### Bug Fixes + +* **collateralinfo:** use msrp for term length ([1962cda](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/1962cda)) +* **Labels:** Update all powersports labels to read Personal Water Craft ([aab02cc](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/aab02cc)) +* **vehiclecollateral:** use msrp for loan terms ([914f36b](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/914f36b)) +* **TCPA:** Opt-out now sent to CRIF correctly + + +### Features + +* **DisclosureInfo:** Fixes a compile error ([72fd9db](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/72fd9db)), closes [#1085](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1085) +* **ApiTranslator:** Adds disclosure information to loan application submission ([3d6103a](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/3d6103a)), closes [#1085](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1085) + + + + +## 1.0.10 (2018-10-26) + +### Bug Fixes +* **Powersports** Fix the power sports label to be Personal Water Craft / Jet Ski. +* **LoanTypes** Fixes Signature loan apicode and adds unique api translator for headers. +* **DecisionPage** allow all servers besides production to display the loan id. (QA) +* **ApiTranslator** Properly translate secondary phone #. +* **ApiTranslator** Include middle name for tattle spouse. +* **ApiTranslator** Fix HMDA notice disclosed field. +* **CRIF** Fix HMDA determination fields, gender, ethnicity, race, fix not-applicable value. + +### Features +* **CRIF** Alimony/Child support are no longer INCOME +* **LoanLimits** Adjust vehicle loan limits to 250 from 5000 + + +## 1.0.8 (2018-10-24) + + +### Bug Fixes + +* **ApiTranslator:** Correctly send the hmdaNoticeDisclosed field to the server. ([014c185](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/014c185)), closes [#1069](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/issues/1069) +* **Api:** Correctly format the hmda fields for do not disclose situations. + + + +## [1.0.7](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/v1.0.6...v1.0.7) (2018-10-22) + + + + +## [1.0.6](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/compare/d314e83...v1.0.6) (2018-10-22) + + +### Bug Fixes + +* **project:** Add commit validation using husky and enable conventional changelog ([334bd20](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/334bd20)) + + +### Features + +* Install and create basic setup for Commitizen ([d314e83](https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/commits/d314e83)) diff --git a/LCU.WebApps.External.OnlineLoanApp/Dockerfile b/LCU.WebApps.External.OnlineLoanApp/Dockerfile new file mode 100644 index 0000000..d542565 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/Dockerfile @@ -0,0 +1,33 @@ +# docker build -t ola-ui . +# docker run --rm -p 8080:80 --name ola-ui ola-ui + +FROM node:14-bullseye AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --unsafe-perm --before=2025-09-01 --ignore-scripts + +# Copy the source code. +COPY . . + +# Run scripts skipped during npm ci (generates src/environments/version.ts). +RUN npm run postinstall + +# Build the production bundle. Using npx avoids platform-specific npm script wrappers. +# Note: base-href/deploy-url set for hosting under /loanapp. +RUN npx ng build \ + --base-href="/loanapp/" \ + --deploy-url="/loanapp/" + +FROM nginx:1.25-alpine AS runtime + +COPY docker/nginx.conf /tmp/default.conf +RUN rm -f /etc/nginx/conf.d/default.conf && \ + mv /tmp/default.conf /etc/nginx/conf.d/default.conf + +COPY --from=build /app/dist/ui /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/LCU.WebApps.External.OnlineLoanApp/README.md b/LCU.WebApps.External.OnlineLoanApp/README.md new file mode 100644 index 0000000..ef1864f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/README.md @@ -0,0 +1,372 @@ +# 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 Credit Union Loan Application + +This readme outlines the developer process, basic coding style guide and provides guidance and details around the technical approach to the user interface. You should always review and update this document to encompass your changes. + +# Known Contacts + +The known contacts for this application are Jeff Knaub and Jerry Wenzen. + +# Getting Started +Before you can work with the code, build it, or deploy it you will need a proper developmenet environment. The required environment is basically Git, Node, and something to work with C#. +This environment code has been wrapped up into a Powershell script that should run on any Windows 10 machine and produce a viable development environment to carry you through this guide. +The script is available in the root of this directory as `setup-env.ps1`. + +You should start by reviewing and possibly running that script if needed. + +## Testing Notes on LandmarkCu.com / online banking + +The application is now integrated with Online Banking. Testing this is more difficult and involves having online banking +pointed at the dev application. The process flow is documented in the architecture documents, but also briefly included +here. + +1. User logs into online banking +2. User clicks the apply for loan button +3. Online banking POSTs a SAML token to the api end point. +4. If valid a redirect to the application is returned, and cookies will be attached. + +Cookies are required, and test online banking accounts are required. Additionally, there is a pipe from Azure to +on-premise so that we can call the core for member information. + +## Working With The Code +Code is checked out from the Azure DevOps Repository, which is currently [https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard] with the back end code +located at [https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizardAPI]. + +Work should be done in a branch from Develop. In the shared folder there is a software development process document that talks about this, you should start there. A brief introduction will be included here. + +Start by checking out the repository, and specifically the develop branch. You should be branching off of develop +with your own working branch. When you are complete, you will create a pull request within VSTS here: [https://landmarkcu.visualstudio.com/LoanWizard/_git/LoanWizard/pullrequests] + +Before submitting your pull request, review the following: + * Did you commit with commitizen? `git cz` (see below) + * Did you add all needed unit testing, and ensure all existing tests are passing? + * Have you run the e2e tests and ensured they all pass? + +In order to work and submit code, the project will need to be built and run. Instructions follow. Generally the project doesn't need to be built, just run locally. However, running a production build after major changes will ensure the production build is still working before CI runs. + +### Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. +You can also use npm to run these commands: +Run `npm run build` to build the project with default settings. +Run `npm run build-nvisia` to build the project with default settings. +Run `npm run build-release` to build the project with release settings (configuration for qa). +Run `npm run build-prod` to build the project with production settings (and build optimizer). + + +### Run (Development server) + +Run `npm run start` for a dev server. Navigate to `https://localhost:4200/`. The app will automatically reload if you change any of the source files. +Note that this will run with a proxy in place so that you can contact the web services. + +This start script is in the package.json file, and runs `ng serve --proxy-config proxy.conf.json --configuration=local --ssl` + +### Run (Develop, QA, Prod) + +Deployments to Develop, QQ, and Prod are automatically handled by Azure Dev-Ops. See the additional resources section below; as there is a document outlining the software development process, as well as the cloud setup Azure. + +### Running unit tests + +Run `npm run test` to run the unit tests with code coverage. +Run `npm run test-debug` to run the unit tests without code coverage, but with full stack traces. +Run `npm run test-interactive` if you want to use real chrome for debugging and not headless. +Firefox, IE, and Edge are also supported in this project. Run the ng test command and pass in the desired browser(s) into the browser argument +Example: ng test --watch=false --browsers Firefox,Chrome,IE,Edge --no-progress + +### Running end-to-end tests + +Tests run against Chrome and Firefox in parallel. +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Run `npm run update-e2e` if someone has changed the protractor config and you are getting errors; this usually fixes the problem. You may also need +to run this if you are missing one of the browsers in the configuration. + +### Resources + There is additional documentation available within the Landmark shared folders; you should look for the following files, or ask someone for them if you can't find them. Start in the "Phase 1 Final Documents" folder under the 'LCU Credit App 2018' folder: + + * LCU Credit App 2018: This is a folder + * The following documents, plus additional documents should be available in this folder, get access to it as soon as possible. + * Architecture Document: Landmark Credit Union - Credit Application - SAD + * This is the software architecture description. + * Site Layout / Reference: Landmark_Site_Reference.XLSX + * This shows urls/locations for source code and deployables and the Azure configuration + * Software Process: landmark_software_process.docx + * Outlines the process for working with the source code. + * Azure Key Vault keys + * Outlines each key used by the API and it's purpose + * Additional notes on OLA's coding and components can also be found in the repo's wiki + * https://landmarkcu.visualstudio.com/LoanWizard/_wiki/wikis/Loan%20Wizard%20Wiki/ + +### Suggestion for IDE +* Visual Studio Code +* Webstorm + +# Architecture + + The best explanation for the architecture can be found by referencing the document listed in the resources section above. However if you are looking to do + something specific, here are some tips. + + A. Add a new loan type + 0. If you are adding a sub-type to an existing type, you do not need to follow all of these steps, these instructions cover adding a new category of loan type. + 0. See: loan-type.ts, credit-card-outline.ts, credit-card-routing.module.ts, application-wizard.service.ts, credit-application.service.ts, credit-application-dao.service.ts, api-translator.service.ts + 1. Begin in loan-type.ts file; add your type. + 2. Create an Outline, start by looking at credit-card-outline.ts. + 3. With your outline created, create a set of routes similar to credit-card-routing.module.ts + 4. Add any needed sets of fields to common-groups.ts + 5. Create a new pages in the loan-wizard/wizards folder. + 6. Create any new field groups needed in the loan-wizard/field-groups folder. + 7. If you need custom waypoints, that is in loan-wizard/controls folder. + 8. update the CreditApplicationService to support any new fields. + 9. update the ApplicationWizardService to support any new fields and pages. + 10. Update the api-translator for any fields that need to be submitted to the back end, this is in shared/api-translator. + 11. Add your loan to app/select-loan if needed. + 12. Make sure to add additional unit testing as needed for all new code. + 99. Don't forget you may need to change the back end services! + + B. Change an environment + 1. There is currently local, prod, e2e, and default. These are in src/environments, but you must also change angular.json!!!!! + + C. Add e2e testing + 1. This is under the e2e folder. + 2. There is a folder for each major loan type, and a shared folder for common pages. + 3. Add your folder and tests per the existing pattern, adding to shared when possible. + 4. In the protractor.conf.js file is where we specify which test files are run, and on which browsers. + 5. Don't forget to update your protractor install, there is a script in the package.json "npm run update-e2e" + + D. Something not listed here + 1. The file structure for this project is organized as a hybrid - most code is organized by feature, with shared code organized under + the shared folder. If you are looking for something with common behavior, start in app/shared. Otherwise it's best to start in app/loan-wizard. + 2. It's fairly easy to find the code for a specific page - use the route shown in your browser, like this: + > https://applydev.azurewebsites.net/loan-wizard/cc/st/step-2 + The 'CC' part is the category, and the 'ST' is the type. The 'step-2' is the page. + 1. Check the various routing modules to find your category (cc is credit card), this is *-routing.module.ts + 2. Find the route in that module corresponding to the page (step-2 in this case), the component listed is the code for + that page. Start there. In this case, CC leads to credit-card-routing.module.ts which leads to the PersonalInfoPageComponent. + 3. Beyond this, you should refer to the architectural document and spend some time reviewing the code. + + +# Releases + +Releases are handled by anyone on the team with access to VSTS. Generally when a pull request is accepted +to develop, that will be automatically built and deployed to DEV. The Dev application is here: [https://applydev.landmarkcu.com]. The +same is true for the back end / API. When you want to make a release, you must revise the changelog (CHANGELOG.md file). This +is a two step process: + 1. run 'npm run release' in the develop branch, this will: + A. Update the version in package.json automatically. + B. Update the CHANGELOG.md file automatically + 2. Update/Review the CHANGELOG.md as needed, usually for: + A. Add backend features/fixes/etc + B. Add missing ticket numbers (or 'no ticket') + C. Remove duplicates + +You can force a version number if needed by changing the package.json, package-log.json, and CHANGELOG.md. Once you are +done revising you can 'git commit --amend --no-edit' and then 'git push -f' will push the release to develop. + +To get to QA, develop must be merged into the release branch and pushed. This will automatically cause the release +branch to be build and the resulting release to be pushed to QA. + +To get to production, release must be merged into master and pushed. This will automatically cause the +code to be built and packaged as artifacts. However the release process is manual, and can be done +here: [https://landmarkcu.visualstudio.com/LoanWizard/_release] + +When creating releases, it's important not to forget to make a release and update the change notes as +needed. There is information on creating a release below. An important tip is that on the website +you can hover over the landmark copyright in the bottom left to see the released client version. + +## Setup Commitizen and Produce changelogs and release notes + +Commitizen is a command line tool that helps format commit messages with a series of prompts that are used to +generate a commit message. We use this tool to standardize our commits for tracking a changelog and producing release +notes. + + * `npm install commitizen -g` + * `npm install commitizen --save-dev` // Already done for you, skip. + * `npm install cz-conventional-changelog --save-dev` // Already done for you, skip. + * `npm install --save-dev validate-commit-msg` // Already done for you, skip. + * `npm install -g conventional-changelog-cli` + + Instead of committing with `git commit` commit with `git cz` + This will force your messages into the format (): + + Definition | Description + -----------|------------ + feat | A new feature + fix | A bug fix + docs | Documentation only changes + style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) + refactor | A code change that neither fixes a bug nor adds a feature + perf | A code change that improves performance + test | Adding missing or correcting existing tests + chore | Changes to the build process or auxiliary tools and libraries such as documentation generation + + Here is how semantic-release will map these to release types and how it maps to [semantic verioning](https://semver.org/#summary): + + Commit message | Release type + ----------------|-------------- + `fix(pencil): stop graphite breaking `
`when too much pressure applied` | Patch Release (PATCH) + `feat(pencil): add 'graphiteWidth' option` | Feature Release (MINOR) + `BREAKING CHANGE: The graphiteWidth option `
`has been removed. The default graphite`
` width of 10mm is always used for performance reasons.` | Breaking Release (MAJOR) + + ## Produce or update the changelog + + You shouldn't need to do this, it should be generated for you when you create a release (below) + + * Create changelog from scratch: `conventional-changelog -p angular -i CHANGELOG.md -s -r 0` + * Update the changelog: `conventional-changelog -p angular -i CHANGELOG.md -s ` + + ## Produce a release + + * Use: `npm run release` + + Resources: + * http://www.nullfactory.net/2017/04/enforce-commit-message-standards-and-generate-changelogs-via-git/ + * https://dev.bleacherreport.com/how-we-use-commitizen-to-clean-up-commit-messages-a16790dcd2fd + * http://devboosts.com/2017/03/28/conventional-commits/ + * https://medium.com/@LeonardoBrAu/why-git-squash-and-rebase-commits-and-how-we-do-it-87aa17406e21 + +# ADA Compliance + + This application is targeting level AA ADA Compliance, here is the checklist you should keep in mind when developing: + + * Every image, video file, audio file, plug-in, etc. has an alt tag + * Complex graphics are accompanied by detailed text descriptions + * The alt descriptions describe the purpose of the objects + * If an image is also used as a link, the alt tag describes the graphics and the link destination + * Decorative graphics with no other function have empty alt descriptions (alt=””) + * Data tables have the column and row headers appropriately identified (using the tag) + * Make sure the page does not contain repeatedly flashing images + * Check to make sure the page does not contain a strobe effect + * All scripts and plug-ins and the content within them are accessible to assistive technologies, or else an alternative means of accessing equivalent content is provided + * When form controls are text input fields use the LABEL element + * When text is not available use the title attribute + * Include any special instructions within field labels + * Make sure that form fields are in a logical tab order + * Include a ‘Skip Navigation’ button to help those using text readers + * SVG icons should not focusable by tabbing in IE (utilize `focusable="false"` on all icon tags) + + ## Coding tools for ADA Compliance + * Refer to here for help: https://getbootstrap.com/docs/4.0/getting-started/accessibility/ + * Refer to here for practical examples: http://www.punkchip.com/aria-basic-findings/ + * use aria-label, aria-expanded, and aria-hidden as needed. + * Bootstrap: sr-only, bootstrap provides sr-only so that you can show things to people with screen readers. (sr-only stands for screen readers only) + +# Style Concerns + + 1) Make sure the validators listed are full and correct. + 2) Make sure the error display component shows all validators you are using. + 3) Make sure default value is provided at the model level. + 4) A field cannot be a number, be part of a select, and have blank ('') as the default. Make these non-numbers if possible. + 5) Select boxes should all start on their default, which should derive from the MODEL. + 6) Select boxes title/default should be "Please Choose" if they do not have a default value. + 7) Select boxes and text inputs should have class="form-control". + 8) every
declaration should have an `(ngSubmit)="commandNext()"`. + 9) Next button should be at the LEFT, the back button should be BEFORE it. + 10) Next button should say "Continue", not "Next". + + * Always keep the web development checklist in mind: http://webdevchecklist.com/ + * `(click)` bindings should bind to a CommandXXX function with Command as a prefix. This denotes to anyone looking at the component that a given function is + UX driven. + * Keep minimal functionality in Components, they should act as minimal controllers. Refactor work and business logic to a service. + * You should have unit tests for all components and services. + * Take your time with naming and ask the team for help; but take naming seriously so that your components and services are recognizable. + * Review the .editorconfig file if you are notices style differences in PRs, resolve any problems with the team. + * DO NOT use angular material for the loan application system! Angular material should only be used for the admin system. + +# Libraries + + * https://github.com/ngx-translate/core + +## Admin System Dependencies + * @angular/cdk + * @angular/material + +## Angular Material + * Angular material is a library used exclusively for the admin system. + * Provides the datatable with native sorting/paging/filtering + +## Angular +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8 with the command `ng new ui --routing --style scss` +The project's package.json has been updated with various libraries and scripts; it should always be reviewed. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. +We do not generally use the code scaffolding. + +# FAQ + +Q. I'm running the unit test and I see output like this: +`HeadlessChrome 68.0.3440 (Windows 10.0.0) XYZComponent should use the service to add address controls to the form group FAILED + [object ErrorEvent] thrown +` + +A. Re-run the tests using `npm run test-debug`, which will show the full stack trace but not provide code coverage. + +Q. What do I need to know about NGX-Translate + +A. The primary translation file is en.json, and is loaded asynchronously (it could be moved out of project); this is setup in the app.module.ts by + using a TranslateHttpLoader. Some entries have HTML, if you want to use HTML in an entry, you have to bind to [innerHtml] + Html example: `
` + non-html example `
{{'COPY.DISCLOSURES' | translate: localState}}
` + You should refer to the ngx-translate documentation for full usage and instructions. + +Q. What do I need to know about Security? + +A. There are many security concerns outlines in the architecture documents in the share (see resources above). To add to it, there are multiple security mechanisms at play. One is for Landmark employees, and uses the ADAL-Angular-6 library to authenticate against Azure AD. The other is with NCR/DI and is used to authenticate members; this is handld in the API and the client uses this via checking for cookies. Be careful not to mix up these two methods. + +# File Upload Component and Azure Blob Storage +The file upload component allows users and dealerships to upload files to ACTion. This is handled with both Azure Blob Storage and CosmosDB. CosmosDB has a list of files associated with a loan application that contains information about the file and the GUID of the file in Blob Storage. + +When an application is submitted or a dealership clicks transmit, the LoanApplicationController will get the CosmosDB record for the list of files and loop over them to transmit them to ACTion. After the files have been transmitted to ACTion, they are removed from Blob Storage. + +The files that aren't ever transmitted to ACTion will remain in Blob Storage (this can happen from API failure or if a dealership closes the form without transmitting). Files that are older than 30 days will be removed automatically by a rule set in the Lifecycle section of Azure. + +# Dealership Portal +The dealership portal can be accessed via ```/dealership-portal``` and allows dealerships to upload documents to an ACTion loan. There are four required fields that are used to authenticate a dealership: dealership code, dealership password, application ID, and the primary applicant's last name/full business name. + +## Dealership Code +```(string, case-sensitive, max length: 60)``` + +2-3 digit code that is automatically generated by ACTion when a dealership is added. It takes the first letter of the dealership's name and then increments the number based on other dealerships with the same first letter. So if there were 9 dealerships that started with B (```B1-B9```), the next B-named dealership would become ```B10```. + +It also appears that it converts dealerships that begin with a number to the first letter of that number, e.g. ```5``` -> ```F```; so ```5 Corners Dodge``` becomes ```F6```, as it is the 6th dealership to begin with ```F```. + +The dealership code can be found within ACTion. +## Dealership Password +```(string, case-sensitive, max-length 30)``` + +This password is not a password in the traditional sense, as it is being stored in the plain-text field ```costCenterCode``` within ACTion. Dealership's will not have access to create/change their password, so it must be done by someone at Landmark with access to ACTion. +## Application ID +```(integer)``` + +Self-explanatory +## Primary Applicant's Last Name or Business Full Name +```(string, case-insensitive, max-length: 250)``` + +When the authentication request is made, the application is taken from ACTion, the ```primaryBorrower``` field is inspected for ```idRef``` which contains the GUID of the primary borrower. The primary borrower can be an individual or a company. Once this ID is found, the API loops over the individuals and companies associated with the application until it finds the matching ID. + +Once the matching ID is found, it will return the ```lastName``` if it is an individual or the ```tradeName``` if it is a company. The request much match this value exactly, but is not case-sensitive. + +## Other notes +This portal utilizes the file upload component, which is the same component used for the loan application process. Any changes to this component that is specific to the dealership portal must be made with caution as those changes will affect all loan processes that use it. + +In non-production environments, you can force an error to happen to test what happens if a file transmission error occurs by uploading the first file with the name ```do-not-allow-transmission.jpg```. The API will see this file and return a ```500```. diff --git a/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 new file mode 100644 index 0000000..8b1994d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/install_dependencies.ps1 @@ -0,0 +1 @@ +cinst -y kind diff --git a/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/kind.cluster.config b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/kind.cluster.config new file mode 100644 index 0000000..b33f889 --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/setup_and_deploy.ps1 new file mode 100644 index 0000000..dd4b4a4 --- /dev/null +++ b/LCU.WebApps.External.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:1.0 .. + +kind load docker-image local/online-loan-app:1.0 + +kubectl apply -k ..\_deploy\overlays\local diff --git a/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/teardown.ps1 b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/teardown.ps1 new file mode 100644 index 0000000..64866e8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/_kube_local_testing/teardown.ps1 @@ -0,0 +1 @@ +kind delete cluster diff --git a/LCU.WebApps.External.OnlineLoanApp/angular.json b/LCU.WebApps.External.OnlineLoanApp/angular.json new file mode 100644 index 0000000..2ee9f0b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/angular.json @@ -0,0 +1,174 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "ui": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": { + "@schematics/angular:component": { + "styleext": "scss" + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/ui", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/web.config", + "src/Robots.txt" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [ + "./node_modules/@fortawesome/fontawesome/index.js", + "./node_modules/@fortawesome/fontawesome-free-solid/index.js", + "./node_modules/@fortawesome/fontawesome-free-regular/index.js" + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + }, + "configurations": { + "production": {}, + "local": { + "optimization": false, + "outputHashing": "none", + "sourceMap": true, + "extractCss": false, + "namedChunks": true, + "aot": false, + "extractLicenses": false, + "vendorChunk": true, + "buildOptimizer": false + }, + "dev": { + "optimization": false, + "outputHashing": "none", + "sourceMap": true, + "extractCss": false, + "namedChunks": true, + "aot": false, + "extractLicenses": false, + "vendorChunk": true, + "buildOptimizer": false + }, + "e2e": { + "optimization": false, + "outputHashing": "none", + "sourceMap": true, + "extractCss": false, + "namedChunks": true, + "aot": false, + "extractLicenses": false, + "vendorChunk": true, + "buildOptimizer": false + }, + "qa": {}, + "uat": {} + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "ui:build" + }, + "configurations": { + "production": { + "browserTarget": "ui:build:production" + }, + "local": { + "browserTarget": "ui:build:local" + }, + "uat": { + "browserTarget": "ui:build:uat" + }, + "e2e": { + "browserTarget": "ui:build:e2e" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "ui:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "ui-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "ui:serve:e2e" + }, + "configurations": { + "production": { + "devServerTarget": "ui:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "ui" +} diff --git a/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml new file mode 100644 index 0000000..6ca9285 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-k8s-pipeline.yaml @@ -0,0 +1,53 @@ +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.onlineloanapp +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.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml new file mode 100644 index 0000000..f63f0b5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-feature-branches.yaml @@ -0,0 +1,10 @@ +trigger: + branches: + include: + - '*' + exclude: + - 'main' + - 'develop' + +extends: + template: azure-k8s-pipeline.yaml diff --git a/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-prd.yaml new file mode 100644 index 0000000..8e66ed4 --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-qa.yaml new file mode 100644 index 0000000..30ce939 --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/azure-pipelines-uat.yaml new file mode 100644 index 0000000..e33f5d3 --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/prd-pvars.yaml new file mode 100644 index 0000000..9b650cd --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/qa-pvars.yaml new file mode 100644 index 0000000..dfc1bfb --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml b/LCU.WebApps.External.OnlineLoanApp/azdo_pipelines/config-files/uat-pvars.yaml new file mode 100644 index 0000000..10d7ce3 --- /dev/null +++ b/LCU.WebApps.External.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.WebApps.External.OnlineLoanApp/docker/nginx.conf b/LCU.WebApps.External.OnlineLoanApp/docker/nginx.conf new file mode 100644 index 0000000..99cf224 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/docker/nginx.conf @@ -0,0 +1,24 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Serve the app under /loanapp, stripping the prefix for static assets and + # falling back to the SPA entrypoint for deep links. + location /loanapp/ { + rewrite ^/loanapp/(.*)$ /$1 break; + try_files $uri $uri/ /index.html; + } + + # Fallback for any other paths (e.g., health checks). + location / { + try_files $uri $uri/ /index.html; + } + + # Basic security headers. + add_header X-Content-Type-Options "nosniff"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/protractor.conf.js b/LCU.WebApps.External.OnlineLoanApp/e2e/protractor.conf.js new file mode 100644 index 0000000..3142a02 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/protractor.conf.js @@ -0,0 +1,35 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/credit-card/*.e2e-spec.ts', + './src/select-loan/*.e2e-spec.ts', + './src/heloc/*.e2e-spec.ts', + './src/vehicle/*.e2e-spec.ts', + './src/signature/*.e2e-spec.ts' + ], + multiCapabilities: [{ + 'browserName': 'chrome' + }, { + 'browserName': 'firefox' + }], + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.e2e.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + // Do not do this! browser.waitForAngularEnabled(false); + } +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-full-spouses.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-full-spouses.e2e-spec.ts new file mode 100644 index 0000000..680ff28 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-full-spouses.e2e-spec.ts @@ -0,0 +1,153 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { CreditCardPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { CreditCardDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SpouseTypePagePo } from '../shared/pages/spouse-type-page.po'; + +describe('It should create a credit card application for 4 full info individuals', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new CreditCardPageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.A (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsPrimaryMarriedButNotMarriedToCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.A (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.A (Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept FULL INFO SPOUSE input for Applicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsFullInfoSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.B (Applicant\'s Spouse Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalNoMarital(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.B (Applicant\'s Spouse Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressSame(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.B (Applicant\'s Spouse Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.C (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.C (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.C (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept FULL INFO SPOUSE input for CoApplicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsFullInfoSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.D (CoApplicant\'s Spouse Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalNoMarital(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.D (CoApplicant\'s Spouse Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressSame(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.D (CoApplicant\'s Spouse Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-go-back.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-go-back.e2e-spec.ts new file mode 100644 index 0000000..00953b3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-go-back.e2e-spec.ts @@ -0,0 +1,129 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { CreditCardPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { CreditCardDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { WizardPagePo } from '../shared/pages/wizard-page.po'; + +describe('It should allow free navigation of a completed credit card application', () => { + + it('should accept maximum inputs for page 1.', () => { + const page = new CreditCardPageOnePo(); + browser.get(page.PAGE_URL); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + page.showEmploymentFields(); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + // Will no longer happen. + // page.fillSecondIncome(); + // expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + + it('should jump back to the beginning.', () => { + const urlList = [ + 'http://localhost:4200/loan-wizard/cc/cb/co-applicant-financial', + 'http://localhost:4200/loan-wizard/cc/cb/co-applicant-residential', + 'http://localhost:4200/loan-wizard/cc/cb/co-applicant-personal', + 'http://localhost:4200/loan-wizard/cc/cb/step-4', + 'http://localhost:4200/loan-wizard/cc/cb/step-3', + 'http://localhost:4200/loan-wizard/cc/cb/step-2', + 'http://localhost:4200/loan-wizard/cc/cb/step-1' + ]; + const page = new WizardPagePo(); + urlList.forEach((eachUrl) => { + expect(page.canGoPrevious()).toEqual(true); + page.goPrevious(); + browser.getCurrentUrl().then((newUrl) => { + expect(newUrl).toEqual(eachUrl); + }); + expect(page.canGoNext()).toEqual(true); + + }, this); + + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-maximum.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-maximum.e2e-spec.ts new file mode 100644 index 0000000..bd8a134 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-maximum.e2e-spec.ts @@ -0,0 +1,144 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { CreditCardPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { CreditCardDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; + +describe('It should create a maximum credit card application ', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new CreditCardPageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + // page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.COAPP_FIELDS); + expect(inputArray.length).toEqual(page.COAPP_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-minimal.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-minimal.e2e-spec.ts new file mode 100644 index 0000000..31a5adb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-minimal.e2e-spec.ts @@ -0,0 +1,85 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { CreditCardPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { CreditCardDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; + +describe('It should create a minimal credit card application ', () => { + + it('should accept basic inputs for page 1 (Member/Loan Type)', () => { + const page = new CreditCardPageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.fillFieldsForFailureToConfirmJointCredit(); + expect(page.canGoNext()).toEqual(false); + page.dismissModalByOkayButton(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + // For a minimal credit card, don't enter any income :-) + // expect(page.canGoNext()).toEqual(false); + // page.showEmploymentFields(); + // inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + // expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + // expect(page.canGoNext()).toEqual(false); + // page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + + // We could go to the next page, but we'd create X loans every time we run these tests... +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-tattle-spouses.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-tattle-spouses.e2e-spec.ts new file mode 100644 index 0000000..801b485 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/credit-card-tattle-spouses.e2e-spec.ts @@ -0,0 +1,130 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { CreditCardPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { CreditCardDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SpouseTypePagePo } from '../shared/pages/spouse-type-page.po'; +import { TattleSpousePagePo } from '../shared/pages/tattle-spouse-page.po'; + +describe('It should create a credit card application for 2 full info individuals + 2 tattle spouses', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new CreditCardPageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.A (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsPrimaryMarriedButNotMarriedToCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.A (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.A (Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept TATTLE SPOUSE input for Applicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsTattleSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for tattle spouse page (Applicant\'s Spouse Tattle Info)', () => { + const page = new TattleSpousePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept maximum inputs for page 2.C (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.C (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.C (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept TATTLE SPOUSE input for CoApplicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsTattleSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for tattle spouse page (CoApplicant\'s Spouse Tattle Info)', () => { + const page = new TattleSpousePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-disclosures.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-disclosures.po.ts new file mode 100644 index 0000000..11f9426 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-disclosures.po.ts @@ -0,0 +1,34 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; + +export class CreditCardDisclosuresPagePo { + + public readonly PAGE_URL = '/loan-wizard/cc/cb/disclosures'; + + public readonly DISCLOSE_TRUTHFUL = 'truthful'; + public readonly DISCLOSE_CONTACT = 'contact'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [ + this.DISCLOSE_TRUTHFUL, this.DISCLOSE_CONTACT, + this.NEXT_BUTTON]; + + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + const truthful = element(by.id(this.DISCLOSE_TRUTHFUL)); + PageObjectTools.clickCheckBox(truthful); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-one.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-one.po.ts new file mode 100644 index 0000000..79d3093 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/credit-card/pages/page-one.po.ts @@ -0,0 +1,77 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; +import { TestInputSet, TestInputCommand, TestingInputType } from '../../shared/data-driven/test-input-command'; + +export class CreditCardPageOnePo { + + public readonly PAGE_URL = '/loan-wizard/cc/cb/step-1'; + + public readonly LIMIT_REQUESTED = 'inputLimitRequested'; + public readonly ANOTHER_PERSON = 'inputApplicantType'; + public readonly LIVE_IN_WI = 'inputLivesInMaritalPropertyState'; + public readonly JOINT_CREDIT = 'inputConfirmJointCredit'; + public readonly CO_APP_LIVE_IN_WI = 'inputCoAppLivesInMaritalPropertyState'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly MODAL_CLOSE_BUTTON = 'modalCloseButton'; + public readonly MODAL_OKAY_BUTTON = 'modalOkayButton'; + public readonly ALL_FIELDS = [this.LIMIT_REQUESTED, this.ANOTHER_PERSON, this.LIVE_IN_WI, this.NEXT_BUTTON]; + public readonly HIDDEN_FIELDS_1 = [this.JOINT_CREDIT, this.CO_APP_LIVE_IN_WI]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 1234, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 1234, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.CO_APP_LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly JOINT_CREDIT_DATA: TestInputSet = [ + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsForFailureToConfirmJointCredit(): void { + PageObjectTools.fillFields(this.JOINT_CREDIT_DATA); + } + + public dismissModalByOkayButton(): void { + const modalOkayButton = element(by.id(this.MODAL_OKAY_BUTTON)); + modalOkayButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement() { + return PageObjectTools.getElementsIdField(this.LIMIT_REQUESTED); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-maximum.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-maximum.e2e-spec.ts new file mode 100644 index 0000000..b1618b2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-maximum.e2e-spec.ts @@ -0,0 +1,177 @@ +import { browser, protractor } from 'protractor'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { HelocPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { HmdaPagePo } from '../shared/pages/hmda-page.po'; +import { CollateralPagePo } from '../shared/pages/collateral-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { CreditCardDisclosuresPagePo } from '../credit-card/pages/page-disclosures.po'; + +describe('It should create a maximum HELOC application ', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new HelocPageOnePo(); + browser.get(page.PAGE_URL); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for HMDA page (HMDA: Race/Ethnicity/Gender)', () => { + const page = new HmdaPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS, page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.COAPP_FIELDS); + expect(inputArray.length).toEqual(page.COAPP_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for co-applicant HMDA page (Co-applicant HMDA: Race/Ethnicity/Gender)', () => { + const page = new HmdaPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for collateral page (Collateral)', () => { + const page = new CollateralPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.ADDRESS_FIELDS, ...page.LIEN_POSITION_FIELDS]); + expect(inputArray.length).toEqual([...page.ALL_FIELDS, ...page.ADDRESS_FIELDS, ...page.LIEN_POSITION_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-minimal.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-minimal.e2e-spec.ts new file mode 100644 index 0000000..e65e678 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/heloc-minimal.e2e-spec.ts @@ -0,0 +1,99 @@ +import { browser, by, element, ElementFinder, protractor } from 'protractor'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { HelocPageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { HmdaPagePo } from '../shared/pages/hmda-page.po'; +import { CollateralPagePo } from '../shared/pages/collateral-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { CreditCardDisclosuresPagePo } from '../credit-card/pages/page-disclosures.po'; + +describe('It should create a minimal HELOC application ', () => { + + it('should accept basic inputs for page 1 (Member/Loan Type)', () => { + const page = new HelocPageOnePo(); + browser.get(page.PAGE_URL); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 3.A (HMDA: Race/Ethnicity/Gender)', () => { + const page = new HmdaPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS, page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept basic inputs for page 4.A (Collateral)', () => { + const page = new CollateralPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept basic inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/pages/page-one.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/pages/page-one.po.ts new file mode 100644 index 0000000..86f07a5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/heloc/pages/page-one.po.ts @@ -0,0 +1,56 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; +import { TestInputSet, TestingInputType } from '../../shared/data-driven/test-input-command'; + +export class HelocPageOnePo { + + public readonly PAGE_URL = '/loan-wizard/eq/he/step-1'; + + public readonly LIMIT_REQUESTED = 'inputLimitRequested'; + public readonly ANOTHER_PERSON = 'inputApplicantType'; + public readonly LIVE_IN_WI = 'inputLivesInMaritalPropertyState'; + public readonly JOINT_CREDIT = 'inputConfirmJointCredit'; + public readonly CO_APP_LIVE_IN_WI = 'inputCoAppLivesInMaritalPropertyState'; + public readonly PURPOSE_OF_LOAN = 'inputPurposeOfLoan'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [this.LIMIT_REQUESTED, this.ANOTHER_PERSON, this.LIVE_IN_WI, this.NEXT_BUTTON, this.PURPOSE_OF_LOAN]; + public readonly HIDDEN_FIELDS_1 = [this.JOINT_CREDIT, this.CO_APP_LIVE_IN_WI]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 5432, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.PURPOSE_OF_LOAN, fieldValue: 1, fieldType: TestingInputType.SELECT } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 5432, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.CO_APP_LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.PURPOSE_OF_LOAN, fieldValue: 2, fieldType: TestingInputType.SELECT } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-credit.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-credit.po.ts new file mode 100644 index 0000000..ff992be --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-credit.po.ts @@ -0,0 +1,10 @@ +import { SelectLoanPo } from './select-loan.po'; + +export class SelectLoanCreditPo extends SelectLoanPo { + + public readonly PAGE_URL = '/select-loan/credit-card'; + public readonly CASH_BACK = 'cc-cash'; + public readonly REWARDS = 'cc-reward'; + public readonly STUDENT = 'cc-student'; + public readonly SECURED = 'cc-secured'; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-home.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-home.po.ts new file mode 100644 index 0000000..6722d31 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-home.po.ts @@ -0,0 +1,11 @@ +import { SelectLoanPo } from './select-loan.po'; + +export class SelectLoanHomePo extends SelectLoanPo { + + public readonly PAGE_URL = '/select-loan/home-equity'; + public readonly HELOC = 'eq-heloc'; + public readonly HOME_EQUITY = 'eq-fixed'; + public readonly MORT_REFI = 'eq-refi'; + public readonly MANU_HOME = 'eq-manu'; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-personal.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-personal.po.ts new file mode 100644 index 0000000..28b9d57 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-personal.po.ts @@ -0,0 +1,7 @@ +import { SelectLoanPo } from './select-loan.po'; + +export class SelectLoanPersonalPo extends SelectLoanPo { + + public readonly PAGE_URL = '/loan-wizard/sl/pe'; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle-sub.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle-sub.po.ts new file mode 100644 index 0000000..227f2f6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle-sub.po.ts @@ -0,0 +1,9 @@ +import { SelectLoanPo } from './select-loan.po'; + +export class SelectLoanVehicleSubPo extends SelectLoanPo { + + public PAGE_URL = '/select-loan/sub-vehicle'; + public readonly LOAN = 've-loan'; + public readonly PRE = 've-pre'; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle.po.ts new file mode 100644 index 0000000..3729231 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan-vehicle.po.ts @@ -0,0 +1,11 @@ +import { SelectLoanPo } from './select-loan.po'; + +export class SelectLoanVehiclePo extends SelectLoanPo { + + public readonly PAGE_URL = '/select-loan/vehicle'; + public readonly AUTO = 've-sub'; + public readonly BOAT = 've-boat'; + public readonly POWERSPORTS = 've-power'; + public readonly TRAILER = 've-trailer'; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan.po.ts new file mode 100644 index 0000000..5e837fd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/pages/select-loan.po.ts @@ -0,0 +1,42 @@ +import {browser, by, element } from 'protractor'; + +export class SelectLoanPo { + + public PAGE_URL = '/'; + public readonly CREDIT_CARD_CARD = 'cc-card'; + public readonly HOME_EQUITY_CARD = 'he-card'; + public readonly VEHICLE_CARD = 've-card'; + public readonly PERSONAL_CARD = 'pe-card'; + public readonly PREVIOUS_BUTTON = 'previous'; + + + navigateTo() { + return browser.get(this.PAGE_URL); + } + + getHeaderBar() { + return element(by.css('lw-navigation')); + } + + getPageTitle() { + return browser.getTitle(); + } + + getTextByClassName(className: string) { + return element(by.className(className)).getText(); + } + + isElementEnabled(eId: string) { + return element(by.id(eId)).isEnabled(); + } + + clickCard(eId: string): void { + const card = element(by.id(eId)); + card.click(); + } + + goPrevious(): void { + const nextButton = element(by.id(this.PREVIOUS_BUTTON)); + nextButton.click(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/select-loan.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/select-loan.e2e-spec.ts new file mode 100644 index 0000000..1d03a77 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/select-loan/select-loan.e2e-spec.ts @@ -0,0 +1,107 @@ +import { SelectLoanPo } from './pages/select-loan.po'; +import { SelectLoanCreditPo } from './pages/select-loan-credit.po'; +import { browser } from 'protractor'; +import { SelectLoanVehiclePo } from './pages/select-loan-vehicle.po'; +import { SelectLoanHomePo } from './pages/select-loan-home.po'; +import { SelectLoanPersonalPo } from './pages/select-loan-personal.po'; +import { SelectLoanVehicleSubPo } from './pages/select-loan-vehicle-sub.po'; + +describe('Select Loan', () => { + +const landingPage = new SelectLoanPo(); + + it('should display landing page with correct text and cards', () => { + landingPage.navigateTo(); + expect(landingPage.getHeaderBar()).toBeDefined(); + const text = landingPage.getTextByClassName('loan-header'); + expect(text).toEqual('Start a new loan application'); + expect(landingPage.isElementEnabled(landingPage.CREDIT_CARD_CARD)).toEqual(true); + expect(landingPage.isElementEnabled(landingPage.HOME_EQUITY_CARD)).toEqual(true); + expect(landingPage.isElementEnabled(landingPage.PERSONAL_CARD)).toEqual(true); + expect(landingPage.isElementEnabled(landingPage.VEHICLE_CARD)).toEqual(true); + landingPage.clickCard(landingPage.CREDIT_CARD_CARD); + }); + + it('should display credit card landing page with correct text and cards', () => { + const page = new SelectLoanCreditPo(); + expect(page.getHeaderBar()).toBeDefined(); + const text = page.getTextByClassName('loan-header'); + expect(text).toEqual('Select a credit card type'); + expect(page.isElementEnabled(page.CASH_BACK)).toEqual(true); + expect(page.isElementEnabled(page.REWARDS)).toEqual(true); + expect(page.isElementEnabled(page.SECURED)).toEqual(true); + expect(page.isElementEnabled(page.STUDENT)).toEqual(true); + page.clickCard(page.CASH_BACK); + browser.navigate().back(); + page.clickCard(page.REWARDS); + browser.navigate().back(); + page.clickCard(page.SECURED); + browser.navigate().back(); + page.clickCard(page.STUDENT); + browser.navigate().back(); + page.goPrevious(); + landingPage.clickCard(landingPage.VEHICLE_CARD); + }); + + it('should display vehicle landing page with correct text and cards', () => { + const page = new SelectLoanVehiclePo(); + expect(page.getHeaderBar()).toBeDefined(); + const text = page.getTextByClassName('loan-header'); + expect(text).toEqual('Select a vehicle type'); + expect(page.isElementEnabled(page.BOAT)).toEqual(true); + expect(page.isElementEnabled(page.POWERSPORTS)).toEqual(true); + expect(page.isElementEnabled(page.TRAILER)).toEqual(true); + expect(page.isElementEnabled(page.AUTO)).toEqual(true); + page.clickCard(page.BOAT); + browser.navigate().back(); + page.clickCard(page.POWERSPORTS); + browser.navigate().back(); + page.clickCard(page.TRAILER); + browser.navigate().back(); + page.clickCard(page.AUTO); + }); + + it('should display vehicle sub landing page with correct text and cards', () => { + const page = new SelectLoanVehicleSubPo(); + expect(page.getHeaderBar()).toBeDefined(); + const text = page.getTextByClassName('loan-header'); + expect(text).toEqual('Select a vehicle type'); + expect(page.isElementEnabled(page.LOAN)).toEqual(true); + expect(page.isElementEnabled(page.PRE)).toEqual(true); + page.clickCard(page.LOAN); + browser.navigate().back(); + page.clickCard(page.PRE); + browser.navigate().back(); + page.goPrevious(); + page.goPrevious(); + landingPage.clickCard(landingPage.HOME_EQUITY_CARD); + }); + + it('should display home equity landing page with correct text and cards', () => { + const page = new SelectLoanHomePo(); + expect(page.getHeaderBar()).toBeDefined(); + const text = page.getTextByClassName('loan-header'); + expect(text).toEqual('Select a home equity type'); + expect(page.isElementEnabled(page.HOME_EQUITY)).toEqual(true); + expect(page.isElementEnabled(page.MANU_HOME)).toEqual(true); + expect(page.isElementEnabled(page.MORT_REFI)).toEqual(true); + expect(page.isElementEnabled(page.HELOC)).toEqual(true); + page.clickCard(page.HOME_EQUITY); + browser.navigate().back(); + page.clickCard(page.MANU_HOME); + browser.navigate().back(); + page.clickCard(page.MORT_REFI); + browser.navigate().back(); + page.clickCard(page.HELOC); + browser.navigate().back(); + page.goPrevious(); + landingPage.clickCard(landingPage.PERSONAL_CARD); + }); + + it('should start personal loan ', () => { + const page = new SelectLoanPersonalPo(); + expect(page.getHeaderBar()).toBeDefined(); + browser.navigate().back(); + expect(landingPage.getHeaderBar()).toBeDefined(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/data-driven/test-input-command.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/data-driven/test-input-command.ts new file mode 100644 index 0000000..3941a9e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/data-driven/test-input-command.ts @@ -0,0 +1,12 @@ + +export enum TestingInputType { + TEXT, SELECT, CHECK, RADIO +} + +export interface TestInputCommand { + fieldId: string; + fieldValue: any; + fieldType: TestingInputType; +} + +export type TestInputSet = Array; diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-data-tools.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-data-tools.ts new file mode 100644 index 0000000..3a069a0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-data-tools.ts @@ -0,0 +1,22 @@ + +export class PageDataTools { + + public static readonly numbersList = '1234567890'; + public static readonly alphabetList = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + public static createRandomEmail(startLength = 10, endLength = 10): string { + const begin = this.createRandomString(startLength); + const end = this.createRandomString(endLength); + const email = [begin, '@', end, '.com']; + return email.join(''); + } + + public static createRandomString(stringLength: number) { + const characters = this.alphabetList; + let result = ''; + for (let j = stringLength; j > 0; --j) { + result += characters[Math.floor(Math.random() * characters.length)]; + } + return result; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-object-tools.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-object-tools.ts new file mode 100644 index 0000000..ae674d1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/page-object-tools.ts @@ -0,0 +1,87 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { TestingInputType, TestInputCommand, TestInputSet } from './data-driven/test-input-command'; + +export class PageObjectTools { + + public static urlChanged(url): any { + return function () { + return browser.getCurrentUrl().then(function (actualUrl) { + return url !== actualUrl; + }); + }; + } + + public static fillFields(dataSet: TestInputSet, prefix?: string): void { + dataSet.forEach((command: TestInputCommand) => { + PageObjectTools.executeTestInputCommand(command, prefix); + }, this); + } + + static executeTestInputCommand(command: TestInputCommand, prefix?: string): any { + const fullId = prefix ? prefix + command.fieldId : command.fieldId; + const currentField = element(by.id(fullId)); + if (!currentField || !currentField.isDisplayed()) { + throw new Error('Couldn\'t execute command, field not found: ' + fullId); + } + switch (command.fieldType) { + default: + throw new Error('You must specify a field type for field Id: ' + fullId); + case TestingInputType.TEXT: + currentField.sendKeys(command.fieldValue); + break; + case TestingInputType.SELECT: + PageObjectTools.selectDropDownField(currentField, command.fieldValue); + break; + case TestingInputType.CHECK: + PageObjectTools.clickCheckBox(currentField); + break; + case TestingInputType.RADIO: + PageObjectTools.selectRadioButton(currentField, command.fieldValue); + break; + } + } + + public static findFieldsInPageByIdArray(fieldsIdSet: Array, prefix?: string) { + let foundField = null; + const usedPrefix = prefix || ''; + const allFields = []; + fieldsIdSet.forEach((fieldId, index) => { + foundField = element(by.id(usedPrefix + fieldId)); + if (foundField) { + allFields.push(foundField); + } + }); + return allFields; + } + + public static selectDropDownField(elementFinder: ElementFinder, optionNumber: number) { + const desiredOption = elementFinder.all(by.tagName('option')).get(optionNumber); + const EC = protractor.ExpectedConditions; + + browser.wait(EC.visibilityOf(desiredOption), 5000).then(() => { + desiredOption.click(); + }); + } + + public static selectRadioButton(elementFinder: ElementFinder, optionNumber: number) { + const desiredOption = elementFinder.all(by.tagName('label')).get(optionNumber); + const EC = protractor.ExpectedConditions; + + browser.wait(EC.visibilityOf(desiredOption), 5000).then(() => { + desiredOption.click(); + }); + } + + static clickCheckBox(checkbox: ElementFinder) { + browser.executeScript('arguments[0].click();', checkbox.getWebElement()); + } + + public static getElementsIdField(fieldId: string, prefix?: string): any { + const firstElement = element( by.id( prefix ? prefix + fieldId : fieldId)); + return firstElement.getAttribute( 'id' ); + } + + public static getFocusedElement(): any { + return browser.driver.switchTo().activeElement().getAttribute('id'); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/collateral-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/collateral-page.po.ts new file mode 100644 index 0000000..7031cea --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/collateral-page.po.ts @@ -0,0 +1,88 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestInputSet, TestingInputType } from '../data-driven/test-input-command'; + +export class CollateralPagePo { + + public readonly ESTIMATED_VALUE = '-inputEstimatedValue'; + public readonly PROPERTY_TYPE = '-inputPropertyType'; + public readonly HAS_FIRST_MORTGAGE = 'inputFirstMortgage'; + public readonly HAS_SECOND_MORTGAGE = 'inputSecondMortgage'; + public readonly PAYOFF_FIRST_MORTGAGE = 'payoffFirstMortgage'; + public readonly PAYOFF_SECOND_MORTGAGE = 'payoffSecondMortgage'; + public readonly IS_SAME_ADDRESS = 'isAddressSame'; + public readonly IS_BEING_IMPROVED = 'inputBeingImproved'; + public readonly IS_LISTED_FOR_SALE = 'inputListedForSale'; + // address information + public readonly ADDRESS_1 = '-inputAddress1'; + public readonly ADDRESS_2 = '-inputAddress2'; + public readonly CITY = '-inputCity'; + public readonly STATE = '-inputState'; + public readonly ZIP = '-inputZip'; + public readonly OCCUPANCY_TYPE = '-inputOccupancyType'; + + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [this.ESTIMATED_VALUE, this.PROPERTY_TYPE, + this.IS_SAME_ADDRESS, this.IS_BEING_IMPROVED, this.IS_LISTED_FOR_SALE, this.NEXT_BUTTON]; + public readonly ADDRESS_FIELDS = [ this.OCCUPANCY_TYPE, this.ADDRESS_1, this.ADDRESS_2, this.CITY, this.STATE, this.ZIP]; + public readonly LIEN_POSITION_FIELDS = [ this.HAS_FIRST_MORTGAGE, this.HAS_SECOND_MORTGAGE, this.PAYOFF_FIRST_MORTGAGE,this.PAYOFF_SECOND_MORTGAGE]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.IS_SAME_ADDRESS, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.IS_BEING_IMPROVED, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.IS_LISTED_FOR_SALE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.ESTIMATED_VALUE, fieldValue: '12345', fieldType: TestingInputType.TEXT }, + { fieldId: this.PROPERTY_TYPE, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.HAS_FIRST_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.HAS_SECOND_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.IS_SAME_ADDRESS, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.ADDRESS_1, fieldValue: '1000 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT }, + { fieldId: this.IS_BEING_IMPROVED, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.IS_LISTED_FOR_SALE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.OCCUPANCY_TYPE, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.ESTIMATED_VALUE, fieldValue: '12345', fieldType: TestingInputType.TEXT }, + { fieldId: this.PROPERTY_TYPE, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.HAS_FIRST_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.HAS_SECOND_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.PAYOFF_FIRST_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.CHECK }, + { fieldId: this.PAYOFF_SECOND_MORTGAGE, fieldValue: 0, fieldType: TestingInputType.CHECK }, + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement() { + // it focuses on the yes/no radio, not the overall input + return PageObjectTools.getElementsIdField('isSameYes'); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/contact-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/contact-page.po.ts new file mode 100644 index 0000000..0547162 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/contact-page.po.ts @@ -0,0 +1,125 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { PageDataTools } from '../page-data-tools'; +import { TestInputSet, TestingInputType } from '../data-driven/test-input-command'; + +export class ContactPagePo { + + public readonly FIRST_NAME = 'inputFirstName'; + public readonly MIDDLE_INITIAL = 'inputMiddleInitial'; + public readonly LAST_NAME = 'inputLastName'; + public readonly DOB = 'inputDOB'; + public readonly SUFFIX = 'inputSuffix'; + public readonly SSN = 'inputSSN'; + public readonly PRIMARY_PHONE = 'inputPrimaryPhoneNumber'; + public readonly PRIMARY_PHONE_TYPE = 'inputPrimaryPhoneType'; + public readonly SECONDARY_PHONE = 'inputSecondaryPhoneNumber'; + public readonly SECONDARY_PHONE_TYPE = 'inputSecondaryPhoneType'; + public readonly EMAIL = 'inputEmail'; + public readonly MARITAL_STATUS = 'inputMaritalStatus'; + public readonly CO_APPLICANT_SPOUSE = 'inputCoApplicantSpouse'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [ + this.FIRST_NAME, this.MIDDLE_INITIAL, this.LAST_NAME, this.DOB, this.SUFFIX, this.SSN, this.PRIMARY_PHONE, + this.PRIMARY_PHONE_TYPE, this.SECONDARY_PHONE, this.SECONDARY_PHONE_TYPE, this.EMAIL, this.NEXT_BUTTON]; + public readonly COAPP_FIELDS = [ ...this.ALL_FIELDS, this.MARITAL_STATUS, this.CO_APPLICANT_SPOUSE]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.FIRST_NAME, fieldValue: 'John', fieldType: TestingInputType.TEXT }, + { fieldId: this.MIDDLE_INITIAL, fieldValue: 'A', fieldType: TestingInputType.TEXT }, + { fieldId: this.LAST_NAME, fieldValue: 'DOE', fieldType: TestingInputType.TEXT }, + { fieldId: this.DOB, fieldValue: '01011999', fieldType: TestingInputType.TEXT }, + { fieldId: this.SSN, fieldValue: '111223333', fieldType: TestingInputType.TEXT }, + { fieldId: this.PRIMARY_PHONE, fieldValue: '8475551234', fieldType: TestingInputType.TEXT }, + { fieldId: this.EMAIL, fieldValue: PageDataTools.createRandomEmail(), fieldType: TestingInputType.TEXT }, + { fieldId: this.MARITAL_STATUS, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public readonly MINIMAL_DATA_NO_MARITAL: TestInputSet = [ + { fieldId: this.FIRST_NAME, fieldValue: 'John', fieldType: TestingInputType.TEXT }, + { fieldId: this.MIDDLE_INITIAL, fieldValue: 'A', fieldType: TestingInputType.TEXT }, + { fieldId: this.LAST_NAME, fieldValue: 'DOE', fieldType: TestingInputType.TEXT }, + { fieldId: this.DOB, fieldValue: '01011999', fieldType: TestingInputType.TEXT }, + { fieldId: this.SSN, fieldValue: '111223333', fieldType: TestingInputType.TEXT }, + { fieldId: this.PRIMARY_PHONE, fieldValue: '8475551234', fieldType: TestingInputType.TEXT }, + { fieldId: this.EMAIL, fieldValue: PageDataTools.createRandomEmail(), fieldType: TestingInputType.TEXT } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + ...this.MINIMAL_DATA, + { fieldId: this.SUFFIX, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.PRIMARY_PHONE_TYPE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.SECONDARY_PHONE, fieldValue: '8475561234', fieldType: TestingInputType.TEXT }, + { fieldId: this.SECONDARY_PHONE_TYPE, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_COAPP_DATA: TestInputSet = [ + { fieldId: this.FIRST_NAME, fieldValue: 'Jane', fieldType: TestingInputType.TEXT }, + { fieldId: this.MIDDLE_INITIAL, fieldValue: 'B', fieldType: TestingInputType.TEXT }, + { fieldId: this.LAST_NAME, fieldValue: 'DOE', fieldType: TestingInputType.TEXT }, + { fieldId: this.DOB, fieldValue: '02021998', fieldType: TestingInputType.TEXT }, + { fieldId: this.SUFFIX, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.SSN, fieldValue: '333223333', fieldType: TestingInputType.TEXT }, + { fieldId: this.PRIMARY_PHONE, fieldValue: '8475554321', fieldType: TestingInputType.TEXT }, + { fieldId: this.EMAIL, fieldValue: PageDataTools.createRandomEmail(), fieldType: TestingInputType.TEXT }, + { fieldId: this.PRIMARY_PHONE_TYPE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.SECONDARY_PHONE, fieldValue: '8475564321', fieldType: TestingInputType.TEXT }, + { fieldId: this.SECONDARY_PHONE_TYPE, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.MARITAL_STATUS, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_COAPP_DATA_PRIMARY: TestInputSet = [ + ...this.MAXIMUM_COAPP_DATA, + { fieldId: this.CO_APPLICANT_SPOUSE, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_PRIMARY_NOT_MARRIED_TO_COAPP: TestInputSet = [ + ...this.MAXIMUM_COAPP_DATA, + { fieldId: this.CO_APPLICANT_SPOUSE, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMinimalNoMarital(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA_NO_MARITAL); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + public fillFieldsMaximumCoApplicant(): void { + PageObjectTools.fillFields(this.MAXIMUM_COAPP_DATA_PRIMARY); + } + + public fillFieldsMaximumCoApplicantNonPrimary(): void { + PageObjectTools.fillFields(this.MAXIMUM_COAPP_DATA); + } + + public fillFieldsPrimaryMarriedButNotMarriedToCoApplicant(): void { + PageObjectTools.fillFields(this.MAXIMUM_PRIMARY_NOT_MARRIED_TO_COAPP); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement(prefix?: string) { + return PageObjectTools.getElementsIdField(this.FIRST_NAME, prefix); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/employment-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/employment-page.po.ts new file mode 100644 index 0000000..1fd5c08 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/employment-page.po.ts @@ -0,0 +1,137 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestingInputType, TestInputSet } from '../data-driven/test-input-command'; + +export class EmploymentPagePo { + + public readonly DEFAULT_PREFIX = ''; + public readonly PRIOR0_PREFIX = 'priorEmployer0-'; + + public readonly EMPLOYMENT_INDICATED = 'hasEmploymentIncome0'; + public readonly OTHER_INCOME_INDICATED = 'hasOtherIncome0'; + public readonly IS_PAYING_CHILD_SUPPORT = 'isPayingChildSupport'; + + public readonly EMPLOYER_NAME = 'inputEmployerName'; + public readonly JOB_TITLE = 'inputJobTitle'; + public readonly SALARY = 'inputSalary'; + public readonly OTHER_INCOME = 'inputOtherAnnualIncome'; + public readonly OTHER_INCOME_SOURCE = 'inputOtherAnnualIncomeSource'; + public readonly WORK_PHONE = 'inputWorkPhoneNumber'; + public readonly EMPLOYMENT_TYPE = 'inputEmploymentType'; + public readonly SCHEDULE = 'inputSchedule'; + public readonly EMPLOYMENT_LENGTH_YEARS = 'inputEmploymentLengthYears'; + public readonly EMPLOYMENT_LENGTH_MONTHS = 'inputEmploymentLengthMonths'; + public readonly CHILD_SUPPORT_AMOUNT = 'inputChildSupportAmount'; + public readonly WEEKLY_HOURS = 'inputWeeklyHours'; + public readonly NEXT_BUTTON = 'buttonNext'; + + public readonly STARTING_FIELDS = [this.EMPLOYMENT_INDICATED, this.OTHER_INCOME_INDICATED, this.IS_PAYING_CHILD_SUPPORT]; + public readonly EMPLOYMENT_FIELDS = [ + this.EMPLOYER_NAME, this.JOB_TITLE, this.SALARY, this.WORK_PHONE, this.EMPLOYMENT_TYPE, this.SCHEDULE, + this.EMPLOYMENT_LENGTH_YEARS, this.EMPLOYMENT_LENGTH_MONTHS]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.EMPLOYER_NAME, fieldValue: 'Acme Packaging', fieldType: TestingInputType.TEXT }, + { fieldId: this.JOB_TITLE, fieldValue: 'Acme Packer', fieldType: TestingInputType.TEXT }, + { fieldId: this.SALARY, fieldValue: 30000, fieldType: TestingInputType.TEXT }, + { fieldId: this.EMPLOYMENT_TYPE, fieldValue: 1, fieldType: TestingInputType.SELECT }, + { fieldId: this.SCHEDULE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.EMPLOYMENT_LENGTH_YEARS, fieldValue: 3, fieldType: TestingInputType.SELECT }, + { fieldId: this.EMPLOYMENT_LENGTH_MONTHS, fieldValue: 2, fieldType: TestingInputType.SELECT } + ]; + + public readonly SECOND_INCOME_DATA: TestInputSet = [ + { fieldId: this.EMPLOYER_NAME, fieldValue: 'Acme Delivery', fieldType: TestingInputType.TEXT }, + { fieldId: this.JOB_TITLE, fieldValue: 'Delivery Driver', fieldType: TestingInputType.TEXT }, + { fieldId: this.SALARY, fieldValue: 15, fieldType: TestingInputType.TEXT }, + { fieldId: this.EMPLOYMENT_TYPE, fieldValue: 1, fieldType: TestingInputType.SELECT }, + { fieldId: this.SCHEDULE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.EMPLOYMENT_LENGTH_YEARS, fieldValue: 3, fieldType: TestingInputType.SELECT }, + { fieldId: this.EMPLOYMENT_LENGTH_MONTHS, fieldValue: 2, fieldType: TestingInputType.SELECT } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + ...this.MINIMAL_DATA, + { fieldId: this.WORK_PHONE, fieldValue: 4148765309, fieldType: TestingInputType.TEXT }, + ]; + + public readonly OTHER_INCOME_DATA: TestInputSet = [ + { fieldId: this.OTHER_INCOME, fieldValue: 20000, fieldType: TestingInputType.TEXT }, + { fieldId: this.OTHER_INCOME_SOURCE, fieldValue: 'Stuff1', fieldType: TestingInputType.TEXT } + ]; + + public readonly CHILD_SUPPORT_DATA: TestInputSet = [ + { fieldId: this.CHILD_SUPPORT_AMOUNT, fieldValue: 200, fieldType: TestingInputType.TEXT } + ]; + + // Page hides everything by default + public showEmploymentFields(): void { + const showEmployment = element(by.id(this.EMPLOYMENT_INDICATED)); + PageObjectTools.clickCheckBox(showEmployment); + } + + public showOtherIncomeFields(): void { + const showOtherIncomeOne = element(by.id(this.OTHER_INCOME_INDICATED)); + PageObjectTools.clickCheckBox(showOtherIncomeOne); + } + + public toggleOtherIncomeGroupOne(): void { + const showOtherIncomeOne = element(by.id(this.OTHER_INCOME_INDICATED)); + PageObjectTools.clickCheckBox(showOtherIncomeOne); + } + + public clearOtherIncomeGroupOne(): void { + const clearOtherIncomeOneSource = element(by.id(this.OTHER_INCOME_SOURCE)); + const clearOtherIncomeOne = element(by.id(this.OTHER_INCOME)); + clearOtherIncomeOneSource.clear(); + clearOtherIncomeOne.clear(); + } + + public showChildSupportFields(): void { + const showChildSupport = element(by.id(this.IS_PAYING_CHILD_SUPPORT)); + PageObjectTools.clickCheckBox(showChildSupport); + } + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA, this.DEFAULT_PREFIX); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA, this.DEFAULT_PREFIX); + } + + public fillChildSupportFields(): void { + PageObjectTools.fillFields(this.CHILD_SUPPORT_DATA); + } + + public fillOtherIncomeFields(): void { + PageObjectTools.fillFields(this.OTHER_INCOME_DATA); + } + + /** + * Please note, this may need to be adjusted to match the current required employment length. + * As of 09/14/18, this isn't called... + */ + public markSingleIncomeNotEnough(): void { + PageObjectTools.executeTestInputCommand( + { fieldId: this.EMPLOYMENT_LENGTH_YEARS, fieldValue: 1, fieldType: TestingInputType.SELECT }, this.DEFAULT_PREFIX); + } + + public fillSecondIncome(): void { + PageObjectTools.fillFields(this.SECOND_INCOME_DATA, this.PRIOR0_PREFIX); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/hmda-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/hmda-page.po.ts new file mode 100644 index 0000000..85f4b69 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/hmda-page.po.ts @@ -0,0 +1,122 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestInputSet, TestingInputType } from '../data-driven/test-input-command'; + +export class HmdaPagePo { + + public readonly DEFAULT_PREFIX = 'applicant-'; + // race information + public readonly AMERICAN_INDIAN_ALASKA_NATIVE = 'inputAmericanIndianAlaskaNative'; + public readonly AMERICAN_INDIAN_ALASKA_NATIVE_TEXT = 'inputAmericanIndianAlaskaNativeText'; + public readonly BLACK_OR_AFRICAN_AMERICAN = 'inputBlackOrAfricanAmerican'; + public readonly ASIAN = 'inputAsian'; + public readonly ASIAN_INDIAN = 'inputAsianIndian'; + public readonly CHINESE = 'inputChinese'; + public readonly FILIPINO = 'inputFilipino'; + public readonly JAPANESE = 'inputJapanese'; + public readonly KOREAN = 'inputKorean'; + public readonly VIETNAMESE = 'inputVietnamese'; + public readonly OTHER_ASIAN = 'inputOtherAsian'; + public readonly OTHER_ASIAN_TEXT = 'inputOtherAsianText'; + public readonly HAWAIIAN = 'inputHawaiian'; + public readonly NATIVE_HAWAIIAN = 'inputNativeHawaiian'; + public readonly GUAMANIAN_CHAMORRO = 'inputNativeGuamanianChamorro'; + public readonly SAMOAN = 'inputNativeSamoan'; + public readonly OTHER_PACIFIC_ISLANDER = 'inputOtherPacificIslander'; + public readonly OTHER_PACIFIC_ISLANDER_TEXT = 'inputOtherPacificIslanderText'; + public readonly WHITE = 'inputWhite'; + public readonly RACE_DID_NOT_PROVIDE = 'inputRaceDidNotProvide'; + // ethnicity information + public readonly HISPANIC_OR_LATINO = 'inputHispanicOrLatino'; + public readonly MEXICAN = 'inputMexican'; + public readonly PUERTO_RICAN = 'inputPuertoRican'; + public readonly CUBAN = 'inputCuban'; + public readonly OTHER_HISPANIC_LATINO = 'inputOtherHispanicLatino'; + public readonly OTHER_HISPANIC_LATINO_TEXT = 'inputOtherHispanicLatinoText'; + public readonly NOT_HISPANIC_OR_LATINO = 'inputNotHispanicOrLatino'; + public readonly ETHNICITY_DID_NOT_PROVIDE = 'inputEthnicityDidNotProvide'; + // gender information + public readonly FEMALE = 'inputFemale'; + public readonly MALE = 'inputMale'; + public readonly GENDER_DID_NOT_PROVIDE = 'inputGenderDidNotProvide'; + + public readonly NEXT_BUTTON = 'buttonNext'; + + public readonly GENDER_FIELDS = [ + this.MALE, this.FEMALE, this.GENDER_DID_NOT_PROVIDE + ]; + + public readonly RACE_FIELDS = [ + this.AMERICAN_INDIAN_ALASKA_NATIVE, this.AMERICAN_INDIAN_ALASKA_NATIVE_TEXT, this.BLACK_OR_AFRICAN_AMERICAN, this.ASIAN, + this.ASIAN_INDIAN, this.CHINESE, this.FILIPINO, this.JAPANESE, this.KOREAN, this.VIETNAMESE, this.OTHER_ASIAN, this.OTHER_ASIAN_TEXT, + this.HAWAIIAN, this.NATIVE_HAWAIIAN, this.GUAMANIAN_CHAMORRO, this.SAMOAN, this.OTHER_PACIFIC_ISLANDER, + this.WHITE, this.RACE_DID_NOT_PROVIDE + ]; + + public readonly ETHNICITY_FIELDS = [ + this.HISPANIC_OR_LATINO, this.MEXICAN, this.PUERTO_RICAN, this.CUBAN, this.OTHER_HISPANIC_LATINO, + this.OTHER_HISPANIC_LATINO_TEXT, this.NOT_HISPANIC_OR_LATINO, this.ETHNICITY_DID_NOT_PROVIDE, + ]; + + public readonly ALL_FIELDS = [ + ...this.RACE_FIELDS, ...this.GENDER_FIELDS, ...this.ETHNICITY_FIELDS, this.NEXT_BUTTON + ]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.RACE_DID_NOT_PROVIDE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.ETHNICITY_DID_NOT_PROVIDE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.GENDER_DID_NOT_PROVIDE, fieldValue: 1, fieldType: TestingInputType.CHECK } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + ...this.MINIMAL_DATA, + { fieldId: this.AMERICAN_INDIAN_ALASKA_NATIVE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.AMERICAN_INDIAN_ALASKA_NATIVE_TEXT, fieldValue: 'Enrolled principal or tribe', fieldType: TestingInputType.TEXT }, + { fieldId: this.BLACK_OR_AFRICAN_AMERICAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.ASIAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.ASIAN_INDIAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.CHINESE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.FILIPINO, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.JAPANESE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.KOREAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.VIETNAMESE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_ASIAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_ASIAN_TEXT, fieldValue: 'Hmong, Laotian, Thai,Pakistani, Cambodian', fieldType: TestingInputType.TEXT }, + { fieldId: this.HAWAIIAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.NATIVE_HAWAIIAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.GUAMANIAN_CHAMORRO, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.SAMOAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_PACIFIC_ISLANDER, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_PACIFIC_ISLANDER_TEXT, fieldValue: 'Fijian, Tongan', fieldType: TestingInputType.TEXT }, + { fieldId: this.WHITE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.HISPANIC_OR_LATINO, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.MEXICAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.PUERTO_RICAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.CUBAN, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_HISPANIC_LATINO, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.OTHER_HISPANIC_LATINO_TEXT, fieldValue: 'Argentinean,Colombian, Dominican', fieldType: TestingInputType.TEXT }, + { fieldId: this.NOT_HISPANIC_OR_LATINO, fieldValue: 1, fieldType: TestingInputType.CHECK }, + { fieldId: this.MALE, fieldValue: 1, fieldType: TestingInputType.CHECK }, + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA , this.DEFAULT_PREFIX); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA , this.DEFAULT_PREFIX); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/residence-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/residence-page.po.ts new file mode 100644 index 0000000..57850ff --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/residence-page.po.ts @@ -0,0 +1,143 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestingInputType, TestInputSet } from '../data-driven/test-input-command'; + +export class ResidencePagePo { + + public readonly DEFAULT_PREFIX = 'addressInfo-'; + public readonly MAILING_PREFIX = 'mailingInfo-'; + public readonly PRIOR_PREFIX = '0-'; + public readonly COAPP_PREFIX = '-'; + + public readonly ADDRESS_LINE_1 = 'inputAddress1'; + public readonly ADDRESS_LINE_2 = 'inputAddress2'; + public readonly CITY = 'inputCity'; + public readonly STATE = 'inputState'; + public readonly ZIP = 'inputZip'; + public readonly RESIDENCE_STATUS = 'inputResidenceStatus'; + public readonly RESIDENCE_MONTHLY_PAYMENT = 'inputResidenceMonthlyPayment'; + public readonly RESIDENCE_TIME_YEARS = 'inputResidenceTimeYears'; + public readonly RESIDENCE_TIME_MONTHS = 'inputResidenceTimeMonths'; + public readonly MAILING_SAME = 'isMailingSameYes'; + public readonly IS_ADDRESS_SAME = 'isAddressSame'; + public readonly NEXT_BUTTON = 'buttonNext'; + + public readonly ALL_FIELDS = [ + this.ADDRESS_LINE_1, this.ADDRESS_LINE_2, this.CITY, this.STATE, this.ZIP, this.RESIDENCE_STATUS, + this.RESIDENCE_MONTHLY_PAYMENT, this.RESIDENCE_TIME_YEARS, this.RESIDENCE_TIME_MONTHS, this.MAILING_SAME + ]; + public readonly MAILING_FIELDS = [ + this.ADDRESS_LINE_1, this.ADDRESS_LINE_2, this.CITY, this.STATE, this.ZIP + ]; + public readonly COAPP_FIELDS = [ + ...this.ALL_FIELDS + ]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.ADDRESS_LINE_1, fieldValue: '999 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_MONTHLY_PAYMENT, fieldValue: 1500, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_STATUS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_YEARS, fieldValue: 4, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_MONTHS, fieldValue: 4, fieldType: TestingInputType.SELECT } + ]; + + public readonly MINIMAL_DATA_IN_WI: TestInputSet = [ + { fieldId: this.ADDRESS_LINE_1, fieldValue: '999 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_MONTHLY_PAYMENT, fieldValue: 1500, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_STATUS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_YEARS, fieldValue: 4, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_MONTHS, fieldValue: 4, fieldType: TestingInputType.SELECT } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.ADDRESS_LINE_1, fieldValue: '1000 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_MONTHLY_PAYMENT, fieldValue: 1500, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_STATUS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_YEARS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_MONTHS, fieldValue: 2, fieldType: TestingInputType.SELECT } + ]; + + public readonly MAILING_DATA: TestInputSet = [ + { fieldId: this.ADDRESS_LINE_1, fieldValue: '111 North 7th St.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53215, fieldType: TestingInputType.TEXT } + ]; + + public readonly PRIOR_RESIDENCE_DATA: TestInputSet = [ + { fieldId: this.ADDRESS_LINE_1, fieldValue: '1000 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT }, + { fieldId: this.RESIDENCE_STATUS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_YEARS, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.RESIDENCE_TIME_MONTHS, fieldValue: 2, fieldType: TestingInputType.SELECT } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA, this.DEFAULT_PREFIX); + } + + public showMailingAddress(): void { + const mailingIsSameCheck = element(by.id(this.DEFAULT_PREFIX + this.MAILING_SAME)); + PageObjectTools.clickCheckBox(mailingIsSameCheck); + } + + public showSecondAddress(): void { + PageObjectTools.executeTestInputCommand( + { fieldId: this.RESIDENCE_TIME_YEARS, fieldValue: 2, fieldType: TestingInputType.SELECT }, this.DEFAULT_PREFIX); + } + + public markAddressNotSame(): void { + PageObjectTools.executeTestInputCommand( + { fieldId: this.IS_ADDRESS_SAME, fieldValue: 1, fieldType: TestingInputType.RADIO }); + } + + public markAddressSame(): void { + PageObjectTools.executeTestInputCommand( + { fieldId: this.IS_ADDRESS_SAME, fieldValue: 0, fieldType: TestingInputType.RADIO }); + } + + public fillFieldsCoApplicant(): void { + PageObjectTools.fillFields(this.MAILING_DATA, this.COAPP_PREFIX); + } + + public fillAdditionalFieldsMaximum(): void { + PageObjectTools.fillFields(this.PRIOR_RESIDENCE_DATA, this.PRIOR_PREFIX); + PageObjectTools.fillFields(this.MAILING_DATA, this.MAILING_PREFIX); + } + + public fillFieldsMinimalLivingInWI(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA_IN_WI, this.DEFAULT_PREFIX); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement(prefix?: string) { + return PageObjectTools.getElementsIdField(this.ADDRESS_LINE_1, prefix); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/spouse-type-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/spouse-type-page.po.ts new file mode 100644 index 0000000..248f107 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/spouse-type-page.po.ts @@ -0,0 +1,41 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestingInputType, TestInputSet } from '../data-driven/test-input-command'; + +export class SpouseTypePagePo { + + public readonly IS_FULL_INFO_SPOUSE = 'inputIsFullInfoSpouse'; + public readonly NEXT_BUTTON = 'buttonNext'; + + public readonly STARTING_FIELDS = [this.IS_FULL_INFO_SPOUSE]; + + public readonly FULL_INFO_SPOUSE_DATA: TestInputSet = [ + { fieldId: this.IS_FULL_INFO_SPOUSE, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly TATTLE_SPOUSE_DATA: TestInputSet = [ + { fieldId: this.IS_FULL_INFO_SPOUSE, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsFullInfoSpouse(): void { + PageObjectTools.fillFields(this.FULL_INFO_SPOUSE_DATA); + } + + public fillFieldsTattleSpouse(): void { + PageObjectTools.fillFields(this.TATTLE_SPOUSE_DATA); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/summary-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/summary-page.po.ts new file mode 100644 index 0000000..5e82ee2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/summary-page.po.ts @@ -0,0 +1,34 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { PageDataTools } from '../page-data-tools'; +import { TestInputSet, TestingInputType } from '../data-driven/test-input-command'; + +export class SummaryPagePo { + + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly NOTE_FIELD = 'inputNote'; + public readonly PRIMARY_CONTACT = 'inputContact'; + public readonly PROMO_CODE = 'inputPromoCode'; + public readonly ALL_FIELDS = [this.NOTE_FIELD, this.NEXT_BUTTON]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.PRIMARY_CONTACT, fieldValue: 1, fieldType: TestingInputType.SELECT }, + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/tattle-spouse-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/tattle-spouse-page.po.ts new file mode 100644 index 0000000..f43bfa0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/tattle-spouse-page.po.ts @@ -0,0 +1,75 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestingInputType, TestInputSet } from '../data-driven/test-input-command'; + +export class TattleSpousePagePo { + + public readonly FIRST_NAME = 'inputFirstName'; + public readonly MIDDLE_INITIAL = 'inputMiddleInitial'; + public readonly LAST_NAME = 'inputLastName'; + + public readonly ADDRESS_LINE_1 = 'inputAddress1'; + public readonly ADDRESS_LINE_2 = 'inputAddress2'; + public readonly CITY = 'inputCity'; + public readonly STATE = 'inputState'; + public readonly ZIP = 'inputZip'; + + public readonly IS_ADDRESS_SAME = 'isAddressSame'; + public readonly NEXT_BUTTON = 'buttonNext'; + + public readonly ALL_FIELDS = [ + this.ADDRESS_LINE_1, this.ADDRESS_LINE_2, this.CITY, this.STATE, this.ZIP, this.IS_ADDRESS_SAME, + this.FIRST_NAME, this.MIDDLE_INITIAL, this.LAST_NAME + ]; + public readonly MAILING_FIELDS = [ + this.ADDRESS_LINE_1, this.ADDRESS_LINE_2, this.CITY, this.STATE, this.ZIP + ]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.FIRST_NAME, fieldValue: 'Bob', fieldType: TestingInputType.TEXT }, + { fieldId: this.MIDDLE_INITIAL, fieldValue: 'B', fieldType: TestingInputType.TEXT }, + { fieldId: this.LAST_NAME, fieldValue: 'Uecker', fieldType: TestingInputType.TEXT }, + { fieldId: this.IS_ADDRESS_SAME, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.FIRST_NAME, fieldValue: 'Bob', fieldType: TestingInputType.TEXT }, + { fieldId: this.MIDDLE_INITIAL, fieldValue: 'B', fieldType: TestingInputType.TEXT }, + { fieldId: this.LAST_NAME, fieldValue: 'Uecker', fieldType: TestingInputType.TEXT }, + { fieldId: this.IS_ADDRESS_SAME, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.ADDRESS_LINE_1, fieldValue: '1000 South 8th Pl.', fieldType: TestingInputType.TEXT }, + { fieldId: this.CITY, fieldValue: 'Codeville', fieldType: TestingInputType.TEXT }, + { fieldId: this.STATE, fieldValue: 40, fieldType: TestingInputType.SELECT }, + { fieldId: this.ZIP, fieldValue: 53207, fieldType: TestingInputType.TEXT } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement(prefix?: string) { + return PageObjectTools.getElementsIdField(this.ADDRESS_LINE_1, prefix); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/vehicle-collateral-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/vehicle-collateral-page.po.ts new file mode 100644 index 0000000..8224ac1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/vehicle-collateral-page.po.ts @@ -0,0 +1,93 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../page-object-tools'; +import { TestInputSet, TestingInputType } from '../data-driven/test-input-command'; + +export class VehicleCollateralPagePo { + + // text input fields + public readonly VALUE = 'inputValue'; + public readonly YEAR = 'inputYear'; + public readonly MAKE = 'inputMake'; + public readonly MODEL = 'inputModel'; + public readonly MILEAGE = 'inputMileage'; + // drop down menus + public readonly TERM = 'inputTerm'; + public readonly CONDITION = 'inputCondition'; + public readonly LOAN_PURPOSE = 'inputLoanPurpose'; + // radio/pill buttons + public readonly TYPE = 'inputType'; + public readonly SELLER = 'inputSeller'; + + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [this.VALUE, this.CONDITION, this.YEAR, + this.MAKE, this.MODEL, this.MILEAGE, this.TERM, this.LOAN_PURPOSE, this.TYPE, this.SELLER, this.NEXT_BUTTON]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.VALUE, fieldValue: '12345', fieldType: TestingInputType.TEXT }, + { fieldId: this.TYPE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.CONDITION, fieldValue: 1, fieldType: TestingInputType.SELECT }, + { fieldId: this.YEAR, fieldValue: '2018', fieldType: TestingInputType.TEXT }, + { fieldId: this.MAKE, fieldValue: 'Subaru', fieldType: TestingInputType.TEXT }, + { fieldId: this.MODEL, fieldValue: 'Outback', fieldType: TestingInputType.TEXT }, + { fieldId: this.MILEAGE, fieldValue: '17320', fieldType: TestingInputType.TEXT }, + { fieldId: this.TERM, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.LOAN_PURPOSE, fieldValue: 1, fieldType: TestingInputType.SELECT } + ]; + + public readonly VEHICLE_SELLER_INFO: TestInputSet = [ + { fieldId: this.SELLER, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.VALUE, fieldValue: '12345', fieldType: TestingInputType.TEXT }, + { fieldId: this.TYPE, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.CONDITION, fieldValue: 1, fieldType: TestingInputType.SELECT }, + { fieldId: this.YEAR, fieldValue: '2018', fieldType: TestingInputType.TEXT }, + { fieldId: this.MAKE, fieldValue: 'Subaru', fieldType: TestingInputType.TEXT }, + { fieldId: this.MODEL, fieldValue: 'Outback', fieldType: TestingInputType.TEXT }, + { fieldId: this.MILEAGE, fieldValue: '17320', fieldType: TestingInputType.TEXT }, + { fieldId: this.TERM, fieldValue: 2, fieldType: TestingInputType.SELECT }, + { fieldId: this.LOAN_PURPOSE, fieldValue: 2, fieldType: TestingInputType.SELECT }, + ...this.VEHICLE_SELLER_INFO + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public canClickDropdown(): promise.Promise { + const dropdown = element(by.id(this.TERM)); + + if (dropdown) { + return dropdown.isEnabled(); + } + throw new Error('Couldn\'t click the dropbox'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement() { + return PageObjectTools.getElementsIdField(this.VALUE); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/wizard-page.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/wizard-page.po.ts new file mode 100644 index 0000000..2ad51de --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/pages/wizard-page.po.ts @@ -0,0 +1,35 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; + +export class WizardPagePo { + + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly PREVIOUS_BUTTON = 'buttonPrevious'; + public readonly ALL_FIELDS = [this.PREVIOUS_BUTTON, this.NEXT_BUTTON]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public canGoPrevious(): promise.Promise { + const previousButton = element(by.id(this.PREVIOUS_BUTTON)); + if (previousButton) { + return previousButton.isEnabled(); + } + throw new Error('Couldn\'t get previous button'); + } + + public goNext(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public goPrevious(): void { + const nextButton = element(by.id(this.PREVIOUS_BUTTON)); + nextButton.click(); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/shared-app.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/shared-app.po.ts new file mode 100644 index 0000000..fea6c16 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/shared/shared-app.po.ts @@ -0,0 +1,91 @@ +import {browser, by, element, ElementFinder, protractor} from 'protractor'; +import { MaskingService } from 'src/app/shared/masking/masking.service'; + +export interface ElementIndexer { + [key: string]: ElementFinder; +} + +export class SharedAppPo { + public numbersList = '1234567890'; + public alphabetList = 'abcdfghijklmnopqrstuvwxyzABCDFGHIJKLMNOPQRSTUVWXYZ'; + public emptyPhone = MaskingService.emptyPhone; + + navigateTo() { + return browser.get('/'); + } + + navigateToSpecific(location: string) { + return browser.get(location); + } + + getHeaderBar() { + return element(by.css('lw-navigation')); + } + + getInputById(id: string): ElementFinder { + return element(by.id(id)); + } + + createRandomString(stringLength: number) { + const characters = this.alphabetList; + let result = ''; + for (let j = stringLength; j > 0; --j) { + result += characters[Math.floor(Math.random() * characters.length)]; + } + return result; + } + + createRandomNumber(numberLength: number) { + const numbers = this.numbersList; + let result = ''; + for (let j = numberLength; j > 0; --j) { + const nextNum = numbers[Math.floor(Math.random() * numbers.length)]; + if (nextNum !== '0') { + result += numbers[Math.floor(Math.random() * numbers.length)]; + } else { + j++; + } + } + return result; + } + + createRandomEmail(startLength = 10, endLength = 10): string { + const begin = this.createRandomString(startLength); + const end = this.createRandomString(endLength); + const email = [begin, '@', end, '.com']; + return email.join(''); + } + + getPageTitle() { + return browser.getTitle(); + } + + // used mainly in demos / development + sleep(time: number) { + browser.sleep(time); + } + + makeSSN(inputValue) { + return MaskingService.makeSSN(inputValue); + } + + makePhone(inputValue) { + return MaskingService.makePhone(inputValue); + } + + makeDOB(inputValue) { + return MaskingService.makeDOB(inputValue); + } + + makeNumber(inputValue) { + return MaskingService.makeNumber(inputValue); + } + + selectDropDownField(elementFinder: ElementFinder, optionNumber: number) { + const desiredOption = elementFinder.all(by.tagName('option')).get(optionNumber); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(desiredOption), 5000).then(() => { + desiredOption.click(); + }); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-disclosures.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-disclosures.po.ts new file mode 100644 index 0000000..9df316b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-disclosures.po.ts @@ -0,0 +1,34 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; + +export class SignatureDisclosuresPagePo { + + public readonly PAGE_URL = '/loan-wizard/sl/pe/disclosures'; + + public readonly DISCLOSE_TRUTHFUL = 'truthful'; + public readonly DISCLOSE_CONTACT = 'contact'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly ALL_FIELDS = [ + this.DISCLOSE_TRUTHFUL, this.DISCLOSE_CONTACT, + this.NEXT_BUTTON]; + + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsMinimal(): void { + const truthful = element(by.id(this.DISCLOSE_TRUTHFUL)); + PageObjectTools.clickCheckBox(truthful); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-one.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-one.po.ts new file mode 100644 index 0000000..ff8097d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/pages/page-one.po.ts @@ -0,0 +1,92 @@ +import { by, element, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; +import { TestInputSet, TestingInputType } from '../../shared/data-driven/test-input-command'; + +export class SignaturePageOnePo { + + public readonly PAGE_URL = '/loan-wizard/sl/pe/step-1'; + + public readonly LIMIT_REQUESTED = 'inputLimitRequested'; + public readonly PURPOSE_OF_LOAN_SIGNATURE = 'inputPurposeOfLoanSignature'; + public readonly ANOTHER_PERSON = 'inputApplicantType'; + public readonly LIVE_IN_WI = 'inputLivesInMaritalPropertyState'; + public readonly JOINT_CREDIT = 'inputConfirmJointCredit'; + public readonly SELECT_TERM = 'inputTerm'; + public readonly CO_APP_LIVE_IN_WI = 'inputCoAppLivesInMaritalPropertyState'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly MODAL_CLOSE_BUTTON = 'modalCloseButton'; + public readonly MODAL_OKAY_BUTTON = 'modalOkayButton'; + public readonly ALL_FIELDS = [this.SELECT_TERM, this.LIMIT_REQUESTED, this.ANOTHER_PERSON, this.LIVE_IN_WI, this.NEXT_BUTTON, this.PURPOSE_OF_LOAN_SIGNATURE]; + public readonly HIDDEN_FIELDS_1 = [this.JOINT_CREDIT, this.CO_APP_LIVE_IN_WI]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 1234, fieldType: TestingInputType.TEXT }, + { fieldId: this.PURPOSE_OF_LOAN_SIGNATURE, fieldValue: "Other", fieldType: TestingInputType.TEXT}, + { fieldId: this.ANOTHER_PERSON, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.SELECT_TERM, fieldValue: 1, fieldType: TestingInputType.SELECT } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 1234, fieldType: TestingInputType.TEXT }, + { fieldId: this.PURPOSE_OF_LOAN_SIGNATURE, fieldValue: "Other", fieldType: TestingInputType.TEXT}, + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.CO_APP_LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.SELECT_TERM, fieldValue: 1, fieldType: TestingInputType.SELECT } + ]; + + public readonly JOINT_CREDIT_DATA: TestInputSet = [ + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public canClickDropdown(): promise.Promise { + const dropdown = element(by.id(this.SELECT_TERM)); + + if (dropdown) { + return dropdown.isEnabled(); + } + throw new Error('Couldn\'t click the dropbox'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsForFailureToConfirmJointCredit(): void { + PageObjectTools.fillFields(this.JOINT_CREDIT_DATA); + } + + public dismissModalByOkayButton(): void { + const modalOkayButton = element(by.id(this.MODAL_OKAY_BUTTON)); + modalOkayButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement() { + return PageObjectTools.getElementsIdField(this.LIMIT_REQUESTED); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-full-spouses.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-full-spouses.e2e-spec.ts new file mode 100644 index 0000000..e8073c1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-full-spouses.e2e-spec.ts @@ -0,0 +1,156 @@ +import { browser } from 'protractor'; +import { SignaturePageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { SignatureDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SpouseTypePagePo } from '../shared/pages/spouse-type-page.po'; + +describe('It should create a signature loan application for 4 full info individuals', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new SignaturePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canClickDropdown()).toEqual(false); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.A (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsPrimaryMarriedButNotMarriedToCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.A (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.A (Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept FULL INFO SPOUSE input for Applicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsFullInfoSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.B (Applicant\'s Spouse Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalNoMarital(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.B (Applicant\'s Spouse Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressSame(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.B (Applicant\'s Spouse Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.C (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.C (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.C (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept FULL INFO SPOUSE input for CoApplicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsFullInfoSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.D (CoApplicant\'s Spouse Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalNoMarital(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.D (CoApplicant\'s Spouse Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressSame(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.D (CoApplicant\'s Spouse Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new SignatureDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-go-back.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-go-back.e2e-spec.ts new file mode 100644 index 0000000..eb43978 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-go-back.e2e-spec.ts @@ -0,0 +1,127 @@ +import { browser } from 'protractor'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { WizardPagePo } from '../shared/pages/wizard-page.po'; +import { SignaturePageOnePo } from './pages/page-one.po'; +import { SignatureDisclosuresPagePo } from './pages/page-disclosures.po'; + +describe('It should allow free navigation of a completed signature loan application', () => { + + it('should accept maximum inputs for page 1.', () => { + const page = new SignaturePageOnePo(); + browser.get(page.PAGE_URL); + expect(page.canClickDropdown()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + page.showEmploymentFields(); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + // Will no longer happen. + // expect(page.canGoNext()).toEqual(false); + // page.fillSecondIncome(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new SignatureDisclosuresPagePo(); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + + it('should jump back to the beginning.', () => { + const urlList = [ + 'http://localhost:4200/loan-wizard/sl/pe/co-applicant-financial', + 'http://localhost:4200/loan-wizard/sl/pe/co-applicant-residential', + 'http://localhost:4200/loan-wizard/sl/pe/co-applicant-personal', + 'http://localhost:4200/loan-wizard/sl/pe/step-4', + 'http://localhost:4200/loan-wizard/sl/pe/step-3', + 'http://localhost:4200/loan-wizard/sl/pe/step-2', + 'http://localhost:4200/loan-wizard/sl/pe/step-1' + ]; + const page = new WizardPagePo(); + urlList.forEach((eachUrl) => { + expect(page.canGoPrevious()).toEqual(true); + page.goPrevious(); + browser.getCurrentUrl().then((newUrl) => { + expect(newUrl).toEqual(eachUrl); + }); + expect(page.canGoNext()).toEqual(true); + + }, this); + + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-maximum.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-maximum.e2e-spec.ts new file mode 100644 index 0000000..aac3cd9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-maximum.e2e-spec.ts @@ -0,0 +1,141 @@ +import { browser } from 'protractor'; +import { SignaturePageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SignatureDisclosuresPagePo } from './pages/page-disclosures.po'; +import { protractor } from 'protractor/built/ptor'; + +describe('It should create a maximum signature loan application ', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new SignaturePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canClickDropdown()).toEqual(false); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.COAPP_FIELDS); + expect(inputArray.length).toEqual(page.COAPP_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new SignatureDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-minimal.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-minimal.e2e-spec.ts new file mode 100644 index 0000000..2ebb1f1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-minimal.e2e-spec.ts @@ -0,0 +1,78 @@ +import { browser } from 'protractor'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SignaturePageOnePo } from './pages/page-one.po'; +import { SignatureDisclosuresPagePo } from './pages/page-disclosures.po'; + +describe('It should create a minimal signature loan application ', () => { + + it('should accept basic inputs for page 1 (Member/Loan Type)', () => { + const page = new SignaturePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + expect(page.canClickDropdown()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.fillFieldsForFailureToConfirmJointCredit(); + expect(page.canGoNext()).toEqual(false); + page.dismissModalByOkayButton(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 5 (Disclosures)', () => { + const page = new SignatureDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-tattle-spouses.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-tattle-spouses.e2e-spec.ts new file mode 100644 index 0000000..e4de95e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/signature/signature-tattle-spouses.e2e-spec.ts @@ -0,0 +1,132 @@ +import { browser } from 'protractor'; +import { SignaturePageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { SignatureDisclosuresPagePo } from './pages/page-disclosures.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { SpouseTypePagePo } from '../shared/pages/spouse-type-page.po'; +import { TattleSpousePagePo } from '../shared/pages/tattle-spouse-page.po'; + +describe('It should create a signature loan application for 2 full info individuals + 2 tattle spouses', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new SignaturePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canClickDropdown()).toEqual(false); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 2.A (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsPrimaryMarriedButNotMarriedToCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.A (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.A (Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept TATTLE SPOUSE input for Applicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsTattleSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for tattle spouse page (Applicant\'s Spouse Tattle Info)', () => { + const page = new TattleSpousePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept maximum inputs for page 2.C (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 3.C (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimalLivingInWI(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for page 4.C (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept TATTLE SPOUSE input for CoApplicant spouse type page', () => { + const page = new SpouseTypePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsTattleSpouse(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept minimum inputs for tattle spouse page (CoApplicant\'s Spouse Tattle Info)', () => { + const page = new TattleSpousePagePo(); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should maximum inputs for page 5 (Disclosures)', () => { + const page = new SignatureDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/pages/page-one.po.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/pages/page-one.po.ts new file mode 100644 index 0000000..b335c89 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/pages/page-one.po.ts @@ -0,0 +1,77 @@ +import { browser, by, element, ElementFinder, protractor, promise } from 'protractor'; +import { PageObjectTools } from '../../shared/page-object-tools'; +import { TestInputSet, TestInputCommand, TestingInputType } from '../../shared/data-driven/test-input-command'; + +export class VehiclePageOnePo { + + public readonly PAGE_URL = '/loan-wizard/ve/auto/step-1'; + + public readonly LIMIT_REQUESTED = 'inputLimitRequested'; + public readonly ANOTHER_PERSON = 'inputApplicantType'; + public readonly LIVE_IN_WI = 'inputLivesInMaritalPropertyState'; + public readonly JOINT_CREDIT = 'inputConfirmJointCredit'; + public readonly CO_APP_LIVE_IN_WI = 'inputCoAppLivesInMaritalPropertyState'; + public readonly NEXT_BUTTON = 'buttonNext'; + public readonly MODAL_CLOSE_BUTTON = 'modalCloseButton'; + public readonly MODAL_OKAY_BUTTON = 'modalOkayButton'; + public readonly ALL_FIELDS = [this.LIMIT_REQUESTED, this.ANOTHER_PERSON, this.LIVE_IN_WI, this.NEXT_BUTTON]; + public readonly HIDDEN_FIELDS_1 = [this.JOINT_CREDIT, this.CO_APP_LIVE_IN_WI]; + + public readonly MINIMAL_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 54321, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 1, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public readonly MAXIMUM_DATA: TestInputSet = [ + { fieldId: this.LIMIT_REQUESTED, fieldValue: 54321, fieldType: TestingInputType.TEXT }, + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.CO_APP_LIVE_IN_WI, fieldValue: 0, fieldType: TestingInputType.RADIO } + ]; + + public readonly JOINT_CREDIT_DATA: TestInputSet = [ + { fieldId: this.ANOTHER_PERSON, fieldValue: 0, fieldType: TestingInputType.RADIO }, + { fieldId: this.JOINT_CREDIT, fieldValue: 1, fieldType: TestingInputType.RADIO } + ]; + + public canGoNext(): promise.Promise { + const nextButton = element(by.id(this.NEXT_BUTTON)); + if (nextButton) { + return nextButton.isEnabled(); + } + throw new Error('Couldn\'t get next button'); + } + + public submit(): void { + const nextButton = element(by.id(this.NEXT_BUTTON)); + nextButton.click(); + } + + public fillFieldsForFailureToConfirmJointCredit(): void { + PageObjectTools.fillFields(this.JOINT_CREDIT_DATA); + } + + public dismissModalByOkayButton(): void { + const modalOkayButton = element(by.id(this.MODAL_OKAY_BUTTON)); + modalOkayButton.click(); + } + + public fillFieldsMinimal(): void { + PageObjectTools.fillFields(this.MINIMAL_DATA); + } + + public fillFieldsMaximum(): void { + PageObjectTools.fillFields(this.MAXIMUM_DATA); + } + + public getFocusedElement() { + return PageObjectTools.getFocusedElement(); + } + + public getFirstElement() { + return PageObjectTools.getElementsIdField(this.LIMIT_REQUESTED); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-maximum.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-maximum.e2e-spec.ts new file mode 100644 index 0000000..2f9d6a5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-maximum.e2e-spec.ts @@ -0,0 +1,152 @@ +import { browser, protractor } from 'protractor'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { VehiclePageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { CreditCardDisclosuresPagePo } from '../credit-card/pages/page-disclosures.po'; +import { VehicleCollateralPagePo } from '../shared/pages/vehicle-collateral-page.po'; + +describe('It should create a maximum vehicle application ', () => { + + it('should accept maximum inputs for page 1 (Member/Loan Type)', () => { + const page = new VehiclePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.ALL_FIELDS, ...page.HIDDEN_FIELDS_1]); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length + page.HIDDEN_FIELDS_1.length); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicant(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS, page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 2.A (Co-Applicant Personal Info)', () => { + const page = new ContactPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.COAPP_FIELDS); + expect(inputArray.length).toEqual(page.COAPP_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximumCoApplicantNonPrimary(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept maximum inputs for page 3.A (Co-applicant Residence)', () => { + const page = new ResidencePagePo(); + page.markAddressNotSame(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement(page.DEFAULT_PREFIX)); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + page.showMailingAddress(); + page.showSecondAddress(); + page.fillAdditionalFieldsMaximum(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.A (Co-applicant Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.showOtherIncomeFields(); + page.showChildSupportFields(); + page.fillFieldsMaximum(); + expect(page.canGoNext()).toEqual(false); + page.fillChildSupportFields(); + page.fillOtherIncomeFields(); + page.markSingleIncomeNotEnough(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 4.B (Vehicle Collateral)', () => { + const page = new VehicleCollateralPagePo(); + const EC = protractor.ExpectedConditions; + browser.wait(EC.visibilityOf(page.getFirstElement()), 5000).then(() => {}); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canClickDropdown()).toEqual(false); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMaximum(); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept maximum inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-minimal.e2e-spec.ts b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-minimal.e2e-spec.ts new file mode 100644 index 0000000..41a78b3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/src/vehicle/vehicle-minimal.e2e-spec.ts @@ -0,0 +1,96 @@ +import { browser } from 'protractor'; +import { PageObjectTools } from '../shared/page-object-tools'; +import { VehiclePageOnePo } from './pages/page-one.po'; +import { ContactPagePo } from '../shared/pages/contact-page.po'; +import { EmploymentPagePo } from '../shared/pages/employment-page.po'; +import { ResidencePagePo } from '../shared/pages/residence-page.po'; +import { SummaryPagePo } from '../shared/pages/summary-page.po'; +import { CreditCardDisclosuresPagePo } from '../credit-card/pages/page-disclosures.po'; +import { VehicleCollateralPagePo } from '../shared/pages/vehicle-collateral-page.po'; + +describe('It should create a minimal vehicle application ', () => { + + it('should accept basic inputs for page 1 (Member/Loan Type)', () => { + const page = new VehiclePageOnePo(); + browser.get(page.PAGE_URL); + browser.sleep(1000); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.fillFieldsForFailureToConfirmJointCredit(); + expect(page.canGoNext()).toEqual(false); + page.dismissModalByOkayButton(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 2 (Personal Info)', () => { + const page = new ContactPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 3 (Residence)', () => { + const page = new ResidencePagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for page 4 (Employment)', () => { + const page = new EmploymentPagePo(); + let inputArray = PageObjectTools.findFieldsInPageByIdArray(page.STARTING_FIELDS, page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual(page.STARTING_FIELDS.length); + expect(page.canGoNext()).toEqual(true); + page.showEmploymentFields(); + inputArray = PageObjectTools.findFieldsInPageByIdArray([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS], page.DEFAULT_PREFIX); + expect(inputArray.length).toEqual([...page.STARTING_FIELDS, ...page.EMPLOYMENT_FIELDS].length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should accept basic inputs for collateral page 4.A (Vehicle Collateral)', () => { + const page = new VehicleCollateralPagePo(); + expect(page.getFocusedElement()).toEqual(page.getFirstElement()); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canClickDropdown()).toEqual(false); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canClickDropdown()).toEqual(true); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + + it('should accept basic inputs for page 5 (Disclosures)', () => { + const page = new CreditCardDisclosuresPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + expect(page.canGoNext()).toEqual(false); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + page.submit(); + }); + + it('should show application information on page 6 (Summary)', () => { + const page = new SummaryPagePo(); + const inputArray = PageObjectTools.findFieldsInPageByIdArray(page.ALL_FIELDS); + expect(inputArray.length).toEqual(page.ALL_FIELDS.length); + page.fillFieldsMinimal(); + expect(page.canGoNext()).toEqual(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/e2e/tsconfig.e2e.json b/LCU.WebApps.External.OnlineLoanApp/e2e/tsconfig.e2e.json new file mode 100644 index 0000000..a6dd622 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Deployment.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Deployment.yaml new file mode 100644 index 0000000..5058052 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Deployment.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app +spec: + replicas: 1 + selector: + matchLabels: + app: online-loan-app + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: online-loan-app + spec: + containers: + - name: online-loan-app + env: [] + image: online-loan-app + resources: + limits: + memory: "1536Mi" + requests: + memory: "1536Mi" + cpu: "100m" + ports: + - containerPort: 80 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: / + port: 80 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: / + port: 80 diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Service.yaml new file mode 100644 index 0000000..f3d6d80 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/Service.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: online-loan-app +spec: + selector: + app: online-loan-app + ports: + - port: 80 + targetPort: 80 \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/base/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..2a3c36c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - Deployment.yaml + - Service.yaml + - virtual-service.yaml + +images: +- name: online-loan-app + newName: landmarkcu/lcu.api.internal.onlineloanapp \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/base/virtual-service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/virtual-service.yaml new file mode 100644 index 0000000..abaf011 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/base/virtual-service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app +spec: + hosts: [] + gateways: + - istio-system/ingress-gateway + http: + - name: online-loan-app + match: + - uri: + prefix: / + route: + - destination: + host: online-loan-app + port: + number: 80 diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml new file mode 100644 index 0000000..ac472c6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/kustomization.yaml @@ -0,0 +1,7 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml +images: +- name: online-loan-app + newName: landmarkcu/lcu.api.internal.onlineloanapp-onlineloanapp diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..f39b072 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app +spec: + template: + spec: + containers: + - name: online-loan-app + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml new file mode 100644 index 0000000..894aa6c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: online-loan-app-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.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/local/normal/set_virtual_service.yaml new file mode 100644 index 0000000..1891c1f --- /dev/null +++ b/LCU.WebApps.External.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 +spec: + hosts: + - online-loan-app.apps.azure.lcu-internal.com diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/kustomization.yaml new file mode 100644 index 0000000..aa8191d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/kustomization.yaml @@ -0,0 +1,7 @@ +resources: +- ../../../base +patchesStrategicMerge: +- set_appsettings_deployment.yaml +images: +- name: online-loan-app + newName: landmarkcu/lcu.api.internal.onlineloanapp-onlineloanapp \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/set_appsettings_deployment.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..f8dfd0e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/common/set_appsettings_deployment.yaml @@ -0,0 +1,13 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app +spec: + template: + spec: + containers: + - name: online-loan-app + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/configmap.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/configmap.yaml new file mode 100644 index 0000000..d5e3794 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: online-loan-app-configmap +data: + primary.json: | + { + "Primary": { + "SeqUrl": "https://seq.lcu-internal.com", + "CorsAllowedOrigins": [ + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://api.lcu-internal.com" + ] + } + } diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml new file mode 100644 index 0000000..7fa3e2a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- configmap.yaml +- ../common +patchesStrategicMerge: +- set_virtual_service.yaml diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml new file mode 100644 index 0000000..0290bd7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/prd/normal/set_virtual_service.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app +spec: + hosts: + - prd-online-loan-app.apps.azure.lcu-internal.com + http: + - name: online-loan-app + match: + - uri: + prefix: / + route: + - destination: + host: online-loan-app + port: + number: 80 diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/kustomization.yaml new file mode 100644 index 0000000..aa8191d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/kustomization.yaml @@ -0,0 +1,7 @@ +resources: +- ../../../base +patchesStrategicMerge: +- set_appsettings_deployment.yaml +images: +- name: online-loan-app + newName: landmarkcu/lcu.api.internal.onlineloanapp-onlineloanapp \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/set_appsettings_deployment.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..d66f027 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app +spec: + template: + spec: + containers: + - name: online-loan-app + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml new file mode 100644 index 0000000..a738d0b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/configmap.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/configmap.yaml new file mode 100644 index 0000000..1b8eedd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: online-loan-app-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" + ] + } + } diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml new file mode 100644 index 0000000..7fa3e2a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- configmap.yaml +- ../common +patchesStrategicMerge: +- set_virtual_service.yaml diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml new file mode 100644 index 0000000..e874e33 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/qa/normal/set_virtual_service.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app +spec: + hosts: + - qa-online-loan-app.apps.azure.lcu-internal.com + http: + - name: online-loan-app + match: + - uri: + prefix: / + route: + - destination: + host: online-loan-app + port: + number: 80 \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/kustomization.yaml new file mode 100644 index 0000000..d355113 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/kustomization.yaml @@ -0,0 +1,7 @@ +resources: +- ../../../base +patchesStrategicMerge: +- set_appsettings_deployment.yaml +images: +- name: online-loan-app + newName: landmarkcu/lcu.api.internal.onlineloanapp-onlineloanapp diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/set_appsettings_deployment.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..d0b21e5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: online-loan-app +spec: + template: + spec: + containers: + - name: online-loan-app + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Staging" diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/configmap.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/configmap.yaml new file mode 100644 index 0000000..1dcb1d9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: online-loan-app-configmap +data: + primary.json: | + { + "Primary": { + "SeqUrl": "https://uat-seq.lcu-internal.com", + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://uat-api.lcu-internal.com" + ] + } + } diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml new file mode 100644 index 0000000..7fa3e2a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- configmap.yaml +- ../common +patchesStrategicMerge: +- set_virtual_service.yaml diff --git a/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml new file mode 100644 index 0000000..0064ab3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/kustomize/overlays/uat/normal/set_virtual_service.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: online-loan-app +spec: + hosts: + - uat-online-loan-app.apps.azure.lcu-internal.com + http: + - name: online-loan-app + match: + - uri: + prefix: / + route: + - destination: + host: online-loan-app + port: + number: 80 diff --git a/LCU.WebApps.External.OnlineLoanApp/package-lock.json b/LCU.WebApps.External.OnlineLoanApp/package-lock.json new file mode 100644 index 0000000..4e6098a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/package-lock.json @@ -0,0 +1,30466 @@ +{ + "name": "ui", + "version": "4.11.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "4.11.0", + "hasInstallScript": true, + "dependencies": { + "@angular/animations": "^6.0.3", + "@angular/cdk": "6.4.7", + "@angular/common": "^6.0.3", + "@angular/compiler": "^6.0.3", + "@angular/core": "^6.0.3", + "@angular/forms": "^6.0.3", + "@angular/http": "^6.0.3", + "@angular/material": "6.4.7", + "@angular/platform-browser": "^6.0.3", + "@angular/platform-browser-dynamic": "^6.0.3", + "@angular/router": "^6.0.3", + "@fortawesome/fontawesome": "^1.1.8", + "@fortawesome/fontawesome-free-regular": "^5.0.13", + "@fortawesome/fontawesome-free-solid": "^5.0.13", + "@ng-bootstrap/ng-bootstrap": "^2.1.0", + "@ngx-translate/core": "^10.0.2", + "@ngx-translate/http-loader": "^3.0.1", + "angular2-text-mask": "^9.0.0", + "bootstrap": "^4.1.1", + "core-js": "^2.5.4", + "file-saver": "^2.0.0", + "fs-extra": "^7.0.0", + "git-describe": "^4.0.3", + "microsoft-adal-angular6": "^1.0.0", + "rxjs": "^6.0.0", + "text-mask-addons": "^3.7.2", + "zone.js": "^0.8.26" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~0.8.9", + "@angular/cli": "~6.0.8", + "@angular/compiler-cli": "^6.0.3", + "@angular/language-service": "^6.0.3", + "@types/jasmine": "~2.8.6", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.2.1", + "commitizen": "^3.0.4", + "cz-conventional-changelog": "^2.1.0", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "~1.1.1", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-junit-reporter": "^1.2.0", + "karma-spec-reporter": "^0.0.36", + "ng-bullet": "^1.0.3", + "protractor": "~5.3.0", + "puppeteer": "^1.5.0", + "sass": "^1.49.9", + "standard-version": "^4.4.0", + "ts-node": "~5.0.1", + "tslint": "~5.9.1", + "typescript": "~2.7.2", + "validate-commit-msg": "^2.14.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.6.8.tgz", + "integrity": "sha512-ZKTm/zC61iY9IBHOEAKoMSzZpvhkmv+1O/HHzpHEuR551jCzu6vSyCmMY9Z7GBcccscCV+hjeSMwgFrFRcqlkw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.6.8", + "rxjs": "^6.0.0" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.8.9.tgz", + "integrity": "sha512-J6o0MwIG1cJT29p87c7uUn7NY3QLEoQOVw4VXWM9cqG9bv99VK7f7eOSDhHJbXn7Snm4XYrye0zRa3RFXhMG+A==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.8.9", + "@angular-devkit/build-optimizer": "0.8.9", + "@angular-devkit/build-webpack": "0.8.9", + "@angular-devkit/core": "0.8.9", + "@ngtools/webpack": "6.2.9", + "ajv": "6.4.0", + "autoprefixer": "8.6.5", + "circular-dependency-plugin": "5.0.2", + "clean-css": "4.2.1", + "copy-webpack-plugin": "4.5.2", + "file-loader": "1.1.11", + "glob": "7.1.3", + "html-webpack-plugin": "3.2.0", + "istanbul": "0.4.5", + "istanbul-instrumenter-loader": "3.0.1", + "karma-source-map-support": "1.3.0", + "less": "3.8.1", + "less-loader": "4.1.0", + "license-webpack-plugin": "1.5.0", + "loader-utils": "1.1.0", + "mini-css-extract-plugin": "0.4.4", + "minimatch": "3.0.4", + "opn": "5.4.0", + "parse5": "4.0.0", + "portfinder": "1.0.19", + "postcss": "6.0.23", + "postcss-import": "11.1.0", + "postcss-loader": "2.1.6", + "postcss-url": "7.3.2", + "raw-loader": "0.5.1", + "rxjs": "6.2.2", + "sass-loader": "7.1.0", + "semver": "5.6.0", + "source-map-loader": "0.2.4", + "source-map-support": "0.5.9", + "stats-webpack-plugin": "0.6.2", + "style-loader": "0.21.0", + "stylus": "0.54.5", + "stylus-loader": "3.0.2", + "tree-kill": "1.2.1", + "uglifyjs-webpack-plugin": "1.3.0", + "url-loader": "1.1.2", + "webpack": "4.16.4", + "webpack-dev-middleware": "3.4.0", + "webpack-dev-server": "3.1.14", + "webpack-merge": "4.1.4", + "webpack-sources": "1.3.0", + "webpack-subresource-integrity": "1.1.0-rc.4" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + }, + "optionalDependencies": { + "node-sass": "^4.9.3" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.8.9.tgz", + "integrity": "sha512-2tiGPkvJyFY/G3a27uC8r6Jj3H5m8SxjMqhjNUQ5AtNumweTBPt3YIYMNAvHUmxG0nA9upDolVXFmoQGK9AhKQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "dependencies": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + }, + "optionalDependencies": { + "fsevents": "^1.2.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "optional": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "optional": true, + "dependencies": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "optional": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "optional": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "optional": true, + "dependencies": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/node-gyp/node_modules/semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/node-sass": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "2.2.5", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "bin": { + "node-sass": "bin/node-sass" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/opn": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", + "integrity": "sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "optional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "optional": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "optional": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/sass-graph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "dev": true, + "optional": true, + "dependencies": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^13.3.2" + }, + "bin": { + "sassgraph": "bin/sassgraph" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "optional": true, + "dependencies": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/scss-tokenizer/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "optional": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "optional": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/string-width/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.", + "dev": true, + "optional": true, + "dependencies": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "optional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "optional": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/@angular-devkit/build-optimizer": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.8.9.tgz", + "integrity": "sha512-h8u5iAhSmt0TsLDZXZCmOkXZDMgP2itLkgZvOIsGInyMAESJuWK4P1qegMSv2R5ELOsinJiuhe218M4K2enEdA==", + "dev": true, + "dependencies": { + "loader-utils": "1.1.0", + "source-map": "0.5.7", + "typescript": "2.9.2", + "webpack-sources": "1.3.0" + }, + "bin": { + "build-optimizer": "src/build-optimizer/cli.js", + "purify": "src/purify/cli.js" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-optimizer/node_modules/typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.8.9.tgz", + "integrity": "sha512-2csJ6utodPSLABTXfBLymYLrndJURF3xVqVjEDzUFl9zLqK1YOkKH4XPr12vfH8SfAtvzIutNLRxBtAuWJmDlw==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.8.9", + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + }, + "peerDependencies": { + "webpack": "^4.6.0", + "webpack-dev-server": "^3.1.4" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.8.9.tgz", + "integrity": "sha512-2tiGPkvJyFY/G3a27uC8r6Jj3H5m8SxjMqhjNUQ5AtNumweTBPt3YIYMNAvHUmxG0nA9upDolVXFmoQGK9AhKQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "dependencies": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + }, + "optionalDependencies": { + "fsevents": "^1.2.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.6.8.tgz", + "integrity": "sha512-rkIa1OSVWTt4g9leLSK/PsqOj3HZbDKHbZjqlslyfVa3AyCeiumFoOgViOVXlYgPX3HHDbE5uH24nyUWSD8uww==", + "dev": true, + "dependencies": { + "ajv": "~6.4.0", + "chokidar": "^2.0.3", + "rxjs": "^6.0.0", + "source-map": "^0.5.6" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "node_modules/@angular-devkit/core/node_modules/uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.6.8.tgz", + "integrity": "sha512-R4YqAUdo62wtrhX/5HSRGSKXNTWqfQb66ZE6m8jj6GEJNFKdNXMdxOchxr07LCiKTxfh1w6G3nGzxIsu/+D4KA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.6.8", + "rxjs": "^6.0.0" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular/animations": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.0.4.tgz", + "integrity": "sha512-Ro1XEwShk8XRAogfu73fKLTBLnND1s+MGhN+ymwr7ib9hqxVr7jMNE+MXPqG2/1BX9c+7NHb30B8G4woQn62vw==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/core": "6.0.4" + } + }, + "node_modules/@angular/cdk": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-6.4.7.tgz", + "integrity": "sha512-18x0U66fLD5kGQWZ9n3nb75xQouXlWs7kUDaTd8HTrHpT1s2QIAqlLd1KxfrYiVhsEC2jPQaoiae7VnBlcvkBg==", + "dependencies": { + "tslib": "^1.7.1" + }, + "peerDependencies": { + "@angular/common": ">=6.0.0-beta.0 <7.0.0", + "@angular/core": ">=6.0.0-beta.0 <7.0.0" + } + }, + "node_modules/@angular/cli": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.0.8.tgz", + "integrity": "sha512-DhH1Zq5Yonthw6zh6W07fhf+9XrAZbD1fcQ0MrmbxlieCfLlTAdBqyK2LavFCKwSZkUMLF6UHM3+jiNRVZSSIg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@angular-devkit/architect": "0.6.8", + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "@schematics/angular": "0.6.8", + "@schematics/update": "0.6.8", + "opn": "~5.3.0", + "resolve": "^1.1.7", + "rxjs": "^6.0.0", + "semver": "^5.1.0", + "silent-error": "^1.0.0", + "symbol-observable": "^1.2.0", + "yargs-parser": "^10.0.0" + }, + "bin": { + "ng": "bin/ng" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@angular/cli/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular/cli/node_modules/yargs-parser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.0.0.tgz", + "integrity": "sha512-+DHejWujTVYeMHLff8U96rLc4uE4Emncoftvn5AjhB1Jw1pWxLzgBUT/WYbPrHmy6YPEBTZQx5myHhVcuuu64g==", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/@angular/common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.0.4.tgz", + "integrity": "sha512-z2UE6a43u49omsqjzwdHhwD+wpCPauuD13FMkVQMLwzugKhNri0LjMk5I0I9xLz9OyXICm2B+wPAQN4d07savg==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/core": "6.0.4", + "rxjs": "^6.0.0" + } + }, + "node_modules/@angular/compiler": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.0.4.tgz", + "integrity": "sha512-qwjuPacuRKUroD+xev653mljV9Jwhi2tvwGo/cVadq0isJ2rVH1hO6sa1FWFnNf1KPnyolMobZlMgg8DFW/yRw==", + "dependencies": { + "tslib": "^1.9.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.0.4.tgz", + "integrity": "sha512-bkN21JJrsbIdXNfOc9I9iB7AETUe4/QvApC25R3/pERSyhkK7UNNHTqg4FY5xMdSxlZd1ccWj8rvSbS+hFbTBw==", + "dev": true, + "dependencies": { + "chokidar": "^1.4.2", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "tsickle": "^0.29.0" + }, + "bin": { + "ng-xi18n": "src/extract_i18n.js", + "ngc": "src/main.js" + }, + "engines": { + "node": ">=8.0" + }, + "peerDependencies": { + "@angular/compiler": "6.0.4", + "typescript": ">=2.7.2 <2.8" + } + }, + "node_modules/@angular/compiler-cli/node_modules/anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "dependencies": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", + "dev": true, + "dependencies": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.0.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/@angular/core": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.0.4.tgz", + "integrity": "sha512-WCW8wyRoIKkjGjOjVLFpUyhQw/K0//s3W0qo1/vH6m9njC63jP/VP+2Xnt8bdtgeET2NYUeyl+w/0vUXqYlnEA==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "rxjs": "^6.0.0", + "zone.js": "~0.8.26" + } + }, + "node_modules/@angular/forms": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.0.4.tgz", + "integrity": "sha512-JcKyCCkAwF3NlWuwPxtpRhFX/8QbFUkA4W0N62L+Odm8G0/P4QhMw/99o5kRs5xrsQRVyFlGvdYOrkgDyyZkGg==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": "6.0.4", + "@angular/core": "6.0.4", + "@angular/platform-browser": "6.0.4", + "rxjs": "^6.0.0" + } + }, + "node_modules/@angular/http": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.0.4.tgz", + "integrity": "sha512-bq7cS0/5hM3Af3FolEe7HpKP19/7MJWIPGi5k9rYCGM4dXKFiKa+LA/SbXE0DMiKxhAQbZDwQ9HN04r70DRyBQ==", + "deprecated": "Package no longer supported. Use @angular/common instead, see https://angular.io/guide/deprecations#angularhttp", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/core": "6.0.4", + "@angular/platform-browser": "6.0.4", + "rxjs": "^6.0.0" + } + }, + "node_modules/@angular/language-service": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-6.0.4.tgz", + "integrity": "sha512-LyFh/sFtnyjJJYO5UKM31nGSSkmGYFMwqzwbG+l0QNQXNoa9jj+OApvu0oOY5F0WP9DBrOAzj74j2M5olprtMQ==", + "dev": true + }, + "node_modules/@angular/material": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-6.4.7.tgz", + "integrity": "sha512-SdNx7Xovi24Kw9eU6lkLhY/7f2M7L9F+/uh6XuPr4jbGgCUVVpeeVI5ztZhsZRbj1sN+/r1p5w8u62apWWl5Ww==", + "dependencies": { + "tslib": "^1.7.1" + }, + "optionalDependencies": { + "parse5": "^5.0.0" + }, + "peerDependencies": { + "@angular/cdk": "6.4.7", + "@angular/common": ">=6.0.0-beta.0 <7.0.0", + "@angular/core": ">=6.0.0-beta.0 <7.0.0" + } + }, + "node_modules/@angular/material/node_modules/parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, + "node_modules/@angular/platform-browser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.0.4.tgz", + "integrity": "sha512-4y6rSqOLuBs+RL0CIM+RZ5T8a5Tm1NisZcTUGvM8lVYPNkqTzNfHQXhBq8mTgRw9JopEwOj+UEqBbgrNX715yQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": "6.0.4", + "@angular/core": "6.0.4" + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.0.4.tgz", + "integrity": "sha512-uh7MoQi9kyKPFHrD7HPOU5gWyhF4pPJuGltyMTOtM/f5TNL9QQI2EugijKnnSFLQ8+7UQjmFAVeRqH9jylhIyQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": "6.0.4", + "@angular/compiler": "6.0.4", + "@angular/core": "6.0.4", + "@angular/platform-browser": "6.0.4" + } + }, + "node_modules/@angular/router": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.0.4.tgz", + "integrity": "sha512-IKUtbbRtHN8I+Hp8/pLqBPyXJmQHrPFGzKU3H43TovD++mmT5AaYVCeJgkGUB//wjtLN3y0X/yrmyWFWcN35Gw==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": "6.0.4", + "@angular/core": "6.0.4", + "@angular/platform-browser": "6.0.4", + "rxjs": "^6.0.0" + } + }, + "node_modules/@fortawesome/fontawesome": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome/-/fontawesome-1.1.8.tgz", + "integrity": "sha512-c0/MtkPVT0fmiFcCyYoPjkG9PkMOvfrZw2+0BaJ+Rh6UEcK1AR/LaRgrHHjUkbAbs9LXxQJhFS8CJ4uSnK2+JA==", + "deprecated": "This package is deprecated. See https://git.io/fNCzJ for information about upgrading.", + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.7.tgz", + "integrity": "sha512-ego8jRVSHfq/iq4KRZJKQeUAdi3ZjGNrqw4oPN3fNdvTBnLCSntwVCnc37bsAJP9UB8MhrTfPnZYxkv2vpS4pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free-regular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-regular/-/fontawesome-free-regular-5.0.13.tgz", + "integrity": "sha512-36lz9Idww1L4QaaTcv7GZiOeIP9emJFDUsedvRovI10kmwyd6rN0PKkIjnq0FB4foLhX4Rou8vnbCCmjtqiLug==", + "deprecated": "This package is deprecated. See https://git.io/fNCzJ for information about upgrading.", + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free-solid": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.13.tgz", + "integrity": "sha512-b+krVnqkdDt52Yfev0x0ZZgtxBQsLw00Zfa3uaVWIDzpNZVtrEXuxldUSUaN/ihgGhSNi8VpvDAdNPVgCKOSxw==", + "deprecated": "This package is deprecated. See https://git.io/fNCzJ for information about upgrading.", + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-2.1.0.tgz", + "integrity": "sha512-ElqT/uayvnAbA9Klt0IZSbQ+BZtyEPdGDGojmYWQEFDNmTmismkM9Cohx4Hy6PakGAItCy7Ghu1isR8ZKkdtnw==", + "peerDependencies": { + "@angular/common": "^6.0.0", + "@angular/core": "^6.0.0", + "@angular/forms": "^6.0.0", + "rxjs": "^6.0.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.2.9.tgz", + "integrity": "sha512-wZ9ROI4FdA9gnx21ULtA8u6Gmtig3BbT6EfUh2uYisjyL4P9k2p22sfY/Txwu/InnF4LDHOs5xZutOnGvC87vw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2", + "tree-kill": "1.2.1", + "webpack-sources": "1.3.0" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + }, + "peerDependencies": { + "typescript": "~2.4.0 || ~2.5.0 || ~2.6.0 || ~2.7.0 || ~2.8.0 || ~2.9.0", + "webpack": "^4.0.0" + } + }, + "node_modules/@ngtools/webpack/node_modules/@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "dependencies": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@ngtools/webpack/node_modules/ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "node_modules/@ngtools/webpack/node_modules/chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + }, + "optionalDependencies": { + "fsevents": "^1.2.2" + } + }, + "node_modules/@ngtools/webpack/node_modules/rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@ngtools/webpack/node_modules/uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-10.0.2.tgz", + "integrity": "sha512-7nM3DrJaqKswwtJlbu2kuKNl+hE8Isr18sKsKvGGpSxQk+G0gO0reDlx2PhUNus7TJTkA1C59vU/JoN8hIvZ4g==", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/core": ">=6.0.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-3.0.1.tgz", + "integrity": "sha1-ILD5i8bCUyESnT4zAqs8xInApCo=", + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": ">=6.0.0-rc.0", + "@angular/core": ">=6.0.0-rc.0", + "@ngx-translate/core": ">=10.0.0" + } + }, + "node_modules/@schematics/angular": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.6.8.tgz", + "integrity": "sha512-9kRphqTYG5Df/I8fvnT1zMsw0YNDPO9tl18tQZXj4am4raT7l9UCr+WkwJdlBoA5pwG6baWE9sL0iGWV/bzF/g==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "typescript": ">=2.6.2 <2.8" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@schematics/update": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.6.8.tgz", + "integrity": "sha512-1Uq7LYnwL2wBwGVCgNz76QAR13ghAk+2vDDHOi+VX5+usHManxydrpoMGeX66OBPd+y5D3D2MFb+8mYHE7mygg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "npm-registry-client": "^8.5.1", + "rxjs": "^6.0.0", + "semver": "^5.3.0", + "semver-intersect": "^1.1.2" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/@types/adal": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@types/adal/-/adal-1.0.29.tgz", + "integrity": "sha512-6kvyTWzue5lJ8xECAgVdMrs5+UEyPeflzU/L83QdufGnvjdyCiCWVgkfinMB7133ik/x+02T0q1070Z6/ZK6eQ==", + "deprecated": "'@types/adal' is now '@types/adal-angular'", + "dependencies": { + "@types/angular": "*" + } + }, + "node_modules/@types/angular": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.51.tgz", + "integrity": "sha512-wYU+/zlJWih7ZmonWVjGQ18tG7GboI9asMNjRBM5fpIFJWXSioQttCTw9qGL44cP82ghM8sCV9apEqm1zBDq2w==" + }, + "node_modules/@types/jasmine": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", + "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", + "dev": true + }, + "node_modules/@types/jasminewd2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz", + "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==", + "dev": true, + "dependencies": { + "@types/jasmine": "*" + } + }, + "node_modules/@types/node": { + "version": "8.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", + "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "dev": true + }, + "node_modules/@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "node_modules/@types/selenium-webdriver": { + "version": "2.53.43", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz", + "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.13.tgz", + "integrity": "sha512-49nwvW/Hx9i+OYHg+mRhKZfAlqThr11Dqz8TsrvqGKMhdI2ijy3KBJOun2Z4770TPjrIJhR6KxChQIDaz8clDA==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "debug": "^3.1.0", + "mamacro": "^0.0.3" + } + }, + "node_modules/@webassemblyjs/ast/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/ast/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.13.tgz", + "integrity": "sha512-vrvvB18Kh4uyghSKb0NTv+2WZx871WL2NzwMj61jcq2bXkyhRC+8Q0oD7JGVf0+5i/fKQYQSBCNMMsDMRVAMqA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.13.tgz", + "integrity": "sha512-dBh2CWYqjaDlvMmRP/kudxpdh30uXjIbpkLj9HQe+qtYlwvYjPRjdQXrq1cTAAOUSMTtzqbXIxEdEZmyKfcwsg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.13.tgz", + "integrity": "sha512-v7igWf1mHcpJNbn4m7e77XOAWXCDT76Xe7Is1VQFXc4K5jRcFrl9D0NrqM4XifQ0bXiuTSkTKMYqDxu5MhNljA==", + "dev": true, + "dependencies": { + "debug": "^3.1.0" + } + }, + "node_modules/@webassemblyjs/helper-buffer/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/helper-buffer/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.13.tgz", + "integrity": "sha512-yN6ScQQDFCiAXnVctdVO/J5NQRbwyTbQzsGzEgXsAnrxhjp0xihh+nNHQTMrq5UhOqTb5LykpJAvEv9AT0jnAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/wast-printer": "1.5.13" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.13.tgz", + "integrity": "sha512-hSIKzbXjVMRvy3Jzhgu+vDd/aswJ+UMEnLRCkZDdknZO3Z9e6rp1DAs0tdLItjCFqkz9+0BeOPK/mk3eYvVzZg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.13.tgz", + "integrity": "sha512-zxJXULGPLB7r+k+wIlvGlXpT4CYppRz8fLUM/xobGHc9Z3T6qlmJD9ySJ2jknuktuuiR9AjnNpKYDECyaiX+QQ==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "mamacro": "^0.0.3" + } + }, + "node_modules/@webassemblyjs/helper-module-context/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/helper-module-context/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.13.tgz", + "integrity": "sha512-0n3SoNGLvbJIZPhtMFq0XmmnA/YmQBXaZKQZcW8maGKwLpVcgjNrxpFZHEOLKjXJYVN5Il8vSfG7nRX50Zn+aw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.13.tgz", + "integrity": "sha512-IJ/goicOZ5TT1axZFSnlAtz4m8KEjYr12BNOANAwGFPKXM4byEDaMNXYowHMG0yKV9a397eU/NlibFaLwr1fbw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "debug": "^3.1.0" + } + }, + "node_modules/@webassemblyjs/helper-wasm-section/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/helper-wasm-section/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.5.13.tgz", + "integrity": "sha512-TseswvXEPpG5TCBKoLx9tT7+/GMACjC1ruo09j46ULRZWYm8XHpDWaosOjTnI7kr4SRJFzA6MWoUkAB+YCGKKg==", + "dev": true, + "dependencies": { + "ieee754": "^1.1.11" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.5.13.tgz", + "integrity": "sha512-0NRMxrL+GG3eISGZBmLBLAVjphbN8Si15s7jzThaw1UE9e5BY1oH49/+MA1xBzxpf1OW5sf9OrPDOclk9wj2yg==", + "dev": true, + "dependencies": { + "long": "4.0.0" + } + }, + "node_modules/@webassemblyjs/leb128/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.5.13.tgz", + "integrity": "sha512-Ve1ilU2N48Ew0lVGB8FqY7V7hXjaC4+PeZM+vDYxEd+R2iQ0q+Wb3Rw8v0Ri0+rxhoz6gVGsnQNb4FjRiEH/Ng==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.13.tgz", + "integrity": "sha512-X7ZNW4+Hga4f2NmqENnHke2V/mGYK/xnybJSIXImt1ulxbCOEs/A+ZK/Km2jgihjyVxp/0z0hwIcxC6PrkWtgw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/helper-wasm-section": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "@webassemblyjs/wast-printer": "1.5.13", + "debug": "^3.1.0" + } + }, + "node_modules/@webassemblyjs/wasm-edit/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/wasm-edit/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.13.tgz", + "integrity": "sha512-yfv94Se8R73zmr8GAYzezFHc3lDwE/lBXQddSiIZEKZFuqy7yWtm3KMwA1uGbv5G1WphimJxboXHR80IgX1hQA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.13.tgz", + "integrity": "sha512-IkXSkgzVhQ0QYAdIayuCWMmXSYx0dHGU8Ah/AxJf1gBvstMWVnzJnBwLsXLyD87VSBIcsqkmZ28dVb0mOC3oBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "debug": "^3.1.0" + } + }, + "node_modules/@webassemblyjs/wasm-opt/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.13.tgz", + "integrity": "sha512-XnYoIcu2iqq8/LrtmdnN3T+bRjqYFjRHqWbqK3osD/0r/Fcv4d9ecRzjVtC29ENEuNTK4mQ9yyxCBCbK8S/cpg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.5.13.tgz", + "integrity": "sha512-Lbz65T0LQ1LgzKiUytl34CwuhMNhaCLgrh0JW4rJBN6INnBB8NMwUfQM+FxTnLY9qJ+lHJL/gCM5xYhB9oWi4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/floating-point-hex-parser": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-code-frame": "1.5.13", + "@webassemblyjs/helper-fsm": "1.5.13", + "long": "^3.2.0", + "mamacro": "^0.0.3" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.5.13.tgz", + "integrity": "sha512-QcwogrdqcBh8Z+eUF8SG+ag5iwQSXxQJELBEHmLkk790wgQgnIMmntT2sMAMw53GiFNckArf5X0bsCA44j3lWQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "long": "^3.2.0" + } + }, + "node_modules/abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "dev": true, + "dependencies": { + "acorn": "^5.0.0" + } + }, + "node_modules/adal-angular": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/adal-angular/-/adal-angular-1.0.17.tgz", + "integrity": "sha1-bpNuDkH5HTsqiOf/ypwvb29WLMQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true, + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "node_modules/agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ajv/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "optional": true, + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/align-text/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/angular2-text-mask": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/angular2-text-mask/-/angular2-text-mask-9.0.0.tgz", + "integrity": "sha512-iALcnhJPS1zvX48d86rgUgDe/crX6XfhZrXC4Gdlo2/YwZW7u7KJZY6/b3ieSCIWVq/E6p+wDCzeo3E6leRjDA==", + "dependencies": { + "text-mask-core": "^5.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/app-root-path": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", + "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "dependencies": { + "default-require-extensions": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "node_modules/asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "node_modules/async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "node_modules/async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true, + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "8.6.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.5.tgz", + "integrity": "sha512-PLWJN3Xo/rycNkx+mp8iBDMTm3FeWe4VmYaZDSqL5QQB9sLsQkG5k8n+LNDFnhh9kdq2K+egL/icpctOmDHwig==", + "dev": true, + "dependencies": { + "browserslist": "^3.2.8", + "caniuse-lite": "^1.0.30000864", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^6.0.23", + "postcss-value-parser": "^3.2.3" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "dependencies": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "node_modules/base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "dependencies": { + "callsite": "1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "node_modules/block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "optional": true, + "dependencies": { + "inherits": "~2.0.0" + }, + "engines": { + "node": "0.4 || >=0.5.8" + } + }, + "node_modules/blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "blocking-proxy": "built/lib/bin.js" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/blocking-proxy/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "node_modules/bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/bootstrap": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.1.tgz", + "integrity": "sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.14.3" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "dependencies": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30000844", + "electron-to-chromium": "^1.3.47" + }, + "bin": { + "browserslist": "cli.js" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "node_modules/builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cachedir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.1.0.tgz", + "integrity": "sha512-xGBpPqoBvn3unBW7oxgb8aJn42K0m9m1/wyjmazah10Fq7bROGG3kRAE6OIyr3U3PIJUqGuebhCEdMk9OKJG0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "dependencies": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001035", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz", + "integrity": "sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ==", + "dev": true + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "dependencies": { + "has-flag": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.1.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/circular-dependency-plugin": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz", + "integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.1" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/cliui/node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.0", + "shallow-clone": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/codelyzer": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz", + "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==", + "dev": true, + "dependencies": { + "app-root-path": "^2.0.1", + "css-selector-tokenizer": "^0.7.0", + "cssauron": "^1.4.0", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.6", + "sprintf-js": "^1.0.3" + }, + "peerDependencies": { + "@angular/common": ">=2.3.1 <7.0.0 || >6.0.0-beta <7.0.0", + "@angular/compiler": ">=2.3.1 <7.0.0 || >6.0.0-beta <7.0.0", + "@angular/core": ">=2.3.1 <7.0.0 || >6.0.0-beta <7.0.0", + "@angular/platform-browser": ">=2.3.1 <7.0.0 || >6.0.0-beta <7.0.0", + "@angular/platform-browser-dynamic": ">=2.3.1 <7.0.0 || >6.0.0-beta <7.0.0", + "tslint": "^5.0.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "dependencies": { + "color-name": "1.1.1" + } + }, + "node_modules/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "dependencies": { + "lodash": "^4.5.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "node_modules/commitizen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-3.0.4.tgz", + "integrity": "sha512-djR5F7RBsGALyUEm/B1H/85nsN4L1F5DhWN+9/efSwqHDSyhw2MK6MF2VRuD26PUqGkQbcUlYO61btkTWjcjVw==", + "dev": true, + "dependencies": { + "cachedir": "2.1.0", + "cz-conventional-changelog": "2.1.0", + "dedent": "0.7.0", + "detect-indent": "^5.0.0", + "find-node-modules": "1.0.4", + "find-root": "1.1.0", + "fs-extra": "^7.0.0", + "glob": "7.1.3", + "inquirer": "6.2.0", + "lodash": "4.17.11", + "minimist": "1.2.0", + "shelljs": "0.7.6", + "strip-json-comments": "2.0.1" + }, + "bin": { + "commitizen": "bin/commitizen", + "git-cz": "bin/git-cz" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commitizen/node_modules/detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/commitizen/node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/commitizen/node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/commitizen/node_modules/lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "node_modules/commitizen/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + } + }, + "node_modules/compare-versions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", + "dev": true + }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true, + "optional": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-1.1.24.tgz", + "integrity": "sha512-2WcSUst4Y3Z4hHvoMTWXMJr/DmgVdLiMOVY1Kak2LfFz+GIz2KDp5naqbFesYbfXPmaZ5p491dO0FWZIJoJw1Q==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^1.6.6", + "conventional-changelog-atom": "^0.2.8", + "conventional-changelog-codemirror": "^0.3.8", + "conventional-changelog-core": "^2.0.11", + "conventional-changelog-ember": "^0.3.12", + "conventional-changelog-eslint": "^1.0.9", + "conventional-changelog-express": "^0.3.6", + "conventional-changelog-jquery": "^0.1.0", + "conventional-changelog-jscs": "^0.1.0", + "conventional-changelog-jshint": "^0.3.8", + "conventional-changelog-preset-loader": "^1.1.8" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", + "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", + "dev": true, + "dependencies": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-angular/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-0.2.8.tgz", + "integrity": "sha512-8pPZqhMbrnltNBizjoDCb/Sz85KyUXNDQxuAEYAU5V/eHn0okMBVjqc8aHWYpHrytyZWvMGbayOlDv7i8kEf6g==", + "dev": true, + "dependencies": { + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-atom/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-0.3.8.tgz", + "integrity": "sha512-3HFZKtBXTaUCHvz7ai6nk2+psRIkldDoNzCsom0egDtVmPsvvHZkzjynhdQyULfacRSsBTaiQ0ol6nBOL4dDiQ==", + "dev": true, + "dependencies": { + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-codemirror/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-core": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", + "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", + "dev": true, + "dependencies": { + "conventional-changelog-writer": "^3.0.9", + "conventional-commits-parser": "^2.1.7", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "^1.3.6", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^1.3.6", + "lodash": "^4.2.1", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^1.1.0", + "read-pkg-up": "^1.0.1", + "through2": "^2.0.0" + } + }, + "node_modules/conventional-changelog-core/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-0.3.12.tgz", + "integrity": "sha512-mmJzA7uzbrOqeF89dMMi6z17O07ORTXlTMArnLG9ZTX4oLaKNolUlxFUFlFm9JUoVWajVpaHQWjxH1EOQ+ARoQ==", + "dev": true, + "dependencies": { + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-ember/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-1.0.9.tgz", + "integrity": "sha512-h87nfVh2fdk9fJIvz26wCBsbDC/KxqCc5wSlNMZbXcARtbgNbNDIF7Y7ctokFdnxkzVdaHsbINkh548T9eBA7Q==", + "dev": true, + "dependencies": { + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-eslint/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-express": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-0.3.6.tgz", + "integrity": "sha512-3iWVtBJZ9RnRnZveNDzOD8QRn6g6vUif0qVTWWyi5nUIAbuN1FfPVyKdAlJJfp5Im+dE8Kiy/d2SpaX/0X678Q==", + "dev": true, + "dependencies": { + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-express/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-0.1.0.tgz", + "integrity": "sha1-Agg5cWLjhGmG5xJztsecW1+A9RA=", + "dev": true, + "dependencies": { + "q": "^1.4.1" + } + }, + "node_modules/conventional-changelog-jscs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jscs/-/conventional-changelog-jscs-0.1.0.tgz", + "integrity": "sha1-BHnrRDzH1yxYvwvPDvHURKkvDlw=", + "dev": true, + "dependencies": { + "q": "^1.4.1" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-0.3.8.tgz", + "integrity": "sha512-hn9QU4ZI/5V50wKPJNPGT4gEWgiBFpV6adieILW4MaUFynuDYOvQ71EMSj3EznJyKi/KzuXpc9dGmX8njZMjig==", + "dev": true, + "dependencies": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "node_modules/conventional-changelog-jshint/node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", + "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==", + "dev": true + }, + "node_modules/conventional-changelog-writer": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", + "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", + "dev": true, + "dependencies": { + "compare-func": "^1.3.1", + "conventional-commits-filter": "^1.1.6", + "dateformat": "^3.0.0", + "handlebars": "^4.0.2", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "semver": "^5.5.0", + "split": "^1.0.0", + "through2": "^2.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + } + }, + "node_modules/conventional-changelog-writer/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "dependencies": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/conventional-changelog-writer/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "dependencies": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-changelog-writer/node_modules/trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commit-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz", + "integrity": "sha1-XblXOdbCEqy+e29lahG5QLqmiUY=", + "dev": true + }, + "node_modules/conventional-commits-filter": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", + "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", + "dev": true, + "dependencies": { + "is-subset": "^0.1.1", + "modify-values": "^1.0.0" + } + }, + "node_modules/conventional-commits-parser": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", + "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", + "dev": true, + "dependencies": { + "is-text-path": "^1.0.0", + "JSONStream": "^1.0.4", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0", + "trim-off-newlines": "^1.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + } + }, + "node_modules/conventional-commits-parser/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "dependencies": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/conventional-commits-parser/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "dependencies": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-commits-parser/node_modules/trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/conventional-recommended-bump": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-1.2.1.tgz", + "integrity": "sha512-oJjG6DkRgtnr/t/VrPdzmf4XZv8c4xKVJrVT4zrSHd92KEL+EYxSbYoKq8lQ7U5yLMw7130wrcQTLRjM/T+d4w==", + "dev": true, + "dependencies": { + "concat-stream": "^1.4.10", + "conventional-commits-filter": "^1.1.1", + "conventional-commits-parser": "^2.1.1", + "git-raw-commits": "^1.3.0", + "git-semver-tags": "^1.3.0", + "meow": "^3.3.0", + "object-assign": "^4.0.1" + }, + "bin": { + "conventional-recommended-bump": "cli.js" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==", + "dev": true, + "dependencies": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "globby": "^7.1.1", + "is-glob": "^4.0.0", + "loader-utils": "^1.1.0", + "minimatch": "^3.0.4", + "p-limit": "^1.0.0", + "serialize-javascript": "^1.4.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js." + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cosmiconfig/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/css-parse": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", + "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=", + "dev": true + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "dependencies": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + } + }, + "node_modules/css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "dependencies": { + "through": "X.X.X" + } + }, + "node_modules/cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + } + }, + "node_modules/cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=", + "dev": true + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "node_modules/cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "node_modules/cz-conventional-changelog": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz", + "integrity": "sha1-L0vHOQ4yROTfKT5ro1Hkx0Cnx2Q=", + "dev": true, + "dependencies": { + "conventional-commit-types": "^2.0.0", + "lodash.map": "^4.5.1", + "longest": "^1.0.1", + "right-pad": "^1.0.1", + "word-wrap": "^1.0.3" + } + }, + "node_modules/dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/default-gateway": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", + "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==", + "dev": true, + "os": [ + "android", + "darwin", + "freebsd", + "linux", + "openbsd", + "sunos", + "win32" + ], + "dependencies": { + "execa": "^0.10.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/default-gateway/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "dependencies": { + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/default-require-extensions/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "dependencies": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/del/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "node_modules/detect-file": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "dev": true, + "dependencies": { + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "dependencies": { + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "node_modules/dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotgitignore": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-1.0.3.tgz", + "integrity": "sha512-eu5XjSstm0WXQsARgo6kPjkINYZlOUW+z/KtAAIBjHa5mUpMPrxJytbPIndWz6GubBuuuH5ljtVcXKnVnH5q8w==", + "dev": true, + "dependencies": { + "find-up": "^2.1.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "dependencies": { + "jsbn": "~0.1.0" + } + }, + "node_modules/edge-launcher": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", + "integrity": "sha1-60Cq+9Bnpup27/+rBke81VCbN7I=", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.376.tgz", + "integrity": "sha512-cv/PYVz5szeMz192ngilmezyPNFkUjuynuL2vNdiqIrio440nfTDdc0JJU0TS2KHLSVCs9gBbt4CFqM+HcBnjw==", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "dev": true, + "dependencies": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "optional": true + }, + "node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "dependencies": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + } + }, + "node_modules/engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "dependencies": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "dependencies": { + "mime-types": "~2.1.11", + "negotiator": "0.6.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "node_modules/entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "dependencies": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=0.12.0" + }, + "optionalDependencies": { + "source-map": "~0.2.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "dependencies": { + "estraverse": "^4.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "dev": true + }, + "node_modules/events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "dependencies": { + "original": "^1.0.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "dependencies": { + "array-slice": "^0.2.3", + "array-unique": "^0.2.1", + "braces": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "dependencies": { + "expand-range": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "dependencies": { + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "node_modules/express/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "dependencies": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + }, + "engines": { + "node": ">= 4.3 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz", + "integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g==" + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "dependencies": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/find-node-modules": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-1.0.4.tgz", + "integrity": "sha1-tt6zzMtpnIcDdne87eLF9YYrJVA=", + "dev": true, + "dependencies": { + "findup-sync": "0.4.2", + "merge": "^1.2.0" + } + }, + "node_modules/find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/findup": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", + "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", + "dev": true, + "dependencies": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "bin": { + "findup": "bin/findup.js" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/findup-sync": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.2.tgz", + "integrity": "sha1-qBF9D3MST1pFRoOVef5S1xKfteU=", + "dev": true, + "dependencies": { + "detect-file": "^0.1.0", + "is-glob": "^2.0.1", + "micromatch": "^2.3.7", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/findup-sync/node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup/node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/findup/node_modules/commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", + "dev": true, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.0.tgz", + "integrity": "sha512-fdrt472/9qQ6Kgjvb935ig6vJCuofpBUD14f9Vb+SLlm7xIe4Qva5gey8EKtv8lp7ahE1wilg3xL1znpVGtZIA==", + "dev": true, + "dependencies": { + "debug": "^3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/follow-redirects/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "dependencies": { + "null-check": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", + "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "bundleDependencies": [ + "node-pre-gyp" + ], + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fsevents/node_modules/abbrev": { + "version": "1.1.1", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/ansi-regex": { + "version": "2.1.1", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/are-we-there-yet": { + "version": "1.1.4", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/fsevents/node_modules/balanced-match": { + "version": "1.0.0", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/brace-expansion": { + "version": "1.1.11", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fsevents/node_modules/chownr": { + "version": "1.0.1", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/code-point-at": { + "version": "1.1.0", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/concat-map": { + "version": "0.0.1", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/console-control-strings": { + "version": "1.1.0", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/debug": { + "version": "2.6.9", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/fsevents/node_modules/deep-extend": { + "version": "0.5.1", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/delegates": { + "version": "1.0.0", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/detect-libc": { + "version": "1.0.3", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, + "inBundle": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/fsevents/node_modules/fs-minipass": { + "version": "1.2.5", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "minipass": "^2.2.1" + } + }, + "node_modules/fsevents/node_modules/fs.realpath": { + "version": "1.0.0", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/gauge": { + "version": "2.7.4", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/fsevents/node_modules/glob": { + "version": "7.1.2", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/has-unicode": { + "version": "2.0.1", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/iconv-lite": { + "version": "0.4.21", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "safer-buffer": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/ignore-walk": { + "version": "3.0.1", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/fsevents/node_modules/inflight": { + "version": "1.0.6", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/fsevents/node_modules/inherits": { + "version": "2.0.3", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/ini": { + "version": "1.3.5", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "deprecated": "Please update to ini >=1.3.6 to avoid a prototype pollution issue", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/isarray": { + "version": "1.0.0", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/minimatch": { + "version": "3.0.4", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/minimist": { + "version": "0.0.8", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/minipass": { + "version": "2.2.4", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "node_modules/fsevents/node_modules/minizlib": { + "version": "1.1.0", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "minipass": "^2.2.1" + } + }, + "node_modules/fsevents/node_modules/mkdirp": { + "version": "0.5.1", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fsevents/node_modules/ms": { + "version": "2.0.0", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/needle": { + "version": "2.2.0", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 0.10.x" + } + }, + "node_modules/fsevents/node_modules/node-pre-gyp": { + "version": "0.10.0", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/fsevents/node_modules/nopt": { + "version": "4.0.1", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/fsevents/node_modules/npm-bundled": { + "version": "1.0.3", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/npm-packlist": { + "version": "1.1.10", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "node_modules/fsevents/node_modules/npmlog": { + "version": "4.1.2", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/fsevents/node_modules/number-is-nan": { + "version": "1.0.1", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/object-assign": { + "version": "4.1.1", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/once": { + "version": "1.4.0", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/fsevents/node_modules/os-homedir": { + "version": "1.0.2", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/os-tmpdir": { + "version": "1.0.2", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/osenv": { + "version": "0.1.5", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/fsevents/node_modules/path-is-absolute": { + "version": "1.0.1", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/process-nextick-args": { + "version": "2.0.0", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/rc": { + "version": "1.2.7", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/fsevents/node_modules/rc/node_modules/minimist": { + "version": "1.2.0", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/fsevents/node_modules/rimraf": { + "version": "2.6.2", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/fsevents/node_modules/safe-buffer": { + "version": "5.1.1", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/sax": { + "version": "1.2.4", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/semver": { + "version": "5.5.0", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true, + "inBundle": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/fsevents/node_modules/set-blocking": { + "version": "2.0.0", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/signal-exit": { + "version": "3.0.2", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fsevents/node_modules/string-width": { + "version": "1.0.2", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/strip-ansi": { + "version": "3.0.1", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/tar": { + "version": "4.4.1", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/fsevents/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/wide-align": { + "version": "1.1.2", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "string-width": "^1.0.2" + } + }, + "node_modules/fsevents/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/yallist": { + "version": "3.0.2", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "optional": true, + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "node_modules/get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "bin": { + "get-pkg-repo": "cli.js" + } + }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/git-describe": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.0.3.tgz", + "integrity": "sha1-2BSCqK2nodgeBg1DisjhG0o5AzY=", + "dependencies": { + "lodash": "^4.16.6" + }, + "engines": { + "node": ">=0.12.17" + }, + "optionalDependencies": { + "semver": "^5.3.0" + } + }, + "node_modules/git-raw-commits": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", + "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", + "dev": true, + "dependencies": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + } + }, + "node_modules/git-raw-commits/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "dependencies": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/git-raw-commits/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "dependencies": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-raw-commits/node_modules/trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "dependencies": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-remote-origin-url/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/git-semver-tags": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", + "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", + "dev": true, + "dependencies": { + "meow": "^4.0.0", + "semver": "^5.5.0" + }, + "bin": { + "git-semver-tags": "cli.js" + } + }, + "node_modules/git-semver-tags/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "dependencies": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/git-semver-tags/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "dependencies": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags/node_modules/trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "dependencies": { + "ini": "^1.3.2" + } + }, + "node_modules/glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true, + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-modules/node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "optional": true, + "dependencies": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "node_modules/handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "dependencies": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^2.6" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/handlebars/node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/handlebars/node_modules/uglify-js/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/has-binary/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true, + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "dependencies": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/html-minifier/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + }, + "node_modules/html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "deprecated": "3.x is no longer supported", + "dev": true, + "dependencies": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "engines": { + "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/htmlparser2/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "dependencies": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", + "dev": true, + "dependencies": { + "http-proxy": "^1.16.2", + "is-glob": "^4.0.0", + "lodash": "^4.17.5", + "micromatch": "^3.1.9" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "dependencies": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "node_modules/iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "node_modules/immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "node_modules/import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "dependencies": { + "import-from": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "dependencies": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/in-publish": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "dev": true, + "optional": true, + "bin": { + "in-install": "in-install.js", + "in-publish": "in-publish.js", + "not-in-install": "not-in-install.js", + "not-in-publish": "not-in-publish.js" + } + }, + "node_modules/indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "deprecated": "Please update to ini >=1.3.6 to avoid a prototype pollution issue", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/inquirer": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", + "integrity": "sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/internal-ip": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", + "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==", + "dev": true, + "os": [ + "android", + "darwin", + "freebsd", + "linux", + "openbsd", + "sunos", + "win32" + ], + "dependencies": { + "default-gateway": "^2.6.0", + "ipaddr.js": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "dependencies": { + "builtin-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-odd/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "node_modules/is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "node_modules/istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", + "dev": true, + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" + } + }, + "node_modules/istanbul-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", + "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "dev": true, + "dependencies": { + "async": "^2.1.4", + "compare-versions": "^3.1.0", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-hook": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-report": "^1.1.4", + "istanbul-lib-source-maps": "^1.2.4", + "istanbul-reports": "^1.3.0", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" + } + }, + "node_modules/istanbul-api/node_modules/async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "dev": true, + "dependencies": { + "convert-source-map": "^1.5.0", + "istanbul-lib-instrument": "^1.7.3", + "loader-utils": "^1.1.0", + "schema-utils": "^0.3.0" + }, + "engines": { + "node": ">= 4.8 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/istanbul-instrumenter-loader/node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/istanbul-instrumenter-loader/node_modules/schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "dependencies": { + "ajv": "^5.0.0" + }, + "engines": { + "node": ">= 4.3 < 5.0.0 || >= 5.10" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "dev": true + }, + "node_modules/istanbul-lib-hook": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "deprecated": "1.2.0 should have been a major version bump", + "dev": true, + "dependencies": { + "append-transform": "^1.0.0" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "dev": true, + "dependencies": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "node_modules/istanbul-lib-report": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", + "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/istanbul-reports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", + "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "dev": true, + "dependencies": { + "handlebars": "^4.0.3" + } + }, + "node_modules/istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/istanbul/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "node_modules/istanbul/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "dependencies": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", + "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "dev": true + }, + "node_modules/jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "dependencies": { + "colors": "1.1.2" + } + }, + "node_modules/jasmine/node_modules/jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + }, + "node_modules/jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true, + "engines": { + "node": ">= 6.9.x" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT", + "peer": true + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true, + "optional": true + }, + "node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "node_modules/jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "deprecated": "Please use the native JSON object instead of JSON 3", + "dev": true + }, + "node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/jszip": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", + "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "dev": true, + "dependencies": { + "core-js": "~2.3.0", + "es6-promise": "~3.0.2", + "lie": "~3.1.0", + "pako": "~1.0.2", + "readable-stream": "~2.0.6" + } + }, + "node_modules/jszip/node_modules/core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true + }, + "node_modules/jszip/node_modules/es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "node_modules/jszip/node_modules/process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "node_modules/karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "dev": true, + "dependencies": { + "bluebird": "^3.3.0", + "body-parser": "^1.16.1", + "chokidar": "^1.4.1", + "colors": "^1.1.0", + "combine-lists": "^1.0.0", + "connect": "^3.6.0", + "core-js": "^2.2.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "expand-braces": "^0.1.1", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^3.8.0", + "log4js": "^0.6.31", + "mime": "^1.3.4", + "minimatch": "^3.0.2", + "optimist": "^0.6.1", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "safe-buffer": "^5.0.1", + "socket.io": "1.7.3", + "source-map": "^0.5.3", + "tmp": "0.0.31", + "useragent": "^2.1.12" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": "0.10 || 0.12 || 4 || 5 || 6 || 7 || 8" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", + "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "dev": true, + "dependencies": { + "fs-access": "^1.0.0", + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage-istanbul-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.1.tgz", + "integrity": "sha512-UcgrHkFehI5+ivMouD8NH/UOHiX4oCAtwaANylzPFdcAuD52fnCUuelacq2gh8tZ4ydhU3+xiXofSq7j5Ehygw==", + "dev": true, + "dependencies": { + "istanbul-api": "^1.3.1", + "minimatch": "^3.0.4" + } + }, + "node_modules/karma-edge-launcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", + "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "dev": true, + "dependencies": { + "edge-launcher": "1.2.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-firefox-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", + "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", + "dev": true + }, + "node_modules/karma-ie-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", + "integrity": "sha1-SXmGhCxJAZA0bNifVJTKmDDG1Zw=", + "dev": true, + "dependencies": { + "lodash": "^4.6.1" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-jasmine": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", + "dev": true, + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "jasmine-core": "*", + "karma": "*" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "dev": true, + "dependencies": { + "karma-jasmine": "^1.0.2" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-junit-reporter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.2.0.tgz", + "integrity": "sha1-T5xAzt+xo5X4rvh2q/lhiZF8Y5Y=", + "dev": true, + "dependencies": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "8.2.2" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-junit-reporter/node_modules/xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz", + "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma-spec-reporter": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.36.tgz", + "integrity": "sha512-11bvOl1x6ryKZph7kmbmMpbi8vsngEGxGOoeTlIcDaH3ab3j8aPJnZ+r+K/SS0sBSGy5VGkGYO2+hLct7hw/6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "1.4.0" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-spec-reporter/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/karma/node_modules/anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "dependencies": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "node_modules/karma/node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", + "dev": true, + "dependencies": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.0.0" + } + }, + "node_modules/karma/node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/karma/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "node_modules/karma/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/less": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz", + "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==", + "dev": true, + "dependencies": { + "clone": "^2.1.2" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=4" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz", + "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "loader-utils": "^1.1.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">= 4.8 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "less": "^2.3.1 || ^3.0.0", + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Of/H79rZqm2aeg4RnP9SMSh19qkKemoLT5VaJV58uH5AxeYWEcBgGFs753JEJ/Hm6BPvQVfIlrrjoBwYj8p7Tw==", + "dev": true, + "dependencies": { + "ejs": "^2.5.7" + }, + "peerDependencies": { + "webpack-sources": ">=1.0.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, + "node_modules/lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "node_modules/log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "dependencies": { + "readable-stream": "~1.0.2", + "semver": "~4.3.3" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/log4js/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/log4js/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/log4js/node_modules/semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/log4js/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "node_modules/loglevel": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", + "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-loglevel?utm_medium=referral&utm_source=npm_fund" + } + }, + "node_modules/long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "node_modules/lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "node_modules/mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "dependencies": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "dev": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/microsoft-adal-angular6": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/microsoft-adal-angular6/-/microsoft-adal-angular6-1.0.0.tgz", + "integrity": "sha512-AU5/WfeHfyb/YhSjewaS/Cqr/SttV1msg33yReH/7B3EtBwlI3+logvo3ez6wJe37D/TYHtxXaoHsX8fYQOcxg==", + "dependencies": { + "@types/adal": "^1.0.29", + "adal-angular": "^1.0.15", + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": "^6.0.0-rc.0 || ^6.0.0", + "@angular/core": "^6.0.0-rc.0 || ^6.0.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.4.tgz", + "integrity": "sha512-o+Jm+ocb0asEngdM6FsZWtZsRzA8koFUudIDwYUfl94M3PejPHG7Vopw5hN9V8WsMkSFpm3tZP3Fesz89EyrfQ==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "engines": { + "node": ">= 6.9.0 <7.0.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^4.4.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "node_modules/minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "dev": true, + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "deprecated": "Critical bug fixed in v2.0.1, please upgrade to the latest version.", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "node_modules/nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "dev": true, + "optional": true + }, + "node_modules/nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "node_modules/ng-bullet": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ng-bullet/-/ng-bullet-1.0.3.tgz", + "integrity": "sha512-qacsE/w/pLlBxebort1rkrE2B4Vc3idutcpe7tYiHVarz0V6Q5SN8E3d6NUp4UFBMOucpHlCpaASp7qEOsxM1Q==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "peerDependencies": { + "@angular/common": ">=5.0.0", + "@angular/core": ">=5.0.0", + "jasmine-core": ">=2.5.0" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-forge": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", + "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.6.0", + "osenv": "^0.1.5", + "semver": "^5.5.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm-registry-client": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.5.1.tgz", + "integrity": "sha512-7rjGF2eA7hKDidGyEWmHTiKfXkbrcQAsGL/Rh4Rt3x3YNRNHhwaTzVJfW3aNvvlhg4G62VCluif0sLCb/i51Hg==", + "dev": true, + "dependencies": { + "concat-stream": "^1.5.2", + "graceful-fs": "^4.1.6", + "normalize-package-data": "~1.0.1 || ^2.0.0", + "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "once": "^1.3.3", + "request": "^2.74.0", + "retry": "^0.10.0", + "safe-buffer": "^5.1.1", + "semver": "2 >=2.2.1 || 3.x || 4 || 5", + "slide": "^1.1.3", + "ssri": "^5.2.4" + }, + "optionalDependencies": { + "npmlog": "2 || ^3.1.0 || ^4.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", + "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.omit/node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "dependencies": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "node_modules/optimist/node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "dependencies": { + "url-parse": "^1.4.3" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "dependencies": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "node_modules/parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/portfinder": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz", + "integrity": "sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw==", + "dev": true, + "dependencies": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-import": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz", + "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==", + "dev": true, + "dependencies": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "node_modules/postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/postcss-loader": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.6.tgz", + "integrity": "sha512-hgiWSc13xVQAq25cVw80CH0l49ZKlAnU1hKPOdRrNj89bokRr/bZF2nT+hebPPF9c9xs8c3gw3Fr2nxtmXYnNg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "postcss": "^6.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^0.4.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/postcss-url": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz", + "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==", + "dev": true, + "dependencies": { + "mime": "^1.4.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.0", + "postcss": "^6.0.1", + "xxhashjs": "^0.2.1" + } + }, + "node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "dependencies": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "node_modules/protractor": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.2.tgz", + "integrity": "sha512-pw4uwwiy5lHZjIguxNpkEwJJa7hVz+bJsvaTI+IbXlfn2qXwzbF8eghW/RmrZwE2sGx82I8etb8lVjQ+JrjejA==", + "dev": true, + "dependencies": { + "@types/node": "^6.0.46", + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "~2.53.39", + "blocking-proxy": "^1.0.0", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "optimist": "~0.6.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "^1.0.0", + "webdriver-manager": "^12.0.6" + }, + "bin": { + "protractor": "bin/protractor", + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/protractor/node_modules/@types/node": { + "version": "6.0.112", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.112.tgz", + "integrity": "sha512-HnekQWq9t3Gl5aBYYi8sGyOWm85M5ub2llMrpQkRY94eJEUhsUr8qYNaeefv22cxxm+D67a+5zIzpl+dpFxdjQ==", + "dev": true + }, + "node_modules/protractor/node_modules/adm-zip": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", + "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", + "dev": true, + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/protractor/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "dependencies": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/protractor/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/protractor/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/protractor/node_modules/webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "dev": true, + "dependencies": { + "adm-zip": "^0.4.7", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.78.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + }, + "bin": { + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.5.0.tgz", + "integrity": "sha512-eELwFtFxL+uhmg4jPZOZXzSrPEYy4CaYQNbcchBbfxY+KjMpnv6XGf/aYWaQG49OTpfi2/DMziXtDM8XuJgoUA==", + "deprecated": "Version no longer supported. Upgrade to @latest", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "debug": "^3.1.0", + "extract-zip": "^1.6.6", + "https-proxy-agent": "^2.2.1", + "mime": "^2.0.3", + "progress": "^2.0.0", + "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.1", + "ws": "^5.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/puppeteer/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/puppeteer/node_modules/mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/puppeteer/node_modules/ws": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.1.tgz", + "integrity": "sha512-2NkHdPKjDBj3CHdnAGNpmlliryKqF+n9MYXX7/wsVC4yqYocKreKNjydPDvT3wShAZnndlM0RytEfTALCDvz7A==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "dependencies": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + }, + "node_modules/raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "dependencies": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "dependencies": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "node_modules/regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/renderkid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", + "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", + "dev": true, + "dependencies": { + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "strip-ansi": "^3.0.0", + "utila": "^0.4.0" + } + }, + "node_modules/repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true, + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/right-pad": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz", + "integrity": "sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "dependencies": { + "is-promise": "^2.1.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/rxjs": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", + "integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.49.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", + "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "dev": true, + "dependencies": { + "clone-deep": "^2.0.1", + "loader-utils": "^1.0.1", + "lodash.tail": "^4.1.1", + "neo-async": "^2.5.0", + "pify": "^3.0.0", + "semver": "^5.5.0" + }, + "engines": { + "node": ">= 6.9.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/sass/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sass/node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/sass/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sass/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/sass/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/sass/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", + "dev": true + }, + "node_modules/schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "node_modules/selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "dependencies": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/selenium-webdriver/node_modules/tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/selfsigned": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", + "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, + "dependencies": { + "node-forge": "0.9.0" + } + }, + "node_modules/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "dependencies": { + "semver": "^5.3.0" + } + }, + "node_modules/semver-intersect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.3.1.tgz", + "integrity": "sha1-j6hKnhAovSOeRTDRo+GB5pjYhLo=", + "dev": true, + "dependencies": { + "semver": "^5.0.0" + } + }, + "node_modules/semver-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", + "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shelljs": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.6.tgz", + "integrity": "sha1-N5zM+1a5HIYB5HkzVutTgpJN6a0=", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "iojs": "*", + "node": ">=0.11.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/silent-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", + "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", + "dev": true, + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "dependencies": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + } + }, + "node_modules/socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "dependencies": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "dependencies": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "dependencies": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + } + }, + "node_modules/socket.io-parser/node_modules/component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/socket.io-parser/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "node_modules/socket.io/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/socket.io/node_modules/object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "node_modules/sockjs-client": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz", + "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==", + "dev": true, + "dependencies": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sockjs-client/node_modules/faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sockjs-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz", + "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==", + "dev": true, + "dependencies": { + "async": "^2.5.0", + "loader-utils": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/source-map-loader/node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", + "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "dependencies": { + "through2": "^2.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "dashdash": "^1.12.0", + "getpass": "^0.1.1", + "safer-buffer": "^2.0.2" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + }, + "optionalDependencies": { + "bcrypt-pbkdf": "^1.0.0", + "ecc-jsbn": "~0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "node_modules/ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/standard-version": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-4.4.0.tgz", + "integrity": "sha512-jJ8FZhnmh9xJRQLnaXiGRLaAUNItIH29lOQZGpL5fd4+jUHto9Ij6SPCYN86h6ZNNXkYq2TYiIVVF7gVyC+pcQ==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "conventional-changelog": "^1.1.0", + "conventional-recommended-bump": "^1.0.0", + "dotgitignore": "^1.0.3", + "figures": "^1.5.0", + "fs-access": "^1.0.0", + "semver": "^5.1.0", + "yargs": "^8.0.1" + }, + "bin": { + "standard-version": "bin/cli.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/standard-version/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-version/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-version/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/standard-version/node_modules/cliui/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-version/node_modules/figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-version/node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "dependencies": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-version/node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/standard-version/node_modules/y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "node_modules/standard-version/node_modules/yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + } + }, + "node_modules/standard-version/node_modules/yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz", + "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=", + "dev": true, + "dependencies": { + "lodash": "^4.17.4" + }, + "peerDependencies": { + "webpack": ">=1.0.0" + } + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "optional": true, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "dependencies": { + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-loader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz", + "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "schema-utils": "^0.4.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "dev": true, + "dependencies": { + "css-parse": "1.7.x", + "debug": "*", + "glob": "7.0.x", + "mkdirp": "0.5.x", + "sax": "0.5.x", + "source-map": "0.1.x" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", + "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "when": "~3.6.x" + }, + "peerDependencies": { + "stylus": ">=0.52.4" + } + }, + "node_modules/stylus/node_modules/glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus/node_modules/source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/text-mask-addons": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/text-mask-addons/-/text-mask-addons-3.7.2.tgz", + "integrity": "sha512-uezfAuWcDJ/B/lu5ooz38DEdI8NfIVX2xKwshLs40Er8MBGSppklOqOYw32wEmCUP7XPVX2u3u2TdQcIRuBLXA==" + }, + "node_modules/text-mask-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/text-mask-core/-/text-mask-core-5.1.1.tgz", + "integrity": "sha512-tcHJSs0jlHpIed2flCuZZhUIGN1KfDsN/qCW7lkYxyXkKt4OLd3GXCHPFsY9rOLhmyC43ZRTM+tCxizBzluWpw==" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "dependencies": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tree-kill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", + "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "optional": true, + "dependencies": { + "glob": "^7.1.2" + } + }, + "node_modules/ts-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz", + "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==", + "dev": true, + "dependencies": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.3", + "yn": "^2.0.0" + }, + "bin": { + "ts-node": "dist/bin.js" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ts-node/node_modules/chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-node/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/tsickle": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.29.0.tgz", + "integrity": "sha512-JpID0Lv8/irRtPmqJJxb5fCwfZhjZeKmav9Zna7UjqVuJoSbI49Wue/c2PPybX1SbRrjl7bbI/JsCl0dSUJygA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map": "^0.6.0", + "source-map-support": "^0.5.0" + }, + "bin": { + "tsickle": "src/main.js" + }, + "peerDependencies": { + "typescript": ">=2.4.2 <2.9" + } + }, + "node_modules/tsickle/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "node_modules/tsickle/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" + }, + "node_modules/tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev" + } + }, + "node_modules/tslint/node_modules/chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev" + } + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "node_modules/typescript": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", + "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "dependencies": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-js/node_modules/commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "node_modules/uglifyjs-webpack-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", + "dev": true, + "dependencies": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "engines": { + "node": ">= 4.8 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", + "dev": true, + "dependencies": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "node_modules/union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/url-loader/node_modules/mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "dependencies": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/validate-commit-msg": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/validate-commit-msg/-/validate-commit-msg-2.14.0.tgz", + "integrity": "sha1-5Tg2kQEsuycNzAvCpO/+vhSJDqw=", + "deprecated": "Check out CommitLint which provides the same functionality with a more user-focused experience.", + "dev": true, + "dependencies": { + "conventional-commit-types": "^2.0.0", + "find-parent-dir": "^0.3.0", + "findup": "0.1.5", + "semver-regex": "1.0.0" + }, + "bin": { + "validate-commit-msg": "lib/cli.js" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "dependencies": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webdriver-js-extender": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz", + "integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=", + "dev": true, + "dependencies": { + "@types/selenium-webdriver": "^2.53.35", + "selenium-webdriver": "^2.53.2" + } + }, + "node_modules/webdriver-js-extender/node_modules/sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "node_modules/webdriver-js-extender/node_modules/selenium-webdriver": { + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "dependencies": { + "adm-zip": "0.4.4", + "rimraf": "^2.2.8", + "tmp": "0.0.24", + "ws": "^1.0.1", + "xml2js": "0.4.4" + }, + "engines": { + "node": ">= 4.2.x" + } + }, + "node_modules/webdriver-js-extender/node_modules/tmp": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webdriver-js-extender/node_modules/xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true, + "dependencies": { + "sax": "0.6.x", + "xmlbuilder": ">=1.0.0" + } + }, + "node_modules/webpack": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.16.4.tgz", + "integrity": "sha512-RqUfwp4qMqv3oFwBQQOoK69C2tdu2FHJEqPABPqgjGDvOIOLqkTOhmmdJjpiRabzNAAH1ahmkA3z4xowlHN+VA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/wasm-edit": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "acorn": "^5.6.2", + "acorn-dynamic-import": "^3.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^0.4.4", + "tapable": "^1.0.0", + "uglifyjs-webpack-plugin": "^1.2.4", + "watchpack": "^1.5.0", + "webpack-sources": "^1.0.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "dependencies": { + "source-list-map": "~0.1.7", + "source-map": "~0.4.1" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/webpack-core/node_modules/source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "node_modules/webpack-core/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz", + "integrity": "sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA==", + "dev": true, + "dependencies": { + "memory-fs": "~0.4.1", + "mime": "^2.3.1", + "range-parser": "^1.0.3", + "webpack-log": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", + "dev": true, + "dependencies": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.0.0", + "compression": "^1.5.2", + "connect-history-api-fallback": "^1.3.0", + "debug": "^3.1.0", + "del": "^3.0.0", + "express": "^4.16.2", + "html-entities": "^1.2.0", + "http-proxy-middleware": "~0.18.0", + "import-local": "^2.0.0", + "internal-ip": "^3.0.1", + "ip": "^1.1.5", + "killable": "^1.0.0", + "loglevel": "^1.4.1", + "opn": "^5.1.0", + "portfinder": "^1.0.9", + "schema-utils": "^1.0.0", + "selfsigned": "^1.9.1", + "semver": "^5.6.0", + "serve-index": "^1.7.2", + "sockjs": "0.3.19", + "sockjs-client": "1.3.0", + "spdy": "^4.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^5.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "3.4.0", + "webpack-log": "^2.0.0", + "yargs": "12.0.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 6.11.5" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/webpack-dev-server/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/webpack-dev-server/node_modules/decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "dev": true, + "dependencies": { + "xregexp": "4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "dependencies": { + "invert-kv": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack-dev-server/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", + "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "dev": true, + "dependencies": { + "cliui": "^4.0.0", + "decamelize": "^2.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^10.1.0" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-merge": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz", + "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5" + } + }, + "node_modules/webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "1.1.0-rc.4", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz", + "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=", + "dev": true, + "dependencies": { + "webpack-core": "^0.6.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "webpack": "^1.12.11 || ~2 || ~3 || ~4" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "dev": true + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "dependencies": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "node_modules/wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xml2js/node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xregexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "dev": true, + "dependencies": { + "cuint": "^0.2.2" + } + }, + "node_modules/y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "optional": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "dependencies": { + "fd-slicer": "~1.0.1" + } + }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "node_modules/yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/zone.js": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", + "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" + } + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.6.8.tgz", + "integrity": "sha512-ZKTm/zC61iY9IBHOEAKoMSzZpvhkmv+1O/HHzpHEuR551jCzu6vSyCmMY9Z7GBcccscCV+hjeSMwgFrFRcqlkw==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "rxjs": "^6.0.0" + } + }, + "@angular-devkit/build-angular": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.8.9.tgz", + "integrity": "sha512-J6o0MwIG1cJT29p87c7uUn7NY3QLEoQOVw4VXWM9cqG9bv99VK7f7eOSDhHJbXn7Snm4XYrye0zRa3RFXhMG+A==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.8.9", + "@angular-devkit/build-optimizer": "0.8.9", + "@angular-devkit/build-webpack": "0.8.9", + "@angular-devkit/core": "0.8.9", + "@ngtools/webpack": "6.2.9", + "ajv": "6.4.0", + "autoprefixer": "8.6.5", + "circular-dependency-plugin": "5.0.2", + "clean-css": "4.2.1", + "copy-webpack-plugin": "4.5.2", + "file-loader": "1.1.11", + "glob": "7.1.3", + "html-webpack-plugin": "3.2.0", + "istanbul": "0.4.5", + "istanbul-instrumenter-loader": "3.0.1", + "karma-source-map-support": "1.3.0", + "less": "3.8.1", + "less-loader": "4.1.0", + "license-webpack-plugin": "1.5.0", + "loader-utils": "1.1.0", + "mini-css-extract-plugin": "0.4.4", + "minimatch": "3.0.4", + "node-sass": "^4.9.3", + "opn": "5.4.0", + "parse5": "4.0.0", + "portfinder": "1.0.19", + "postcss": "6.0.23", + "postcss-import": "11.1.0", + "postcss-loader": "2.1.6", + "postcss-url": "7.3.2", + "raw-loader": "0.5.1", + "rxjs": "6.2.2", + "sass-loader": "7.1.0", + "semver": "5.6.0", + "source-map-loader": "0.2.4", + "source-map-support": "0.5.9", + "stats-webpack-plugin": "0.6.2", + "style-loader": "0.21.0", + "stylus": "0.54.5", + "stylus-loader": "3.0.2", + "tree-kill": "1.2.1", + "uglifyjs-webpack-plugin": "1.3.0", + "url-loader": "1.1.2", + "webpack": "4.16.4", + "webpack-dev-middleware": "3.4.0", + "webpack-dev-server": "3.1.14", + "webpack-merge": "4.1.4", + "webpack-sources": "1.3.0", + "webpack-subresource-integrity": "1.1.0-rc.4" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.8.9.tgz", + "integrity": "sha512-2tiGPkvJyFY/G3a27uC8r6Jj3H5m8SxjMqhjNUQ5AtNumweTBPt3YIYMNAvHUmxG0nA9upDolVXFmoQGK9AhKQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + } + }, + "@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "optional": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "optional": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "optional": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "optional": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "optional": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "optional": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + } + } + }, + "node-sass": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", + "dev": true, + "optional": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "2.2.5", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + } + }, + "opn": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", + "integrity": "sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "optional": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "optional": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "optional": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "optional": true + }, + "rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "sass-graph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^13.3.2" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "optional": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "optional": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "optional": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "optional": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + } + } + }, + "@angular-devkit/build-optimizer": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.8.9.tgz", + "integrity": "sha512-h8u5iAhSmt0TsLDZXZCmOkXZDMgP2itLkgZvOIsGInyMAESJuWK4P1qegMSv2R5ELOsinJiuhe218M4K2enEdA==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7", + "typescript": "2.9.2", + "webpack-sources": "1.3.0" + }, + "dependencies": { + "typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.8.9.tgz", + "integrity": "sha512-2csJ6utodPSLABTXfBLymYLrndJURF3xVqVjEDzUFl9zLqK1YOkKH4XPr12vfH8SfAtvzIutNLRxBtAuWJmDlw==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.8.9", + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.8.9.tgz", + "integrity": "sha512-2tiGPkvJyFY/G3a27uC8r6Jj3H5m8SxjMqhjNUQ5AtNumweTBPt3YIYMNAvHUmxG0nA9upDolVXFmoQGK9AhKQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2" + } + }, + "@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "@angular-devkit/core": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.6.8.tgz", + "integrity": "sha512-rkIa1OSVWTt4g9leLSK/PsqOj3HZbDKHbZjqlslyfVa3AyCeiumFoOgViOVXlYgPX3HHDbE5uH24nyUWSD8uww==", + "dev": true, + "requires": { + "ajv": "~6.4.0", + "chokidar": "^2.0.3", + "rxjs": "^6.0.0", + "source-map": "^0.5.6" + }, + "dependencies": { + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "@angular-devkit/schematics": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.6.8.tgz", + "integrity": "sha512-R4YqAUdo62wtrhX/5HSRGSKXNTWqfQb66ZE6m8jj6GEJNFKdNXMdxOchxr07LCiKTxfh1w6G3nGzxIsu/+D4KA==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "rxjs": "^6.0.0" + } + }, + "@angular/animations": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.0.4.tgz", + "integrity": "sha512-Ro1XEwShk8XRAogfu73fKLTBLnND1s+MGhN+ymwr7ib9hqxVr7jMNE+MXPqG2/1BX9c+7NHb30B8G4woQn62vw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/cdk": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-6.4.7.tgz", + "integrity": "sha512-18x0U66fLD5kGQWZ9n3nb75xQouXlWs7kUDaTd8HTrHpT1s2QIAqlLd1KxfrYiVhsEC2jPQaoiae7VnBlcvkBg==", + "requires": { + "tslib": "^1.7.1" + } + }, + "@angular/cli": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.0.8.tgz", + "integrity": "sha512-DhH1Zq5Yonthw6zh6W07fhf+9XrAZbD1fcQ0MrmbxlieCfLlTAdBqyK2LavFCKwSZkUMLF6UHM3+jiNRVZSSIg==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.6.8", + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "@schematics/angular": "0.6.8", + "@schematics/update": "0.6.8", + "opn": "~5.3.0", + "resolve": "^1.1.7", + "rxjs": "^6.0.0", + "semver": "^5.1.0", + "silent-error": "^1.0.0", + "symbol-observable": "^1.2.0", + "yargs-parser": "^10.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "yargs-parser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.0.0.tgz", + "integrity": "sha512-+DHejWujTVYeMHLff8U96rLc4uE4Emncoftvn5AjhB1Jw1pWxLzgBUT/WYbPrHmy6YPEBTZQx5myHhVcuuu64g==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "@angular/common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.0.4.tgz", + "integrity": "sha512-z2UE6a43u49omsqjzwdHhwD+wpCPauuD13FMkVQMLwzugKhNri0LjMk5I0I9xLz9OyXICm2B+wPAQN4d07savg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.0.4.tgz", + "integrity": "sha512-qwjuPacuRKUroD+xev653mljV9Jwhi2tvwGo/cVadq0isJ2rVH1hO6sa1FWFnNf1KPnyolMobZlMgg8DFW/yRw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler-cli": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.0.4.tgz", + "integrity": "sha512-bkN21JJrsbIdXNfOc9I9iB7AETUe4/QvApC25R3/pERSyhkK7UNNHTqg4FY5xMdSxlZd1ccWj8rvSbS+hFbTBw==", + "dev": true, + "requires": { + "chokidar": "^1.4.2", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "tsickle": "^0.29.0" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "@angular/core": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.0.4.tgz", + "integrity": "sha512-WCW8wyRoIKkjGjOjVLFpUyhQw/K0//s3W0qo1/vH6m9njC63jP/VP+2Xnt8bdtgeET2NYUeyl+w/0vUXqYlnEA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/forms": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.0.4.tgz", + "integrity": "sha512-JcKyCCkAwF3NlWuwPxtpRhFX/8QbFUkA4W0N62L+Odm8G0/P4QhMw/99o5kRs5xrsQRVyFlGvdYOrkgDyyZkGg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/http": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.0.4.tgz", + "integrity": "sha512-bq7cS0/5hM3Af3FolEe7HpKP19/7MJWIPGi5k9rYCGM4dXKFiKa+LA/SbXE0DMiKxhAQbZDwQ9HN04r70DRyBQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/language-service": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-6.0.4.tgz", + "integrity": "sha512-LyFh/sFtnyjJJYO5UKM31nGSSkmGYFMwqzwbG+l0QNQXNoa9jj+OApvu0oOY5F0WP9DBrOAzj74j2M5olprtMQ==", + "dev": true + }, + "@angular/material": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-6.4.7.tgz", + "integrity": "sha512-SdNx7Xovi24Kw9eU6lkLhY/7f2M7L9F+/uh6XuPr4jbGgCUVVpeeVI5ztZhsZRbj1sN+/r1p5w8u62apWWl5Ww==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^1.7.1" + }, + "dependencies": { + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + } + } + }, + "@angular/platform-browser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.0.4.tgz", + "integrity": "sha512-4y6rSqOLuBs+RL0CIM+RZ5T8a5Tm1NisZcTUGvM8lVYPNkqTzNfHQXhBq8mTgRw9JopEwOj+UEqBbgrNX715yQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.0.4.tgz", + "integrity": "sha512-uh7MoQi9kyKPFHrD7HPOU5gWyhF4pPJuGltyMTOtM/f5TNL9QQI2EugijKnnSFLQ8+7UQjmFAVeRqH9jylhIyQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/router": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.0.4.tgz", + "integrity": "sha512-IKUtbbRtHN8I+Hp8/pLqBPyXJmQHrPFGzKU3H43TovD++mmT5AaYVCeJgkGUB//wjtLN3y0X/yrmyWFWcN35Gw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@fortawesome/fontawesome": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome/-/fontawesome-1.1.8.tgz", + "integrity": "sha512-c0/MtkPVT0fmiFcCyYoPjkG9PkMOvfrZw2+0BaJ+Rh6UEcK1AR/LaRgrHHjUkbAbs9LXxQJhFS8CJ4uSnK2+JA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.7.tgz", + "integrity": "sha512-ego8jRVSHfq/iq4KRZJKQeUAdi3ZjGNrqw4oPN3fNdvTBnLCSntwVCnc37bsAJP9UB8MhrTfPnZYxkv2vpS4pg==" + }, + "@fortawesome/fontawesome-free-regular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-regular/-/fontawesome-free-regular-5.0.13.tgz", + "integrity": "sha512-36lz9Idww1L4QaaTcv7GZiOeIP9emJFDUsedvRovI10kmwyd6rN0PKkIjnq0FB4foLhX4Rou8vnbCCmjtqiLug==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + } + }, + "@fortawesome/fontawesome-free-solid": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.13.tgz", + "integrity": "sha512-b+krVnqkdDt52Yfev0x0ZZgtxBQsLw00Zfa3uaVWIDzpNZVtrEXuxldUSUaN/ihgGhSNi8VpvDAdNPVgCKOSxw==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.1.7" + } + }, + "@ng-bootstrap/ng-bootstrap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-2.1.0.tgz", + "integrity": "sha512-ElqT/uayvnAbA9Klt0IZSbQ+BZtyEPdGDGojmYWQEFDNmTmismkM9Cohx4Hy6PakGAItCy7Ghu1isR8ZKkdtnw==", + "requires": {} + }, + "@ngtools/webpack": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.2.9.tgz", + "integrity": "sha512-wZ9ROI4FdA9gnx21ULtA8u6Gmtig3BbT6EfUh2uYisjyL4P9k2p22sfY/Txwu/InnF4LDHOs5xZutOnGvC87vw==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.8.9", + "rxjs": "6.2.2", + "tree-kill": "1.2.1", + "webpack-sources": "1.3.0" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.8.9.tgz", + "integrity": "sha512-Umax3YKBPTQy360TeoSNaIIOJOKoXvN/S2WNTV8wDjSWWNiWLTIlckWMb9DVsafAifjUi0mtOLRFuM4YatKgTw==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "chokidar": "2.0.4", + "rxjs": "6.2.2", + "source-map": "0.5.7" + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha512-uWK8ISUH3jdo9gTIgylnj4QQDKFL6SbQcx9LtwXJ+2biBCSNIhc41aYJO0W1/w+6tEMPsopQ0cOCnWX39u5o0w==", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha512-SoboS4c924cg+wR2vxl8fospPPli3ZmVPIkRpJEWcrGIPeE8Tr3m9zNIyjYKn9YlF8EgiXQDCy3XVZxSFNjh8A==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "@ngx-translate/core": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-10.0.2.tgz", + "integrity": "sha512-7nM3DrJaqKswwtJlbu2kuKNl+hE8Isr18sKsKvGGpSxQk+G0gO0reDlx2PhUNus7TJTkA1C59vU/JoN8hIvZ4g==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@ngx-translate/http-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-3.0.1.tgz", + "integrity": "sha1-ILD5i8bCUyESnT4zAqs8xInApCo=", + "requires": { + "tslib": "^1.9.0" + } + }, + "@schematics/angular": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.6.8.tgz", + "integrity": "sha512-9kRphqTYG5Df/I8fvnT1zMsw0YNDPO9tl18tQZXj4am4raT7l9UCr+WkwJdlBoA5pwG6baWE9sL0iGWV/bzF/g==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "typescript": ">=2.6.2 <2.8" + } + }, + "@schematics/update": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.6.8.tgz", + "integrity": "sha512-1Uq7LYnwL2wBwGVCgNz76QAR13ghAk+2vDDHOi+VX5+usHManxydrpoMGeX66OBPd+y5D3D2MFb+8mYHE7mygg==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.6.8", + "@angular-devkit/schematics": "0.6.8", + "npm-registry-client": "^8.5.1", + "rxjs": "^6.0.0", + "semver": "^5.3.0", + "semver-intersect": "^1.1.2" + } + }, + "@types/adal": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@types/adal/-/adal-1.0.29.tgz", + "integrity": "sha512-6kvyTWzue5lJ8xECAgVdMrs5+UEyPeflzU/L83QdufGnvjdyCiCWVgkfinMB7133ik/x+02T0q1070Z6/ZK6eQ==", + "requires": { + "@types/angular": "*" + } + }, + "@types/angular": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.51.tgz", + "integrity": "sha512-wYU+/zlJWih7ZmonWVjGQ18tG7GboI9asMNjRBM5fpIFJWXSioQttCTw9qGL44cP82ghM8sCV9apEqm1zBDq2w==" + }, + "@types/jasmine": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", + "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", + "dev": true + }, + "@types/jasminewd2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz", + "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==", + "dev": true, + "requires": { + "@types/jasmine": "*" + } + }, + "@types/node": { + "version": "8.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", + "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "2.53.43", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz", + "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.13.tgz", + "integrity": "sha512-49nwvW/Hx9i+OYHg+mRhKZfAlqThr11Dqz8TsrvqGKMhdI2ijy3KBJOun2Z4770TPjrIJhR6KxChQIDaz8clDA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "debug": "^3.1.0", + "mamacro": "^0.0.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.13.tgz", + "integrity": "sha512-vrvvB18Kh4uyghSKb0NTv+2WZx871WL2NzwMj61jcq2bXkyhRC+8Q0oD7JGVf0+5i/fKQYQSBCNMMsDMRVAMqA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.13.tgz", + "integrity": "sha512-dBh2CWYqjaDlvMmRP/kudxpdh30uXjIbpkLj9HQe+qtYlwvYjPRjdQXrq1cTAAOUSMTtzqbXIxEdEZmyKfcwsg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.13.tgz", + "integrity": "sha512-v7igWf1mHcpJNbn4m7e77XOAWXCDT76Xe7Is1VQFXc4K5jRcFrl9D0NrqM4XifQ0bXiuTSkTKMYqDxu5MhNljA==", + "dev": true, + "requires": { + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.13.tgz", + "integrity": "sha512-yN6ScQQDFCiAXnVctdVO/J5NQRbwyTbQzsGzEgXsAnrxhjp0xihh+nNHQTMrq5UhOqTb5LykpJAvEv9AT0jnAQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.5.13" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.13.tgz", + "integrity": "sha512-hSIKzbXjVMRvy3Jzhgu+vDd/aswJ+UMEnLRCkZDdknZO3Z9e6rp1DAs0tdLItjCFqkz9+0BeOPK/mk3eYvVzZg==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.13.tgz", + "integrity": "sha512-zxJXULGPLB7r+k+wIlvGlXpT4CYppRz8fLUM/xobGHc9Z3T6qlmJD9ySJ2jknuktuuiR9AjnNpKYDECyaiX+QQ==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "mamacro": "^0.0.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.13.tgz", + "integrity": "sha512-0n3SoNGLvbJIZPhtMFq0XmmnA/YmQBXaZKQZcW8maGKwLpVcgjNrxpFZHEOLKjXJYVN5Il8vSfG7nRX50Zn+aw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.13.tgz", + "integrity": "sha512-IJ/goicOZ5TT1axZFSnlAtz4m8KEjYr12BNOANAwGFPKXM4byEDaMNXYowHMG0yKV9a397eU/NlibFaLwr1fbw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/ieee754": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.5.13.tgz", + "integrity": "sha512-TseswvXEPpG5TCBKoLx9tT7+/GMACjC1ruo09j46ULRZWYm8XHpDWaosOjTnI7kr4SRJFzA6MWoUkAB+YCGKKg==", + "dev": true, + "requires": { + "ieee754": "^1.1.11" + } + }, + "@webassemblyjs/leb128": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.5.13.tgz", + "integrity": "sha512-0NRMxrL+GG3eISGZBmLBLAVjphbN8Si15s7jzThaw1UE9e5BY1oH49/+MA1xBzxpf1OW5sf9OrPDOclk9wj2yg==", + "dev": true, + "requires": { + "long": "4.0.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + } + } + }, + "@webassemblyjs/utf8": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.5.13.tgz", + "integrity": "sha512-Ve1ilU2N48Ew0lVGB8FqY7V7hXjaC4+PeZM+vDYxEd+R2iQ0q+Wb3Rw8v0Ri0+rxhoz6gVGsnQNb4FjRiEH/Ng==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.13.tgz", + "integrity": "sha512-X7ZNW4+Hga4f2NmqENnHke2V/mGYK/xnybJSIXImt1ulxbCOEs/A+ZK/Km2jgihjyVxp/0z0hwIcxC6PrkWtgw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/helper-wasm-section": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "@webassemblyjs/wast-printer": "1.5.13", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.13.tgz", + "integrity": "sha512-yfv94Se8R73zmr8GAYzezFHc3lDwE/lBXQddSiIZEKZFuqy7yWtm3KMwA1uGbv5G1WphimJxboXHR80IgX1hQA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.13.tgz", + "integrity": "sha512-IkXSkgzVhQ0QYAdIayuCWMmXSYx0dHGU8Ah/AxJf1gBvstMWVnzJnBwLsXLyD87VSBIcsqkmZ28dVb0mOC3oBg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.13.tgz", + "integrity": "sha512-XnYoIcu2iqq8/LrtmdnN3T+bRjqYFjRHqWbqK3osD/0r/Fcv4d9ecRzjVtC29ENEuNTK4mQ9yyxCBCbK8S/cpg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.5.13.tgz", + "integrity": "sha512-Lbz65T0LQ1LgzKiUytl34CwuhMNhaCLgrh0JW4rJBN6INnBB8NMwUfQM+FxTnLY9qJ+lHJL/gCM5xYhB9oWi4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/floating-point-hex-parser": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-code-frame": "1.5.13", + "@webassemblyjs/helper-fsm": "1.5.13", + "long": "^3.2.0", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.5.13.tgz", + "integrity": "sha512-QcwogrdqcBh8Z+eUF8SG+ag5iwQSXxQJELBEHmLkk790wgQgnIMmntT2sMAMw53GiFNckArf5X0bsCA44j3lWQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "long": "^3.2.0" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "dependencies": { + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + } + } + }, + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "dev": true, + "requires": { + "acorn": "^5.0.0" + } + }, + "adal-angular": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/adal-angular/-/adal-angular-1.0.17.tgz", + "integrity": "sha1-bpNuDkH5HTsqiOf/ypwvb29WLMQ=" + }, + "adm-zip": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "requires": {} + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true, + "requires": {} + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "angular2-text-mask": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/angular2-text-mask/-/angular2-text-mask-9.0.0.tgz", + "integrity": "sha512-iALcnhJPS1zvX48d86rgUgDe/crX6XfhZrXC4Gdlo2/YwZW7u7KJZY6/b3ieSCIWVq/E6p+wDCzeo3E6leRjDA==", + "requires": { + "text-mask-core": "^5.0.0" + } + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "app-root-path": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", + "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true, + "optional": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "autoprefixer": { + "version": "8.6.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.5.tgz", + "integrity": "sha512-PLWJN3Xo/rycNkx+mp8iBDMTm3FeWe4VmYaZDSqL5QQB9sLsQkG5k8n+LNDFnhh9kdq2K+egL/icpctOmDHwig==", + "dev": true, + "requires": { + "browserslist": "^3.2.8", + "caniuse-lite": "^1.0.30000864", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^6.0.23", + "postcss-value-parser": "^3.2.3" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "optional": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "bootstrap": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.1.tgz", + "integrity": "sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg==", + "requires": {} + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000844", + "electron-to-chromium": "^1.3.47" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "dev": true, + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cachedir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.1.0.tgz", + "integrity": "sha512-xGBpPqoBvn3unBW7oxgb8aJn42K0m9m1/wyjmazah10Fq7bROGG3kRAE6OIyr3U3PIJUqGuebhCEdMk9OKJG0A==", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001035", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz", + "integrity": "sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "requires": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.1.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-dependency-plugin": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz", + "integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==", + "dev": true, + "requires": {} + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.0", + "shallow-clone": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codelyzer": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz", + "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==", + "dev": true, + "requires": { + "app-root-path": "^2.0.1", + "css-selector-tokenizer": "^0.7.0", + "cssauron": "^1.4.0", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.6", + "sprintf-js": "^1.0.3" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "^4.5.0" + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "commitizen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-3.0.4.tgz", + "integrity": "sha512-djR5F7RBsGALyUEm/B1H/85nsN4L1F5DhWN+9/efSwqHDSyhw2MK6MF2VRuD26PUqGkQbcUlYO61btkTWjcjVw==", + "dev": true, + "requires": { + "cachedir": "2.1.0", + "cz-conventional-changelog": "2.1.0", + "dedent": "0.7.0", + "detect-indent": "^5.0.0", + "find-node-modules": "1.0.4", + "find-root": "1.1.0", + "fs-extra": "^7.0.0", + "glob": "7.1.3", + "inquirer": "6.2.0", + "lodash": "4.17.11", + "minimist": "1.2.0", + "shelljs": "0.7.6", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "dev": true + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + } + }, + "compare-versions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true, + "optional": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "conventional-changelog": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-1.1.24.tgz", + "integrity": "sha512-2WcSUst4Y3Z4hHvoMTWXMJr/DmgVdLiMOVY1Kak2LfFz+GIz2KDp5naqbFesYbfXPmaZ5p491dO0FWZIJoJw1Q==", + "dev": true, + "requires": { + "conventional-changelog-angular": "^1.6.6", + "conventional-changelog-atom": "^0.2.8", + "conventional-changelog-codemirror": "^0.3.8", + "conventional-changelog-core": "^2.0.11", + "conventional-changelog-ember": "^0.3.12", + "conventional-changelog-eslint": "^1.0.9", + "conventional-changelog-express": "^0.3.6", + "conventional-changelog-jquery": "^0.1.0", + "conventional-changelog-jscs": "^0.1.0", + "conventional-changelog-jshint": "^0.3.8", + "conventional-changelog-preset-loader": "^1.1.8" + } + }, + "conventional-changelog-angular": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", + "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-atom": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-0.2.8.tgz", + "integrity": "sha512-8pPZqhMbrnltNBizjoDCb/Sz85KyUXNDQxuAEYAU5V/eHn0okMBVjqc8aHWYpHrytyZWvMGbayOlDv7i8kEf6g==", + "dev": true, + "requires": { + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-codemirror": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-0.3.8.tgz", + "integrity": "sha512-3HFZKtBXTaUCHvz7ai6nk2+psRIkldDoNzCsom0egDtVmPsvvHZkzjynhdQyULfacRSsBTaiQ0ol6nBOL4dDiQ==", + "dev": true, + "requires": { + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-core": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", + "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", + "dev": true, + "requires": { + "conventional-changelog-writer": "^3.0.9", + "conventional-commits-parser": "^2.1.7", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "^1.3.6", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^1.3.6", + "lodash": "^4.2.1", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^1.1.0", + "read-pkg-up": "^1.0.1", + "through2": "^2.0.0" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-ember": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-0.3.12.tgz", + "integrity": "sha512-mmJzA7uzbrOqeF89dMMi6z17O07ORTXlTMArnLG9ZTX4oLaKNolUlxFUFlFm9JUoVWajVpaHQWjxH1EOQ+ARoQ==", + "dev": true, + "requires": { + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-eslint": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-1.0.9.tgz", + "integrity": "sha512-h87nfVh2fdk9fJIvz26wCBsbDC/KxqCc5wSlNMZbXcARtbgNbNDIF7Y7ctokFdnxkzVdaHsbINkh548T9eBA7Q==", + "dev": true, + "requires": { + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-express": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-0.3.6.tgz", + "integrity": "sha512-3iWVtBJZ9RnRnZveNDzOD8QRn6g6vUif0qVTWWyi5nUIAbuN1FfPVyKdAlJJfp5Im+dE8Kiy/d2SpaX/0X678Q==", + "dev": true, + "requires": { + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-jquery": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-0.1.0.tgz", + "integrity": "sha1-Agg5cWLjhGmG5xJztsecW1+A9RA=", + "dev": true, + "requires": { + "q": "^1.4.1" + } + }, + "conventional-changelog-jscs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jscs/-/conventional-changelog-jscs-0.1.0.tgz", + "integrity": "sha1-BHnrRDzH1yxYvwvPDvHURKkvDlw=", + "dev": true, + "requires": { + "q": "^1.4.1" + } + }, + "conventional-changelog-jshint": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-0.3.8.tgz", + "integrity": "sha512-hn9QU4ZI/5V50wKPJNPGT4gEWgiBFpV6adieILW4MaUFynuDYOvQ71EMSj3EznJyKi/KzuXpc9dGmX8njZMjig==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + }, + "dependencies": { + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + } + } + }, + "conventional-changelog-preset-loader": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", + "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==", + "dev": true + }, + "conventional-changelog-writer": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", + "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "conventional-commits-filter": "^1.1.6", + "dateformat": "^3.0.0", + "handlebars": "^4.0.2", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "semver": "^5.5.0", + "split": "^1.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "conventional-commit-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz", + "integrity": "sha1-XblXOdbCEqy+e29lahG5QLqmiUY=", + "dev": true + }, + "conventional-commits-filter": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", + "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", + "dev": true, + "requires": { + "is-subset": "^0.1.1", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", + "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", + "dev": true, + "requires": { + "is-text-path": "^1.0.0", + "JSONStream": "^1.0.4", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0", + "trim-off-newlines": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "conventional-recommended-bump": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-1.2.1.tgz", + "integrity": "sha512-oJjG6DkRgtnr/t/VrPdzmf4XZv8c4xKVJrVT4zrSHd92KEL+EYxSbYoKq8lQ7U5yLMw7130wrcQTLRjM/T+d4w==", + "dev": true, + "requires": { + "concat-stream": "^1.4.10", + "conventional-commits-filter": "^1.1.1", + "conventional-commits-parser": "^2.1.1", + "git-raw-commits": "^1.3.0", + "git-semver-tags": "^1.3.0", + "meow": "^3.3.0", + "object-assign": "^4.0.1" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==", + "dev": true, + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "globby": "^7.1.1", + "is-glob": "^4.0.0", + "loader-utils": "^1.1.0", + "minimatch": "^3.0.4", + "p-limit": "^1.0.0", + "serialize-javascript": "^1.4.0" + } + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-parse": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", + "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "X.X.X" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "cz-conventional-changelog": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz", + "integrity": "sha1-L0vHOQ4yROTfKT5ro1Hkx0Cnx2Q=", + "dev": true, + "requires": { + "conventional-commit-types": "^2.0.0", + "lodash.map": "^4.5.1", + "longest": "^1.0.1", + "right-pad": "^1.0.1", + "word-wrap": "^1.0.3" + } + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-gateway": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", + "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==", + "dev": true, + "requires": { + "execa": "^0.10.0", + "ip-regex": "^2.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + } + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "dev": true, + "requires": { + "fs-exists-sync": "^0.1.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "dotgitignore": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-1.0.3.tgz", + "integrity": "sha512-eu5XjSstm0WXQsARgo6kPjkINYZlOUW+z/KtAAIBjHa5mUpMPrxJytbPIndWz6GubBuuuH5ljtVcXKnVnH5q8w==", + "dev": true, + "requires": { + "find-up": "^2.1.0", + "minimatch": "^3.0.4" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "edge-launcher": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", + "integrity": "sha1-60Cq+9Bnpup27/+rBke81VCbN7I=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.376.tgz", + "integrity": "sha512-cv/PYVz5szeMz192ngilmezyPNFkUjuynuL2vNdiqIrio440nfTDdc0JJU0TS2KHLSVCs9gBbt4CFqM+HcBnjw==", + "dev": true + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "optional": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "requires": { + "mime-types": "~2.1.11", + "negotiator": "0.6.1" + } + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "enhanced-resolve": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "dev": true + }, + "events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "^0.2.3", + "array-unique": "^0.2.1", + "braces": "^0.1.2" + }, + "dependencies": { + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "^0.1.0" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true, + "requires": { + "os-homedir": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + } + }, + "file-saver": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz", + "integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g==" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-node-modules": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-1.0.4.tgz", + "integrity": "sha1-tt6zzMtpnIcDdne87eLF9YYrJVA=", + "dev": true, + "requires": { + "findup-sync": "0.4.2", + "merge": "^1.2.0" + } + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "findup": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", + "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", + "dev": true, + "requires": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", + "dev": true + } + } + }, + "findup-sync": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.2.tgz", + "integrity": "sha1-qBF9D3MST1pFRoOVef5S1xKfteU=", + "dev": true, + "requires": { + "detect-file": "^0.1.0", + "is-glob": "^2.0.1", + "micromatch": "^2.3.7", + "resolve-dir": "^0.1.0" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.0.tgz", + "integrity": "sha512-fdrt472/9qQ6Kgjvb935ig6vJCuofpBUD14f9Vb+SLlm7xIe4Qva5gey8EKtv8lp7ahE1wilg3xL1znpVGtZIA==", + "dev": true, + "requires": { + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "^1.0.0" + } + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs-extra": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", + "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.2.4", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true, + "dev": true, + "optional": true + }, + "string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "1.0.2", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.2", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "optional": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-describe": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.0.3.tgz", + "integrity": "sha1-2BSCqK2nodgeBg1DisjhG0o5AzY=", + "requires": { + "lodash": "^4.16.6", + "semver": "^5.3.0" + } + }, + "git-raw-commits": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", + "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "git-semver-tags": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", + "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", + "dev": true, + "requires": { + "meow": "^4.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true, + "requires": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "dependencies": { + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + } + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "dependencies": { + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + } + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "optional": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + } + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", + "dev": true, + "requires": { + "http-proxy": "^1.16.2", + "is-glob": "^4.0.0", + "lodash": "^4.17.5", + "micromatch": "^3.1.9" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "dev": true, + "optional": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", + "integrity": "sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "internal-ip": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", + "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==", + "dev": true, + "requires": { + "default-gateway": "^2.6.0", + "ipaddr.js": "^1.5.2" + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", + "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "dev": true, + "requires": { + "async": "^2.1.4", + "compare-versions": "^3.1.0", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-hook": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-report": "^1.1.4", + "istanbul-lib-source-maps": "^1.2.4", + "istanbul-reports": "^1.3.0", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + } + } + }, + "istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "dev": true, + "requires": { + "convert-source-map": "^1.5.0", + "istanbul-lib-instrument": "^1.7.3", + "loader-utils": "^1.1.0", + "schema-utils": "^0.3.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "^5.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", + "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", + "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "dev": true, + "requires": { + "handlebars": "^4.0.3" + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "requires": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + } + } + }, + "jasmine-core": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", + "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true + }, + "jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "peer": true + }, + "js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true, + "optional": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jszip": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", + "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "dev": true, + "requires": { + "core-js": "~2.3.0", + "es6-promise": "~3.0.2", + "lie": "~3.1.0", + "pako": "~1.0.2", + "readable-stream": "~2.0.6" + }, + "dependencies": { + "core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "dev": true + }, + "es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "dev": true, + "requires": { + "bluebird": "^3.3.0", + "body-parser": "^1.16.1", + "chokidar": "^1.4.1", + "colors": "^1.1.0", + "combine-lists": "^1.0.0", + "connect": "^3.6.0", + "core-js": "^2.2.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "expand-braces": "^0.1.1", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^3.8.0", + "log4js": "^0.6.31", + "mime": "^1.3.4", + "minimatch": "^3.0.2", + "optimist": "^0.6.1", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "safe-buffer": "^5.0.1", + "socket.io": "1.7.3", + "source-map": "^0.5.3", + "tmp": "0.0.31", + "useragent": "^2.1.12" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } + } + }, + "karma-chrome-launcher": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", + "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "dev": true, + "requires": { + "fs-access": "^1.0.0", + "which": "^1.2.1" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.1.tgz", + "integrity": "sha512-UcgrHkFehI5+ivMouD8NH/UOHiX4oCAtwaANylzPFdcAuD52fnCUuelacq2gh8tZ4ydhU3+xiXofSq7j5Ehygw==", + "dev": true, + "requires": { + "istanbul-api": "^1.3.1", + "minimatch": "^3.0.4" + } + }, + "karma-edge-launcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", + "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "dev": true, + "requires": { + "edge-launcher": "1.2.2" + } + }, + "karma-firefox-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", + "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", + "dev": true + }, + "karma-ie-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", + "integrity": "sha1-SXmGhCxJAZA0bNifVJTKmDDG1Zw=", + "dev": true, + "requires": { + "lodash": "^4.6.1" + } + }, + "karma-jasmine": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", + "dev": true, + "requires": {} + }, + "karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "dev": true, + "requires": { + "karma-jasmine": "^1.0.2" + } + }, + "karma-junit-reporter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.2.0.tgz", + "integrity": "sha1-T5xAzt+xo5X4rvh2q/lhiZF8Y5Y=", + "dev": true, + "requires": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "8.2.2" + }, + "dependencies": { + "xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", + "dev": true + } + } + }, + "karma-source-map-support": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz", + "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "karma-spec-reporter": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.36.tgz", + "integrity": "sha512-11bvOl1x6ryKZph7kmbmMpbi8vsngEGxGOoeTlIcDaH3ab3j8aPJnZ+r+K/SS0sBSGy5VGkGYO2+hLct7hw/6w==", + "dev": true, + "requires": { + "colors": "1.4.0" + }, + "dependencies": { + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + } + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "less": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz", + "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "less-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz", + "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "loader-utils": "^1.1.0", + "pify": "^3.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "license-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Of/H79rZqm2aeg4RnP9SMSh19qkKemoLT5VaJV58uH5AxeYWEcBgGFs753JEJ/Hm6BPvQVfIlrrjoBwYj8p7Tw==", + "dev": true, + "requires": { + "ejs": "^2.5.7" + } + }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "~1.0.2", + "semver": "~4.3.3" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "loglevel": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", + "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "dev": true + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "^3.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "microsoft-adal-angular6": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/microsoft-adal-angular6/-/microsoft-adal-angular6-1.0.0.tgz", + "integrity": "sha512-AU5/WfeHfyb/YhSjewaS/Cqr/SttV1msg33yReH/7B3EtBwlI3+logvo3ez6wJe37D/TYHtxXaoHsX8fYQOcxg==", + "requires": { + "@types/adal": "^1.0.29", + "adal-angular": "^1.0.15", + "tslib": "^1.9.0" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.4.tgz", + "integrity": "sha512-o+Jm+ocb0asEngdM6FsZWtZsRzA8koFUudIDwYUfl94M3PejPHG7Vopw5hN9V8WsMkSFpm3tZP3Fesz89EyrfQ==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "ng-bullet": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ng-bullet/-/ng-bullet-1.0.3.tgz", + "integrity": "sha512-qacsE/w/pLlBxebort1rkrE2B4Vc3idutcpe7tYiHVarz0V6Q5SN8E3d6NUp4UFBMOucpHlCpaASp7qEOsxM1Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-forge": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-package-arg": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", + "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.6.0", + "osenv": "^0.1.5", + "semver": "^5.5.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-registry-client": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.5.1.tgz", + "integrity": "sha512-7rjGF2eA7hKDidGyEWmHTiKfXkbrcQAsGL/Rh4Rt3x3YNRNHhwaTzVJfW3aNvvlhg4G62VCluif0sLCb/i51Hg==", + "dev": true, + "requires": { + "concat-stream": "^1.5.2", + "graceful-fs": "^4.1.6", + "normalize-package-data": "~1.0.1 || ^2.0.0", + "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "npmlog": "2 || ^3.1.0 || ^4.0.0", + "once": "^1.3.3", + "request": "^2.74.0", + "retry": "^0.10.0", + "safe-buffer": "^5.1.1", + "semver": "2 >=2.2.1 || 3.x || 4 || 5", + "slide": "^1.1.3", + "ssri": "^5.2.4" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", + "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "peer": true + }, + "portfinder": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz", + "integrity": "sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-import": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz", + "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.6.tgz", + "integrity": "sha512-hgiWSc13xVQAq25cVw80CH0l49ZKlAnU1hKPOdRrNj89bokRr/bZF2nT+hebPPF9c9xs8c3gw3Fr2nxtmXYnNg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^6.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^0.4.0" + } + }, + "postcss-url": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz", + "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==", + "dev": true, + "requires": { + "mime": "^1.4.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.0", + "postcss": "^6.0.1", + "xxhashjs": "^0.2.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "~2.0.3" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "protractor": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.2.tgz", + "integrity": "sha512-pw4uwwiy5lHZjIguxNpkEwJJa7hVz+bJsvaTI+IbXlfn2qXwzbF8eghW/RmrZwE2sGx82I8etb8lVjQ+JrjejA==", + "dev": true, + "requires": { + "@types/node": "^6.0.46", + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "~2.53.39", + "blocking-proxy": "^1.0.0", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "optimist": "~0.6.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "^1.0.0", + "webdriver-manager": "^12.0.6" + }, + "dependencies": { + "@types/node": { + "version": "6.0.112", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.112.tgz", + "integrity": "sha512-HnekQWq9t3Gl5aBYYi8sGyOWm85M5ub2llMrpQkRY94eJEUhsUr8qYNaeefv22cxxm+D67a+5zIzpl+dpFxdjQ==", + "dev": true + }, + "adm-zip": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", + "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "dev": true, + "requires": { + "adm-zip": "^0.4.7", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.78.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + } + } + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "puppeteer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.5.0.tgz", + "integrity": "sha512-eELwFtFxL+uhmg4jPZOZXzSrPEYy4CaYQNbcchBbfxY+KjMpnv6XGf/aYWaQG49OTpfi2/DMziXtDM8XuJgoUA==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "extract-zip": "^1.6.6", + "https-proxy-agent": "^2.2.1", + "mime": "^2.0.3", + "progress": "^2.0.0", + "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.1", + "ws": "^5.1.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true + }, + "ws": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.1.tgz", + "integrity": "sha512-2NkHdPKjDBj3CHdnAGNpmlliryKqF+n9MYXX7/wsVC4yqYocKreKNjydPDvT3wShAZnndlM0RytEfTALCDvz7A==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "dependencies": { + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", + "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", + "dev": true, + "requires": { + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "strip-ansi": "^3.0.0", + "utila": "^0.4.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true, + "requires": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "right-pad": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz", + "integrity": "sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", + "integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.49.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", + "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "sass-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "dev": true, + "requires": { + "clone-deep": "^2.0.1", + "loader-utils": "^1.0.1", + "lodash.tail": "^4.1.1", + "neo-async": "^2.5.0", + "pify": "^3.0.0", + "semver": "^5.5.0" + } + }, + "saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", + "dev": true + }, + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "requires": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "dependencies": { + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + } + } + }, + "selfsigned": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", + "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, + "requires": { + "node-forge": "0.9.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "devOptional": true + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "semver-intersect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.3.1.tgz", + "integrity": "sha1-j6hKnhAovSOeRTDRo+GB5pjYhLo=", + "dev": true, + "requires": { + "semver": "^5.0.0" + } + }, + "semver-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", + "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "dependencies": { + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + } + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shelljs": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.6.tgz", + "integrity": "sha1-N5zM+1a5HIYB5HkzVutTgpJN6a0=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "silent-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", + "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", + "dev": true, + "requires": { + "debug": "^2.2.0" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "requires": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "sockjs-client": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz", + "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-loader": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz", + "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==", + "dev": true, + "requires": { + "async": "^2.5.0", + "loader-utils": "^1.1.0" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + } + } + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "spdy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", + "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "^2.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "standard-version": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-4.4.0.tgz", + "integrity": "sha512-jJ8FZhnmh9xJRQLnaXiGRLaAUNItIH29lOQZGpL5fd4+jUHto9Ij6SPCYN86h6ZNNXkYq2TYiIVVF7gVyC+pcQ==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "conventional-changelog": "^1.1.0", + "conventional-recommended-bump": "^1.0.0", + "dotgitignore": "^1.0.3", + "figures": "^1.5.0", + "fs-access": "^1.0.0", + "semver": "^5.1.0", + "yargs": "^8.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stats-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz", + "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=", + "dev": true, + "requires": { + "lodash": "^4.17.4" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "optional": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "style-loader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz", + "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^0.4.5" + } + }, + "stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "dev": true, + "requires": { + "css-parse": "1.7.x", + "debug": "*", + "glob": "7.0.x", + "mkdirp": "0.5.x", + "sax": "0.5.x", + "source-map": "0.1.x" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "stylus-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", + "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "when": "~3.6.x" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-mask-addons": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/text-mask-addons/-/text-mask-addons-3.7.2.tgz", + "integrity": "sha512-uezfAuWcDJ/B/lu5ooz38DEdI8NfIVX2xKwshLs40Er8MBGSppklOqOYw32wEmCUP7XPVX2u3u2TdQcIRuBLXA==" + }, + "text-mask-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/text-mask-core/-/text-mask-core-5.1.1.tgz", + "integrity": "sha512-tcHJSs0jlHpIed2flCuZZhUIGN1KfDsN/qCW7lkYxyXkKt4OLd3GXCHPFsY9rOLhmyC43ZRTM+tCxizBzluWpw==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tree-kill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", + "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.2" + } + }, + "ts-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz", + "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.3", + "yn": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tsickle": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.29.0.tgz", + "integrity": "sha512-JpID0Lv8/irRtPmqJJxb5fCwfZhjZeKmav9Zna7UjqVuJoSbI49Wue/c2PPybX1SbRrjl7bbI/JsCl0dSUJygA==", + "dev": true, + "requires": { + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map": "^0.6.0", + "source-map-support": "^0.5.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "tslib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", + "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "dev": true + }, + "uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "requires": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", + "dev": true, + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "dev": true, + "requires": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + } + } + } + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "validate-commit-msg": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/validate-commit-msg/-/validate-commit-msg-2.14.0.tgz", + "integrity": "sha1-5Tg2kQEsuycNzAvCpO/+vhSJDqw=", + "dev": true, + "requires": { + "conventional-commit-types": "^2.0.0", + "find-parent-dir": "^0.3.0", + "findup": "0.1.5", + "semver-regex": "1.0.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webdriver-js-extender": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz", + "integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=", + "dev": true, + "requires": { + "@types/selenium-webdriver": "^2.53.35", + "selenium-webdriver": "^2.53.2" + }, + "dependencies": { + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "selenium-webdriver": { + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "requires": { + "adm-zip": "0.4.4", + "rimraf": "^2.2.8", + "tmp": "0.0.24", + "ws": "^1.0.1", + "xml2js": "0.4.4" + } + }, + "tmp": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true + }, + "xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true, + "requires": { + "sax": "0.6.x", + "xmlbuilder": ">=1.0.0" + } + } + } + }, + "webpack": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.16.4.tgz", + "integrity": "sha512-RqUfwp4qMqv3oFwBQQOoK69C2tdu2FHJEqPABPqgjGDvOIOLqkTOhmmdJjpiRabzNAAH1ahmkA3z4xowlHN+VA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/wasm-edit": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "acorn": "^5.6.2", + "acorn-dynamic-import": "^3.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^0.4.4", + "tapable": "^1.0.0", + "uglifyjs-webpack-plugin": "^1.2.4", + "watchpack": "^1.5.0", + "webpack-sources": "^1.0.1" + } + }, + "webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "requires": { + "source-list-map": "~0.1.7", + "source-map": "~0.4.1" + }, + "dependencies": { + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz", + "integrity": "sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA==", + "dev": true, + "requires": { + "memory-fs": "~0.4.1", + "mime": "^2.3.1", + "range-parser": "^1.0.3", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.0.0", + "compression": "^1.5.2", + "connect-history-api-fallback": "^1.3.0", + "debug": "^3.1.0", + "del": "^3.0.0", + "express": "^4.16.2", + "html-entities": "^1.2.0", + "http-proxy-middleware": "~0.18.0", + "import-local": "^2.0.0", + "internal-ip": "^3.0.1", + "ip": "^1.1.5", + "killable": "^1.0.0", + "loglevel": "^1.4.1", + "opn": "^5.1.0", + "portfinder": "^1.0.9", + "schema-utils": "^1.0.0", + "selfsigned": "^1.9.1", + "semver": "^5.6.0", + "serve-index": "^1.7.2", + "sockjs": "0.3.19", + "sockjs-client": "1.3.0", + "spdy": "^4.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^5.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "3.4.0", + "webpack-log": "^2.0.0", + "yargs": "12.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "dev": true, + "requires": { + "xregexp": "4.0.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "yargs": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", + "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^2.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^10.1.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-merge": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz", + "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-subresource-integrity": { + "version": "1.1.0-rc.4", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz", + "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=", + "dev": true, + "requires": { + "webpack-core": "^0.6.8" + } + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "requires": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + } + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xregexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "dev": true, + "requires": { + "cuint": "^0.2.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "optional": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "optional": true + } + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, + "zone.js": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", + "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/package.json b/LCU.WebApps.External.OnlineLoanApp/package.json new file mode 100644 index 0000000..ec005f6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/package.json @@ -0,0 +1,106 @@ +{ + "name": "ui", + "version": "4.11.0", + "scripts": { + "ng": "ng", + "update-e2e": "webdriver-manager update", + "start": "SET NODE_OPTIONS=--openssl-legacy-provider && ng serve --proxy-config proxy.conf.json --configuration=local --ssl", + "build-develop": "SET NODE_OPTIONS=--openssl-legacy-provider && ng build --configuration=dev", + "build-release": "SET NODE_OPTIONS=--openssl-legacy-provider && ng build --configuration=qa", + "build-master": "SET NODE_OPTIONS=--openssl-legacy-provider && ng build --prod --build-optimizer", + "test": "ng test --watch=false --code-coverage --no-progress", + "test-debug": "ng test --watch=false --source-map=false --no-progress", + "test-interactive": "ng test --watch=false --browsers=Chrome --no-progress", + "lint": "ng lint", + "e2e": "ng e2e", + "postinstall": "node version.js", + "commitmsg": "validate-commit-msg", + "release": "standard-version" + }, + "private": true, + "dependencies": { + "@angular/animations": "^6.0.3", + "@angular/cdk": "6.4.7", + "@angular/common": "^6.0.3", + "@angular/compiler": "^6.0.3", + "@angular/core": "^6.0.3", + "@angular/forms": "^6.0.3", + "@angular/http": "^6.0.3", + "@angular/material": "6.4.7", + "@angular/platform-browser": "^6.0.3", + "@angular/platform-browser-dynamic": "^6.0.3", + "@angular/router": "^6.0.3", + "@fortawesome/fontawesome": "^1.1.8", + "@fortawesome/fontawesome-free-regular": "^5.0.13", + "@fortawesome/fontawesome-free-solid": "^5.0.13", + "@ng-bootstrap/ng-bootstrap": "^2.1.0", + "@ngx-translate/core": "^10.0.2", + "@ngx-translate/http-loader": "^3.0.1", + "angular2-text-mask": "^9.0.0", + "bootstrap": "^4.1.1", + "core-js": "^2.5.4", + "file-saver": "^2.0.0", + "fs-extra": "^7.0.0", + "git-describe": "^4.0.3", + "microsoft-adal-angular6": "^1.0.0", + "rxjs": "^6.0.0", + "text-mask-addons": "^3.7.2", + "zone.js": "^0.8.26" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~0.8.9", + "@angular/cli": "~6.0.8", + "@angular/compiler-cli": "^6.0.3", + "@angular/language-service": "^6.0.3", + "@types/jasmine": "~2.8.6", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.2.1", + "commitizen": "^3.0.4", + "cz-conventional-changelog": "^2.1.0", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.0", + "karma-spec-reporter": "^0.0.36", + "karma-edge-launcher": "^0.4.2", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "~1.1.1", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-junit-reporter": "^1.2.0", + "ng-bullet": "^1.0.3", + "protractor": "~5.3.0", + "puppeteer": "^1.5.0", + "sass": "^1.49.9", + "standard-version": "^4.4.0", + "ts-node": "~5.0.1", + "tslint": "~5.9.1", + "typescript": "~2.7.2", + "validate-commit-msg": "^2.14.0" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + }, + "validate-commit-msg": { + "types": [ + "feat", + "fix", + "docs", + "style", + "refactor", + "perf", + "test", + "chore", + "revert" + ], + "warnOnFail": false, + "maxSubjectLength": 100, + "subjectPattern": "^.*", + "subjectPatternErrorMsg": "Subject must be in format 'Commit message'", + "helpMessage": "" + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/setup-env.ps1 b/LCU.WebApps.External.OnlineLoanApp/setup-env.ps1 new file mode 100644 index 0000000..e78587a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/setup-env.ps1 @@ -0,0 +1,44 @@ +# Landmark Credit Union +# Loan Wizard Environment Setup +# Running this script on a Windows 10 machine should install all of the software you need +# to work on the project for both the client and api. +# You can use 'choco upgrade all' to keep software up to date. + +Write-Output "Starting environment setup, if you use Visual Studio, or Webstorm, install those separately" + +# 1. Create an update function. +function Update-Environment-Path +{ + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") ` + + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") +} + +# 2. Get the Choco package manager, to enable the rest of this script +if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; exit } +Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | iex +Update-Environment-Path + +# 3. Git for source code management. +choco install git --yes --params '/GitAndUnixToolsOnPath' +Update-Environment-Path + +# 4. Node for node and npm. +# 10.14.1 to avoid e2e problems +choco install nodejs --version 10.14.1 --yes +Update-Environment-Path + +# 5. VS Code +choco install visualstudiocode --yes # includes dotnet +Update-Environment-Path +code --install-extension CoenraadS.bracket-pair-colorizer +code --install-extension ms-vscode.PowerShell +code --install-extension ms-vscode.csharp +code --install-extension eg2.vscode-npm-script +code --install-extension christian-kohler.npm-intellisense + +# 6. major browsers +choco install googlechrome --yes +choco install firefox --yes +Update-Environment-Path + +Write-Output "Complete" diff --git a/LCU.WebApps.External.OnlineLoanApp/src/Robots.txt b/LCU.WebApps.External.OnlineLoanApp/src/Robots.txt new file mode 100644 index 0000000..c6742d8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/Robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/LCU.WebApps.External.OnlineLoanApp/src/_variables.scss b/LCU.WebApps.External.OnlineLoanApp/src/_variables.scss new file mode 100644 index 0000000..45e6383 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/_variables.scss @@ -0,0 +1,6 @@ +$font-family-base: Arial, sans-serif; + +$primary: #005B9A; +$secondary: #7ca550; // #FCCA10; +$warning: #F5961E; +$body-bg: #FFFFFF; \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-groups.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-groups.ts new file mode 100644 index 0000000..da516da --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-groups.ts @@ -0,0 +1,30 @@ +import { Validators } from '@angular/forms'; + + +export class AdminGroups { + private static DATE_PATTERN = /^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/; + + + public static readonly FILTER = { + group: 'filterInformation', + fields: [ + {field: 'startDate', validation: [Validators.required, Validators.pattern(AdminGroups.DATE_PATTERN)]}, + {field: 'endDate', validation: [Validators.required, Validators.pattern(AdminGroups.DATE_PATTERN)]}, + {field: 'loanType', validation: []}, + {field: 'branch', validation: []}, + {field: 'decisioned', validation: []}, + {field: 'submitted', validation: []}, + {field: 'inProgress', validation: []}, + {field: 'error', validation: []}, + {field: 'deleted', validation: []}, + ] + }; + + public static readonly CONFIG = { + group: 'configInformation', + fields: [ + {field: 'appStatus', validation: []}, + ] + }; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-routing.module.ts new file mode 100644 index 0000000..769a608 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin-routing.module.ts @@ -0,0 +1,31 @@ +import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { AdminComponent } from './admin.component'; +import { LoanTableComponent } from './loan-table/loan-table.component'; +import { LoanDataComponent } from './loan-data/loan-data.component'; +import { BranchConfigComponent } from './branch-config/branch-config.component'; +import { BranchNameConfigComponent } from './branch-name-config/branch-name-config.component'; +import { SystemConfigComponent } from './system-config/system-config.component'; +import { SchumerPreviewComponent } from './schumer-boxes/schumer-preview.component'; + +const routes: Routes = [ + { + path: '', + component: AdminComponent, + children: [ + {path: 'branch-config', component: BranchConfigComponent}, + {path: 'branch-name-config', component: BranchNameConfigComponent}, + {path: 'loan-table', component: LoanTableComponent}, + {path: 'system-config', component: SystemConfigComponent}, + {path: 'schumer-preview', component: SchumerPreviewComponent}, + {path: 'loan-data/:id', component: LoanDataComponent}, + {path: '', redirectTo: '/admin', pathMatch: 'prefix'}, + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AdminRoutingModule {} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.html new file mode 100644 index 0000000..4e25ed4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.ts new file mode 100644 index 0000000..7d738e0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'lw-admin', + templateUrl: './admin.component.html', + styleUrls: ['./admin.component.scss'] +}) +export class AdminComponent { + constructor(public router: Router ) {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.module.ts new file mode 100644 index 0000000..6316c04 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/admin.module.ts @@ -0,0 +1,68 @@ +import { NgModule } from '@angular/core'; +import { MatSortModule } from '@angular/material/sort'; +import { MatPaginatorModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, + MatTooltipModule, MatSnackBarModule, MatSlideToggleModule } from '@angular/material'; +import { MatTableModule } from '@angular/material/table'; +import { MatInputModule } from '@angular/material/input'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { AdminComponent } from './admin.component'; +import { LoanTableComponent } from './loan-table/loan-table.component'; +import { LoanDataComponent } from './loan-data/loan-data.component'; +import { AdminRoutingModule } from './admin-routing.module'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BranchConfigComponent } from './branch-config/branch-config.component'; +import { BranchNameConfigComponent } from './branch-name-config/branch-name-config.component'; +import { AdminLandingComponent } from './landing/admin-landing.component'; +import { MainCardAdminComponent } from './cards/main-card/main-card.component'; +import { TopNavigationComponent } from './navigation/top-navigation.component'; +import { SharedModule } from '../shared/shared.module'; +import { SnackBarErrorComponent } from './snack-bar-error/snack-bar-error.component'; +import { SystemConfigComponent } from './system-config/system-config.component'; +import { SnackBarService } from './snack-bar-error/snack-bar.service'; +import { SchumerPreviewComponent } from './schumer-boxes/schumer-preview.component'; + +@NgModule({ + imports: [ + AdminRoutingModule, + CommonModule, + SharedModule, + FormsModule, + MatSortModule, + MatPaginatorModule, + MatTableModule, + MatInputModule, + MatTooltipModule, + MatDatepickerModule, + MatNativeDateModule, + MatSelectModule, + MatSnackBarModule, + MatCheckboxModule, + MatSlideToggleModule, + ReactiveFormsModule, + TranslateModule, + ], + entryComponents: [ + SnackBarErrorComponent + ], + declarations: [ + AdminComponent, + AdminLandingComponent, + BranchConfigComponent, + BranchNameConfigComponent, + LoanTableComponent, + LoanDataComponent, + SnackBarErrorComponent, + MainCardAdminComponent, + TopNavigationComponent, + SchumerPreviewComponent, + SystemConfigComponent + ], + providers: [ + MatDatepickerModule, + SnackBarService + ], +}) + +export class AdminModule {} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.html new file mode 100644 index 0000000..8d85b9e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.html @@ -0,0 +1,81 @@ + +
+ +
+

BRANCHMAP.TITLE

+ BRANCHMAP.SUBTEXT +

+ BRANCHMAP.DATETEXT{{lastUpdated | date:'short'}} +

+
+
+
+
+
+
+
BRANCHMAP.DOWNLOAD
+ +
+
+
+
+
+
+
+
+
+
BRANCHMAP.UPLOAD
+ +
+
+
+
+ +
+
+ +
+
+
+
+
Please confirm your upload
+
    +
  • There are currently {{localState.compareResult.currentCount}} branch mappings.
  • +
  • There will be {{localState.compareResult.newCount}} branch mappings.
  • +
  • {{warning}}
  • +
+ +
+
+
There was a problem with your upload, it will not be saved.
+
    +
  • {{localState.compareResult.errorMessage}}
  • +
  • {{warning}}
  • +
+
+
+
+
+ +
+ + + + + + + + + + + +
ZipCode{{loan.zipcode}} Channel ID{{loan.channelId}}
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.scss new file mode 100644 index 0000000..c76ae58 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.scss @@ -0,0 +1,38 @@ +table { + width: 100%; +} + +.mat-form-field { + margin-right: 10px; + font-size: .875em; + width: 20%; +} + +.mat-header-cell { + font-size: 1em; + font-weight: bold; + color: black; +} + +.mat-checkbox { + margin-right: 10px; + :host ::ng-deep .mat-checkbox-background { + background-color: #005B9A + } +} + +.btn-disabled { + margin-bottom: 0px; + margin-left: 0px; +} + +.btn-less-padding { + padding-top: 3px; + padding-bottom: 3px; + padding-left: 8px; + padding-right: 8px; +} + +.shift-right { + margin-left: 5px; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.spec.ts new file mode 100644 index 0000000..cc43d5a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.spec.ts @@ -0,0 +1,66 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { BranchConfigComponent } from './branch-config.component'; +import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule, + MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe'; +import { BranchConfigService } from './branch-config.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TopNavigationComponent } from '../navigation/top-navigation.component'; +import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('BranchConfigComponent', () => { + let component: BranchConfigComponent; + let fixture: ComponentFixture; + let service: BranchConfigService; + let snackBar: MatSnackBar; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + FormsModule, + MatSortModule, + MatPaginatorModule, + MatTableModule, + MatInputModule, + MatSnackBarModule, + MatDatepickerModule, + MatNativeDateModule, + MatSelectModule, + MatCheckboxModule, + ReactiveFormsModule, + HttpClientTestingModule, + NoopAnimationsModule, + TranslateModule.forRoot() + ], + declarations: [ + SnackBarErrorComponent, + BranchConfigComponent, + LoanCodePipe, + TopNavigationComponent + ], + providers: [ + ] + }); + }); + + + beforeEach(() => { + service = TestBed.get(BranchConfigService); + snackBar = TestBed.get(MatSnackBar); + fixture = TestBed.createComponent(BranchConfigComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(service).toBeTruthy(); + fixture.detectChanges(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.ts new file mode 100644 index 0000000..701f0d0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.component.ts @@ -0,0 +1,126 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { BranchConfigService } from './branch-config.service'; +import { Subscription } from 'rxjs'; +import { MatTableDataSource, MatSort } from '@angular/material'; +import { BranchConfig } from './branch-config'; +import { ServerError } from '../snack-bar-error/server-error'; +import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component'; +import { SnackBarService } from '../snack-bar-error/snack-bar.service'; + +@Component({ + selector: 'lw-branch-config', + templateUrl: './branch-config.component.html', + styleUrls: ['./branch-config.component.scss'] +}) +export class BranchConfigComponent implements OnInit, OnDestroy { + + private dataSubscription: Subscription; + public dataSource: MatTableDataSource; + public lastUpdated: Date; + public displayedColumns: string[] = ['zipcode', 'channelId']; + public localState = { + uploadConfirmed: false, + compareResult: null, + pendingData: null + }; + + @ViewChild(MatSort) sort: MatSort; + + @ViewChild('fileImportInput') fileImportInput: any; + + constructor(private branchConfigService: BranchConfigService, private snackBarService: SnackBarService) {} + + public ngOnInit() { + this.subscribeToSourceData(); + if (!this.branchConfigService.hasBranchConfig()) { + this.loadBranchConfig(); + } + } + + public loadBranchConfig(): void { + this.branchConfigService.loadBranchConfig().subscribe( + (success: BranchConfig) => { + this.branchConfigService.setBranchConfig(success); + }, + (error) => { + this.snackBarService.throwServerError(error, 'BRANCHMAP.LOAD_ERROR'); + } + ); + } + + public subscribeToSourceData(): void { + this.dataSubscription = this.branchConfigService.branchConfig$.subscribe((branchConfig) => { + if (branchConfig) { + this.dataSource = new MatTableDataSource(branchConfig.branchCodesByZip); + this.lastUpdated = branchConfig.date; + this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase(); + this.dataSource.sort = this.sort; + } + }); + } + + public commandExportCSV() { + this.branchConfigService.exportCSV(this.dataSource.data); + } + + public ngOnDestroy(): void { + if (this.dataSubscription) { + this.dataSubscription.unsubscribe(); + } + } + + public fileChangeListener($event): void { + const input = $event.target; + const currentFile = input.files[0]; + if (currentFile) { + const reader = new FileReader(); + reader.readAsText(currentFile); + + reader.onload = (data) => { + this.localState.pendingData = reader.result; + this.localState.compareResult = this.branchConfigService.validateUpload(this.localState.pendingData); + this.fileReset(); + }; + + reader.onerror = function () { + alert('Unable to read ' + currentFile); + }; + } + } + + public fileReset(): void { + this.fileImportInput.nativeElement.value = ''; + } + + public resetLocalState(): void { + this.localState = { + uploadConfirmed: false, + compareResult: null, + pendingData: null + }; + } + + public handleLocalStateError(errorResult): void { + this.localState.compareResult.warnings = []; + let field; + for (field in errorResult.error) { + if (errorResult.error.hasOwnProperty(field)) { + this.localState.compareResult.warnings.push(field + ': ' + errorResult.error[field][0]); + } + } + this.localState.compareResult.inError = true; + this.localState.compareResult.errorMessage = 'The server rejected the mapping.'; + } + + public commandConfirmSave(): void { + this.branchConfigService.saveNewBranchMapping(this.localState.compareResult.convertedData).subscribe( + (result) => { + this.resetLocalState(); + }, + (errorResult) => { + this.snackBarService.throwServerError(errorResult, 'BRANCHMAP.LOAD_ERROR'); + this.handleLocalStateError(errorResult); + } + ); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.spec.ts new file mode 100644 index 0000000..7c878fb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.spec.ts @@ -0,0 +1,175 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { configureTestSuite } from 'ng-bullet'; +import { BranchConfigService } from './branch-config.service'; +import { environment } from 'src/environments/environment'; +import { FilterUtil } from '../filter/filter-util'; +import { Filter } from '../filter/filter'; +import { BranchConfig, BranchData } from './branch-config'; + + +describe('BranchConfig Service', () => { + + let service: BranchConfigService; + let httpTestingController: HttpTestingController; + const urlBase: string = environment.urlBase; + const baseConfig = { + id: '5', + date: new Date(), + branchCodesByZip: [ + { + zipcode: '53207', + channelId: 9196721 + }, + { + zipcode: '53209', + channelId: 9196721 + }, + { + zipcode: '53215', + channelId: 9196722 + } + ] + }; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + BranchConfigService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(BranchConfigService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should build branch config url', () => { + const url = service.buildBranchConfigURL(); + expect(url).toBe(urlBase + '/BranchConfig'); + }); + + it('should identify if there is a branch config', () => { + const apps = service.hasBranchConfig(); + expect(apps).toEqual(false); + }); + + it('should load branch config', () => { + const testBranchConfig = new BranchConfig(); + testBranchConfig.branchCodesByZip = []; + testBranchConfig.date = new Date(); + testBranchConfig.id = '555'; + + service.loadBranchConfig().subscribe((data) => { + expect(data).toEqual(testBranchConfig); + service.setBranchConfig(data); + } + ); + const req = httpTestingController.expectOne(service.buildBranchConfigURL()); + + // Assert that the request is a GET. + expect(req.request.method).toEqual('GET'); + + // Assert that the request contains the appropriate header. + expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'))); + + // Respond with mock data, causing Observable to resolve. + // Subscribe callback asserts that correct data was returned. + req.flush(testBranchConfig); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + }); + + it('should save a branch config', () => { + const testBranchConfig = new BranchConfig(); + + service.submitBranchConfig(testBranchConfig).subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildBranchConfigURL()); + expect(req.request.method).toEqual('PUT'); + httpTestingController.verify(); + }); + + it('should validate an upload.', () => { + service.setBranchConfig(baseConfig); + const newConfig = + `ZIP,ChannelId + 53207,9196722 + 53209,9196721 + 53215,9196722 + `; + + const result = service.validateUpload(newConfig); + expect(result.inError).toEqual(false); + expect(result.currentCount).toEqual(baseConfig.branchCodesByZip.length); + expect(result.newCount).toEqual(3); + expect(result.warnings.length).toEqual(1); + }); + + it('should check for removed entries in an upload.', () => { + service.setBranchConfig(baseConfig); + expect(service.checkForRemovedEntries(baseConfig.branchCodesByZip, baseConfig.branchCodesByZip)).toEqual([]); + const newConfig = { + id: '5', + branchCodesByZip: [ + { + zipcode: '53207', + channelId: 9196721 + }, + { + zipcode: '53215', + channelId: 9196722 + } + ] + }; + expect(service.checkForRemovedEntries(baseConfig.branchCodesByZip, newConfig.branchCodesByZip)).toEqual([ + 'Zipcode 53209 is not in the new set.' + ]); + + }); + + it('should create branch data from a row of an upload.', () => { + service.setBranchConfig(baseConfig); + expect(service.createBranchDataFromRow('53215,9196722', 1)).toEqual( + { + zipcode: '53215', + channelId: 9196722 + } + ); + expect(() => { service.createBranchDataFromRow('53215', 0); }).toThrow(); + expect(service.createBranchDataFromRow('', 1)).toBeNull(); + }); + + it('should find existing rows given an uploaded row. ', () => { + service.setBranchConfig(baseConfig); + expect(service.findExistingRow(baseConfig.branchCodesByZip, { + zipcode: '53215', + channelId: 9196725 + })).toEqual({ + zipcode: '53215', + channelId: 9196722 + } + ); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.ts new file mode 100644 index 0000000..17da088 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.service.ts @@ -0,0 +1,160 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ExportService } from 'src/app/shared/export/export.service'; +import { BranchConfig, BranchData } from './branch-config'; + +@Injectable({ + providedIn: 'root' +}) +export class BranchConfigService { + + private branchConfigSubject: BehaviorSubject = new BehaviorSubject(null); + public branchConfig$ = this.branchConfigSubject.asObservable(); + + constructor(private httpClient: HttpClient, private exportService: ExportService) {} + + public buildBranchConfigURL(): string { + return environment.urlBase + '/BranchConfig'; + } + + public buildHeader(): HttpHeaders { + return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')); + } + + public hasBranchConfig(): boolean { + return this.branchConfigSubject.value !== null; + } + + public loadBranchConfig(): Observable { + const headers = this.buildHeader(); + return this.httpClient.get(this.buildBranchConfigURL(), {headers, withCredentials: true}).pipe( + map((success) => { + const config = new BranchConfig(); + config.id = success['id']; + config.date = success['date']; + config.branchCodesByZip = success['branchCodesByZip']; + return config; + }) + ); + } + + public setBranchConfig(branchConfig: BranchConfig): void { + this.branchConfigSubject.next(branchConfig); + } + + public submitBranchConfig(branchConfig: BranchConfig): Observable { + const headers = this.buildHeader(); + return this.httpClient.put(this.buildBranchConfigURL(), branchConfig, {headers, withCredentials: true}) + .pipe( + map((success) => { + const config = new BranchConfig(); + config.id = success['id']; + config.date = success['date']; + config.branchCodesByZip = success['branchCodesByZip']; + this.setBranchConfig(config); + return config; + }) + ); + } + + public exportCSV(dataSource: Array) { + this.exportService.exportBranchConfigAsCSV(dataSource); + } + + public validateUpload(rawData: string): any { + const pendingData = rawData.split('\n'); + const oldData = this.branchConfigSubject.getValue().branchCodesByZip; + const compareData = { + currentCount: oldData.length, + newCount: pendingData.length - 1, + inError: false, + errorMessage: '', + convertedData: [], + warnings: [] + }; + + if (pendingData.length <= 1) { + compareData.inError = true; + compareData.errorMessage = 'There are no rows.'; + return compareData; + } + + let rowData: BranchData; + let foundRow: BranchData; + try { + for (let newDataIndex = 1; newDataIndex < pendingData.length; newDataIndex++) { + rowData = this.createBranchDataFromRow(pendingData[newDataIndex], newDataIndex); + if (rowData) { + foundRow = this.findExistingRow(oldData, rowData); + if (foundRow === null) { + compareData.warnings.push('Zipcode ' + rowData.zipcode + ' is a newly added zipcode.'); + } else if ((foundRow.channelId + '') !== (rowData.channelId + '')) { + compareData.warnings + .push('Zipcode ' + rowData.zipcode + ' was changed from ' + foundRow.channelId + ' to ' + rowData.channelId + '.'); + } + compareData.convertedData.push(rowData); + } + } + compareData.warnings = compareData.warnings.concat(this.checkForRemovedEntries(oldData, compareData.convertedData)); + compareData.newCount = compareData.convertedData.length; + } catch (error) { + compareData.inError = true; + compareData.errorMessage = error.msg; + } + + return compareData; + } + + public checkForRemovedEntries(oldSet: Array, newSet: Array): Array { + const warnings = []; + let found = false; + for (let oldDataIndex = 0; oldDataIndex < oldSet.length; oldDataIndex++) { + for (let newDataIndex = 0; newDataIndex < newSet.length; newDataIndex++) { + if (oldSet[oldDataIndex].zipcode === newSet[newDataIndex].zipcode) { + found = true; + } + } + if (!found) { + warnings.push('Zipcode ' + oldSet[oldDataIndex].zipcode + ' is not in the new set.'); + } + found = false; + } + return warnings; + } + + public saveNewBranchMapping(pendingData: Array): Observable { + const branchConfig = { + id: this.branchConfigSubject.getValue()['id'], + date: new Date(), + branchCodesByZip: pendingData + }; + return this.submitBranchConfig(branchConfig); + } + + public createBranchDataFromRow(dataString: string, rowIndex: number): BranchData { + if (!dataString || !dataString.trim()) { + return null; + } + const cols = dataString.trim().split(','); + + if (!cols || cols.length !== 2) { + throw new Error('Row ' + rowIndex + ' does not have correct number of columns.'); + } + return { + zipcode: cols[0], + channelId: parseInt(cols[1], 10) + }; + } + + public findExistingRow(oldData: Array, rowData: BranchData): BranchData { + for (let oldDataIndex = 0; oldDataIndex < oldData.length; oldDataIndex++) { + if (oldData[oldDataIndex].zipcode === rowData.zipcode) { + return oldData[oldDataIndex]; + } + } + return null; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.ts new file mode 100644 index 0000000..8ba1e34 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-config/branch-config.ts @@ -0,0 +1,10 @@ +export class BranchData { + zipcode: string; + channelId: number; +} + +export class BranchConfig { + public id: string; + public date: Date; + public branchCodesByZip: Array; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.html new file mode 100644 index 0000000..d0c1c7f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.html @@ -0,0 +1,41 @@ + +
+ +
+

BRANCHNAMEMAP.TITLE

+ BRANCHNAMEMAP.SUBTEXT +

+ BRANCHNAMEMAP.DATETEXT{{lastUpdated | date:'short'}} +

+
+
+
+
+
+
+
BRANCHNAMEMAP.UPDATETEXT
+ +
+
+
+
+
+
+ +
+ + + + + + + + + + + +
Channel ID{{loan.channelId}} Branch Name{{loan.branchName}}
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.scss new file mode 100644 index 0000000..f37a35a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.scss @@ -0,0 +1,38 @@ +table { + width: 100%; +} + +.mat-form-field { + margin-right: 10px; + font-size: .875em; + width: 20%; +} + +.mat-header-cell { + font-size: 1em; + font-weight: bold; + color: black; +} + +.mat-checkbox { + margin-right: 10px; + :host ::ng-deep .mat-checkbox-background { + background-color: #005B9A + } +} + +.btn-disabled { + margin-bottom: 0px; + margin-left: 0px; +} + +.btn-less-padding { + padding-top: 3px; + padding-bottom: 3px; + padding-left: 8px; + padding-right: 8px; +} + +.shift-right { + margin-left: 5px; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.spec.ts new file mode 100644 index 0000000..60475e5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.spec.ts @@ -0,0 +1,66 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { BranchNameConfigComponent } from './branch-name-config.component'; +import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule, + MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe'; +import { BranchNameConfigService } from './branch-name-config.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TopNavigationComponent } from '../navigation/top-navigation.component'; +import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('BranchNameConfigComponent', () => { + let component: BranchNameConfigComponent; + let fixture: ComponentFixture; + let service: BranchNameConfigService; + let snackBar: MatSnackBar; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + FormsModule, + MatSortModule, + MatPaginatorModule, + MatTableModule, + MatInputModule, + MatSnackBarModule, + MatDatepickerModule, + MatNativeDateModule, + MatSelectModule, + MatCheckboxModule, + ReactiveFormsModule, + HttpClientTestingModule, + NoopAnimationsModule, + TranslateModule.forRoot() + ], + declarations: [ + SnackBarErrorComponent, + BranchNameConfigComponent, + LoanCodePipe, + TopNavigationComponent + ], + providers: [ + ] + }); + }); + + + beforeEach(() => { + service = TestBed.get(BranchNameConfigService); + snackBar = TestBed.get(MatSnackBar); + fixture = TestBed.createComponent(BranchNameConfigComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(service).toBeTruthy(); + fixture.detectChanges(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.ts new file mode 100644 index 0000000..048c31a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { BranchNameConfigService } from './branch-name-config.service'; +import { Subscription } from 'rxjs'; +import { MatTableDataSource, MatSort } from '@angular/material'; +import { BranchNameConfig } from './branch-name-config'; +import { ServerError } from '../snack-bar-error/server-error'; +import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component'; +import { SnackBarService } from '../snack-bar-error/snack-bar.service'; + +@Component({ + selector: 'lw-branch-Name-config', + templateUrl: './branch-name-config.component.html', + styleUrls: ['./branch-name-config.component.scss'] +}) +export class BranchNameConfigComponent implements OnInit, OnDestroy { + + private dataSubscription: Subscription; + public dataSource: MatTableDataSource; + public lastUpdated: Date; + public displayedColumns: string[] = ['channelId', 'branchName']; + + @ViewChild(MatSort) sort: MatSort; + + constructor(private branchNameConfigService: BranchNameConfigService, private snackBarService: SnackBarService) {} + + public ngOnInit() { + this.subscribeToSourceData(); + if (!this.branchNameConfigService.hasBranchNameConfig()) { + this.loadBranchNameConfig(); + } + } + + public commandUpdate() { + this.loadBranchNameConfig(true); + } + + public loadBranchNameConfig(update: boolean = false): void { + this.branchNameConfigService.loadBranchNameConfig(update).subscribe( + (success: BranchNameConfig) => { + this.branchNameConfigService.setBranchNameConfig(success); + }, + (error) => { + this.snackBarService.throwServerError(error, 'BRANCHNAMEMAP.LOAD_ERROR'); + } + ); + } + + public subscribeToSourceData(): void { + this.dataSubscription = this.branchNameConfigService.branchNameConfig$.subscribe((branchNameConfig) => { + if (branchNameConfig) { + this.dataSource = new MatTableDataSource(branchNameConfig.branchNamesByChannelId); + this.lastUpdated = branchNameConfig.date; + this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase(); + this.dataSource.sort = this.sort; + } + }); + } + + public ngOnDestroy(): void { + if (this.dataSubscription) { + this.dataSubscription.unsubscribe(); + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.spec.ts new file mode 100644 index 0000000..d3b406e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.spec.ts @@ -0,0 +1,106 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { configureTestSuite } from 'ng-bullet'; +import { BranchNameConfigService } from './branch-name-config.service'; +import { environment } from 'src/environments/environment'; +import { BranchNameConfig, BranchNameData } from './branch-name-config'; + +describe('BranchNameConfig Service', () => { + + let service: BranchNameConfigService; + let httpTestingController: HttpTestingController; + const urlBase: string = environment.urlBase; + const baseConfig = { + id: '5', + date: new Date(), + branchNamesByChannelId: [ + { + channelId: 9196721, + branchName: 'Watertown' + }, + { + channelId: 9196721, + branchName: 'Brookfield East' + }, + { + channelId: 9196722, + branchName: 'Waukesha' + } + ] + }; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + BranchNameConfigService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(BranchNameConfigService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should build branchName config url', () => { + const url = service.buildBranchNameConfigURL(); + expect(url).toBe(urlBase + '/BranchConfig/BranchNameConfig'); + }); + + it('should build branchName config update url', () => { + const url = service.buildBranchNameConfigUpdateURL(); + expect(url).toBe(urlBase + '/BranchConfig/UpdateBranchNameConfig'); + }); + + it('should identify if there is a branchName config', () => { + const apps = service.hasBranchNameConfig(); + expect(apps).toEqual(false); + }); + + it('should load branchName config', () => { + const testBranchNameConfig = new BranchNameConfig(); + testBranchNameConfig.branchNamesByChannelId = []; + testBranchNameConfig.date = new Date(); + testBranchNameConfig.id = '555'; + + service.loadBranchNameConfig().subscribe((data) => { + expect(data).toEqual(testBranchNameConfig); + service.setBranchNameConfig(data); + } + ); + const req = httpTestingController.expectOne(service.buildBranchNameConfigURL()); + + // Assert that the request is a GET. + expect(req.request.method).toEqual('GET'); + + // Assert that the request contains the appropriate header. + expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'))); + + // Respond with mock data, causing Observable to resolve. + // Subscribe callback asserts that correct data was returned. + req.flush(testBranchNameConfig); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.ts new file mode 100644 index 0000000..e2b7eb0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { BranchNameConfig, BranchNameData } from './branch-name-config'; + +@Injectable({ + providedIn: 'root' +}) +export class BranchNameConfigService { + + private branchNameConfigSubject: BehaviorSubject = new BehaviorSubject(null); + public branchNameConfig$ = this.branchNameConfigSubject; + + constructor(private httpClient: HttpClient) {} + + public buildBranchNameConfigURL(): string { + return environment.urlBase + '/BranchConfig/BranchNameConfig'; + } + + public buildBranchNameConfigUpdateURL(): string { + return environment.urlBase + '/BranchConfig/UpdateBranchNameConfig'; + } + + public buildHeader(): HttpHeaders { + return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')); + } + + public hasBranchNameConfig(): boolean { + return this.branchNameConfigSubject.value !== null; + } + + public loadBranchNameConfig(update: boolean = false): Observable { + const headers = this.buildHeader(); + const url = update ? this.buildBranchNameConfigUpdateURL() : this.buildBranchNameConfigURL(); + return this.httpClient.get(url, {headers, withCredentials: true}).pipe( + map((success) => { + const config = new BranchNameConfig(); + config.id = success['id']; + config.date = success['date']; + config.branchNamesByChannelId = success['branchNamesByChannelId']; + return config; + }) + ); + } + + public setBranchNameConfig(branchNameConfig: BranchNameConfig): void { + this.branchNameConfigSubject.next(branchNameConfig); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.ts new file mode 100644 index 0000000..a534e99 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/branch-name-config/branch-name-config.ts @@ -0,0 +1,10 @@ +export class BranchNameData { + channelId: number; + branchName: string; +} + +export class BranchNameConfig { + public id: string; + public date: Date; + public branchNamesByChannelId: Array; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/card.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/card.component.scss new file mode 100644 index 0000000..0afb131 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/card.component.scss @@ -0,0 +1,71 @@ +.fa-angle-right { + padding-top: 4px; +} + +.main-card-text { + font-weight: bold; + font-size: 25px; +} +.main-card-footer { + margin-bottom: 10px; +} +.card { + font-family: Arial, sans-serif; + margin-bottom: 25px; + text-decoration: none; + color:#565656; +} +.row { + text-align: center; + padding-right: 0px; + padding-left: 0px; +} +.card-body { + padding-right: 0px; + padding-left: 0px; +} +.main-icon { + margin-top: 10px; + margin-bottom: 8px; +} +.footer-text { + margin-right: 8px; +} +.desktop-view { + font-size: 20px; + margin-right: 8px; +} + +/*mobile*/ +@media (max-width: 767px) { + .fa-angle-right { + padding-top: 0px; + } + .main-card-text { + float: left; + font-size: 17px; + } + .main-card-footer { + margin-bottom: 0px; + } + .card { + margin-bottom: 10px; + } + .footer-text { + float: right; + } + .main-icon { + margin-top: 0px; + margin-bottom: 0px; + } + .desktop-view { + display: none; + } + .mobile-view[style] { + display: inline !important; + } + .card-body { + padding-right: 15px; + padding-left: 15px; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.html new file mode 100644 index 0000000..773f73c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.html @@ -0,0 +1,30 @@ + +
+
+
+
+ + + + +
+
+
+
{{cardName}}
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.scss new file mode 100644 index 0000000..afdf01d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.scss @@ -0,0 +1 @@ +// see card-component.scss for styling \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.spec.ts new file mode 100644 index 0000000..5fa89d3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.spec.ts @@ -0,0 +1,45 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { MainCardAdminComponent } from './main-card.component'; + +describe('MainCardComponent', () => { + let component: MainCardAdminComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + MainCardAdminComponent, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MainCardAdminComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should create with input', () => { + fixture.detectChanges(); + component.desktopIcon = 'fas fa-credit-card fa-3x'; + component.cardName = 'Credit Card'; + component.cardPath = 'credit-card'; + fixture.detectChanges(); + const icon = fixture.nativeElement.querySelectorAll('.fa-credit-card'); + expect(icon).toBeDefined(); + expect(icon.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.ts new file mode 100644 index 0000000..d700b56 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/cards/main-card/main-card.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'lw-main-card', + templateUrl: './main-card.component.html', + styleUrls: ['../card.component.scss'] +}) +export class MainCardAdminComponent { + @Input() + public cardPath: string; + @Input() + public desktopIcon: string; + @Input() + public mobileIcon: string; + @Input() + public cardName: string; + @Input() + public footerName: string; + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-api.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-api.ts new file mode 100644 index 0000000..45fdbf2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-api.ts @@ -0,0 +1,11 @@ +import { FilterField } from './filter-field'; + + +export class FilterApi { + public count: number; + public sortDesc: boolean; + public sortField: string; + public startDate: Date; + public endDate: Date; + public filterField?: FilterField[]; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-field.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-field.ts new file mode 100644 index 0000000..7c8618d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-field.ts @@ -0,0 +1,19 @@ + + +export class FilterField { + public field: string; + public filterValue: string; + public equal: boolean; + public notEqual: boolean; + public greaterThan: boolean; + public lessThan: boolean; + + constructor (field: string, filterValue: string, equal: boolean, notEqual: boolean, greaterThan: boolean, lessThan: boolean) { + this.field = field; + this.filterValue = filterValue; + this.equal = equal; + this.notEqual = notEqual; + this.greaterThan = greaterThan; + this.lessThan = lessThan; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-util.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-util.ts new file mode 100644 index 0000000..b88dd63 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter-util.ts @@ -0,0 +1,90 @@ +import { FilterField } from './filter-field'; +import { FilterApi } from './filter-api'; +import { DateBuilder } from 'src/app/shared/date/date-builder'; +import { Status } from './status'; +import { ApiStatus } from 'src/app/shared/api-models/api-status'; +import { Filter } from './filter'; + + +export class FilterUtil { + + public static getDefaultFilter(): Filter { + const filter: Filter = new Filter(); + filter.count = 1000; + filter.sortDesc = true; + filter.sortField = 'Date'; + filter.startDate = this.buildDefaultStartDate(); + filter.endDate = this.buildDefaultEndDate(); + filter.status = this.getDefaultStatus(); + filter.branch = 'ALL'; + filter.loanType = 'ALL'; + return filter; + } + + public static getDefaultStatus(): Status { + const status: Status = new Status(); + status.error = true; + status.deleted = true; + status.inProgress = true; + status.submitted = false; + status.decisioned = false; + return status; + } + + public static translateFilter(filter: Filter): FilterApi { + const filterApi: FilterApi = new FilterApi(); + filterApi.count = filter.count; + filterApi.startDate = this.buildApiStartDate(filter.startDate); + filterApi.endDate = this.buildApiEndDate(filter.endDate); + filterApi.sortDesc = filter.sortDesc; + filterApi.sortField = filter.sortField; + + const filterField: FilterField [] = [] ; + if (filter.branch !== 'ALL') { + filterField.push(new FilterField('Branch', filter.branch, true, false, false, false)); + } + if (filter.loanType !== 'ALL') { + filterField.push(new FilterField('LoanType', filter.loanType, true, false, false, false)); + } + if (filter.status.error === false) { + filterField.push(new FilterField('Status', ApiStatus.APPLICATION_ERROR, true, true, false, false)); + filterField.push(new FilterField('Status', ApiStatus.VENDOR_ERROR, false, true, false, false)); + } + if (filter.status.deleted === false) { + filterField.push(new FilterField('Status', ApiStatus.DELETED, false, true, false, false)); + } + if (filter.status.inProgress === false) { + filterField.push(new FilterField('Status', ApiStatus.IN_PROGRESS, false, true, false, false)); + } + if (filter.status.submitted === false) { + filterField.push(new FilterField('Status', ApiStatus.SUBMITTED, false, true, false, false)); + } + if (filter.status.decisioned === false) { + filterField.push(new FilterField('Status', ApiStatus.DECISIONED, false, true, false, false)); + } + + filterApi.filterField = filterField; + return filterApi; + } + + public static buildDefaultStartDate(): string { + const startDate = DateBuilder.buildPastDate(7); + return DateBuilder.formatDateAsString(startDate); + } + + public static buildDefaultEndDate(): string { + return DateBuilder.formatDateAsString(new Date()); + } + + public static buildApiStartDate(date: string): Date { + let startDate = DateBuilder.formatStringAsDate(date); + startDate = DateBuilder.setBeginOfDay(startDate); + return startDate; + } + + public static buildApiEndDate(date: string): Date { + let endDate = DateBuilder.formatStringAsDate(date); + endDate = DateBuilder.setEndOfDay(endDate); + return endDate; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter.ts new file mode 100644 index 0000000..18ece76 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/filter.ts @@ -0,0 +1,13 @@ +import { Status } from './status'; + + +export class Filter { + public count: number; + public sortDesc: boolean; + public sortField: string; + public startDate: string; + public endDate: string; + public status: Status; + public branch: string; + public loanType: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/status.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/status.ts new file mode 100644 index 0000000..9126d06 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/filter/status.ts @@ -0,0 +1,9 @@ + + +export class Status { + public error: boolean; + public inProgress: boolean; + public deleted: boolean; + public submitted: boolean; + public decisioned: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.html new file mode 100644 index 0000000..1cc2acd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.html @@ -0,0 +1,24 @@ +
ADMIN.LANDING_TITLE
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.scss new file mode 100644 index 0000000..43ab9b3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.scss @@ -0,0 +1,22 @@ +.loan-header { + font-family: 'Open Sans', sans-serif !important; + font-size:44px; + color:#012E50 +} + +.title-hr { + margin: 0 0 2.5em 0; + color: #B1B1B1; + height: 1px; + border: none; + background-color: #B1B1B1; +} + +/*mobile*/ +@media (max-width: 767px) { + .loan-header { + font-size:30px; + font-weight:400; + text-align: center; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.spec.ts new file mode 100644 index 0000000..40513d0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { AdminLandingComponent } from './admin-landing.component'; +import { MainCardAdminComponent } from '../cards/main-card/main-card.component'; + +describe('AdminLandingComponent', () => { + let component: AdminLandingComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + AdminLandingComponent, + MainCardAdminComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminLandingComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.ts new file mode 100644 index 0000000..0d75937 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/landing/admin-landing.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-admin-landing', + templateUrl: './admin-landing.component.html', + styleUrls: ['./admin-landing.component.scss'] +}) +export class AdminLandingComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.html new file mode 100644 index 0000000..0523d6c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.html @@ -0,0 +1,3 @@ +
{{container | json}}
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.spec.ts new file mode 100644 index 0000000..bd33ba3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.spec.ts @@ -0,0 +1,39 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanDataComponent } from './loan-data.component'; +import { LoanTableService } from '../loan-table/loan-table.service'; +import { MockLoanTableService } from 'src/app/testing/shared-mocks/loan-table.mock.service'; + +describe('LoanDataComponent', () => { + let component: LoanDataComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + ], + declarations: [ + LoanDataComponent + ], + providers: [ + { provide: LoanTableService, useValue: MockLoanTableService } + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanDataComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.ts new file mode 100644 index 0000000..9a00a21 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-data/loan-data.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { LoanTableService } from '../loan-table/loan-table.service'; +import { Subscription } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; + +@Component({ + selector: 'lw-loan-data', + templateUrl: './loan-data.component.html', + styleUrls: ['./loan-data.component.scss'] +}) +export class LoanDataComponent implements OnInit, OnDestroy { + + public container: ApiLoanContainer; + private subscription: Subscription; + + constructor(private loanTableService: LoanTableService, private route: ActivatedRoute, private location: Location) { } + + public ngOnInit() { + this.subscription = this.route.params.subscribe(params => { + if (params.id) { + this.container = this.loanTableService.getApplicationById(params.id); + } + }); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public commandPrevious() { + this.location.back(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.html new file mode 100644 index 0000000..df57b20 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.html @@ -0,0 +1,103 @@ + + +
+ + + + + + +
ADMIN.DATE_ERROR
+
+ + +
ADMIN.DATE_ERROR
+
+
+ + + {{loanType.desc}} + + + + + {{branch}} + + + Decisioned + Submitted + In Progress + Error + Deleted + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id{{loan.id | slice:0:7}} Loan Type{{loan.loanType | loanCode}} + + + + Status + {{loan.status}}Date{{loan.date | date:'short'}}Branch {{loan.branch}} First Name{{loan.firstName}}Last Name{{loan.lastName}}
+ +
+ \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.scss new file mode 100644 index 0000000..4bb254a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.scss @@ -0,0 +1,55 @@ +table { + width: 100%; +} + +.mat-form-field { + margin-right: 10px; + font-size: 14px; + width: 20%; +} + +.mat-header-cell { + font-size: 16px; + color: black; +} + +.mat-checkbox { + margin-right: 10px; + :host ::ng-deep .mat-checkbox-background { + background-color: #005B9A + } +} + +.btn-disabled { + margin-bottom: 0px; + margin-left: 0px; +} + +.btn-less-padding { + padding-top: 3px; + padding-bottom: 3px; + padding-left: 8px; + padding-right: 8px; +} + +.search-tip { + color: #005B9A; + width: 16px; + padding-top: 16px; + margin-right: 4px; +} + +.status-tip { + color: #005B9A; + margin-right: .3em; +} + +::ng-deep .mat-tooltip { + white-space: pre-line !important; +} + +.error { + font-size: 80%; + color: #dc3545; + font-style: italic; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.spec.ts new file mode 100644 index 0000000..c44e999 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.spec.ts @@ -0,0 +1,58 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanTableService } from '../loan-table/loan-table.service'; +import { MockLoanTableService } from 'src/app/testing/shared-mocks/loan-table.mock.service'; +import { LoanTableComponent } from './loan-table.component'; +import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule, + MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe'; +import { TopNavigationComponent } from '../navigation/top-navigation.component'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('LoanTableComponent', () => { + let component: LoanTableComponent; + let fixture: ComponentFixture; + let service: LoanTableService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + FormsModule, + MatSortModule, + MatPaginatorModule, + MatTableModule, + MatInputModule, + MatDatepickerModule, + MatNativeDateModule, + MatSnackBarModule, + MatSelectModule, + MatCheckboxModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + LoanTableComponent, + LoanCodePipe, + TopNavigationComponent + ], + providers: [ + { provide: LoanTableService, useValue: MockLoanTableService } + ] + }); + }); + + + beforeEach(() => { + service = TestBed.get(LoanTableService); + fixture = TestBed.createComponent(LoanTableComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(service).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.ts new file mode 100644 index 0000000..0546adf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.component.ts @@ -0,0 +1,195 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { LoanTableService } from './loan-table.service'; +import { Subscription } from 'rxjs'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { MatTableDataSource, MatPaginator, MatSort } from '@angular/material'; +import { Filter } from '../filter/filter'; +import { FormGroup } from '@angular/forms'; +import { FilterUtil } from '../filter/filter-util'; +import { BranchOptions } from 'src/app/shared/options/branch-options'; +import { SelectOptions } from 'src/app/shared/options/select-options'; +import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component'; +import { ServerError } from '../snack-bar-error/server-error'; +import { SnackBarService } from '../snack-bar-error/snack-bar.service'; +import { BranchNameConfig } from './loan-table'; + +@Component({ + selector: 'lw-loan-table', + templateUrl: './loan-table.component.html', + styleUrls: ['./loan-table.component.scss'] +}) + +export class LoanTableComponent implements OnInit, OnDestroy { + public readonly FORM_STATE_INVALID = 'INVALID'; + public pageForm: FormGroup; + public filterGroup: FormGroup; + public formSubscription: Subscription; + private dataSubscription: Subscription; + public dataSource: MatTableDataSource; + private branchNameConfigDataSubscription: Subscription; + public branchNameConfigDataSource: MatTableDataSource; + public displayedColumns: string[] = ['id', 'loanType', 'status', 'date', 'branch', 'firstName', 'lastName', 'submit', 'delete']; + public filter: Filter; + public lastUpdated: Date; + public branches: string[] = []; + public loanTypes: Array<{value: any, desc: string}>; + public localState = { + filterDisabled: true, + filterIndicator: false, + }; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(private loanTableService: LoanTableService, private snackBarService: SnackBarService) {} + + public ngOnInit() { + this.filter = this.loanTableService.getFilter(); + this.loanTypes = SelectOptions.loanOptions; + this.subscribeToBranchNameSourceData(); + if (!this.loanTableService.hasBranchNameConfig()) { + this.loadBranchNameConfig(); + } + this.setupForm(); + if (!this.loanTableService.hasApplications()) { + this.loadApplications(); + } + this.subscribeToDataSource(); + } + + public loadBranchNameConfig(): void { + this.loanTableService.loadBranchNameConfig().subscribe( + (success: BranchNameConfig) => { + this.loanTableService.setbranchNameConfig(success); + }, + (error) => { + this.snackBarService.throwServerError(error, 'BRANCHNAMEMAP.LOAD_ERROR'); + } + ); + } + + public subscribeToBranchNameSourceData(): void { + this.dataSubscription = this.loanTableService.branchNameConfig$.subscribe((branchNameConfig) => { + if (branchNameConfig) { + this.branchNameConfigDataSource = new MatTableDataSource(branchNameConfig.branchNamesByChannelId); + this.lastUpdated = branchNameConfig.date; + this.branchNameConfigDataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase(); + this.branchNameConfigDataSource.sort = this.sort; + var branchList = this.branchNameConfigDataSource.data.map(value => value.branchName); + this.branches = branchList.sort(); + } + }); + } + + public getBranchName(item) { + var branchName = [item.branchName]; + return branchName; + } + + public loadApplications(): void { + this.localState.filterIndicator = true; + this.loanTableService.loadApplications(FilterUtil.translateFilter(this.filter)).subscribe( + (success: ApiLoanContainer[]) => { + this.loanTableService.setApplications(success); + this.localState.filterIndicator = false; + }, + (error) => { + this.localState.filterIndicator = false; + this.snackBarService.throwServerError(error, 'ADMIN.LOAD_ERROR'); + } + ); + } + + public setupForm(): void { + this.pageForm = this.loanTableService.createPageGroup(); + this.filterGroup = this.pageForm.get(this.loanTableService.FILTER_FORM_NAME); + this.formSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.filterDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.patchForm(); + } + + public patchForm(): void { + this.filterGroup.patchValue({startDate: this.filter.startDate}); + this.filterGroup.patchValue({endDate: this.filter.endDate}); + this.filterGroup.patchValue({loanType: this.filter.loanType}); + this.filterGroup.patchValue({branch: this.filter.branch}); + this.filterGroup.patchValue({deleted: this.filter.status.deleted}); + this.filterGroup.patchValue({error: this.filter.status.error}); + this.filterGroup.patchValue({inProgress: this.filter.status.inProgress}); + this.filterGroup.patchValue({submitted: this.filter.status.submitted}); + this.filterGroup.patchValue({decisioned: this.filter.status.decisioned}); + } + + public subscribeToDataSource(): void { + this.dataSubscription = this.loanTableService.applicationSet$.subscribe((loanContainers) => { + this.dataSource = new MatTableDataSource(loanContainers); + this.dataSource.paginator = this.paginator; + this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase(); + this.dataSource.sort = this.sort; + }); + } + + public commandExportCSV(): void { + this.loanTableService.exportCSV(this.dataSource.filteredData); + } + + public commandSearch(searchValue: string): void { + this.dataSource.filter = searchValue.trim().toLowerCase(); + if (this.dataSource.paginator) { + this.dataSource.paginator.firstPage(); + } + } + + public commandFilter(): void { + this.translateFormToFilter(); + this.loanTableService.setFilter(this.filter); + this.loadApplications(); + } + + public commandDelete(loanId: string): void { + this.loanTableService.deleteApplication(loanId).subscribe( + (result) => { + this.loadApplications(); + }, + (error) => { + this.snackBarService.throwServerError(error, 'ADMIN.DELETE_ERROR'); + } + ); + } + + public commandSubmit(loanId: string): void { + this.loanTableService.submitApplication(loanId).subscribe( + (result) => { + this.loadApplications(); + }, + (error) => { + this.snackBarService.throwServerError(error, 'ADMIN.SUBMIT_ERROR'); + } + ); + } + + public translateFormToFilter(): void { + this.filter.startDate = this.filterGroup.value.startDate; + this.filter.endDate = this.filterGroup.value.endDate; + this.filter.loanType = this.filterGroup.value.loanType; + this.filter.branch = this.filterGroup.value.branch; + this.filter.status.deleted = this.filterGroup.value.deleted; + this.filter.status.error = this.filterGroup.value.error; + this.filter.status.inProgress = this.filterGroup.value.inProgress; + this.filter.status.submitted = this.filterGroup.value.submitted; + this.filter.status.decisioned = this.filterGroup.value.decisioned; + } + + public ngOnDestroy(): void { + if (this.formSubscription) { + this.formSubscription.unsubscribe(); + } + if (this.dataSubscription) { + this.dataSubscription.unsubscribe(); + } + if (this.branchNameConfigDataSubscription){ + this.branchNameConfigDataSubscription.unsubscribe(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.spec.ts new file mode 100644 index 0000000..f72f065 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.spec.ts @@ -0,0 +1,131 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpHeaders } from '@angular/common/http'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanTableService } from './loan-table.service'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { TestApiLoanContainer } from 'src/app/testing/shared-mocks/test-api-loan-container'; +import { environment } from 'src/environments/environment'; +import { FilterUtil } from '../filter/filter-util'; +import { TranslateModule } from '@ngx-translate/core'; + + +describe('LoanTable Service', () => { + + let service: LoanTableService; + const loanContainer: ApiLoanContainer = TestApiLoanContainer; + const loanContainers: ApiLoanContainer[] = [loanContainer]; + let httpTestingController: HttpTestingController; + const urlBase: string = environment.urlBase; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + LoanTableService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(LoanTableService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should build storage url', () => { + const url = service.buildStorageFilterURL(); + expect(url).toBe(urlBase + '/Storage/Filter'); + }); + + it('should build loan application url', () => { + const url = service.buildLoanApplicationURL(); + expect(url).toBe(urlBase + '/LoanApplication/Submit'); + }); + + it('should identify if there are applications', () => { + const apps = service.hasApplications(); + expect(apps).toEqual(false); + }); + + it('should load applications', () => { + service.loadApplications(FilterUtil.translateFilter(FilterUtil.getDefaultFilter())).subscribe((data) => { + expect(data.test).toEqual('x'); + service.setApplications(data); + } + ); + const req = httpTestingController.expectOne(service.buildStorageFilterURL()); + + // Assert that the request is a POST. + expect(req.request.method).toEqual('POST'); + + // Assert that the request contains the appropriate header. + expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'))); + + // Respond with mock data, causing Observable to resolve. + // Subscribe callback asserts that correct data was returned. + req.flush({'test': 'x'}); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + }); + + it('should delete application', () => { + const loanId = '1234'; + service.deleteApplication(loanId).subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildStorageDeleteURL() + '/' + loanId); + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'))); + httpTestingController.verify(); + }); + + it('should submit application', () => { + const loanId = '1234'; + service.submitApplication(loanId).subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildLoanApplicationURL() + '/' + loanId); + expect(req.request.method).toEqual('POST'); + httpTestingController.verify(); + }); + + it('should set and get loan containers', () => { + service.setApplications(loanContainers); + let subLoanApps; + service.applicationSet$.subscribe((containers) => { + subLoanApps = containers; + }); + expect(subLoanApps).toEqual(loanContainers); + }); + + it('should get application by id', () => { + service.setApplications(loanContainers); + const loan = service.getApplicationById('12341234'); + expect(loan.branch).toBe('milwaukee'); + }); + + it('should fail with bad application id', () => { + service.setApplications(loanContainers); + expect(function() { service.getApplicationById('0'); }) + .toThrow(new Error('Application not found')); + }); + + it('should fail with no applications loaded', () => { + expect(function() { service.getApplicationById('0'); }) + .toThrow(new Error('Applications are not loaded')); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.ts new file mode 100644 index 0000000..445a289 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.service.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { FilterApi } from '../filter/filter-api'; +import { ExportService } from 'src/app/shared/export/export.service'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { FilterUtil } from '../filter/filter-util'; +import { Filter } from '../filter/filter'; +import { AdminGroups } from '../admin-groups'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { BranchOptions } from 'src/app/shared/options/branch-options'; +import { map } from 'rxjs/operators'; +import { BranchNameConfig, BranchNameData } from './loan-table'; + +@Injectable({ + providedIn: 'root' +}) +export class LoanTableService { + + private applicationSet: BehaviorSubject> = new BehaviorSubject>([]); + public applicationSet$ = this.applicationSet.asObservable(); + private branchNameConfigSubject: BehaviorSubject = new BehaviorSubject(null); + public branchNameConfig$ = this.branchNameConfigSubject.asObservable(); + public readonly FILTER_FORM_NAME = 'filterInformation'; + private filter = FilterUtil.getDefaultFilter(); + + constructor(private formBuilder: FormBuilder, private httpClient: HttpClient, private exportService: ExportService, private configurationService: ConfigurationService) {} + + public buildHeader(): HttpHeaders { + return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')); + } + + public buildStorageDeleteURL(): string { + return environment.urlBase + '/Storage/Loan'; + } + + public buildStorageFilterURL(): string { + return environment.urlBase + '/Storage/Filter'; + } + + public buildLoanApplicationURL(): string { + return environment.urlBase + '/LoanApplication/Submit'; + } + + public buildBranchNameConfigURL(): string { + return environment.urlBase + '/BranchConfig/BranchNameConfig'; + } + + public loadApplications(filterApi: FilterApi): Observable { + const headers = this.buildHeader(); + return this.httpClient.post(this.buildStorageFilterURL(), filterApi, {headers, withCredentials: true}); + } + + public hasBranchNameConfig(): boolean { + return this.branchNameConfigSubject.value !== null; + } + + public loadBranchNameConfig(): Observable { + const headers = this.buildHeader(); + return this.httpClient.get(this.buildBranchNameConfigURL(), {headers, withCredentials: true}).pipe( + map((success) => { + const config = new BranchNameConfig(); + config.id = success['id']; + config.date = success['date']; + config.branchNamesByChannelId = success['branchNamesByChannelId']; + return config; + }) + ); + } + + public setbranchNameConfig(branchNameConfig: BranchNameConfig): void { + this.branchNameConfigSubject.next(branchNameConfig); + } + + public deleteApplication(loanId: string): Observable { + const headers = this.buildHeader(); + return this.httpClient.delete(this.buildStorageDeleteURL() + '/' + loanId, {headers, withCredentials: true}); + } + + public submitApplication(loanId: string): Observable { + return this.httpClient.post(this.buildLoanApplicationURL() + '/' + loanId, ''); + } + + public hasApplications(): boolean { + return !!(this.applicationSet.value.length); + } + + public setApplications(applications: ApiLoanContainer[]): void { + this.applicationSet.next(applications); + } + + public getApplicationById(loanId: string): ApiLoanContainer { + if (!this.applicationSet.value.length) { + throw new Error('Applications are not loaded'); + } + const single = this.applicationSet.value.find(x => x.id === loanId); + if (!single) { + throw new Error('Application not found'); + } + return single; + } + + public getFilter() { + return this.filter; + } + + public setFilter(filter: Filter) { + this.filter = filter; + } + + public exportCSV(dataSource: ApiLoanContainer[]) { + this.exportService.exportLoanContainerAsCSV(dataSource); + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const filterGroup = this.createFilterGroup(); + group.addControl(this.FILTER_FORM_NAME, filterGroup); + return group; + } + + public createFilterGroup(): FormGroup { + const filterGroup = this.formBuilder.group({}); + + AdminGroups.FILTER.fields.forEach((eachField) => { + filterGroup.addControl(eachField.field, this.formBuilder.control('')); + filterGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return filterGroup; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.ts new file mode 100644 index 0000000..a534e99 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/loan-table/loan-table.ts @@ -0,0 +1,10 @@ +export class BranchNameData { + channelId: number; + branchName: string; +} + +export class BranchNameConfig { + public id: string; + public date: Date; + public branchNamesByChannelId: Array; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.html new file mode 100644 index 0000000..198b041 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.html @@ -0,0 +1,2 @@ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.scss new file mode 100644 index 0000000..cb70f71 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.scss @@ -0,0 +1,11 @@ +.btn-home { + margin-bottom: 15px; +} + +.title-hr { + margin: 0 0 15px 0; + color: #B1B1B1; + height: 1px; + border: none; + background-color: #B1B1B1; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.spec.ts new file mode 100644 index 0000000..7cfb6c3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { TopNavigationComponent } from './top-navigation.component'; + +describe('TopNavigationComponent', () => { + let component: TopNavigationComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + TopNavigationComponent, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TopNavigationComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.ts new file mode 100644 index 0000000..f94f314 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/navigation/top-navigation.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'lw-top-navigation', + templateUrl: './top-navigation.component.html', + styleUrls: ['./top-navigation.component.scss'] +}) +export class TopNavigationComponent { + constructor(private router: Router) { } + + public commandHome(): void { + this.router.navigate(['/admin']); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.html new file mode 100644 index 0000000..472ec73 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.html @@ -0,0 +1,20 @@ + +
ADMIN.SCHUMER
+
+
+
+
+ Select a loan type to preview the Schumer box: +
+
+ +
+
+
+ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.scss new file mode 100644 index 0000000..daa3657 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.scss @@ -0,0 +1,11 @@ +$max-width: 770px; + + +@media screen and (max-width: $max-width) { + .url-long { + display: none; + } + .url-short[style] { + display: inline !important; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.ts new file mode 100644 index 0000000..0f35d3a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/schumer-boxes/schumer-preview.component.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'lw-schumer-preview', + templateUrl: './schumer-preview.component.html', + styleUrls: ['./schumer-preview.component.scss'] +}) +export class SchumerPreviewComponent { + + public schumerType = 'CC_CashBack'; + public localState = { + showSchumer: true + }; + + constructor() { + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/server-error.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/server-error.ts new file mode 100644 index 0000000..8cd97c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/server-error.ts @@ -0,0 +1,11 @@ + + +export class ServerError { + public statusCode: string; + public message: string; + + constructor (statusCode: string, message: string) { + this.statusCode = statusCode; + this.message = message; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.html new file mode 100644 index 0000000..76d926b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.html @@ -0,0 +1,4 @@ + +
HTTP Status: {{error.statusCode}}
+
Message: {{error.message | translate}}
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.ts new file mode 100644 index 0000000..d367be1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar-error.component.ts @@ -0,0 +1,21 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_SNACK_BAR_DATA } from '@angular/material'; +import { ServerError } from './server-error'; + + +@Component({ + selector: 'lw-snack-bar-error', + templateUrl: 'snack-bar-error.component.html', + styles: ['snack-bar-error.component.scss'], +}) + + +export class SnackBarErrorComponent { + public error: ServerError; + + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ServerError) { + this.error = data; + } +} + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar.service.ts new file mode 100644 index 0000000..4a81497 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/snack-bar-error/snack-bar.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material'; +import { SnackBarErrorComponent } from './snack-bar-error.component'; +import { ServerError } from './server-error'; + +@Injectable({ + providedIn: 'root' +}) +export class SnackBarService { + public errorDurationInSeconds = 10; + + constructor(public snackBar: MatSnackBar) {} + + public throwServerError(error: any, errorMessage: string): void { + this.snackBar.openFromComponent(SnackBarErrorComponent, { + verticalPosition: 'top', + duration: this.errorDurationInSeconds * 1000, + data: new ServerError(this.getStatusFromError(error), errorMessage) + }); + } + + public throwSuccess(message: string): void { + this.snackBar.open(message, 'Okay', { + verticalPosition: 'top', + }); + } + + public getStatusFromError(error: any): string { + if (error && error['status']) { + return error['status']; + } else { + return 'Unknown'; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.html new file mode 100644 index 0000000..e426dfe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.html @@ -0,0 +1,25 @@ + +
+
+
+
+

CONFIG.TITLE

+ CONFIG.SUBTEXT +
+
+
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.scss new file mode 100644 index 0000000..8765748 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.scss @@ -0,0 +1,16 @@ +.btn-save { + margin-top: 15px; +} + +.alert-secondary hr { + margin-top: 0px; + margin-bottom: 5px; + border-top-color: #c8cbcf; +} + +.alert-secondary { + background-color: #E2E3E5; + color: #383D41; + border-color: #d6d8db; + margin-bottom: 0px; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.ts new file mode 100644 index 0000000..6175e0b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { SystemConfigService } from './system-config.service'; +import { Configuration } from '../../shared/configuration/configuration'; +import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { SnackBarService } from '../snack-bar-error/snack-bar.service'; + +@Component({ + selector: 'lw-system-config', + templateUrl: './system-config.component.html', + styleUrls: ['./system-config.component.scss'] +}) + +export class SystemConfigComponent implements OnInit { + public pageForm: FormGroup; + public configGroup: FormGroup; + public storedSettings: Configuration = new Configuration(); + public formSubscription: Subscription; + public localState = { + saveIndicator: false, + }; + + constructor(private systemConfigService: SystemConfigService, private snackBarService: SnackBarService) {} + + public ngOnInit() { + this.setupForm(); + this.loadStoredSettings(); + } + + public loadStoredSettings() { + this.systemConfigService.getStoredSettings().subscribe ( + (success: Configuration) => { + this.storedSettings = success; + this.patchForm(this.storedSettings); + }, + (error) => { + this.snackBarService.throwServerError(error, 'CONFIG.LOAD_ERROR'); + } + ); + } + + public updateStoredSettings() { + this.localState.saveIndicator = true; + this.systemConfigService.updateStoredSettings(this.storedSettings).subscribe ( + (success: Configuration) => { + this.storedSettings = success; + this.patchForm(this.storedSettings); + this.localState.saveIndicator = false; + this.snackBarService.throwSuccess('Your changes have been successfully updated and published.'); + }, + (error) => { + this.snackBarService.throwServerError(error, 'CONFIG.UPDATE_ERROR'); + this.localState.saveIndicator = false; + } + ); + } + + public commandSaveSettings() { + this.storedSettings.applicationDisabled = this.configGroup.value.appStatus ? false : true; + this.updateStoredSettings(); + } + + public setupForm(): void { + this.pageForm = this.systemConfigService.createPageGroup(); + this.configGroup = this.pageForm.get(this.systemConfigService.CONFIG_FORM_NAME); + } + + public patchForm(config: Configuration): void { + this.configGroup.patchValue({appStatus: config.applicationDisabled ? false : true}); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.service.ts new file mode 100644 index 0000000..522cee1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/admin/system-config/system-config.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Configuration } from '../../shared/configuration/configuration'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { AdminGroups } from '../admin-groups'; + +@Injectable({ + providedIn: 'root' +}) +export class SystemConfigService { + + public readonly CONFIG_FORM_NAME = 'configInformation'; + + constructor(private httpClient: HttpClient, private formBuilder: FormBuilder) {} + + public buildHeader(): HttpHeaders { + return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')); + } + + public buildStoredSettingsURL(): string { + return environment.urlBase + '/UiSettings/StoredSettings'; + } + + public getStoredSettings(): Observable { + const headers = this.buildHeader(); + return this.httpClient.get(this.buildStoredSettingsURL(), {headers, withCredentials: true}); + } + + public updateStoredSettings(settings: Configuration): Observable { + const headers = this.buildHeader(); + return this.httpClient.put(this.buildStoredSettingsURL(), settings, {headers, withCredentials: true}); + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const configGroup = this.createConfigGroup(); + group.addControl(this.CONFIG_FORM_NAME, configGroup); + return group; + } + + public createConfigGroup(): FormGroup { + const configGroup = this.formBuilder.group({}); + + AdminGroups.CONFIG.fields.forEach((eachField) => { + configGroup.addControl(eachField.field, this.formBuilder.control('')); + configGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return configGroup; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/app-routing.module.ts new file mode 100644 index 0000000..312c1c4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/app-routing.module.ts @@ -0,0 +1,125 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { LoanWizardComponent } from './loan-wizard/loan-wizard.component'; +import { SelectLoanComponent } from './select-loan/select-loan.component'; +import { AppUnavailableComponent } from './shared/app-unavailable/app-unavailable-page.component'; +import { DealershipPortalComponent } from './shared/dealership-portal/dealership-portal-page.component'; +import { ConfigurationResolver } from './shared/configuration/configuration.resolver'; +import { ConfigurationService } from './shared/configuration/configuration.service'; +import { TimeoutLandingPageComponent } from './shared/timeout/timeout-landing-page.component'; +import { AppTimeoutGuard } from './shared/guards/app-timeout.guard'; +import { ccRoutes } from './loan-wizard/wizards/credit-card/credit-card-routing.module'; +import { eqRoutes } from './loan-wizard/wizards/equity/equity-routing.module'; +import { veRoutes } from './loan-wizard/wizards/vehicle/vehicle-routing.module'; +import { slRoutes } from './loan-wizard/wizards/signature/signature-routing.module'; +import { SelectCreditCardComponent } from './select-loan/credit-card/select-credit-card.component'; +import { SelectLoanOutletComponent } from './select-loan/outlet/select-loan-outlet.component'; +import { SelectHomeEquityComponent } from './select-loan/home/select-home-equity.component'; +import { SelectVehicleComponent } from './select-loan/vehicle/select-vehicle.component'; +import { SelectSubVehicleComponent } from './select-loan/vehicle/sub-vehicle/select-sub-vehicle.component'; +import { MemberLoginComponent } from './member/member-login/member-login.component'; +import { MemberOutletComponent } from './member/member-outlet/member-outlet.component'; +import { AuthenticationGuard } from 'microsoft-adal-angular6'; +import { AdminRoleGuard } from './shared/guards/admin-role.guard'; +import { environment } from 'src/environments/environment'; +import { TestRoleGuard } from './shared/guards/test-role.guard'; +import { MemberLoginGuard } from './shared/guards/member-login.guard'; +import { MemberVerificationComponent } from './member/member-verification/member-verification.component'; +import { MemberVerificationGuard } from './shared/guards/member-verification-guard'; +import { MemberCoreResolver } from './member/member-core.resolver'; +import { AppDownComponent } from './shared/app-down/app-down-page.component'; + +export const routes: Routes = [ + { + path: 'select-loan', + component: SelectLoanOutletComponent, + resolve: {UiSettings: ConfigurationResolver}, + canActivate: + environment.testGuard ? [AuthenticationGuard, TestRoleGuard, MemberVerificationGuard] : [MemberVerificationGuard], + children: [ + { + path: '', + component: SelectLoanComponent + }, + { + path: 'credit-card', + component: SelectCreditCardComponent + }, + { + path: 'home-equity', + component: SelectHomeEquityComponent, + }, + { + path: 'vehicle', + component: SelectVehicleComponent, + }, + { + path: 'sub-vehicle', + component: SelectSubVehicleComponent, + } + ] + }, + { + path: 'member', + component: MemberOutletComponent, + canActivate: environment.testGuard ? [AuthenticationGuard] : [], + children: [ + { + path: '', + component: MemberLoginComponent + }, + { + path: 'verification', + component: MemberVerificationComponent + }, + ] + }, + { + path: 'admin', + canActivate: [AuthenticationGuard, AdminRoleGuard], + loadChildren: './admin/admin.module#AdminModule' + }, + { + path: 'timeout', + component: TimeoutLandingPageComponent, + canDeactivate: [AppTimeoutGuard] + }, + { + path: 'error', + component: AppUnavailableComponent + }, + { + path: 'dealership-portal', + resolve: {UiSettings: ConfigurationResolver}, + component: DealershipPortalComponent + }, + { + path: 'down', + component: AppDownComponent + }, + { + path: 'loan-wizard', + component: LoanWizardComponent, + resolve: {UiSettings: ConfigurationResolver}, + canActivate: [ MemberLoginGuard ], + canActivateChild: + environment.testGuard ? [AuthenticationGuard, TestRoleGuard, MemberVerificationGuard] : [MemberVerificationGuard], + children: [ + ccRoutes, + eqRoutes, + veRoutes, + slRoutes, + {path: '', redirectTo: '/select-loan', pathMatch: 'prefix'}, + ] + }, + {path: '', redirectTo: '/select-loan', pathMatch: 'full'}, + {path: '**', redirectTo: '/select-loan'}, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {useHash: false, enableTracing: false})], + exports: [RouterModule], + providers: [ConfigurationService, ConfigurationResolver, MemberCoreResolver] +}) + +export class AppRoutingModule {} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.html new file mode 100644 index 0000000..e770858 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.html @@ -0,0 +1,9 @@ +Skip To Content + + + +
+ +
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.spec.ts new file mode 100644 index 0000000..5c5bbd9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.spec.ts @@ -0,0 +1,69 @@ +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {FooterComponent} from './layout/footer/footer.component'; +import {HeaderComponent} from './layout/header/header.component'; +import {TranslateModule} from '@ngx-translate/core'; +import {TimeoutPopupComponent} from './shared/timeout/timeout-popup.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from './shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from './testing/shared-mocks/credit-application-dao.mock.service'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('AppComponent', () => { + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot() + ], + declarations: [ + AppComponent, + FooterComponent, + HeaderComponent, + TimeoutPopupComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render a router outlet', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('router-oulet')).toBeDefined(); + }); + + it('should include a header', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('lw-header')).toBeDefined(); + }); + + it('should include a footer', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('lw-footer')).toBeDefined(); + }); + + it('should include timeout modal', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('lw-timeout-popup')).toBeDefined(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.ts new file mode 100644 index 0000000..cf74a68 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/app.component.ts @@ -0,0 +1,39 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { NavigationEnd, Router } from '@angular/router'; + +@Component({ + selector: 'lw-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit, OnDestroy { + + private subscriptionRef: Subscription; + public skipLinkPath: string; + constructor(translate: TranslateService, + private router: Router) { + translate.setDefaultLang('en'); + translate.use('en'); + } + + ngOnInit() { + this.subscriptionRef = this.router.events.subscribe((event) => { + if (event instanceof NavigationEnd) { + this.scrollToTop(); + } + this.skipLinkPath = this.router.url.includes('#content') ? `${this.router.url}` : `${this.router.url}#content`; + }); + } + + ngOnDestroy() { + if (this.subscriptionRef) { + this.subscriptionRef.unsubscribe(); + } + } + + private scrollToTop(): void { + document.getElementById('header').scrollIntoView(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/app.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/app.module.ts new file mode 100644 index 0000000..99c825e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/app.module.ts @@ -0,0 +1,281 @@ +import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { LoanWizardComponent } from './loan-wizard/loan-wizard.component'; +import { WaypointsComponent } from './layout/waypoints/waypoints.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { InputIconComponent } from './loan-wizard/controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from './loan-wizard/controls/error-display/error-display.component'; +import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { SelectLoanComponent } from './select-loan/select-loan.component'; +import { MemberInfoFormComponent } from './loan-wizard/field-groups/member/member-info-form.component'; +import { PersonalInfoFormComponent } from './loan-wizard/field-groups/personal/personal-info-form.component'; +import { AddressInfoFormComponent } from './loan-wizard/field-groups/address/address-info-form.component'; +import { EmploymentInfoFormComponent } from './loan-wizard/field-groups/employment/employment-info-form.component'; +import { AddressInfoPageComponent } from './loan-wizard/wizards/common/address/address-info-page.component'; +import { MemberInfoPageComponent } from './loan-wizard/wizards/common/member/member-info-page.component'; +import { PersonalInfoPageComponent } from './loan-wizard/wizards/common/personal/personal-info-page.component'; +import { EmploymentInfoPageComponent } from './loan-wizard/wizards/common/employment/employment-info-page.component'; +import { FooterComponent } from './layout/footer/footer.component'; +import { HeaderComponent } from './layout/header/header.component'; +import { HeaderIconComponent } from './layout/header/header-icon.component'; +import { PatriotActComponent } from './shared/patriot-act/patriot-act.component'; +import { IsAddressSameInfoFormComponent } from './loan-wizard/field-groups/address/is-address-same-info-form.component'; +import { CreditCardDisclosureFormComponent } from './loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component'; +import { CreditCardDisclosuresPageComponent } from './loan-wizard/wizards/credit-card/credit-card-disclosures-page.component'; +import { CreditCardSchumerBoxComponent } from './loan-wizard/wizards/credit-card/credit-card-schumer-box.component'; +import { SummaryPageComponent } from './loan-wizard/wizards/common/summary/summary-page.component'; +import { TextMaskModule } from 'angular2-text-mask'; +import { PersonalSummaryComponent } from './loan-wizard/wizards/common/summary/personal-summary.component'; +import { ResidenceSummaryComponent } from './loan-wizard/wizards/common/summary/residence-summary.component'; +import { ApplicationSummaryComponent } from './loan-wizard/wizards/common/summary/application-summary.component'; +import { IncomeSummaryComponent } from './loan-wizard/wizards/common/summary/income-summary.component'; +import { EquityMemberInfoPageComponent } from './loan-wizard/wizards/equity/equity-member-info-page.component'; +import { ResidenceInfoFormComponent } from './loan-wizard/field-groups/address/residence-info-form.component'; +import { DecisionPageComponent } from './loan-wizard/wizards/common/decision/decision-page.component'; +import { BeforeYouBeginComponent } from './shared/before-you-begin/before-you-begin.component'; +import { FileUploadComponent } from './shared/file-upload/file-upload.component'; +import { SsnPipe } from './shared/pipes/ssn.pipe'; +import { PhoneTypePipe } from './shared/pipes/phone-type.pipe'; +import { UploadedFilesPipe } from './shared/pipes/uploaded-files.pipe'; +import { PhoneNumberPipe } from './shared/pipes/phone-number.pipe'; +import { StatePipe } from './shared/pipes/state.pipe'; +import { AddressStatusPipe } from './shared/pipes/address-status.pipe'; +import { EmploymentTypePipe } from './shared/pipes/employment-type.pipe'; +import { CollateralInfoFormComponent } from './loan-wizard/field-groups/equity/collateral-info-form.component'; +import { CollateralInfoPageComponent } from './loan-wizard/wizards/equity/collateral-info-page.component'; +import { CollateralSummaryComponent } from './loan-wizard/wizards/common/summary/collateral-summary.component'; +import { BooleanStringPipe } from './shared/pipes/boolean-string.pipe'; +import { OccupancyTypePipe } from './shared/pipes/occupancy-type.pipe'; +import { PropertyTypePipe, MfgHomePropertyTypePipe } from './shared/pipes/property-type.pipe'; +import { GenderInfoFormComponent } from './loan-wizard/field-groups/hmda/gender-info-form.component'; +import { EthnicityInfoFormComponent } from './loan-wizard/field-groups/hmda/ethnicity-info-form.component'; +import { RaceInfoFormComponent } from './loan-wizard/field-groups/hmda/race-info-form.component'; +import { HmdaInfoPageComponent } from './loan-wizard/wizards/common/hmda/hmda-info-page.component'; +import { ElectronicDisclosuresFormComponent } from './loan-wizard/field-groups/disclosures/electronic-disclosures-form.component'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { OtherIncomeInfoFormComponent } from './loan-wizard/field-groups/employment/other-income-info-form.component'; +import { AlimonyInfoFormComponent } from './loan-wizard/field-groups/employment/alimony-info-form.component'; +import { EmploymentFlagFormComponent } from './loan-wizard/field-groups/employment/financial/employment-flag-form.component'; +import { AlimonyFlagFormComponent } from './loan-wizard/field-groups/employment/financial/alimony-flag-form.component'; +import { OtherIncomeFlagFormComponent } from './loan-wizard/field-groups/employment/financial/other-income-flag-form.component'; +import { VehicleCollateralInfoPageComponent } from './loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component'; +import { VehicleCollateralInfoFormComponent } from './loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component'; +import { AppUnavailableComponent } from './shared/app-unavailable/app-unavailable-page.component'; +import { DealershipPortalComponent } from './shared/dealership-portal/dealership-portal-page.component'; +import { TimeoutPopupComponent } from './shared/timeout/timeout-popup.component'; +import { TimeoutLandingPageComponent } from './shared/timeout/timeout-landing-page.component'; +import { TattleSpousePersonalInfoFormComponent } from './loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component'; +import { TattleSpouseInfoPageComponent } from './loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component'; +import { SpouseTypeInfoPageComponent } from './loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component'; +import { IsFullInfoSpouseInfoFormComponent } from './loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component'; +import { IsRentedLotInfoFormComponent } from './loan-wizard/field-groups/equity/is-rented-lot-info-form.component'; +import { TattleSpouseSummaryComponent } from './loan-wizard/wizards/common/summary/tattle-spouse-summary.component'; +import { environment } from '../environments/environment'; +import { DecisionAnimationComponent } from './layout/decision-animation/decision-animation.component'; +import { JointCreditFormComponent } from './loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component'; +import { ProcessingPageComponent } from './loan-wizard/wizards/common/processing/processing-page.component'; +import { DecisionHeroComponent } from './layout/decision-hero/decision-hero.component'; +import { AutoFocusDirective } from './shared/page-focus/auto-focus.directive'; +import { VehicleSummaryComponent } from './loan-wizard/wizards/common/summary/vehicle-summary.component'; +import { VehicleSellerPipe } from './shared/pipes/vehicle-collateral.pipe'; +import { DisclosureBoxesComponent } from './shared/legal-message/disclosure-boxes.component'; +import { NoteFormComponent } from './loan-wizard/field-groups/summary/note-form.component'; +import { AdditionalInfoFormComponent } from './loan-wizard/field-groups/summary/additional-info-form.component'; +import { BUILD_STAMP } from '../environments/version'; +import { AddressCompareInfoFormComponent } from './loan-wizard/field-groups/address-compare/address-compare-info-form.component'; +import { VehicleTypePipe } from './shared/pipes/vehicle-type.pipe'; +import { AuthenticationGuard, MsAdalAngular6Module } from 'microsoft-adal-angular6'; +import { AppLoaderComponent } from './shared/app-loader/app-loader.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { TcpaComponent } from './shared/legal-message/tcpa.component'; +import { SelectCreditCardComponent } from './select-loan/credit-card/select-credit-card.component'; +import { SelectHomeEquityComponent } from './select-loan/home/select-home-equity.component'; +import { SelectVehicleComponent } from './select-loan/vehicle/select-vehicle.component'; +import { MainCardComponent } from './select-loan/cards/main-card/main-card.component'; +import { SubCardComponent } from './select-loan/cards/sub-card/sub-card.component'; +import { SelectLoanOutletComponent } from './select-loan/outlet/select-loan-outlet.component'; +import { SelectSubVehicleComponent } from './select-loan/vehicle/sub-vehicle/select-sub-vehicle.component'; +import { MemberLoginComponent } from './member/member-login/member-login.component'; +import { MemberOutletComponent } from './member/member-outlet/member-outlet.component'; +import { SharedModule } from './shared/shared.module'; +import { MemberVerificationComponent } from './member/member-verification/member-verification.component'; +import { LoanIconPipe } from './shared/pipes/loan-icon.pipe'; +import { HttpErrorReporter } from './shared/http-error-reporter.service'; +import { AppDownComponent } from './shared/app-down/app-down-page.component'; +function resolveRedirectUri(): string { + if (typeof window === 'undefined') { + return environment.msAuthRedirectUri; + } + + const rawRedirectUri = environment.msAuthRedirectUri; + if (rawRedirectUri) { + try { + const parsed = new URL(rawRedirectUri, window.location.origin); + return `${window.location.origin}${parsed.pathname}${parsed.search}${parsed.hash}`; + } catch (error) { + // Fall back to origin if the configured redirect URI is invalid. + } + } + + const msAuthRedirectUri = window.location.origin; + + return window.location.pathname === '/admin' ? window.location.href : msAuthRedirectUri; +} + +export function adalConfigFactory() { + return { + tenant: '63cd1554-2783-4a86-ae50-b9e6c21c9499', + clientId: '59d54f39-3ff9-4166-8b16-254c00416754', + redirectUri: resolveRedirectUri(), + endpoints: {}, + navigateToLoginRequestUrl: false, + cacheLocation: 'localStorage' + }; +} + +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, environment.baseHref + '/assets/i18n/', '.json?cb=' + new Date().getTime()); +} + +@NgModule({ + declarations: [ + AddressCompareInfoFormComponent, + AddressInfoFormComponent, + AddressInfoPageComponent, + AddressStatusPipe, + AlimonyFlagFormComponent, + AlimonyInfoFormComponent, + AppComponent, + ApplicationSummaryComponent, + AppLoaderComponent, + AppDownComponent, + AppUnavailableComponent, + DealershipPortalComponent, + AutoFocusDirective, + BeforeYouBeginComponent, + FileUploadComponent, + BooleanStringPipe, + CollateralInfoFormComponent, + CollateralInfoPageComponent, + CollateralSummaryComponent, + CreditCardDisclosureFormComponent, + CreditCardDisclosuresPageComponent, + CreditCardSchumerBoxComponent, + DecisionAnimationComponent, + DecisionPageComponent, + DecisionHeroComponent, + DisclosureBoxesComponent, + ElectronicDisclosuresFormComponent, + EmploymentFlagFormComponent, + EmploymentInfoFormComponent, + EmploymentInfoPageComponent, + EmploymentTypePipe, + EthnicityInfoFormComponent, + EquityMemberInfoPageComponent, + ErrorDisplayComponent, + FooterComponent, + GenderInfoFormComponent, + HeaderComponent, + HeaderIconComponent, + HmdaInfoPageComponent, + IncomeSummaryComponent, + InputIconComponent, + IsAddressSameInfoFormComponent, + IsFullInfoSpouseInfoFormComponent, + IsRentedLotInfoFormComponent, + JointCreditFormComponent, + TcpaComponent, + LoanWizardComponent, + LoanIconPipe, + MainCardComponent, + MemberLoginComponent, + MemberVerificationComponent, + MemberOutletComponent, + MemberInfoFormComponent, + MemberInfoPageComponent, + MfgHomePropertyTypePipe, + NoteFormComponent, + AdditionalInfoFormComponent, + OccupancyTypePipe, + OtherIncomeFlagFormComponent, + OtherIncomeInfoFormComponent, + PatriotActComponent, + PersonalInfoFormComponent, + PersonalInfoPageComponent, + PersonalSummaryComponent, + PhoneNumberPipe, + PhoneTypePipe, + UploadedFilesPipe, + ProcessingPageComponent, + PropertyTypePipe, + RaceInfoFormComponent, + ResidenceSummaryComponent, + ResidenceInfoFormComponent, + SelectLoanOutletComponent, + SelectLoanComponent, + SelectCreditCardComponent, + SelectHomeEquityComponent, + SelectVehicleComponent, + SelectSubVehicleComponent, + SpouseTypeInfoPageComponent, + SsnPipe, + StatePipe, + SubCardComponent, + SummaryPageComponent, + TattleSpousePersonalInfoFormComponent, + TattleSpouseInfoPageComponent, + TattleSpouseSummaryComponent, + TimeoutPopupComponent, + TimeoutLandingPageComponent, + VehicleCollateralInfoPageComponent, + VehicleCollateralInfoFormComponent, + VehicleSellerPipe, + VehicleTypePipe, + VehicleSummaryComponent, + WaypointsComponent + ], + imports: [ + AppRoutingModule, + BrowserAnimationsModule, + FormsModule, + HttpClientModule, + NgbModule.forRoot(), + SharedModule, + TextMaskModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: (createTranslateLoader), + deps: [HttpClient] + } + }), + ReactiveFormsModule, + MsAdalAngular6Module + ], + providers: [ + { + provide: 'adalConfig', + useFactory: adalConfigFactory, + }, + AuthenticationGuard, + {provide: ErrorHandler, useClass: HttpErrorReporter} + ], + bootstrap: [AppComponent], + entryComponents: [ + AddressCompareInfoFormComponent, + AddressInfoFormComponent, + CollateralInfoFormComponent, + IsRentedLotInfoFormComponent, + ResidenceInfoFormComponent, + EmploymentInfoFormComponent, + IsAddressSameInfoFormComponent, + IsFullInfoSpouseInfoFormComponent, + MemberInfoFormComponent, + TattleSpousePersonalInfoFormComponent, + TimeoutPopupComponent + ] +}) +export class AppModule {} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.html new file mode 100644 index 0000000..7a831ae --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.spec.ts new file mode 100644 index 0000000..90d9640 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.spec.ts @@ -0,0 +1,67 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { DecisionAnimationComponent } from './decision-animation.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProcessingStep } from '../../loan-wizard/wizards/common/processing/processing-step'; + +describe('Decision Animation Component', () => { + let component: DecisionAnimationComponent; + let fixture: ComponentFixture; + const preMsg = 'test_pre_msg'; + const postMsg = 'test_post_msg'; + const hasStatus = false; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ NgbModule.forRoot()], + declarations: [ DecisionAnimationComponent ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DecisionAnimationComponent); + component = fixture.componentInstance; + component.step = new ProcessingStep( preMsg, postMsg, hasStatus); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create with correct values', () => { + expect(component.step).toBeTruthy(); + expect(component.step.preMessage).toEqual(preMsg); + expect(component.step.postMessage).toEqual(postMsg); + expect(component.step.hasStatus).toEqual(hasStatus); + }); + + it('should set current', () => { + expect(component.step).toBeTruthy(); + component.step.setCurrent(true); + expect(component.step.isActive).toEqual(true); + }); + + it('should set complete', () => { + component.step.setComplete(true); + expect(component.step.isActive).toEqual(false); + expect(component.step.isComplete).toEqual(true); + }); + + it('should get message', () => { + component.step.setComplete(false); + let message = component.step.getMessage(); + expect(message).toEqual(preMsg); + component.step.setComplete(true); + message = component.step.getMessage(); + expect(message).toEqual(postMsg); + }); + + it('should set post message', () => { + const message = 'a different test message'; + expect(component.step.postMessage).toEqual(postMsg); + component.step.setPostMessage(message); + expect(component.step.postMessage).toEqual(message); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.ts new file mode 100644 index 0000000..6108ed1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-animation/decision-animation.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { ProcessingStep } from '../../loan-wizard/wizards/common/processing/processing-step'; + +@Component({ + selector: 'lw-decision-animation', + templateUrl: './decision-animation.component.html', + styleUrls: ['./decision-animation.component.scss'] +}) +export class DecisionAnimationComponent { + + @Input() + step: ProcessingStep; + + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.html new file mode 100644 index 0000000..682e468 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.html @@ -0,0 +1,5 @@ +
+

+

+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.spec.ts new file mode 100644 index 0000000..204d1af --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.spec.ts @@ -0,0 +1,73 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DecisionHeroComponent, HeroType} from './decision-hero.component'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {of} from 'rxjs'; +import {configureTestSuite} from 'ng-bullet'; +import {LoanTypeSet} from '../../shared/loan-type'; + +describe('Decision Hero Component', () => { + let component: DecisionHeroComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [NgbModule.forRoot(), TranslateModule.forRoot()], + declarations: [DecisionHeroComponent] + }); + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [NgbModule.forRoot(), TranslateModule.forRoot()], + declarations: [DecisionHeroComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DecisionHeroComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + translateService = TestBed.get(TranslateService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + component.displayName = 'John'; + component.heroType = HeroType.SUCCESS; + fixture.detectChanges(); + }); + + it('should find the right key for a type', () => { + expect(component).toBeTruthy(); + expect(component.lookupKeyForType(null)).toEqual('DECISION.ERROR_SUBMIT'); + expect(component.lookupKeyForType(HeroType.REVIEW)).toEqual('DECISION.REVIEW'); + expect(component.lookupKeyForType(HeroType.ERROR_SUBMIT)).toEqual('DECISION.ERROR_SUBMIT'); + expect(component.lookupKeyForType(HeroType.ERROR_DECISION)).toEqual('DECISION.ERROR_DECISION'); + }); + + it('should ask the translate service for the messaging', () => { + expect(component).toBeTruthy(); + spyOn(component, 'lookupKeyForType').and.returnValue('DECISION.ERROR_SUBMIT'); + spyOn(translateService, 'get').and.returnValue(of('AAA')); + component.ngOnChanges(null); + expect(translateService.get).toHaveBeenCalledTimes(2); + expect(component.lookupKeyForType).toHaveBeenCalled(); + }); + + it('should ask the translate service for the messaging', () => { + expect(component).toBeTruthy(); + expect(LoanTypeSet[0].decisionReviewKey).toEqual('TEXT_CC'); + component.decisionKey = LoanTypeSet[0].decisionReviewKey; + spyOn(component, 'lookupKeyForType').and.returnValue('DECISION.REVIEW'); + spyOn(translateService, 'get').and.returnValue(of('AAA')); + component.ngOnChanges(null); + expect(translateService.get).toHaveBeenCalledWith('DECISION.REVIEW.TITLE', {displayName: undefined}); + expect(translateService.get).toHaveBeenCalledWith('DECISION.REVIEW.TEXT_CC', {displayName: undefined}); + expect(translateService.get).toHaveBeenCalledTimes(2); + expect(component.lookupKeyForType).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.ts new file mode 100644 index 0000000..ef534a9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/decision-hero/decision-hero.component.ts @@ -0,0 +1,62 @@ +import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; + +export enum HeroType { + SUCCESS, + REVIEW, + DENIED, + ERROR_SUBMIT, + ERROR_DECISION +} + +@Component({ + selector: 'lw-decision-hero', + templateUrl: './decision-hero.component.html', + styleUrls: ['./decision-hero.component.scss'] +}) +export class DecisionHeroComponent implements OnChanges { + + public localState = { + text: '', + title: '' + }; + + @Input() + public decisionKey: string; + + @Input() + public heroType: HeroType; + + @Input() + public displayName: string; + + constructor(private translateService: TranslateService) { + } + + public ngOnChanges(changes: SimpleChanges): void { + const key = this.lookupKeyForType(this.heroType); + const decisionKey = this.decisionKey || 'TEXT'; + this.translateService.get(key + '.TITLE', {displayName: this.displayName}).subscribe((res: string) => { + this.localState.title = res; + }); + this.translateService.get(key + '.' + decisionKey, {displayName: this.displayName}).subscribe((res: string) => { + this.localState.text = res; + }); + } + + public lookupKeyForType(heroType: HeroType) { + switch (heroType) { + case HeroType.SUCCESS: + return 'DECISION.APPROVED'; + case HeroType.REVIEW: + return 'DECISION.REVIEW'; + case HeroType.DENIED: + return 'DECISION.DENIED'; + case HeroType.ERROR_DECISION: + return 'DECISION.ERROR_DECISION'; + default: + case HeroType.ERROR_SUBMIT: + return 'DECISION.ERROR_SUBMIT'; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.html new file mode 100644 index 0000000..f14003d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.html @@ -0,0 +1,27 @@ +
+
+
+ +
+
+ FOOTER.NCUA + +
+ +
+ FOOTER.HOUSING + +
+ +
+ FOOTER.SECURE +
+
+
+ FOOTER.COPY_RIGHT +
+ +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.spec.ts new file mode 100644 index 0000000..3caa61b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FooterComponent } from './footer.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { configureTestSuite } from 'ng-bullet'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('Footer Component', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ NgbModule.forRoot()], + declarations: [ FooterComponent ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.ts new file mode 100644 index 0000000..e46e600 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/footer/footer.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { BUILD_STAMP, VERSION } from '../../../environments/version'; + +@Component({ + selector: 'lw-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'] +}) +export class FooterComponent { + + public readonly routingNumber = '275079714'; + public versionHint = VERSION.version + '-' + BUILD_STAMP; + public currentYear = new Date().getFullYear(); + + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.html new file mode 100644 index 0000000..0e6ba98 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.spec.ts new file mode 100644 index 0000000..9e6f622 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { HeaderIconComponent } from './header-icon.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Header Icon Component', () => { + let component: HeaderIconComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ NgbModule.forRoot()], + declarations: [ HeaderIconComponent ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HeaderIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.ts new file mode 100644 index 0000000..d105a6c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header-icon.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'lw-header-icon', + templateUrl: './header-icon.component.html', + styleUrls: ['./header.component.scss'] +}) +export class HeaderIconComponent { + @Input() + public applicationIcon: string; + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.html new file mode 100644 index 0000000..8af0bfe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.html @@ -0,0 +1,34 @@ +
+
+ + +
+
+
+
+ + {{applicantType?.headerMessageKey | translate}} +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.scss new file mode 100644 index 0000000..54981b3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.scss @@ -0,0 +1,53 @@ +$primary: #005674; +$primaryDark: #053251; +$max-width: 770px; + +.navbar-lmcu { + border-bottom: 3px solid #b3c9e4; +} + +.logo-lmcu { + height: 70px; +} + +.phone-button { + display: none; +} + +.header-information-small { + display: none !important; +} + +@media screen and (max-width: $max-width) { + .logo-lmcu { + height: 35px; + } + .phone-button { + position: absolute; + display: inline; + border: $primary; + right: -15px; + top:0; + background-color: $primary; + height: 60px; + width: 60px; + cursor:pointer; + color: white; + } + .phone-button-icon { + width: 1.5rem; + height: 1.5rem; + } + .phone-button:hover { + background-color: $primaryDark; + color:gray; + } + + .header-information-big { + display: none; + } + .header-information-small { + display: inline !important; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.spec.ts new file mode 100644 index 0000000..063cacb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.spec.ts @@ -0,0 +1,121 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderComponent } from './header.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { HeaderService } from './header.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TextMaskModule } from 'angular2-text-mask'; +import { CreditApplicationDaoService } from '../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { LoanTypeSet } from '../../shared/loan-type'; +import { HeaderIconComponent } from './header-icon.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Header Component', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let service: HeaderService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot()], + declarations: [ HeaderComponent, HeaderIconComponent], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + service = TestBed.get(HeaderService); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should update icons/text based on loan type ( credit card ) ', () => { + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + // set to credit card + applicationWizardService.setLoanType(LoanTypeSet[0].id); + fixture.detectChanges(); + const creditCard = compiled.querySelectorAll('.fa-credit-card'); + const header = compiled.querySelectorAll('.header-information-big .col-6 small'); + const home = compiled.querySelectorAll('.fa-home'); + const vehicle = compiled.querySelectorAll('.fa-car'); + + expect(creditCard).toBeDefined(); + expect(home).toBeDefined(); + expect(vehicle).toBeDefined(); + expect(header).toBeDefined(); + + expect(creditCard.length).toBeGreaterThanOrEqual(1); + expect(home.length).toEqual(0); + expect(vehicle.length).toEqual(0); + expect(header[0].innerHTML).toContain(LoanTypeSet[0].headerMessageKey); + }); + + it('should update icons/text based on loan type ( equity ) ', () => { + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + // set to equity + applicationWizardService.setLoanType(LoanTypeSet[4].id); + fixture.detectChanges(); + const creditCard = compiled.querySelectorAll('.fa-credit-card'); + const header = compiled.querySelectorAll('.header-information-big .col-6 small'); + const home = compiled.querySelectorAll('.fa-home'); + const vehicle = compiled.querySelectorAll('.fa-car'); + + expect(creditCard).toBeDefined(); + expect(home).toBeDefined(); + expect(vehicle).toBeDefined(); + expect(header).toBeDefined(); + + expect(creditCard).toBeDefined(); + expect(home).toBeDefined(); + expect(vehicle).toBeDefined(); + expect(creditCard.length).toEqual(0); + expect(home.length).toBeGreaterThanOrEqual(1); + expect(vehicle.length).toEqual(0); + expect(header[0].innerHTML).toContain(LoanTypeSet[4].headerMessageKey); + }); + + it('should update icons/text based on loan type ( vehicle ) ', () => { + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + // set to vehicle + applicationWizardService.setLoanType(LoanTypeSet[7].id); + fixture.detectChanges(); + const creditCard = compiled.querySelectorAll('.fa-credit-card'); + const header = compiled.querySelectorAll('.header-information-big .col-6 small'); + const home = compiled.querySelectorAll('.fa-home'); + const vehicle = compiled.querySelectorAll('.fa-car'); + + expect(creditCard).toBeDefined(); + expect(home).toBeDefined(); + expect(vehicle).toBeDefined(); + expect(header).toBeDefined(); + + expect(creditCard.length).toEqual(0); + expect(home.length).toEqual(0); + expect(vehicle.length).toBeGreaterThanOrEqual(1); + expect(header[0].innerHTML).toContain(LoanTypeSet[7].headerMessageKey); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.ts new file mode 100644 index 0000000..b51d9bf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.component.ts @@ -0,0 +1,29 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { HeaderService } from './header.service'; +import { Subscription } from 'rxjs'; +import { LoanType } from '../../shared/loan-type'; + +@Component({ + selector: 'lw-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'] +}) +export class HeaderComponent implements OnInit, OnDestroy { + + public applicantType: LoanType; + private subscriptionRef: Subscription; + + constructor(private headerService: HeaderService) { } + + ngOnInit() { + this.subscriptionRef = this.headerService.getApplicantTypeObservable().subscribe(value => { + this.applicantType = value; + }); + } + + ngOnDestroy() { + if (this.subscriptionRef) { + this.subscriptionRef.unsubscribe(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.spec.ts new file mode 100644 index 0000000..ec743b3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.spec.ts @@ -0,0 +1,61 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { HeaderService } from './header.service'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { Router } from '@angular/router'; +import { CreditApplicationDaoService } from '../../shared/credit-application-dao.service'; +import { LoanTypeSet } from '../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Header Service', () => { + + let service: HeaderService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(HeaderService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + router = TestBed.get(Router); + }); + + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should get applicant observable', () => { + spyOn(service, 'getApplicantTypeObservable').and.callThrough(); + const observable$ = service.getApplicantTypeObservable(); + expect(service.getApplicantTypeObservable).toHaveBeenCalled(); + expect(observable$).toBeDefined(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.ts new file mode 100644 index 0000000..588ff78 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/header/header.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LoanType } from '../../shared/loan-type'; +import { CreditApplicationService } from '../../shared/credit-application.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class HeaderService { + + constructor(private creditApplicationService: CreditApplicationService) { + } + + public getApplicantTypeObservable(): Observable { + return this.creditApplicationService.getLoanTypeObservable(); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.spec.ts new file mode 100644 index 0000000..c50ed35 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.spec.ts @@ -0,0 +1,106 @@ +import { Waypoints, WaypointService } from './waypoint.service'; +import { Waypoint } from './waypoint'; + +const stepIndex = 4; // start counting at 0 +const amountOfSteps = 5; // number of steps in the stepper + +describe('Waypoint Service', () => { + let service: WaypointService; + + beforeEach(() => { + service = new WaypointService(); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should have an initial value', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + expect(testData.length).toEqual(amountOfSteps); + const waypoint = testData[0]; + expect(waypoint.id).toEqual(0); + expect(waypoint.isActive).toEqual(false); + }); + + it('should send out updated values on change', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + testData = null; + service.setStepActive(2); + expect( testData ).not.toBeNull(); + }); + + it('should only set a single step as active.', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + expect( testData[0].isActive).toEqual(false); + expect( testData[1].isActive).toEqual(false); + service.setStepActive(0); + expect( testData[0].isActive).toEqual(true); + expect( testData[1].isActive).toEqual(false); + service.setStepActive(1); + expect( testData[0].isActive).toEqual(false); + expect( testData[1].isActive).toEqual(true); + }); + + it('should throw an error on invalid index.', () => { + expect( () => { service.setStepActive(-1); }).toThrowError(); + expect( () => { service.setStepActive(stepIndex); }).not.toThrowError(); + expect( () => { service.setStepActive(stepIndex + 1); }).toThrowError(); + expect( () => { service.setStepActive(99); }).toThrowError(); + }); + + it('should any steps as done.', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + expect( testData[Waypoints.APPLICANT].isComplete).toEqual(false); + expect( testData[Waypoints.REVIEW].isComplete).toEqual(false); + service.setStepActive(Waypoints.REVIEW); + expect( testData[Waypoints.APPLICANT].isComplete).toEqual(true); + expect( testData[Waypoints.REVIEW].isComplete).toEqual(false); + service.setStepActive(Waypoints.FINISH); + expect( testData[Waypoints.APPLICANT].isComplete).toEqual(true); + expect( testData[Waypoints.REVIEW].isComplete).toEqual(true); + }); + + it('should show collateral if it is required.', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + service.showCollateral(true); + expect( testData[Waypoints.COLLATERAL].shouldBeShown).toEqual(true); + service.showCollateral(false); + expect( testData[Waypoints.COLLATERAL].shouldBeShown).toEqual(false); + }); + + it('should set steps as complete up to the active step.', () => { + spyOn(service, 'setStepActive').and.callThrough(); + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + service.setStepActive(Waypoints.REVIEW); + expect(service.setStepActive).toHaveBeenCalled(); + expect(testData[Waypoints.REVIEW].isActive).toBe(true); + expect(testData[Waypoints.APPLICANT].isComplete).toBe(true); + expect(testData[Waypoints.FINISH].isComplete).toBe(false); + }); + + it('should set all steps as complete when finished.', () => { + spyOn(service, 'setAllComplete').and.callThrough(); + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + expect(testData).not.toBeNull(); + service.setAllComplete(true); + expect(service.setAllComplete).toHaveBeenCalled(); + testData.forEach((waypoint) => { + expect(waypoint.isComplete).toBe(true); + }); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.ts new file mode 100644 index 0000000..868bd42 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.service.ts @@ -0,0 +1,162 @@ +import { Waypoint } from './waypoint'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export enum Waypoints { + APPLICANT, + CO_APPLICANT, + COLLATERAL, + REVIEW, + FINISH +} + +@Injectable({ + providedIn: 'root' +}) +export class WaypointService { + + private readonly applicantWayPoint = { + displayText: 'Applicant', + activeImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-blue-01.png`, + inactiveImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-gray-01.png`, + id: 0, + isActive: false, + isComplete: false, + shouldBeShown: true, + comment: null, + displayNumber: 1 + }; + + private readonly coapplicantWayPoint = { + displayText: 'Co-Applicant', + activeImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-blue-01.png`, + inactiveImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-gray-01.png`, + id: 1, + isActive: false, + isComplete: false, + shouldBeShown: false, + comment: null, + displayNumber: 2 + }; + + private readonly collateralWayPoint = { + displayText: 'Collateral', + // TODO ( next PR ) these images can change depending on collateral type, for now $'s are fine + activeImagePath: `${environment.baseHref}/assets/waypoint/collateral-icon-blue.png`, + inactiveImagePath: `${environment.baseHref}/assets/waypoint/collateral-icon-gray.png`, + id: 2, + isActive: false, + isComplete: false, + shouldBeShown: false, + comment: null, + displayNumber: 2 + }; + + private readonly reviewWayPoint = { + displayText: 'Review', + activeImagePath: `${environment.baseHref}/assets/waypoint/review-icon-blue.png`, + inactiveImagePath: `${environment.baseHref}/assets/waypoint/review-icon-gray.png`, + id: 3, + isActive: false, + isComplete: false, + shouldBeShown: true, + comment: null, + displayNumber: 2 + }; + + private readonly finishWayPoint = { + displayText: 'Finish', + activeImagePath: `${environment.baseHref}/assets/waypoint/5-finish-active-icon.png`, + inactiveImagePath: `${environment.baseHref}/assets/waypoint/finish-icon-gray.png`, + id: 4, + isActive: false, + isComplete: false, + shouldBeShown: true, + comment: null, + displayNumber: 3 + }; + + + private readonly defaultWayPointSet = [this.applicantWayPoint, this.coapplicantWayPoint, + this.collateralWayPoint, this.reviewWayPoint, this.finishWayPoint]; + private waypointSet: BehaviorSubject> = new BehaviorSubject>(this.defaultWayPointSet); + public waypointSet$ = this.waypointSet.asObservable(); + + constructor() { } + + private updateDisplayNumbers() { + let counter = 1; + const newWayPoints = this.waypointSet.value.splice(0); + newWayPoints.forEach((waypoint) => { + if ( waypoint.shouldBeShown ) { + waypoint.displayNumber = counter; + counter++; + } + }, this); + this.waypointSet.next(newWayPoints); + } + + public handleActivePageRoute(route: string) { + if (route === 'step-1' || route === 'step-4') { + this.setStepActive(Waypoints.APPLICANT); + } else if (route.includes('co-applicant')) { + this.setStepActive(Waypoints.CO_APPLICANT); + } else if (route.includes('collateral')) { + this.setStepActive(Waypoints.COLLATERAL); + } else if (route === 'cc-disclosures' || route === 'cc-summary') { + this.setStepActive(Waypoints.REVIEW); + } else if (route === 'decision') { + this.setAllComplete(true); + } + } + + public showCoApplicant(shouldShow: boolean) { + const newWayPoints = this.waypointSet.value.splice(0); + newWayPoints[Waypoints.CO_APPLICANT].shouldBeShown = shouldShow; + this.waypointSet.next(newWayPoints); + this.updateDisplayNumbers(); + } + + public showCollateral(shouldShow: boolean) { + const newWayPoints = this.waypointSet.value.splice(0); + newWayPoints[Waypoints.COLLATERAL].shouldBeShown = shouldShow; + this.waypointSet.next(newWayPoints); + this.updateDisplayNumbers(); + } + + public setStepActive(id: number): void { + if (id < 0 || id > Waypoints.FINISH) { + throw new Error('Waypoint ID must be between 0 and ' + Waypoints.FINISH); + } + const newWaypoints = this.waypointSet.value.splice(0); + newWaypoints.forEach((waypoint, index) => { + waypoint.isActive = (newWaypoints[index].id === id); + }, this); + this.waypointSet.next(newWaypoints); + this.setCompleteUpToActive(); + } + + private setCompleteUpToActive(): void { + let activeFlag = false; + const newWaypoints = this.waypointSet.value.splice(0); + newWaypoints.forEach((waypoint, index) => { + if (waypoint.isActive) { + activeFlag = true; + waypoint.isComplete = false; + } else { + waypoint.isComplete = !activeFlag; + } + }, this); + this.waypointSet.next(newWaypoints); + } + + public setAllComplete(complete: boolean): void { + const newWaypoints = this.waypointSet.value.splice(0); + newWaypoints.forEach((waypoint) => { + waypoint.isActive = false; + waypoint.isComplete = complete; + }, this); + this.waypointSet.next(newWaypoints); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.ts new file mode 100644 index 0000000..bddbbf4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoint.ts @@ -0,0 +1,11 @@ +export interface Waypoint { + id: number; + displayText: string; + activeImagePath: string; + inactiveImagePath: string; + isActive: boolean; + isComplete: boolean; + shouldBeShown: boolean; + comment: string; + displayNumber: number; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.html new file mode 100644 index 0000000..4b03385 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.html @@ -0,0 +1,26 @@ + + +
+
+ +
+ +
{{waypoint.displayNumber}}
+
+ +
+ +
{{waypoint.displayNumber}}. {{waypoint.displayText}}
+
+ + + + +
{{waypoint.displayNumber}}. {{waypoint.displayText}}
+ +
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.scss new file mode 100644 index 0000000..c27b13d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.scss @@ -0,0 +1,177 @@ +$primary: #0054A2; +$secondary: #FBFAF9; +$success: #7DA454; +$white: #FFFFFF; +$gray-background: #DFDFDF; +$dark-gray-background: #999999; +$max-width: 770px; //at what point does it switch to 'mobile' view + +.md-stepper-horizontal { + display:table; + width:100%; + margin:0 auto; + background-color: $white; +} +.md-stepper-horizontal .md-step { + display:table-cell; + position:relative; + padding:24px; +} +.md-stepper-horizontal .md-step:active { + border-radius: 15% / 75%; +} +.md-stepper-horizontal .md-step:first-child:active { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.md-stepper-horizontal .md-step:last-child:active { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.md-stepper-horizontal .md-step:first-child .md-step-bar-left, +.md-stepper-horizontal .md-step:last-child .md-step-bar-right { + display:none; +} +.md-stepper-horizontal .md-step .md-step-circle { + display: none; + width:30px; + height:30px; + margin:0 auto; + bottom: 41px; + position: relative; + background-color: $dark-gray-background; + border-radius: 50%; + text-align: center; + line-height:30px; + font-size: 16px; + font-weight: 600; + color: $white; +} +.md-stepper-horizontal.green .md-step.active .md-step-circle { + background-color:#7ca550; +} + +.md-stepper-horizontal .md-step.editable .md-step-circle { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); +} +.md-stepper-horizontal .md-step.editable .md-step-circle:before { + font-family:'FontAwesome'; + font-weight:100; + content: "\f040"; +} +.md-stepper-horizontal .md-step .md-step-title { + margin-top:12px; + font-size:16px; + font-weight:600; +} +.md-stepper-horizontal .md-step .md-step-title, +.md-stepper-horizontal .md-step .md-step-optional { + text-align: center; + color:rgba(0,0,0,.26); +} +.md-stepper-horizontal .md-step.active .md-step-title { + font-weight: 600; + color:rgba(0,0,0,.87); +} +.md-stepper-horizontal .md-step.active.done .md-step-title, +.md-stepper-horizontal .md-step.active.editable .md-step-title { + font-weight:600; +} + +.md-stepper-horizontal .md-step.done .md-step-circle { + background-color: #7ca550; +} + +.md-stepper-horizontal .md-step .md-step-optional { + font-size:12px; +} +.md-stepper-horizontal .md-step.active .md-step-optional { + color:rgba(0,0,0,.54); +} +.md-stepper-horizontal .md-step .md-step-bar-left, +.md-stepper-horizontal .md-step .md-step-bar-right { + position:absolute; + bottom:95px; + height:1px; + border-top:3px solid #DDDDDD; +} +.md-stepper-horizontal .md-step .md-step-bar-right { + z-index: -1; + margin: 0 0 0 0; + color: $gray-background; + left: 50%; + height: 4px; + width: 90%; + top: 40%; + border: none; + background-color: $gray-background; +} +.md-stepper-horizontal .md-step .md-step-bar-left { + z-index: -1; + margin: 0 0 0 0; + color: $gray-background; + height: 4px; + width: 50%; + top: 40%; + border: none; + background-color: $gray-background; +} +.md-stepper-horizontal .md-step .md-step-bar-left.done, +.md-stepper-horizontal .md-step .md-step-bar-right.done { + background-color: $success !important; +} +.md-final-step-text-mobile { + display: none; +} +/*** Mobile view settings ***/ +@media screen and (max-width: $max-width) { + .md-stepper-horizontal .md-step .md-step-bar-right { + top: 32%; + width: 60%; + } + .md-stepper-horizontal .md-step .md-step-bar-left { + top: 32%; + width: 40%; + } + .md-step-image { + display: none !important; + } + .md-stepper-horizontal .md-step{ + padding: 2px; + } + .md-stepper-horizontal .md-step .md-step-circle { + display: block !important; + } + + .md-stepper-horizontal .md-step.active img{ + width: 50px !important; + } + + .md-stepper-horizontal .md-step .md-step-title { + font-size:12px; + } + + .md-final-step-image { + width: 50px !important; + } + .md-final-step-text { + display: none !important; + } + .md-final-step-text-mobile { + display: inherit !important; + } + // way-point fix for IE + @media screen and (-ms-high-contrast: none), (-ms-high-contrast: active) { + .md-stepper-horizontal .md-step .md-step-circle { + display: -ms-flexbox !important; + bottom: 35px !important; + right: 0 !important; + position: relative; + margin-top:50px; + justify-content: center; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.spec.ts new file mode 100644 index 0000000..d0a0a39 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { WaypointsComponent } from './waypoints.component'; +import { Waypoints, WaypointService } from './waypoint.service'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Waypoints Component', () => { + let component: WaypointsComponent; + let fixture: ComponentFixture; + let service: WaypointService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [WaypointsComponent] + }); + }); + + beforeEach(() => { + service = TestBed.get(WaypointService); + fixture = TestBed.createComponent(WaypointsComponent); + fixture.detectChanges(); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(service).toBeTruthy(); + }); + + it('should render all initial provided steps ', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + const compiled = fixture.debugElement.nativeElement; + let steps = compiled.querySelectorAll('.md-step'); + expect( steps).toBeDefined(); + expect( steps.length).toEqual(3); + service.showCollateral(true); + fixture.detectChanges(); + steps = compiled.querySelectorAll('.md-step'); + expect( steps.length).toEqual(4); + service.showCoApplicant(true); + fixture.detectChanges(); + steps = compiled.querySelectorAll('.md-step'); + expect( steps.length).toEqual(testData.length); + }); + + it('should render the complete and done status of each step ', () => { + const compiled = fixture.debugElement.nativeElement; + let steps = compiled.querySelectorAll('.md-step.active'); + expect( steps).toBeDefined(); + expect( steps.length).toEqual(0); + service.setStepActive(Waypoints.REVIEW); + fixture.detectChanges(); + steps = compiled.querySelectorAll('.active'); + expect( steps).toBeDefined(); + expect( steps.length).toEqual(1); + steps = compiled.querySelectorAll('.md-step.done'); + expect( steps).toBeDefined(); + expect( steps.length).toEqual(1); + }); + + it('should render correct number and title for each step ', () => { + let testData = null; + service.waypointSet$.subscribe((data) => testData = data); + const compiled = fixture.debugElement.nativeElement; + let steps = compiled.querySelectorAll('.md-step-title'); + expect( steps).toBeDefined(); + expect( steps[0].textContent).toEqual(testData[0].displayNumber + '. ' + testData[0].displayText ); + steps = compiled.querySelectorAll('.md-step-circle span'); + expect( steps).toBeDefined(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.ts new file mode 100644 index 0000000..273bbde --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/layout/waypoints/waypoints.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { WaypointService } from './waypoint.service'; +import { Subscription } from 'rxjs'; +import { environment } from 'src/environments/environment'; +@Component({ + selector: 'lw-waypoints', + templateUrl: './waypoints.component.html', + styleUrls: ['./waypoints.component.scss'] +}) +export class WaypointsComponent implements OnInit, OnDestroy { + + public waypointSet = []; + public lastWaypoint = 0; + private subscription: Subscription; + public completedCheck = environment.baseHref + '/assets/waypoint/completed-check-icon.png'; + constructor(private waypointService: WaypointService) { } + + public ngOnInit(): void { + this.subscription = this.waypointService.waypointSet$.subscribe((waypointSet) => { + this.waypointSet = waypointSet; + this.lastWaypoint = waypointSet.length - 1; + }); + } + + public ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-page.ts new file mode 100644 index 0000000..506e8b8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-page.ts @@ -0,0 +1,35 @@ +import { ValidatorFn } from '@angular/forms'; +import { ApplicationWizardService } from './application-wizard.service'; +import { CreditApplicationService } from '../shared/credit-application.service'; + +export interface ApplicationField { + field: string; + validation: Array; +} + +export interface ApplicationFieldGroup { + group: string; + fields: Array; +} + +export class ApplicationPage { + + public route: string; + public fieldgroup: Array; + public titleKey; + + public constructor(titleKey: string, route: string, formGroup: Array) { + this.titleKey = titleKey; + this.route = route; + this.fieldgroup = formGroup; + } + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return true; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + return ['ERROR']; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.spec.ts new file mode 100644 index 0000000..3ae6b63 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.spec.ts @@ -0,0 +1,236 @@ +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../shared/credit-application-dao.service'; +import { ApplicationWizardService } from './application-wizard.service'; +import { MaskingService } from '../shared/masking/masking.service'; +import { MemberInfoFormComponent } from './field-groups/member/member-info-form.component'; +import { TextMaskModule } from 'angular2-text-mask'; +import { MemberInfoPageService } from './wizards/common/member/member-info-page.service'; +import { CommonGroups } from './field-groups/common-groups'; +import { LoanTypeSet } from '../shared/loan-type'; +import { of } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CreditApplicationValues } from '../shared/credit-application-values'; +import { TranslateModule } from '@ngx-translate/core'; +import { BorrowerType } from '../shared/borrower-type'; +import { apiTranslatorPreTranslate } from '../testing/shared-mocks/api-translator-pre-post'; +import { EquityMemberInfoPageService } from './wizards/equity/equity-member-info-page.service'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../shared/credit-application.service'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('ApplicationWizardService', () => { + + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: CreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let component: MemberInfoFormComponent; + let fixture: ComponentFixture; + let memberInfoPageService: MemberInfoPageService; + let equityMemberInfoPageService: EquityMemberInfoPageService; + let formBuilder: FormBuilder; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + MemberInfoFormComponent + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(CreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + memberInfoPageService = TestBed.get(MemberInfoPageService); + equityMemberInfoPageService = TestBed.get(EquityMemberInfoPageService); + fixture = TestBed.createComponent(MemberInfoFormComponent); + component = fixture.componentInstance; + component.group = createTestFormGroup(formBuilder); + }); + + it('should create', () => { + expect(applicationWizardService).toBeTruthy(); + }); + + it('should determine whether or not a page can be visited', () => { + const canVisitStep1: boolean = applicationWizardService.canVisitPage('step-1'); + const canVisitStep2: boolean = applicationWizardService.canVisitPage('step-2'); + expect(canVisitStep1).toBe(true); + expect(canVisitStep2).toBe(false); + }); + + + + it('should determine a page can be visited because there were no errors', () => { + spyOn(applicationWizardService, 'getFieldErrors').and.callThrough().and.returnValue([]); + const noErrors: boolean = applicationWizardService.canVisitPage('step-2'); + expect(noErrors).toBe(true); + }); + + it('should determine if there is a co-applicant', () => { + spyOn(MaskingService, 'unMaskNumber').and.callThrough().and.returnValue(1000); + spyOn(applicationWizardService, 'getFieldValue').and.callThrough(); + spyOn(memberInfoPageService, 'createMemberInformationGroup').and.callThrough(); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + component.group.patchValue({ + 'applicantType': CreditApplicationValues.CHOICE_YES, + 'limitRequested': '1000', + 'maritalStatus': CreditApplicationValues.CHOICE_NO + }); + creditApplicationService.saveMemberInformation(component.group.value); + const isCoApplicant: boolean = applicationWizardService.isCoApplicant(); + expect(isCoApplicant).toBe(true); + + component.group.patchValue({ + 'applicantType': CreditApplicationValues.CHOICE_NO, + 'limitRequested': '1000', + 'maritalStatus': CreditApplicationValues.CHOICE_NO + }); + + creditApplicationService.saveMemberInformation(component.group.value); + const changedCoApplicant: boolean = applicationWizardService.isCoApplicant(); + expect(changedCoApplicant).toBe(false); + }); + + it('should submit an application ', () => { + const application = applicationWizardService.getFullCreditApplication(); + spyOn(creditApplicationDaoService, 'submitApplication').and.returnValue(of(true)); + applicationWizardService.submitCreditApplication(application); + expect(creditApplicationDaoService.submitApplication).toHaveBeenCalled(); + }); + + it('should submit an application for decision', () => { + const application = applicationWizardService.getFullCreditApplication(); + spyOn(creditApplicationDaoService, 'decideApplication').and.returnValue(of(true)); + applicationWizardService.submitApplicationForDecision(application); + expect(creditApplicationDaoService.decideApplication).toHaveBeenCalled(); + }); + + it('should update an applications decision status', () => { + spyOn(creditApplicationDaoService, 'getApplicationDecision').and.returnValue(of(true)); + applicationWizardService.updateApplicationDecisionStatus(); + expect(creditApplicationDaoService.getApplicationDecision).toHaveBeenCalled(); + }); + + it('should return data for primary applicant', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const primaryData1 = applicationWizardService.getIndividualGroupValues('employmentInformation', BorrowerType.PRIMARY_BORROWER); + expect(primaryData1).toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[0].employmentInformation); + expect(Array.isArray(primaryData1)).toBeTruthy(); + }); + + it('should return data for secondary applicant', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const primaryData1 = applicationWizardService.getIndividualGroupValues('otherIncomeInformation', BorrowerType.CO_BORROWER); + expect(primaryData1).toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1].otherIncomeInformation); + }); + + it('should return the applicant\'s first name', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(applicationWizardService.getApplicantName(BorrowerType.PRIMARY_BORROWER)) + .toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[0].personalInformation.firstName); + expect(applicationWizardService.getApplicantName(BorrowerType.CO_BORROWER)) + .toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1].personalInformation.firstName); + }); + + it('should return a field value w/ or w/o a group', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(applicationWizardService.getFieldValue('loanType')).toEqual(apiTranslatorPreTranslate.loanType); + expect(applicationWizardService.getFieldValue('estimatedValue', 'collateralInformation')) + .toBe(apiTranslatorPreTranslate.collateralInformation.estimatedValue); + }); + + it('should determine that a loan needs collateral information', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + + // apiTranslatorPreTranslate is using Loan type 8, which does not require collateral. + // 8 used to be MANUFACTURED_HOME_LOAN which is commented out as of 2023. + expect(applicationWizardService.needsCollateral()).toBe(false); + }); + + it('should determine that a loan doesn\'t need collateral information', () => { + const creditCardApplication = apiTranslatorPreTranslate; + creditCardApplication.loanType = LoanTypeSet[0]; + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(creditCardApplication); + expect(applicationWizardService.needsCollateral()).toBe(false); + }); + + it('should return true if the co-borrower is married to the primary borrower', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(applicationWizardService.isCoApplicantSpouse()).toBe(true); + }); + + it('should return false if the co-borrower is not married to the primary borrower', () => { + const applicantsAreNotMarriedToEachOther = apiTranslatorPreTranslate; + applicantsAreNotMarriedToEachOther.hasCoApplicantSpouse = null; + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(applicantsAreNotMarriedToEachOther); + expect(applicationWizardService.isCoApplicantSpouse()).toBe(false); + }); + + // tslint:disable-next-line:max-line-length + it('should return false if the applicant/co-applicant does not need to provide spouse information (if primary and co-borrower are not married to each other)', () => { + apiTranslatorPreTranslate.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_YES; + apiTranslatorPreTranslate.fullInfoBorrowers[0].personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES; + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(applicationWizardService, 'isCoApplicantSpouse').and.callThrough(); + expect(applicationWizardService.isApplicantNonSpouse(BorrowerType.CO_BORROWER)).toBe(false); + }); + + it('should return true if the applicant/co-applicant needs to provide spouse information', () => { + apiTranslatorPreTranslate.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_NO; + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(applicationWizardService, 'isCoApplicantSpouse').and.callThrough(); + expect(applicationWizardService.isApplicantNonSpouse(BorrowerType.PRIMARY_BORROWER)).toBe(true); + }); + + it('should return false if this is not a Manufactured Home', () => { + const mfgHomeNotOnRentedLotApplication = apiTranslatorPreTranslate; + mfgHomeNotOnRentedLotApplication.loanType = LoanTypeSet[4]; + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(mfgHomeNotOnRentedLotApplication); + expect(applicationWizardService.isRentedLotInfoRequired()).toBe(false); + expect(applicationWizardService.isLienPositionInfoRequired()).toBe(true); + }); + + it('should return group values', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(applicationWizardService.getGroupValues('collateralInformation')).toEqual(apiTranslatorPreTranslate.collateralInformation); + expect(applicationWizardService.getGroupValues('someNonExistantGroup')).toEqual({}); + }); + + it('should return an individual from the application', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(applicationWizardService.getIndividualByBorrowerType(BorrowerType.CO_BORROWER)) + .toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1]); + }); + + it('should correctly calculate a simple next route name', () => { + const service = applicationWizardService; + expect(service.getSimpleRouteName('https://applydev.landmarkcu.com/loan-wizard/cc/re/step-2')).toEqual('step-2'); + expect(service.getSimpleRouteName('https://a.x.com/lw/cc/re/step-1?fbclid=IwAR226oQp-ISjjS9U1I440bjA')).toEqual('step-1'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.ts new file mode 100644 index 0000000..0cb612b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/application-wizard.service.ts @@ -0,0 +1,353 @@ +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { FormControl } from '@angular/forms'; +import { CreditApplication, Individual, SpouseType } from '../shared/credit-application'; +import { CreditApplicationDaoService } from '../shared/credit-application-dao.service'; +import { ApplicationPage } from './application-page'; +import { Injectable } from '@angular/core'; +import { CreditApplicationService } from '../shared/credit-application.service'; +import { CreditApplicationValues } from '../shared/credit-application-values'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { WaypointService } from '../layout/waypoints/waypoint.service'; +import { TranslateService } from '@ngx-translate/core'; +import { LoanTitle } from '../shared/loan-type/loan-title'; +import { LoanCategory } from '../shared/loan-type/loan-category'; +import { LoanCode } from '../shared/loan-type/loan-code'; + +@Injectable({ + providedIn: 'root' +}) +export class ApplicationWizardService { + + protected cachedFormControl = new FormControl(''); + // when using inheritance, the implied private field is not clear, so I've declared it implicitly. + protected creditApplicationDaoService: CreditApplicationDaoService; + + constructor(creditApplicationDaoService: CreditApplicationDaoService, + private creditApplicationService: CreditApplicationService, + private translateService: TranslateService, + private wayPointService: WaypointService) { + this.creditApplicationDaoService = creditApplicationDaoService; + } + + public setLoanType(loanType: string, parsedData?: any): void { + this.creditApplicationService.setLoanType(loanType, parsedData); + } + + public resetLoanType(): void { + this.creditApplicationService.resetLoanType(); + } + + public getApplicantName(borrowerType: BorrowerType): string { + const setIndividual = this.getIndividualByBorrowerType(borrowerType); + if (!setIndividual || !setIndividual.personalInformation.firstName) { + return 'Applicant'; + } + + return setIndividual.personalInformation.firstName; + } + + public getBeforeYouBeginMessage(): object { + const message = this.getFullCreditApplication().loanType.beforeYouBeginKey; + if(message) { + return this.translateService.get(message); + } else { + return null; + } + } + + public showProofOfIncomeUploadForm(): boolean { + const application = this.getFullCreditApplication(); + + if(application.loanType.apiCode === LoanCode.EQ_HELOC_Flex) { + return application.limitRequested > 100000 && application.loanType.showProofOfIncomeUploadForm === true + } + + return application.loanType.showProofOfIncomeUploadForm === true; + } + + public canVisitPage(pageRoute: string): boolean { + // You can't visit anything once you have a decision. + const creditApplication: CreditApplication = this.getFullCreditApplication(); + if (creditApplication && creditApplication.hasBeenCompleted) { + return pageRoute === 'decision'; + } + const pages = creditApplication.loanType.pages; + let currentPage = null; + for (let index = 0; index < pages.length; index++) { + currentPage = pages[index]; + if (currentPage.route === pageRoute) { + return true; + } else if (currentPage.determinePageRequired(this) && currentPage.isValidPage(this, this.creditApplicationService).length > 0) { + // redirect the user. + return false; + } + } + } + + public getFieldErrors(fieldName, value, validatorFnSet) { + let validationResult; + const errors = []; + validatorFnSet.forEach((validatorFn) => { + this.cachedFormControl.setValue(value); + validationResult = validatorFn(this.cachedFormControl); + if (validationResult) { + validationResult.field = fieldName; + errors.push(validationResult); + } + }); + return errors; + } + + public getFieldValue(field: string, group?: string) { + const creditApplication: CreditApplication = this.getFullCreditApplication(); + let value; + if (group) { + value = creditApplication[group]; + if (value) { + value = value[field]; + } + } else { + value = creditApplication[field]; + } + return value; + } + + public isCoApplicant(): boolean { + return this.getFieldValue('hasCoApplicant') === CreditApplicationValues.CHOICE_YES; + } + + public needsCollateral(): boolean { + return this.getFullCreditApplication().loanType.requiresCollateral; + } + + public isCoApplicantSpouse(): boolean { + return this.getFullCreditApplication().hasCoApplicantSpouse === CreditApplicationValues.CHOICE_YES; + } + + public isApplicantNonSpouse(individual: BorrowerType): boolean { + const setIndividual = this.getIndividualByBorrowerType(individual); + if (!setIndividual || this.isCoApplicantSpouse()) { + return false; + } + return setIndividual.addressInformation.state === CreditApplicationValues.STATE_REQUIRED_COAPP && + setIndividual.personalInformation.maritalStatus === CreditApplicationValues.CHOICE_YES; + } + + public isRentedLotInfoRequired(): boolean { + return this.getFullCreditApplication().loanType.title === LoanTitle.ManufacturedHome; + } + + public isLienPositionInfoRequired(): boolean { + if (this.getFullCreditApplication().loanType.category === LoanCategory.Equity) { + return this.getFullCreditApplication().loanType.title !== LoanTitle.ManufacturedHome; + } else { + return false; + } + } + + public showPayoffFirstMortgageStatementUploadForm(): boolean { + if (this.getFullCreditApplication().loanType.category === LoanCategory.Equity) { + return this.getFullCreditApplication().loanType.title === LoanTitle.HELOC || this.getFullCreditApplication().loanType.title === LoanTitle.HomeEquityFixed; + } else { + return false; + } + } + + public getSpouseType(borrowerType: BorrowerType): SpouseType { + const individual = this.getIndividualByBorrowerType(borrowerType); + if (borrowerType === BorrowerType.PRIMARY_BORROWER || (borrowerType === BorrowerType.CO_BORROWER && individual)) { + return individual.spouseType; + } + return null; + } + + public getGroupValues(group: string) { + if (!this.getFullCreditApplication()[group]) { + return {}; + } + return this.getFullCreditApplication()[group]; + } + + public getIndividualGroupValues(group: string, individual: BorrowerType) { + const selectedIndividual = this.getIndividualByBorrowerType(individual); + if (selectedIndividual && selectedIndividual[group]) { + return selectedIndividual[group]; + } + return {}; + } + + public getIndividualSpouse(individual: Individual): any { + if (individual.spouseType === SpouseType.FullInfoSpouse) { + return individual.fullInfoSpouse; + } else { + return individual.tattleSpouse; + } + } + + public getIndividualByBorrowerType(individual: BorrowerType) { + const creditApplication = this.getFullCreditApplication(); // DO NOT REMOVE THIS, tests will fail. + switch (individual) { + case BorrowerType.PRIMARY_BORROWER: + return creditApplication.fullInfoBorrowers[0]; + case BorrowerType.CO_BORROWER: + this.addDefaultCoApplicantIfNecessary(); + return creditApplication.fullInfoBorrowers[1]; + case BorrowerType.PRIMARY_BORROWER_SPOUSE: + return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[0]); + case BorrowerType.CO_BORROWER_SPOUSE: + return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[1]); + } + throw new Error('Invalid individual type in getIndividualByBorrowerType: ' + individual); + } + + public addDefaultCoApplicantIfNecessary() { + const creditApplication = this.getFullCreditApplication(); + if (creditApplication.hasCoApplicant === CreditApplicationValues.CHOICE_YES + && creditApplication.fullInfoBorrowers.length === 1) { + creditApplication.fullInfoBorrowers.push(this.creditApplicationService.generateDefaultFullInfoBorrower()); + if (creditApplication.coApplicantLivesInMaritalPropertyState) { + creditApplication.fullInfoBorrowers[1].addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP; + } + if (this.isCoApplicantSpouse()) { + creditApplication.fullInfoBorrowers[1].personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES; + } + } + } + + public getFullCreditApplication(): CreditApplication { + return this.creditApplicationService.getFullCreditApplication(); + } + + public submitCreditApplication(creditApplication: CreditApplication): Observable { + return this.creditApplicationDaoService.submitApplication(creditApplication) + .pipe( + map(result => { + if (result) { + const savedApplication = this.getFullCreditApplication(); + savedApplication.loanId = result['loanApplicationId']; + return true; + } + return false; + }), + catchError((err, caught) => { + return of(false); + }) + ); + } + + public submitApplicationForDecision(creditApplication: CreditApplication): Observable { + return this.creditApplicationDaoService.decideApplication(creditApplication) + .pipe( + map(result => { + return true; + }), + catchError((err, caught) => { + return of(false); + }) + ); + } + + public updateApplicationDecisionStatus(): Observable { + const creditApplication = this.getFullCreditApplication(); + return this.creditApplicationDaoService.getApplicationDecision(creditApplication) + .pipe( + map(result => { + if (result && result.state === 'Ready') { + creditApplication.decision = result; + return true; + } + return false; + }), + catchError((err, caught) => { + return of(false); + }) + ); + } + + public getPageIndexByRouteName(routeName: string): number { + const creditApplication = this.getFullCreditApplication(); + const pages = creditApplication.loanType.pages; + for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) { + if (pages[pageIndex].route === routeName) { + return pageIndex; + } + } + throw new Error('No page found for route name: ' + routeName); + } + + public getNextPageByIndex(currentIndex: number): ApplicationPage { + const creditApplication = this.getFullCreditApplication(); + const pages = creditApplication.loanType.pages; + for (let pageIndex = currentIndex + 1; pageIndex < pages.length; pageIndex++) { + if (pages[pageIndex].determinePageRequired(this)) { + return pages[pageIndex]; + } + } + return null; + } + + public getPreviousPageByIndex(currentIndex: number): ApplicationPage { + const creditApplication = this.getFullCreditApplication(); + const pages = creditApplication.loanType.pages; + for (let pageIndex = currentIndex - 1; pageIndex >= 0; pageIndex--) { + if (pages[pageIndex].determinePageRequired(this)) { + return pages[pageIndex]; + } + } + return null; + } + + public handlePostApplicationEvents(): void { + this.creditApplicationService.clearApplication(); + } + + public getImagePath(): string { + return this.getFullCreditApplication().loanType.imagePath; + } + + public getLoanMessage(): Observable { + const message = this.getFullCreditApplication().loanType.message; + return this.translateService.get(message); + } + + public getNextTopicMessage(routeName: string): string { + const currentPageIndex = this.getPageIndexByRouteName(routeName); + const nextPage = this.getNextPageByIndex(currentPageIndex); + if (nextPage) { + return nextPage.titleKey; + } + return 'NONEXTROUTE'; + } + + public getNextRoute(routeName: string): string { + const currentPageIndex = this.getPageIndexByRouteName(routeName); + const nextPage = this.getNextPageByIndex(currentPageIndex); + if (nextPage) { + return '../' + nextPage.route; + } + return null; + } + + public getPreviousRoute(routeName: string): string { + const currentPageIndex = this.getPageIndexByRouteName(routeName); + const prevPage = this.getPreviousPageByIndex(currentPageIndex); + if (prevPage) { + return '../' + prevPage.route; + } + return null; + } + + public displayWaypointsForPage(currentPage: string) { + this.wayPointService.showCoApplicant(this.isCoApplicant()); + this.wayPointService.showCollateral(this.needsCollateral()); + this.wayPointService.handleActivePageRoute(currentPage); + } + + public getSimpleRouteName(url: string): string { + const simpleurl = url.split('/').pop(); + if (simpleurl) { + return simpleurl.match(/[a-zA-Z0-9\-_]+/)[0]; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.html new file mode 100644 index 0000000..49b011c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.html @@ -0,0 +1,12 @@ +
+
+
{{ ('ERRORS.CUSTOM.' + msgId) }}
+
+
+ +
+ {{eachKey.key}} +
+ +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.scss new file mode 100644 index 0000000..e39da99 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.scss @@ -0,0 +1,5 @@ + +:host { + font-style: italic; + padding-top: 0.25em; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.spec.ts new file mode 100644 index 0000000..1c27849 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.spec.ts @@ -0,0 +1,39 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ErrorDisplayComponent } from './error-display.component'; +import { Component } from '@angular/core'; +import { Validators, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +@Component({ + selector: 'lw-test-errordisplay', + template: '' +}) +class TestErrorDisplayComponent { + + public testControl = new FormControl('initialTestValue', + [Validators.required, Validators.maxLength(5)]); +} + +describe('Error Display Component', () => { + let component: TestErrorDisplayComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, + TranslateModule.forRoot()], + declarations: [ErrorDisplayComponent, TestErrorDisplayComponent] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestErrorDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.ts new file mode 100644 index 0000000..6911a51 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.component.ts @@ -0,0 +1,54 @@ +import { Component, Input, HostBinding, OnDestroy, OnInit } from '@angular/core'; +import { AbstractControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { ErrorDisplayService, ErrorMessageEntry, ErrorMessageMap } from './error-display.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'lw-error-display', + templateUrl: './error-display.component.html', + styleUrls: ['./error-display.component.scss'] +}) +export class ErrorDisplayComponent implements OnInit, OnDestroy { + + @Input() public title: string; + @Input() public inputControl: AbstractControl; + @Input() public msgId: string; + @Input() public msgMap: ErrorMessageMap; + public messageKeySet: Array; + public subscription: Subscription; + + @HostBinding('class.invalid-feedback') showAsFeedback = true; + @HostBinding('class.col-md-10') showAsCol = true; + + constructor(private errorDisplayService: ErrorDisplayService, private translateService: TranslateService) { } + + public ngOnInit() { + if (this.title) { + this.title = this.translateService.instant(this.title); + this.translateService.get(this.title).subscribe((result) => { + this.title = result; + this.addMessageSetSubscription(); + }); + } else { + this.addMessageSetSubscription(); + } + } + + public addMessageSetSubscription(): void { + this.subscription = this.inputControl.valueChanges.subscribe(() => { + if (this.inputControl) { + this.messageKeySet = this.errorDisplayService.getErrorsForControl(this.title, this.inputControl, this.msgMap); + } else { + this.messageKeySet = []; + } + }); + this.messageKeySet = this.errorDisplayService.getErrorsForControl(this.title, this.inputControl, this.msgMap); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.spec.ts new file mode 100644 index 0000000..bd605c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.spec.ts @@ -0,0 +1,53 @@ +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { ErrorDisplayService } from './error-display.service'; + + +describe('Error Display Service', () => { + + let service: ErrorDisplayService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + service = TestBed.get(ErrorDisplayService); + }); + + it('should return a set for an invalid control', () => { + const control1 = new FormControl('', [Validators.required]); + const YODA_KEY = 'required, test is.'; + const TITLE = 'testTitle'; + expect(control1.valid).toEqual(false); + const result = service.getErrorsForControl(TITLE, control1, {'required': YODA_KEY}); + expect(result).toBeTruthy(); + expect(result.length).toEqual(1); + expect(result[0].key).toEqual(YODA_KEY); + expect(result[0].title).toEqual(TITLE); + }); + + it('should return an empty set for valid control', () => { + const control1 = new FormControl('ICanHazValue', [Validators.required]); + const YODA_KEY = 'required, test is.'; + const TITLE = 'testTitle'; + expect(control1.valid).toEqual(true); + const result = service.getErrorsForControl(TITLE, control1, {'required': YODA_KEY}); + expect(result).toBeTruthy(); + expect(result.length).toEqual(0); + }); + + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.ts new file mode 100644 index 0000000..bdbfbdb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/error-display/error-display.service.ts @@ -0,0 +1,128 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl } from '@angular/forms'; +import { FormatService } from '../../../shared/pipes/format.service'; + +export interface ErrorMessageMap { + required?: string; + minlength?: string; + maxlength?: string; + min?: string; + max?: string; + email?: string; + pattern?: string; +} + +export interface ErrorMessageEntry { + key?: string; + title?: string; + value?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class ErrorDisplayService { + + constructor(private formatService: FormatService) {} + + public errorSet = [ + { + 'errorType': 'required', + 'messageKey': 'ERRORS.REQUIRED' + }, + { + 'errorType': 'minlength', + 'messageKey': 'ERRORS.MINLENGTH', + 'valueMap': inputControl => inputControl.errors.minlength.requiredLength + }, + { + 'errorType': 'maxlength', + 'messageKey': 'ERRORS.MAXLENGTH', + 'valueMap': inputControl => inputControl.errors.maxlength.requiredLength + }, + { + 'errorType': 'min', + 'messageKey': 'ERRORS.MINVAL', + 'valueMap': inputControl => inputControl.errors.min.min + }, + { + 'errorType': 'max', + 'messageKey': 'ERRORS.MAXVAL', + 'valueMap': inputControl => inputControl.errors.max.max + }, + { + 'errorType': 'maxCurrency', + 'messageKey': 'ERRORS.MAX_AMOUNT_CURRENCY', + 'valueMap': inputControl => this.formatService.formatCurrencyNoDecimal(inputControl.errors.maxCurrency.max) + }, + { + 'errorType': 'loanValueMinimum', + 'messageKey': 'ERRORS.MINLOANAMT', + 'valueMap': inputControl => this.formatService.formatCurrencyNoDecimal(inputControl.errors.loanValueMinimum.min) + }, + { + 'errorType': 'loanValueMaximum', + 'messageKey': 'ERRORS.MAXLOANAMT', + 'valueMap': inputControl => this.formatService.formatCurrencyNoDecimal(inputControl.errors.loanValueMaximum.max) + }, + { + 'errorType': 'incomeValueMinimum', + 'messageKey': 'ERRORS.MININCAMT', + 'valueMap': inputControl => inputControl.errors.incomeValueMinimum.min + }, + { + 'errorType': 'incomeValueMaximum', + 'messageKey': 'ERRORS.MAXINCAMT', + 'valueMap': inputControl => inputControl.errors.incomeValueMaximum.max + }, + {'errorType': 'email', 'messageKey': 'ERRORS.EMAIL'}, + {'errorType': 'pattern', 'messageKey': 'ERRORS.PATTERN'}, + { + 'errorType': 'requiredState', + 'messageKey': 'ERRORS.REQUIREDSTATE', + 'valueMap': inputControl => inputControl.errors.requiredState.state + }, + { + 'errorType': 'requireWisconsinCollateral', + 'messageKey': 'ERRORS.REQUIRE_WI_COLLATERAL' + }, + { + 'errorType': 'sameValue', + 'messageKey': 'ERRORS.CUSTOM.SAME_VALUE', + 'valueMap': inputControl => inputControl.errors.sameValue + }, + { + 'errorType': 'invalidFiles', + 'messageKey': 'ERRORS.CUSTOM.INVALID_FILES', + 'valueMap': inputControl => inputControl.errors.invalidFiles + }, + { + 'errorType': 'titleBranded', + 'messageKey': 'ERRORS.CUSTOM.TITLE_BRANDED', + 'valueMap': inputControl => inputControl.errors.titleBranded + } + ]; + + public getErrorsForControl(title: string, control: AbstractControl, msgMap: ErrorMessageMap): Array { + const messageKeySet = []; + let currentKey = null; + this.errorSet.forEach((errorPair) => { + if (control.errors && control.errors[errorPair.errorType]) { + if (msgMap && msgMap[errorPair.errorType]) { + currentKey = msgMap[errorPair.errorType]; + } else { + currentKey = errorPair.messageKey; + } + messageKeySet.push( + { + 'key': currentKey, + 'title': title, + 'value': errorPair.valueMap ? errorPair.valueMap(control) : null + }); + } + }, this); + return messageKeySet; + } + +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.html new file mode 100644 index 0000000..1ba8417 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.html @@ -0,0 +1,6 @@ + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.scss new file mode 100644 index 0000000..513dea2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.scss @@ -0,0 +1,45 @@ +:host { + position: absolute; + right: 2rem; + color: gray; + bottom: .15rem; +} +.svg-inline--fa { + vertical-align: .125rem !important; +} + +.error { + color: red; +} + +.success { + color: green; +} + +.input-auto + :host, +.input-lmcu-fixed-width + :host { + position: inherit; + bottom: -0.15rem; +} +// dropdown needs more room +.custom-drop-down :host { + right: 3.5rem !important; +} + +// input with an icon on the right needs more room too +.custom-input-right-icon :host { + right: 3.5rem !important; +} + +svg { + background-color: white; +} + +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + // alignment for IE + .custom-drop-down + :host { + position: inherit; + bottom: 0.5rem; + right: 3.5rem; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.spec.ts new file mode 100644 index 0000000..c0d8b7a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Component } from '@angular/core'; +import { Validators, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { InputIconComponent } from './input-icon.component'; +import { configureTestSuite } from 'ng-bullet'; + +@Component({ + selector: 'lw-test-inputicon', + template: '' +}) +class TestInputIconComponent { + + public testControl = new FormControl('initialTestValue', + [Validators.required, Validators.maxLength(5)]); +} + +describe('Error Display Component', () => { + let component: TestInputIconComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [InputIconComponent, TestInputIconComponent] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestInputIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.ts new file mode 100644 index 0000000..a3137b0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/controls/input-icon/input-icon.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup, AbstractControl } from '@angular/forms'; + +@Component({ + selector: 'lw-input-icon', + templateUrl: './input-icon.component.html', + styleUrls: ['./input-icon.component.scss'] +}) +export class InputIconComponent implements OnInit { + + @Input() + public inputControl: AbstractControl; + + constructor() { } + + public ngOnInit() { + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.html new file mode 100644 index 0000000..f03e829 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.html @@ -0,0 +1,103 @@ +
+
+ +
+
+ ADDRESS_COMPARE.UNABLE_TO_VERIFY +
+
+ ADDRESS_COMPARE.CONFIRM +
+
+
+
+
+
+
ADDRESS_COMPARE.USE_MINE
+ +
+
+
+
ADDRESS_COMPARE.SELECT_MATCHING
+ +
+
+
+
+
+ +
+
+
+ +
+
+ ADDRESS_COMPARE.UNABLE_TO_FIND_MATCH_LINE1 +
+
+ ADDRESS_COMPARE.UNABLE_TO_FIND_MATCH_LINE2 +
+
+
+
+
+
+
ADDRESS_COMPARE.USE_MINE
+ +
+
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.spec.ts new file mode 100644 index 0000000..1639ca0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.spec.ts @@ -0,0 +1,157 @@ +import { TestBed, async, ComponentFixture, tick, fakeAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { AddressCompareInfoFormComponent } from './address-compare-info-form.component'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressValidationService } from 'src/app/shared/address-validation/address-validation.service'; +import { AddressCompareInfoFormService } from './address-compare-info-form.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ApiAddress } from 'src/app/shared/api-models/api-address'; +import { AddressInformation } from '../address/address-information'; +import { AddressDiffInformation } from './address-compare-information'; +import { of } from 'rxjs'; + +const createTestAddressFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('AddressCompareInfoFormComponent', () => { + let component: AddressCompareInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let addressValidationService: AddressValidationService; + let service: AddressCompareInfoFormService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + AddressCompareInfoFormComponent + ], + providers: [ + AddressValidationService + ] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestAddressFormGroup(formBuilder); + service = TestBed.get(AddressCompareInfoFormService); + addressValidationService = TestBed.get(AddressValidationService); + fixture = TestBed.createComponent(AddressCompareInfoFormComponent); + component = fixture.componentInstance; + component.addressInfoGroup = formGroup; + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + // tslint:disable-next-line:max-line-length + it('should create a VALID FormGroup (with default value for the verified address) if we get a good response from address validation service', fakeAsync(() => { + const apiAddress: ApiAddress = { + address1: '1980 OurHouseInTheMiddleOfOur St.', + address2: 'APT 39', + city: 'Madness', + state: 'UK', + postalCode: '54545', + postalCodeExt: '4444' + }; + const uiAddress: AddressInformation = { + addressLine1: '1980 OurHouseInDaMiddleOfOur St.', + addressLine2: 'Apt 39', + zipcode: '54545', + city: 'Madness', + state: 'UK' + }; + + const diff: AddressDiffInformation = { + address1: false, + address2: true, + city: true, + state: true, + postalCode: true, + allFieldsSame: false + }; + + spyOn(addressValidationService, 'getValidatedAddress') + .and.returnValue( + of(apiAddress) + ); + + fixture.detectChanges(); + + component.addressInfoGroup.patchValue(uiAddress); + + component.addressInfoGroup.updateValueAndValidity(); + tick(); + fixture.detectChanges(); + + expect(component.selectionInfoGroup).toBeTruthy(); + expect(component.selectionInfoGroup.valid).toBeTruthy(); + expect(component.selectionInfoGroup.value.addressSelection).toBe('compareAddress'); + expect(component.addressCompareInformation).toEqual({ + compareAddress: apiAddress, + userInput: uiAddress, + addressDiff: diff + }); + })); + + // tslint:disable-next-line:max-line-length + it('should create an INVALID FormGroup (with no default value for the verified address) if we get a NOBODY response from address validation service', fakeAsync(() => { + + const uiAddress: AddressInformation = { + addressLine1: '1980 OurHouseInDaMiddleOfOur St.', + addressLine2: 'Apt 39', + zipcode: '54545', + city: 'Madness', + state: 'UK' + }; + + spyOn(addressValidationService, 'getValidatedAddress') + .and.returnValue( + of(null) + ); + + fixture.detectChanges(); + + component.addressInfoGroup.patchValue(uiAddress); + + component.addressInfoGroup.updateValueAndValidity(); + tick(); + fixture.detectChanges(); + + expect(component.selectionInfoGroup).toBeTruthy(); + expect(component.selectionInfoGroup.valid).toBeFalsy(); + expect(component.selectionInfoGroup.value.addressSelection).toBe(null); + expect(component.addressCompareInformation).toEqual({ + compareAddress: null, + userInput: uiAddress, + addressDiff: null + }); + })); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.ts new file mode 100644 index 0000000..1364256 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.component.ts @@ -0,0 +1,145 @@ +import { Component, Input, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { AddressCompareInformation } from './address-compare-information'; +import { NgbPanelChangeEvent } from '@ng-bootstrap/ng-bootstrap'; +import { Subscription } from 'rxjs'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; +import { AddressInformation } from '../address/address-information'; +import { AddressCompareInfoFormService } from './address-compare-info-form.service'; +import { ApiAddress } from 'src/app/shared/api-models/api-address'; + +@Component({ + selector: 'lw-address-compare-info-form', + templateUrl: './address-compare-info-form.component.html', + styleUrls: ['./address-compare.scss'] + +}) +export class AddressCompareInfoFormComponent implements OnInit, OnDestroy { + + private parentStatusSubscription: Subscription; + private compareStatusSubscription: Subscription; + + private readonly FORM_STATE_INVALID = 'INVALID'; + private readonly COMPARE_ADDRESS_FIELD = 'compareAddress'; + private readonly USER_INPUT_FIELD = 'userInput'; + + @Input() + public addressInfoGroup: FormGroup; + + @Output() + public sendAcceptedAddress: EventEmitter; + + public addressCompareInformation: AddressCompareInformation; + public selectionInfoGroup: FormGroup; + + public localState = { + showNoMatch: false, + confirmDisabled: true + }; + + constructor(private addressCompareInfoFormService: AddressCompareInfoFormService) { + this.sendAcceptedAddress = new EventEmitter(); + } + + public beforeChange(e: NgbPanelChangeEvent) { + if (e.panelId === 'address-compare-panel') { + e.preventDefault(); + } + } + + public ngOnInit() { + this.parentStatusSubscription = this.addressInfoGroup.statusChanges.subscribe(this.onStatusChange.bind(this)); + } + + public ngOnDestroy() { + if (this.parentStatusSubscription) { + this.parentStatusSubscription.unsubscribe(); + } + if (this.compareStatusSubscription) { + this.compareStatusSubscription.unsubscribe(); + } + } + + public onStatusChange(newStatus: string): void { + this.parentStatusSubscription.unsubscribe(); + if (newStatus !== this.FORM_STATE_INVALID) { + const result = this.addressCompareInfoFormService.requestAddressCompare(this.addressInfoGroup.value); + + result.subscribe((compareAddress: ApiAddress) => { + if (!compareAddress) { + this.handleNoMatchFound(); + } else { + this.handleMatchFound(compareAddress); + } + }); + } else { + this.selectionInfoGroup = null; + this.addressCompareInformation = null; + } + this.parentStatusSubscription = this.addressInfoGroup.statusChanges.subscribe(this.onStatusChange.bind(this)); + } + + public handleNoMatchFound(): void { + this.addSelectionGroup(); + this.localState.showNoMatch = true; + this.selectionInfoGroup.patchValue({addressSelection: null}); + this.addressCompareInformation = { + userInput: this.addressInfoGroup.value, + compareAddress: null, + addressDiff: null + }; + this.sendAcceptedAddress.emit(null); + } + + public handleMatchFound(compareAddress: ApiAddress): void { + this.addressCompareInformation = { + userInput: this.addressInfoGroup.value, + compareAddress: compareAddress, + addressDiff: AddressValidationFormatConverter.getAddressValidationDiff(compareAddress, this.addressInfoGroup.value) + }; + + if (!this.selectionInfoGroup && !this.addressCompareInformation.addressDiff.allFieldsSame) { + this.addSelectionGroup(); + this.sendAcceptedAddress.emit(null); + } else if (this.addressCompareInformation.addressDiff.allFieldsSame) { + this.addressCompareInformation = null; + this.selectionInfoGroup = null; + if (this.compareStatusSubscription) { + this.compareStatusSubscription.unsubscribe(); + } + this.sendAcceptedAddress.emit(AddressValidationFormatConverter.makeUiAddress(compareAddress)); + } + + if (this.selectionInfoGroup) { + this.selectionInfoGroup.patchValue({addressSelection: this.COMPARE_ADDRESS_FIELD}); + } + this.localState.showNoMatch = false; + } + + public addSelectionGroup() { + this.selectionInfoGroup = this.addressCompareInfoFormService.createSelectionGroup(); + this.compareStatusSubscription = this.selectionInfoGroup.statusChanges + .subscribe(newStatus => this.localState.confirmDisabled = newStatus === this.FORM_STATE_INVALID); + } + + public commandSendAddressConfirmation() { + const formattedMessage: AddressInformation = this.getValueFromCompareSelection(this.selectionInfoGroup.value.addressSelection); + if (formattedMessage) { + this.sendAcceptedAddress.emit(formattedMessage); + this.addressCompareInformation = null; + this.selectionInfoGroup = null; + } + } + + + public getValueFromCompareSelection(field: string): AddressInformation | null { + if (!field) { + return null; + } else if (field === this.USER_INPUT_FIELD) { + return this.addressCompareInformation.userInput; + } else if (field === this.COMPARE_ADDRESS_FIELD) { + return AddressValidationFormatConverter.makeUiAddress(this.addressCompareInformation.compareAddress); + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.spec.ts new file mode 100644 index 0000000..cc7d94d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.spec.ts @@ -0,0 +1,38 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddressCompareInfoFormService } from './address-compare-info-form.service'; +import { AddressValidationService } from 'src/app/shared/address-validation/address-validation.service'; + +describe('Vehicle Collateral Info Form Service', () => { + + let service: AddressCompareInfoFormService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + AddressValidationService, + ConfigurationService + ] + }).compileComponents(); + + service = TestBed.get(AddressCompareInfoFormService); + })); + + it('should create', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.ts new file mode 100644 index 0000000..0ca0a91 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-info-form.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { AddressValidationService } from 'src/app/shared/address-validation/address-validation.service'; +import { AddressInformation } from '../address/address-information'; +import { Observable } from 'rxjs'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; + +@Injectable({ + providedIn: 'root' +}) +export class AddressCompareInfoFormService { + + public static readonly ADDRESS_FORM_NAME = 'addressInformation'; + public static readonly COMPARE_ADDRESS_FORM_NAME = 'compareAddressInformation'; + + constructor( + private addressValidationService: AddressValidationService, + private formBuilder: FormBuilder) { + } + + public createSelectionGroup(): FormGroup { + const addressCompareGroup = this.formBuilder.group({}); + + CommonGroups.COMPARE_ADDRESS_INFO.fields.forEach((eachField) => { + addressCompareGroup.addControl(eachField.field, this.formBuilder.control('')); + }, this); + + CommonGroups.COMPARE_ADDRESS_INFO.fields.forEach((eachField) => { + addressCompareGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return addressCompareGroup; + } + + public requestAddressCompare(addressInformation: AddressInformation): Observable { + return this.addressValidationService.getValidatedAddress(AddressValidationFormatConverter.makeApiAddress(addressInformation)); + } + + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-information.ts new file mode 100644 index 0000000..0477ada --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare-information.ts @@ -0,0 +1,17 @@ +import { AddressInformation } from '../address/address-information'; +import { ApiAddress } from 'src/app/shared/api-models/api-address'; + +export interface AddressCompareInformation { + userInput: AddressInformation; + compareAddress: ApiAddress; + addressDiff: AddressDiffInformation; +} + +export interface AddressDiffInformation { + address1: boolean; + address2?: boolean; + city: boolean; + state: boolean; + postalCode: boolean; + allFieldsSame: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare.scss new file mode 100644 index 0000000..27ac148 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address-compare/address-compare.scss @@ -0,0 +1,80 @@ +$error: #f63440; + +.horizontal-divide { + margin: 0 0 2em 0; + color: #EAEAEA; + height: 1px; + border: none; + background-color: #EAEAEA; +} + +.btn { + margin-left : 4px; +} + +.confirm { + margin-left : 0px; + margin-bottom: 0px; +} + +.address-select-card { + text-align: left; +} + +.address-select-radio { + float: left; + margin-top: 20px; + margin-bottom: 20px; + margin-right: 6px; + width: 10%; +} + +.address-select-btn, .address-select-btn:visited, .address-select-btn:hover { + padding-left: 30px !important; + padding-right: 10%; + border-radius: 10px; +} + +.btn-group-child { + width: 300px; +} + +@media(max-width: 768px) { + .btn-group-child { + width: unset; + max-width: 300px; + } +} + +.address-select-btn:active, .address-select-btn.active { + color: #7C7C7C; + background-color: #E5EEF7; +} + +#address-selection { + box-sizing: none; +} + +.address-field-diff { + color: $error; + border: 1px $error solid; + padding: 2px; +} + +// Card +.address-compare-panel { + border: 1px solid #0D3151; + border-radius: 5px; + margin-bottom: 15px; +} +// Card Headline +.address-compare-panel-header { + position: relative; + background-color: #0D3151; + color: #ffffff; + font-size: 1.2rem; + letter-spacing: 1.5px; + border-radius: 0; + height: 50px; + padding: 12px; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.html new file mode 100644 index 0000000..1760d71 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.html @@ -0,0 +1,82 @@ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.spec.ts new file mode 100644 index 0000000..3d24d28 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.spec.ts @@ -0,0 +1,112 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; + +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {AddressInfoFormComponent} from './address-info-form.component'; +import {InputIconComponent} from '../../controls/input-icon/input-icon.component'; +import {ErrorDisplayComponent} from '../../controls/error-display/error-display.component'; +import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {CommonGroups} from '../common-groups'; +import {ConfigurationService} from '../../../shared/configuration/configuration.service'; +import {AddressInformation} from './address-information'; +import {By} from '@angular/platform-browser'; +import {TextMaskModule} from 'angular2-text-mask'; +import {TranslateModule} from '@ngx-translate/core'; +import { AutoFocusDirective } from '../../../shared/page-focus/auto-focus.directive'; +import {StatePipe} from '../../../shared/pipes/state.pipe'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UiSettings} from '../../../shared/configuration/ui-settings'; +import {DefaultSettings} from '../../../shared/configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('AddressInfoFormComponent', () => { + let component: AddressInfoFormComponent; + let fixture: ComponentFixture; + let service: ConfigurationService; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: AddressInformation; + let status: string; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + AddressInfoFormComponent, + AutoFocusDirective, + StatePipe + ], + providers: [ + ConfigurationService + ] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(AddressInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + service = TestBed.get(ConfigurationService); + service.saveConfiguration(defaultSettings); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should load the list of states to select', () => { + expect(service).toBeTruthy(); + spyOn(service, 'getConfigurationByField').and.returnValue([{0: 'AA'}]); + fixture.detectChanges(); + expect(service.getConfigurationByField).toHaveBeenCalled(); + expect(component.stateSet[0]).toEqual({0: 'AA'}); + }); + + it('should emit changes', () => { + component.group.patchValue({'city': 'Milwaukee', 'addressLine1': '100 East Wisconsin'}); + + expect(changes.city).toBe('Milwaukee'); + expect(changes.addressLine1).toBe('100 East Wisconsin'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'city': 'Milwaukee', 'addressLine1': '100 East Wisconsin', 'PO Box#': 706}); + expect(changes['PO Box#']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + component.group.patchValue({'city': 'Milwaukee', 'addressLine1': '100 East Wisconsin'}); + expect(status).toBe('INVALID'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.ts new file mode 100644 index 0000000..2c6fc9a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-info-form.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, OnInit, ChangeDetectorRef, Pipe, PipeTransform } from '@angular/core'; +import { FormGroup, Validators } from '@angular/forms'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { MaskingService } from '../../../shared/masking/masking.service'; + +@Component({ + selector: 'lw-address-info-form', + templateUrl: './address-info-form.component.html' +}) +export class AddressInfoFormComponent implements OnInit { + + public zipMask = MaskingService.zipMask; + public numberMask = MaskingService.numberMask; + public stateSet = []; + + @Input() + public group: FormGroup; + @Input() + public idPrefix: string; + + @Input() + public isPrimaryResidenceInput: boolean; + + constructor(private configurationService: ConfigurationService) {} + + public ngOnInit() { + this.stateSet = this.configurationService.getConfigurationByField('stateOptions'); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-information.ts new file mode 100644 index 0000000..02a8d72 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/address-information.ts @@ -0,0 +1,8 @@ +export interface AddressInformation { + addressLine1: string; + addressLine2?: string; + city: string; + state: string; + zipcode: string; + zipcodeExt?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.html new file mode 100644 index 0000000..95c5ba0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.html @@ -0,0 +1,20 @@ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.spec.ts new file mode 100644 index 0000000..7305ac7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.spec.ts @@ -0,0 +1,90 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { IsAddressSameInfoFormComponent } from './is-address-same-info-form.component'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { DataTableService } from '../../../shared/data-table.service'; +import { IsAddressSameInformation } from './is-address-same-information'; +import { By } from '@angular/platform-browser'; +import {TranslateModule} from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.IS_ADDRESS_SAME_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('IsAddressSameInfoFormComponent', () => { + let component: IsAddressSameInfoFormComponent; + let fixture: ComponentFixture; + let service: DataTableService; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: IsAddressSameInformation; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + IsAddressSameInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(IsAddressSameInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + service = TestBed.get(DataTableService); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'isAddressSame': 'yes'}); + expect(changes.isAddressSame).toBe('yes'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'isMailingAddressSame': false}); + expect(changes['isMailingAddressSame']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form', () => { + component.group.patchValue({'isAddressSame': 'yes'}); + expect(status).toBe('VALID'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.ts new file mode 100644 index 0000000..4316ed5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-info-form.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-is-address-same-info-form', + templateUrl: './is-address-same-info-form.component.html' +}) +export class IsAddressSameInfoFormComponent implements OnInit { + + public stateSet = { + coApp: 'FORMS.SAME_ADDRESS.CO_APP', + heloc: 'FORMS.SAME_ADDRESS.HELOC', + spouse: 'FORMS.SAME_ADDRESS.SPOUSE' + }; + + @Input() + public group: FormGroup; + + @Input() + public message: string; + + constructor() {} + + public ngOnInit() {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-information.ts new file mode 100644 index 0000000..6996085 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/is-address-same-information.ts @@ -0,0 +1,3 @@ +export interface IsAddressSameInformation { + isAddressSame: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.html new file mode 100644 index 0000000..3c3614a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.html @@ -0,0 +1,87 @@ +
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+
+
+ + +
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.spec.ts new file mode 100644 index 0000000..b0b7c68 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.spec.ts @@ -0,0 +1,49 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { DataTableService } from '../../../shared/data-table.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ResidenceInfoFormComponent } from './residence-info-form.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('ResidenceInfoFormComponent', () => { + let component: ResidenceInfoFormComponent; + let fixture: ComponentFixture; + let service: DataTableService; + let formBuilder: FormBuilder; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + ResidenceInfoFormComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + fixture = TestBed.createComponent(ResidenceInfoFormComponent); + component = fixture.componentInstance; + service = TestBed.get(DataTableService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.ts new file mode 100644 index 0000000..cb8bafc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-info-form.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { SelectOptions } from '../../../shared/options/select-options'; + +@Component({ + selector: 'lw-residence-info-form', + templateUrl: './residence-info-form.component.html' +}) +export class ResidenceInfoFormComponent implements OnInit { + public numberMask = MaskingService.numberMask; + public numberMaskDecimal = MaskingService.numberMaskWithDecimal; + + @Input() + public group: FormGroup; + + @Input() + public isPrimaryResidenceInput: boolean; + + @Input() + public idPrefix: string; + + @Input() + public isHelocRateMod: boolean; + + public residenceStatusOptions = []; + public residenceYearOptions = SelectOptions.yearOptions; + public residenceMonthOptions = SelectOptions.monthOptions; + + constructor() {} + + public ngOnInit() { + this.residenceStatusOptions = this.isPrimaryResidenceInput ? SelectOptions.residenceStatusPresent : SelectOptions.residenceStatusPast; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-information.ts new file mode 100644 index 0000000..f31c952 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/address/residence-information.ts @@ -0,0 +1,7 @@ +export interface ResidenceInformation { + residenceStatus: string; + residenceMonthlyPayment?: number; + residenceTimeYears: string; + residenceTimeMonths: string; + isMailingAddressSame?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/common-groups.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/common-groups.ts new file mode 100644 index 0000000..5b19301 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/common-groups.ts @@ -0,0 +1,533 @@ +import {Validators} from '@angular/forms'; +import {WizardValidators} from './wizard-validators'; +import {LoanLimitsService} from '../../shared/loan-limit/loan-limits.service'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import {IncomeLimitsService} from '../../shared/income-limit/income-limits.service'; +import {CreditApplicationValues} from '../../shared/credit-application-values'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; + +export class CommonGroups { + + private static PHONE_PATTERN = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/; + private static SSN_PATTERN = /^\d{3}-\d{2}-\d{4}$/; + private static SSN_LAST_FOUR = /^[0-9]{4}$/; + private static ZIPCODE_PATTERN = /^\d{5}(?:\d{2})?$/; + private static DATE_PATTERN = /^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/; + private static YEAR_PATTERN = /^(19|20)\d{2}/; + private static NOTE_PATTERN = /^[\w\d\s.,&\n\r@\-;!']*$/; + private static EMAIL_PATTERN = /[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]+/; + private static ALPHA_PATTERN = /^[a-zA-Z]*$/; + private static NAME_PATTERN = /^[a-zA-Z '-]*$/; + private static NUMERIC_PATTERN = /^[0-9]*$/; + private static ALPHA_NUMERIC_PATTERN = /^[a-zA-Z0-9]*$/ + + public static readonly SPECIAL = { + group: null, + fields: [ + { + field: 'clientMessage', + validation: [Validators.required, Validators.minLength(5), + Validators.maxLength(3450), Validators.pattern(CommonGroups.NOTE_PATTERN)] + } + ] + }; + + public static readonly MEMBER_INFO = { + group: null, + fields: [ + { + field: 'limitRequested', + validation: [Validators.required], + validatorFn: [LoanLimitsService.minValueValidator, LoanLimitsService.maxValueValidator] + }, + {field: 'applicantType', validation: [Validators.required], validatorFn: [WizardValidators.CoApplicantValidator]}, + {field: 'livesInMaritalPropertyState', validation: [Validators.required]} + ] + }; + + public static readonly MEMBER_INFO_CC = { + group: null, + fields: [ + {field: 'applicantType', validation: [Validators.required], validatorFn: [WizardValidators.CoApplicantValidator]}, + {field: 'livesInMaritalPropertyState', validation: [Validators.required]} + ] + }; + + + public static readonly CO_APP_MEMBER_INFO = { + group: null, + fields: [ + {field: 'coApplicantLivesInMaritalPropertyState', validation: [], validatorFn: []}, + {field: 'confirmJointCredit', validation: [], validatorFn: []} + ] + }; + + public static readonly MEMBER_AGREEMENT_REQUIREMENT = { + group: null, + fields: [ + {field: 'agreementRequirement', validation: [Validators.required, Validators.requiredTrue], validatorFn: []} + ] + } + + public static readonly SUMMARY_ADDITIONAL_INFO = { + group: null, + fields: [ + {field: 'modFeeAccount', validation: []} + ] + } + + public static readonly SUMMARY_NOTE = { + group: null, + fields: [ + {field: 'note', validation: [Validators.maxLength(3500), Validators.pattern(CommonGroups.NOTE_PATTERN)]}, + {field: 'primaryContact', validation: [Validators.required, Validators.minLength(6)]}, + {field: 'promoCode', validation: [Validators.maxLength(30)]} + ] + }; + + public static readonly LOGIN = { + group: 'loginInformation', + fields: [ + {field: 'username', validation: [Validators.required, Validators.minLength(1), Validators.maxLength(30)]}, + {field: 'password', validation: [Validators.required, Validators.minLength(1), Validators.maxLength(30)]}, + ] + }; + + public static readonly MEMBER_VERIFICATION = { + group: 'memberInformation', + fields: [ + { + field: 'dob', + validation: [Validators.required, Validators.pattern(CommonGroups.DATE_PATTERN)] + }, + {field: 'ssn', validation: [Validators.required, Validators.pattern(CommonGroups.SSN_LAST_FOUR)]}, + ] + }; + + public static readonly IS_MARRIED = { + group: 'personalInformation', + fields: [ + {field: 'maritalStatus', validation: [Validators.required]} + ] + }; + + public static readonly IS_CO_APPLICANT_SPOUSE = { + group: 'personalInformation', + fields: [ + {field: 'coApplicantSpouse', validation: [Validators.required]} + ] + }; + + public static readonly PERSONAL_INFO = { + group: 'personalInformation', + groupValidator: WizardValidators.PersonalInfoValidator, + fields: [ + { + field: 'firstName', validation: [ + Validators.required, Validators.minLength(2), Validators.maxLength(60), Validators.pattern(CommonGroups.ALPHA_PATTERN) + ] + }, + {field: 'middleInitial', validation: [Validators.maxLength(1), Validators.pattern(CommonGroups.ALPHA_PATTERN)]}, + { + field: 'lastName', validation: [ + Validators.required, Validators.minLength(2), Validators.maxLength(60), Validators.pattern(CommonGroups.ALPHA_PATTERN) + ] + }, + {field: 'suffix', validation: []}, + { + field: 'dateOfBirth', + validation: [Validators.required, Validators.pattern(CommonGroups.DATE_PATTERN)] + }, + {field: 'socialSecurityNumber', validation: [Validators.required, Validators.pattern(CommonGroups.SSN_PATTERN)]}, + {field: 'primaryPhoneNumberType', validation: [Validators.required]}, + { + field: 'primaryPhoneNumber', + validation: [Validators.required, Validators.pattern(CommonGroups.PHONE_PATTERN)] + }, + { + field: 'secondaryPhoneNumberType', + validation: [WizardValidators.RequiredIfValidator('secondaryPhoneNumber')] + }, + { + field: 'secondaryPhoneNumber', + validation: [WizardValidators.FieldDependsOnMe('secondaryPhoneNumberType'), Validators.pattern(CommonGroups.PHONE_PATTERN)] + }, + { + field: 'email', validation: [ + Validators.required, Validators.pattern(CommonGroups.EMAIL_PATTERN), Validators.minLength(3), Validators.maxLength(180) + ] + }, + { + field: 'primaryApplicantEmail', + validation: [ Validators.pattern(CommonGroups.EMAIL_PATTERN) ] + }, + { + field: 'coApplicantEmail', + validation: [ Validators.pattern(CommonGroups.EMAIL_PATTERN), WizardValidators.ValueEqual("primaryApplicantEmail", "Primary Applicant Email Address") ] + }, + { + field: 'files', + validation: [WizardValidators.FileValidator] + }, + { + field: 'isUploading', + validation: [WizardValidators.MustBeFalse] + } + ] + }; + + public static readonly IS_FULL_INFO_SPOUSE = { + group: 'isFullInfoSpouse', + fields: [ + {field: 'isFullInfoSpouse', validation: [Validators.required]} + ] + }; + + public static readonly ADDRESS_INFO = { + group: 'addressInformation', + fields: [ + {field: 'addressLine1', validation: [Validators.required, Validators.minLength(5), Validators.maxLength(40)]}, + {field: 'addressLine2', validation: [Validators.minLength(2), Validators.maxLength(40)]}, + {field: 'zipcode', validation: [Validators.required, Validators.pattern(CommonGroups.ZIPCODE_PATTERN)]}, + {field: 'city', validation: [Validators.required, Validators.minLength(2), Validators.maxLength(50)]}, + {field: 'state', validation: [Validators.required], + validatorFn: [WizardValidators.RequireSpecificStateForAddressValidator, WizardValidators.UpdateCollateralMessage] + } + ] + }; + + public static readonly ADDRESS_VERIFY_STATUS_INFO = { + group: 'addressInformation', + fields: [ + {field: 'addressVerifyStatus', validation: [Validators.requiredTrue]} + ] + }; + + public static readonly COMPARE_ADDRESS_INFO = { + group: 'compareAddressInformation', + fields: [ + {field: 'addressSelection', validation: [Validators.required]}, + ] + }; + + public static readonly RESIDENCE_INFO = { + group: 'residenceInformation', + fields: [ + {field: 'residenceStatus', validation: [Validators.required]}, + {field: 'residenceTimeYears', validation: [Validators.required]}, + {field: 'residenceTimeMonths', validation: [Validators.required]} + ] + }; + + public static readonly PRIMARY_RESIDENCE_INFO = { + group: 'residenceInformation', + fields: [ + {field: 'residenceMonthlyPayment', validation: [Validators.required], validatorFn: [WizardValidators.maxCollateralValueValidator]}, + {field: 'isMailingAddressSame', validation: [Validators.required]} + ] + }; + + public static readonly IS_ADDRESS_SAME_INFO = { + group: 'isAddressSameInformation', + fields: [ + {field: 'isAddressSame', validation: [Validators.required], validatorFn: [WizardValidators.RequireWisconsinCollateralValidator]} + ] + }; + + public static readonly FINANCIAL_INFORMATION = { + group: 'financialInformation', + fields: [ + {field: 'hasEmploymentIncome0', validation: []}, + {field: 'hasEmploymentIncome1', validation: []}, + {field: 'hasEmploymentIncome2', validation: []}, + {field: 'hasOtherIncome0', validation: []}, + {field: 'hasOtherIncome1', validation: []}, + {field: 'hasOtherIncome2', validation: []}, + {field: 'isPayingChildSupport', validation: []} + ] + }; + + public static readonly EMPLOYMENT_INFO = { + group: 'employmentInformation', + fields: [ + {field: 'employerName', validation: [Validators.required, Validators.minLength(1), Validators.maxLength(60)]}, + {field: 'employmentType', validation: [Validators.required]}, + {field: 'jobTitle', validation: [Validators.required, Validators.minLength(1), Validators.maxLength(60)]}, + {field: 'schedule', validation: [Validators.required, WizardValidators.FieldDependsOnMe('salary')]}, + { + field: 'salary', validation: [Validators.required], + validatorFn: [IncomeLimitsService.minValueValidator, IncomeLimitsService.maxValueValidator] + }, + {field: 'employmentLengthYears', validation: [Validators.required]}, + {field: 'employmentLengthMonths', validation: [Validators.required]}, + {field: 'workPhoneNumber', validation: [Validators.pattern(CommonGroups.PHONE_PATTERN)]}, + {field: 'files', validation: [WizardValidators.FileValidator], validatorFn: []}, + {field: 'isUploading', validation: [WizardValidators.MustBeFalse]} + ] + }; + + public static readonly WEEKLY_HOURS_INFO = { + group: 'employmentInformation', + fields: [ + {field: 'weeklyHours', validation: [Validators.required, Validators.max(168), Validators.min(1)]} + ] + }; + + public static readonly INCOME_INFO = { + group: 'otherIncomeInformation', + fields: [ + {field: 'amount', validation: [Validators.required]}, + {field: 'incomeName', validation: [Validators.required, Validators.minLength(1), Validators.maxLength(60)]}, + {field: 'frequency', validation: []}, + {field: 'files', validation: [WizardValidators.FileValidator]}, + {field: 'isUploading', validation: [WizardValidators.MustBeFalse]} + ] + }; + + public static readonly ALIMONY_INFO = { + group: 'alimonyInformation', + fields: [ + {field: 'childSupportAmount', validation: [Validators.required]} + ] + }; + + public static readonly TATTLE_SPOUSE_PERSONAL_INFO = { + group: 'tattleSpousePersonalInformation', + fields: [ + {field: 'firstName', validation: [Validators.required, Validators.minLength(2), Validators.maxLength(60), Validators.pattern(CommonGroups.ALPHA_PATTERN)]}, + {field: 'middleInitial', validation: [Validators.maxLength(1)]}, + {field: 'lastName', validation: [Validators.required, Validators.minLength(2), Validators.maxLength(60), Validators.pattern(CommonGroups.ALPHA_PATTERN)]}, + // @TODO: Per Jeff's request this model is subject to change might be changed back. -ZB + // {field: 'socialSecurityNumber', validation: [Validators.required, Validators.pattern(CommonGroups.SSN_PATTERN)]}, + // {field: 'email', validation: [Validators.email]} + ] + }; + + public static readonly CC_DISCLOSURE_INFO = { + group: 'disclosureInformation', + fields: [ + {field: 'truthful', validation: [Validators.required, Validators.requiredTrue]}, + {field: 'contact', validation: [Validators.required]} + ] + }; + + public static readonly ETHNICITY_INFO = { + group: 'ethnicityInformation', + groupValidator: WizardValidators.RequireOneValidator, + fields: [ + {field: 'hispanicOrLatino', validation: []}, + {field: 'mexican', validation: []}, + {field: 'puertoRican', validation: []}, + {field: 'cuban', validation: []}, + {field: 'otherHispanicLatino', validation: []}, + {field: 'otherHispanicLatinoText', validation: []}, + {field: 'notHispanicOrLatino', validation: []}, + {field: 'notProvided', validation: []} + ] + }; + + public static readonly GENDER_INFO = { + group: 'genderInformation', + groupValidator: WizardValidators.RequireOneValidator, + fields: [ + {field: 'female', validation: []}, + {field: 'male', validation: []}, + // The code below would make it so that the applciant cannot select both "Male" and "Female". + // Code remains in comments due to suggestion on ticket #1116. + // {field: 'female', validation: [], validatorFn: [WizardValidators.HmdaGenderValidator]}, + // {field: 'male', validation: [WizardValidators.FieldDependsOnMe('female')], validatorFn: [WizardValidators.HmdaGenderValidator]}, + {field: 'notProvided', validation: []} + ] + }; + + public static readonly RACE_INFO = { + group: 'raceInformation', + groupValidator: WizardValidators.RequireOneValidator, + fields: [ + {field: 'americanIndianAlaskaNative', validation: []}, + {field: 'americanIndianAlaskaNativeText', validation: []}, + {field: 'blackOrAfricanAmerican', validation: []}, + {field: 'asian', validation: []}, + {field: 'asianIndian', validation: []}, + {field: 'chinese', validation: []}, + {field: 'filipino', validation: []}, + {field: 'japanese', validation: []}, + {field: 'korean', validation: []}, + {field: 'vietnamese', validation: []}, + {field: 'otherAsian', validation: []}, + {field: 'otherAsianText', validation: []}, + {field: 'hawaiian', validation: []}, + {field: 'nativeHawaiian', validation: []}, + {field: 'guamanianChamorro', validation: []}, + {field: 'samoan', validation: []}, + {field: 'otherPacificIslander', validation: []}, + {field: 'otherPacificIslanderText', validation: []}, + {field: 'white', validation: []}, + {field: 'notProvided', validation: []} + ] + }; + + public static readonly MEMBER_INFO_PURPOSE_OF_LOAN = { + group: 'purposeOfLoan', + fields: [ + {field: 'purposeOfLoan', validation: [Validators.required]} + ] + }; + + public static readonly MEMBER_INFO_PURPOSE_OF_LOAN_SIGNATURE = { + group: 'purposeOfLoanSignature', + fields: [ + {field: 'purposeOfLoanSignature', validation: [Validators.required, Validators.maxLength(100)]} + ] + }; + + public static readonly MEMBER_INFO_TERM_OF_LOAN = { + group: 'termOfLoan', + fields: [ + {field: 'termOfLoan', validation: [Validators.required]} + ] + }; + + public static readonly COLLATERAL_INFO = { + group: 'collateralInformation', + fields: [ + {field: 'estimatedValue', validation: [], validatorFn: [WizardValidators.maxCollateralValueValidator, WizardValidators.HelocRateModAppValidator]}, + {field: 'propertyType', validation: [], validatorFn: [WizardValidators.HelocRateModAppValidator]}, + {field: 'isBeingImproved', validation: [Validators.required]}, + {field: 'isListedForSale', validation: [], validatorFn: [WizardValidators.ListedForSaleValidator]}, + {field: 'occupancyType', validation: [Validators.required]}, + // @TODO: Another case of not knowing how to do this validation properly ( task 595 - Bart ) + { + field: 'onRentedLot', + validation: [WizardValidators.FieldsDependsOnMe(['rentPaid', 'annualPropertyTax'])], + validatorFn: [WizardValidators.RentedLotValidator] + }, + {field: 'annualPropertyTax', validation: [], validatorFn: [WizardValidators.AnnualPropertyTaxValidator]}, + {field: 'rentPaid', validation: [], validatorFn: [WizardValidators.RentPaidValidator]}, + {field: 'files', validation: [WizardValidators.FileValidator]}, + {field: 'firstMortgageBalanceFiles', validation: [WizardValidators.FileValidator]}, + {field: 'isUploadingFirstMortgageBalanceFiles', validation: [WizardValidators.MustBeFalse]}, + {field: 'isUploading', validation: [WizardValidators.MustBeFalse]} + ] + }; + + public static readonly LIEN_POSITION_INFO = { + group: 'collateralInformation', + fields: [ + {field: 'hasFirstMortgage', validation: [], validatorFn: [WizardValidators.LienPositionValidator]}, + {field: 'hasSecondMortgage', validation: [], validatorFn: [WizardValidators.LienPositionValidator]}, + {field: 'payoffFirstMortgage', validation: []}, + {field: 'payoffSecondMortgage', validation: []} + ] + }; + + public static readonly LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF = { + group: 'collateralInformation', + fields: [ + {field: 'payoffFirstMortgage', validation: []} + ] + }; + + public static readonly LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF = { + group: 'collateralInformation', + fields: [ + {field: 'payoffSecondMortgage', validation: []} + ] + }; + + public static readonly VEHICLE_COLLATERAL_INFO = { + group: 'vehicleInformation', + fields: [ + {field: 'value', validation: [Validators.required], validatorFn: [WizardValidators.maxCollateralValueValidator]}, + {field: 'type', validation: [Validators.required]}, + {field: 'condition', validation: [Validators.required]}, + { + field: 'year', + validation: [Validators.required, Validators.pattern(CommonGroups.YEAR_PATTERN), + Validators.max(CreditApplicationValues.VEHICLE_CUT_OFF_YEAR_UPPER), + Validators.min(CreditApplicationValues.VEHICLE_CUT_OFF_YEAR_LOWER)] + }, + {field: 'make', validation: [Validators.required, Validators.maxLength(100)]}, + {field: 'model', validation: [Validators.required, Validators.maxLength(100)]}, + {field: 'mileage', validation: [Validators.required, Validators.minLength(1)]}, + {field: 'term', validation: [Validators.required]}, + {field: 'loanPurpose', validation: [Validators.required]}, + {field: 'vin', validation: [Validators.maxLength(20), Validators.pattern(CommonGroups.ALPHA_NUMERIC_PATTERN)]}, + {field: 'licensePlateNumber', validation: [Validators.maxLength(8), Validators.pattern(CommonGroups.ALPHA_NUMERIC_PATTERN)]}, + {field: 'stateTitled', validation: [Validators.maxLength(120), Validators.required], validatorFn: [WizardValidators.VehicleDefaultValidator]}, + {field: 'countyTitled', validation: [Validators.maxLength(20), Validators.required], validatorFn: [WizardValidators.VehicleDefaultValidator]}, + {field: 'municipalityType', validation: []}, + {field: 'municipalityWhereKept', validation: [Validators.maxLength(50)]} + ] + }; + + public static readonly VEHICLE_COLLATERAL_INFO_PARTIAL = { + group: 'vehicleInformation', + fields: [ + {field: 'value', validation: [Validators.required], validatorFn: [WizardValidators.maxCollateralValueValidator]}, + { + field: 'year', + validation: [Validators.required, Validators.pattern(CommonGroups.YEAR_PATTERN), Validators.max(3000), Validators.min(1900)] + }, + {field: 'type', validation: [Validators.required]}, + {field: 'make', validation: [Validators.required, Validators.maxLength(100)]}, + {field: 'model', validation: [Validators.required, Validators.maxLength(100)]}, + {field: 'term', validation: [Validators.required]}, + {field: 'loanPurpose', validation: [Validators.required]}, + {field: 'mileage', validation: []}, + {field: 'vin', validation: [Validators.pattern(CommonGroups.ALPHA_NUMERIC_PATTERN)], validatorFn: [WizardValidators.VINMaxLength]}, + {field: 'municipalityType', validation: []}, + {field: 'municipalityWhereKept', validation: [Validators.maxLength(50)]} + ] + }; + + public static readonly VEHICLE_SELLER_INFO = { + group: 'vehicleInformation', + groupValidator: WizardValidators.BrandedTitleValidator, + fields: [ + {field: 'transferring', validation: [Validators.maxLength(120)], validatorFn: [WizardValidators.RequireTransferring]}, + {field: 'licensePlateNumber', validation: [Validators.maxLength(8), Validators.pattern(CommonGroups.ALPHA_NUMERIC_PATTERN)]}, + {field: 'seller', validation: [Validators.maxLength(120)], validatorFn: [WizardValidators.RequireSeller]}, + {field: 'dealerName', validation: [Validators.maxLength(120)]}, + {field: 'dealerPhone', validation: [Validators.pattern(CommonGroups.PHONE_PATTERN)]}, + {field: 'purchaseContractFiles', validation: [WizardValidators.FileValidator]}, + {field: 'purchaseContractUploading', validation: [WizardValidators.MustBeFalse]}, + {field: 'titleBranded', validation: [Validators.maxLength(120)]}, + {field: 'lien', validation: [Validators.maxLength(120)]} + ] + }; + + public static readonly VEHICLE_BOAT_INFO = { + group: 'vehicleInformation', + fields: [ + {field: 'boatLength', validation: [Validators.maxLength(30), WizardValidators.NumberMaxLengthValidator(15)], validatorFn: []}, + {field: 'boatSameAddress', validation: [Validators.required]}, + {field: 'trailerYear', validation: [Validators.pattern(CommonGroups.YEAR_PATTERN)]}, + {field: 'trailerMake', validation: [Validators.maxLength(60)]}, + {field: 'trailerType', validation: []}, + {field: 'motorYear', validation: [Validators.pattern(CommonGroups.YEAR_PATTERN)]}, + {field: 'motorMake', validation: [Validators.maxLength(60)]}, + {field: 'motorModel', validation: [Validators.maxLength(200)]}, + {field: 'motorType', validation: []}, + {field: 'dnrRegistrationCardFiles', validation: [WizardValidators.FileValidator]}, + {field: 'boatTitleFiles', validation: [WizardValidators.FileValidator]}, + {field: 'trailerTitleFiles', validation: [WizardValidators.FileValidator]}, + {field: 'dnrRegistrationCardUploading', validation: [WizardValidators.MustBeFalse]}, + {field: 'boatTitleUploading', validation: [WizardValidators.MustBeFalse]}, + {field: 'trailerTitleUploading', validation: [WizardValidators.MustBeFalse]} + ] + } + + public static readonly DEALERSHIP_PORTAL = { + group: 'dealershipPortal', + fields: [ + {field: 'dealershipCode', validation: [Validators.required, Validators.maxLength(60)], validatorFn: []}, + {field: 'dealershipPassword', validation: [Validators.required, Validators.maxLength(30)]}, + {field: 'applicationId', validation: [Validators.required, Validators.pattern(CommonGroups.NUMERIC_PATTERN)]}, + {field: 'applicantLastName', validation: [Validators.required, Validators.maxLength(250)]}, + {field: 'applicationFiles', validation: [WizardValidators.FileValidator]}, + {field: 'applicationFilesUploading', validation: [WizardValidators.MustBeFalse]}, + ] + } + static memberService: any; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.html new file mode 100644 index 0000000..864597a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.html @@ -0,0 +1,25 @@ +
+ +
+ + +
+ + + +
+
+ +
+ + +
+ + +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.scss new file mode 100644 index 0000000..2c5065e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.scss @@ -0,0 +1,2 @@ +.rowContainer { height: 100px; display:flex; align-items: center;} +.rowItem { width: 30px; height: 30px; margin-right: 5px; } diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.spec.ts new file mode 100644 index 0000000..c543945 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.spec.ts @@ -0,0 +1,57 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { CreditCardDisclosureFormComponent } from './credit-card-disclosure-form.component'; +import {TranslateModule} from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.CC_DISCLOSURE_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('CreditCardDisclosureBoxesComponent', () => { + let component: CreditCardDisclosureFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + CreditCardDisclosureFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(CreditCardDisclosureFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.ts new file mode 100644 index 0000000..b98c13e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-credit-card-disclosure-form', + templateUrl: './credit-card-disclosure-form.component.html' +}) +export class CreditCardDisclosureFormComponent { + + @Input() + public group: FormGroup; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure.ts new file mode 100644 index 0000000..367797c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure.ts @@ -0,0 +1,5 @@ +export interface DisclosureInformation { + truthful: boolean; + contact: boolean; + date?: Date; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-agreement.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-agreement.html new file mode 100644 index 0000000..d1247e5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-agreement.html @@ -0,0 +1,43 @@ +DISCLOSURE.ELECTRONIC.PART1 + + + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.spec.ts new file mode 100644 index 0000000..142bcae --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.spec.ts @@ -0,0 +1,41 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { ElectronicDisclosuresFormComponent } from './electronic-disclosures-form.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('ElectronicDisclosuresFormComponent', () => { + let component: ElectronicDisclosuresFormComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + ElectronicDisclosuresFormComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ElectronicDisclosuresFormComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.ts new file mode 100644 index 0000000..146a0d9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/disclosures/electronic-disclosures-form.component.ts @@ -0,0 +1,19 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { WindowService } from '../../../shared/window.service'; + +@Component({ + selector: 'lw-electronic-disclosure', + templateUrl: './electronic-disclosures-agreement.html' +}) +export class ElectronicDisclosuresFormComponent { + constructor(private modalService: NgbModal, private windowService: WindowService) {} + @ViewChild('disclosures') modalContent: ElementRef; + open() { + this.modalService.open(this.modalContent, {size: 'lg'}); + } + print() { + this.windowService.showPrintWindowForId('electronicDisclosuresText'); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.html new file mode 100644 index 0000000..166baea --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.html @@ -0,0 +1,19 @@ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.spec.ts new file mode 100644 index 0000000..fb2b414 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.spec.ts @@ -0,0 +1,59 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { AlimonyInfoFormComponent } from './alimony-info-form.component'; +import { configureTestSuite } from 'ng-bullet'; + +const createAlimonyFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.ALIMONY_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('AlimonyInfoFormComponent', () => { + let component: AlimonyInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let alimonyGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + AlimonyInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + alimonyGroup = createAlimonyFormGroup(formBuilder); + fixture = TestBed.createComponent(AlimonyInfoFormComponent); + component = fixture.componentInstance; + component.group = alimonyGroup; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.ts new file mode 100644 index 0000000..1605a28 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-info-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; + +@Component({ + selector: 'lw-alimony-info-form', + templateUrl: './alimony-info-form.component.html' +}) +export class AlimonyInfoFormComponent { + + public numberMask = MaskingService.numberMask; + + @Input() + public group: FormGroup; + + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-information.ts new file mode 100644 index 0000000..24a0948 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/alimony-information.ts @@ -0,0 +1,3 @@ +export interface AlimonyInformation { + childSupportAmount: number; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.html new file mode 100644 index 0000000..4f74424 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.html @@ -0,0 +1,150 @@ +
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ + FORMS.EMPLOYMENT.SALARY_HINT +
+
+
+ +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+
+

FORMS.EMPLOYMENT.LENGTH_OF_EMPLOYMENT

+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.spec.ts new file mode 100644 index 0000000..b580089 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.spec.ts @@ -0,0 +1,83 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { EmploymentInfoFormComponent } from './employment-info-form.component'; +import { EmploymentInformation } from './employment-information'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { TcpaComponent } from '../../../shared/legal-message/tcpa.component'; +import { configureTestSuite } from 'ng-bullet'; +import { FileUploadComponent } from 'src/app/shared/file-upload/file-upload.component'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.EMPLOYMENT_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('EmploymentInfoFormComponent', () => { + let component: EmploymentInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: EmploymentInformation; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + EmploymentInfoFormComponent, + TcpaComponent, + FileUploadComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(EmploymentInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({ 'weeklyHours': 40, 'employerName': 'NVISIA', '401K': 'vested' }); + expect(changes['401K']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + component.group.patchValue({ 'weeklyHours': 40, 'employerName': 'NVISIA' }); + expect(status).toBe('INVALID'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.ts new file mode 100644 index 0000000..6994b9e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-info-form.component.ts @@ -0,0 +1,88 @@ +import { Component, Input, OnInit, OnDestroy, OnChanges, EventEmitter, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { Subject, Subscription } from 'rxjs'; +import { SelectOptions } from '../../../shared/options/select-options'; + +@Component({ + selector: 'lw-employment-info-form', + templateUrl: './employment-info-form.component.html' +}) +export class EmploymentInfoFormComponent implements OnInit, OnDestroy { + + private groupSubscription: Subscription; + public phoneMask = MaskingService.phoneMask; + public numberMask = MaskingService.numberMask; + public numberMaskDecimal = MaskingService.numberMaskWithDecimal; + public noSpecialMask = MaskingService.noSpecial; + + public localState = { + isHourly: false + }; + + // From Swagger: NoneSelected, Contract, FullTime, PartTime, SelfEmployed, Temporary + public employmentTypeOptions = SelectOptions.employmentTypeOptions; + public employmentScheduleOptions = SelectOptions.employmentScheduleOptions; + public employmentYearOptions = SelectOptions.yearOptions; + public employmentMonthOptions = SelectOptions.monthOptions; + + @Input() + public group: FormGroup; + + @Input() + public loanId: string; + + @Input() + public idSuffix = 0; + + @Input() + public borrowerType: string; + + @Input() + public showProofOfIncomeUploadForm: boolean; + + @Input() + public removeEmploymentIncomeFiles: Subject; + + @Output() + public removedAllFiles: EventEmitter; + + constructor() { + this.removedAllFiles = new EventEmitter(); + } + + public ngOnInit() { + if (this.group) { + if (!this.showProofOfIncomeUploadForm) { + this.group.patchValue({ 'files': [] }); + } + this.updateGroupLocalState(); + this.groupSubscription = this.group.valueChanges.subscribe((nextValues) => { + this.updateGroupLocalState(); + }); + } + } + + public ngOnDestroy() { + if (this.groupSubscription) { + this.groupSubscription.unsubscribe(); + } + } + + public updateGroupLocalState(): void { + this.localState.isHourly = !!(this.group && this.group.get('weeklyHours')); + } + + public handleFiles(files: any[]): void { + this.group.patchValue({ 'files': files }); + } + + public removedEmploymentIncomeFiles(removedAllFiles: boolean): void { + this.removedAllFiles.emit(removedAllFiles); + } + + public handleUploadingFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploading': isUploading }); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-information.ts new file mode 100644 index 0000000..1394ffd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/employment-information.ts @@ -0,0 +1,15 @@ +import { IncomeInformation } from './income-information'; + +export interface EmploymentInformation { + employerName: string; + employmentType: string; + jobTitle: string; + salary: number; + schedule: string; + employmentLengthYears: number; + employmentLengthMonths: number; + weeklyHours?: number; + workPhoneNumber?: string; + isUploading?: string; + files?: any[]; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial-information.ts new file mode 100644 index 0000000..c5f2263 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial-information.ts @@ -0,0 +1,5 @@ +export interface FinancialInformation { + hasEmploymentIncome?: boolean; + hasOtherIncome?: boolean; + isPayingChildSupport?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.html new file mode 100644 index 0000000..678af0d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.html @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.spec.ts new file mode 100644 index 0000000..833516d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.spec.ts @@ -0,0 +1,60 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { AlimonyFlagFormComponent } from './alimony-flag-form.component'; +import { CommonGroups } from '../../common-groups'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.FINANCIAL_INFORMATION.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('AlimonyFlagFormComponent', () => { + let component: AlimonyFlagFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + AlimonyFlagFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(AlimonyFlagFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.ts new file mode 100644 index 0000000..bba44e9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/alimony-flag-form.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-alimony-flag-form', + templateUrl: './alimony-flag-form.component.html' +}) +export class AlimonyFlagFormComponent { + + @Input() + public group: FormGroup; + + constructor() { } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.html new file mode 100644 index 0000000..63d78f4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.html @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.spec.ts new file mode 100644 index 0000000..e061212 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.spec.ts @@ -0,0 +1,60 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonGroups } from '../../common-groups'; +import { EmploymentFlagFormComponent } from './employment-flag-form.component'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.FINANCIAL_INFORMATION.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('EmploymentFlagFormComponent', () => { + let component: EmploymentFlagFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + EmploymentFlagFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(EmploymentFlagFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.ts new file mode 100644 index 0000000..ca68e73 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/employment-flag-form.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-employment-flag-form', + templateUrl: './employment-flag-form.component.html' +}) +export class EmploymentFlagFormComponent { + + @Input() + public group: FormGroup; + + @Input() + public idSuffix = 0; + + constructor() { + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.html new file mode 100644 index 0000000..da67f8f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.html @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.spec.ts new file mode 100644 index 0000000..dc88a3f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.spec.ts @@ -0,0 +1,60 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonGroups } from '../../common-groups'; +import { OtherIncomeFlagFormComponent } from './other-income-flag-form.component'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.FINANCIAL_INFORMATION.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('OtherIncomeFlagFormComponent', () => { + let component: OtherIncomeFlagFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + OtherIncomeFlagFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(OtherIncomeFlagFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.ts new file mode 100644 index 0000000..f501e22 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/financial/other-income-flag-form.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-other-income-flag-form', + templateUrl: './other-income-flag-form.component.html' +}) +export class OtherIncomeFlagFormComponent { + + @Input() + public group: FormGroup; + + @Input() + public idSuffix = 0; + + @Input() + public loanId: string; + + constructor() { } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/income-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/income-information.ts new file mode 100644 index 0000000..f020f75 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/income-information.ts @@ -0,0 +1,7 @@ +export interface IncomeInformation { + incomeName: string; + frequency?: string; + amount: number; + isUploading?: string; + files?: any[]; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.html new file mode 100644 index 0000000..29df553 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.html @@ -0,0 +1,51 @@ +
+
+
+ +
+
+
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ + +
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.spec.ts new file mode 100644 index 0000000..ab2a2f8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.spec.ts @@ -0,0 +1,61 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { OtherIncomeInfoFormComponent } from './other-income-info-form.component'; +import { configureTestSuite } from 'ng-bullet'; +import { FileUploadComponent } from 'src/app/shared/file-upload/file-upload.component'; + +const createIncomeFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.INCOME_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('OtherIncomeInfoFormComponent', () => { + let component: OtherIncomeInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let incomeGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + OtherIncomeInfoFormComponent, + FileUploadComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + incomeGroup = createIncomeFormGroup(formBuilder); + fixture = TestBed.createComponent(OtherIncomeInfoFormComponent); + component = fixture.componentInstance; + component.group = incomeGroup; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.ts new file mode 100644 index 0000000..ea1ae25 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/employment/other-income-info-form.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { MaskingService } from '../../../shared/masking/masking.service'; + +@Component({ + selector: 'lw-other-income-info-form', + templateUrl: './other-income-info-form.component.html' +}) +export class OtherIncomeInfoFormComponent { + + public numberMask = MaskingService.numberMask; + + @Input() + public loanId: string; + + @Input() + public idSuffix = 0; + + @Input() + public group: FormGroup; + + @Input() + public borrowerType: string; + + @Input() + public showProofOfIncomeUploadForm: boolean; + + @Input() + public removeOtherIncomeFiles: Subject>; + + @Output() + public removedAllFiles: EventEmitter; + + constructor() { + this.removedAllFiles = new EventEmitter(); + } + + public ngOnInit() { + if (this.group) { + if (!this.showProofOfIncomeUploadForm) { + this.group.patchValue({ 'files': [] }); + } + } + } + + public removedOtherIncomeFiles(removedAllFiles: boolean): void { + this.removedAllFiles.emit(removedAllFiles); + } + + public handleFiles(files: any[]): void { + this.group.patchValue({ 'files': files }); + } + + public handleUploadingFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploading': isUploading }); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.html new file mode 100644 index 0000000..07f5c44 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.html @@ -0,0 +1,182 @@ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ + + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+ +
+
+ + +
+ +
+
+ +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.spec.ts new file mode 100644 index 0000000..8b66b2a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.spec.ts @@ -0,0 +1,137 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { CollateralInfoFormComponent } from './collateral-info-form.component'; +import { CollateralInformation } from './collateral-information'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { CollateralInfoFormService } from './collateral-info-form.service'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.COLLATERAL_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('CollateralInfoFormComponent', () => { + let component: CollateralInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: CollateralInformation; + let status: string; + let service: CollateralInfoFormService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + CollateralInfoFormComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(CollateralInfoFormComponent); + service = TestBed.get(CollateralInfoFormService); + component = fixture.componentInstance; + component.group = formGroup; + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'isListedForSale': CreditApplicationValues.CHOICE_YES}); + expect(changes.isListedForSale).toBe(CreditApplicationValues.CHOICE_YES); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'isMailingAddressSame': CreditApplicationValues.CHOICE_NO}); + expect(changes['isMailingAddressSame']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form', () => { + component.group.patchValue({ + 'estimatedValue': 5446, + 'propertyType': 1, + 'hasFirstMortgage' : CreditApplicationValues.CHOICE_YES, + 'hasSecondMortgage' : CreditApplicationValues.CHOICE_YES, + 'payoffFirstMortgage' : false, + 'payoffSecondMortgage': false, + 'isBeingImproved': CreditApplicationValues.CHOICE_YES, + 'isListedForSale': CreditApplicationValues.CHOICE_NO, + 'occupancyType': 0, + 'onRentedLot': false + }); + expect(status).toBe('VALID'); + }); + + it('should leave isListedForSale true if !isMfgHome and !isHelocRateMod', () => { + component.group.patchValue({ 'isListedForSale': 'true' }) + component.isMfgHome = false; + component.isHelocRateMod = false; + + component.updateIsListedForSale(); + + expect(component.group.get('isListedForSale').value).toBe('true'); + }); + + it('should set isListedForSale to false if isMfgHome', () => { + component.group.patchValue({ 'isListedForSale': 'true' }) + component.isMfgHome = true; + component.isHelocRateMod = false; + + component.updateIsListedForSale(); + + expect(component.group.get('isListedForSale').value).toBe('false'); + }); + + it('should set isListedForSale to false if isHelocRateMod', () => { + component.group.patchValue({ 'isListedForSale': 'true' }) + component.isMfgHome = false; + component.isHelocRateMod = true; + + component.updateIsListedForSale(); + + expect(component.group.get('isListedForSale').value).toBe('false'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.ts new file mode 100644 index 0000000..1abe73a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.component.ts @@ -0,0 +1,125 @@ +import { Component, Input, OnInit, OnDestroy, EventEmitter, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import {CollateralInfoFormService} from './collateral-info-form.service'; +import { Subject, Subscription } from 'rxjs'; +import { CreditApplicationValues } from 'src/app/shared/credit-application-values'; +import { SelectOptions } from '../../../shared/options/select-options'; +import { ThrowStmt } from '@angular/compiler'; + +@Component({ + selector: 'lw-collateral-info-form', + templateUrl: './collateral-info-form.component.html' +}) +export class CollateralInfoFormComponent implements OnInit, OnDestroy { + + public occupancyTypes = SelectOptions.occupancyTypes; + public localState = { + showLienPosition: false, + payoffFirstMortgage: false, + payoffSecondMortgage: false, + showPayoffFirstMortgageStatementUploadForm: false + }; + + @Input() + public group: FormGroup; + + @Input() + public idPrefix: string; + + @Input() + public loanType: string; + + @Input() + public addressSameAsResidence: boolean; + + @Input() + public isHelocRateMod: boolean; + + @Input() + public isMfgHome: boolean; + + @Input() + public loanId: string; + + @Input() + public fileUploadValues: any; + + public selectedTypes; + + public numberMask = MaskingService.numberMask; + + private subscription: Subscription; + + public removeStatementFiles: Subject; + + @Output() + public removedStatementFiles: EventEmitter; + + constructor(private collateralInfoService: CollateralInfoFormService) { + this.removeStatementFiles = new Subject(); + this.removedStatementFiles = new EventEmitter(); + } + + public ngOnInit() { + this.selectedTypes = this.collateralInfoService.getPropertyTypes(); + + this.updateIsListedForSale(); + + this.updateLocalState(this.group.value); + this.subscription = this.group.valueChanges.subscribe(() => { + this.updateLocalState(this.group.value); + }); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public updateIsListedForSale() { + if(this.isMfgHome || this.isHelocRateMod) { + this.group.patchValue({isListedForSale: 'false'}); + } + } + + public updateLocalState(modelValue): void { + + this.localState.showLienPosition = this.collateralInfoService.showLienPosition(); + this.localState.showPayoffFirstMortgageStatementUploadForm = this.collateralInfoService.showPayoffFirstMortgageStatementUploadForm(); + + if (this.localState.showLienPosition) { + if(this.localState.showPayoffFirstMortgageStatementUploadForm && modelValue.hasFirstMortgage === CreditApplicationValues.CHOICE_NO) { + this.removeStatementFiles.next(); + } else { + this.localState.payoffFirstMortgage = (modelValue.hasFirstMortgage === CreditApplicationValues.CHOICE_YES); + this.localState.payoffSecondMortgage = (modelValue.hasSecondMortgage === CreditApplicationValues.CHOICE_YES); + } + } + } + + public handleFiles(files: any[]): void { + this.group.patchValue({ 'files': files }); + } + + public handleFirstMortgageBalanceFiles(files: any[]): void { + this.group.patchValue({ 'firstMortgageBalanceFiles': files }); + } + + public handleUploadingFirstMortgageBalanceFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploadingFirstMortgageBalanceFiles': isUploading }); + } + + public handleUploadingFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploading': isUploading }); + } + + public handleRemovedStatementFiles(removedAllFiles: boolean): void { + if(removedAllFiles) { + this.localState.payoffFirstMortgage = (this.group.value.hasFirstMortgage === CreditApplicationValues.CHOICE_YES); + this.localState.payoffSecondMortgage = (this.group.value.hasSecondMortgage === CreditApplicationValues.CHOICE_YES); + } + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.spec.ts new file mode 100644 index 0000000..240b836 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.spec.ts @@ -0,0 +1,66 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import {CollateralInfoFormService} from './collateral-info-form.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Collateral Info Form Service', () => { + + let service: CollateralInfoFormService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(CollateralInfoFormService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[5].id); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should call function for real estate options', () => { + spyOn(service, 'getPropertyTypes').and.callThrough(); + service.getPropertyTypes(); + expect(service.getPropertyTypes).toHaveBeenCalled(); + }); + + it('should return term options when given a correct loanId', () => { + applicationWizardService.setLoanType(LoanTypeSet[7].id); + spyOn(service, 'getPropertyTypes').and.callThrough(); + + // Only options are Real Estate or Manufactured Home.. Manufactured home was removed in 2023 + expect(service.getPropertyTypes()).toEqual(service.realEstateOptions); + expect(service.getPropertyTypes().length).toBeGreaterThan(1); + }); + + it('should return empty term options when given incorrect loanId', () => { + applicationWizardService.setLoanType(LoanTypeSet[0].id); + spyOn(service, 'getPropertyTypes').and.callThrough(); + expect(service.getPropertyTypes()).toEqual(service.realEstateOptions); + }); + + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.ts new file mode 100644 index 0000000..07dce79 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-info-form.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { SelectOptions } from 'src/app/shared/options/select-options'; + +@Injectable({ + providedIn: 'root' +}) +export class CollateralInfoFormService { + + constructor(private applicationWizardService: ApplicationWizardService) {} + + public realEstateOptions = SelectOptions.realEstateOptions; + public manufacturedHomeRealEstateOptions = SelectOptions.manufacturedHomeRealEstateOptions; + + private loanIdToTerms = { + 'mhl' : this.manufacturedHomeRealEstateOptions + }; + + public getPropertyTypes() { + const loanId = this.applicationWizardService.getFieldValue('loanType').id; + let types = this.realEstateOptions; + if (this.loanIdToTerms[loanId]) { + types = this.loanIdToTerms[loanId]; + } + return types; + } + + public showLienPosition(): boolean { + return this.applicationWizardService.isLienPositionInfoRequired(); + } + + public showPayoffFirstMortgageStatementUploadForm(): boolean { + return this.applicationWizardService.showPayoffFirstMortgageStatementUploadForm(); + } + +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-information.ts new file mode 100644 index 0000000..2b1bc60 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/collateral-information.ts @@ -0,0 +1,14 @@ +export interface CollateralInformation { + estimatedValue: number; + propertyType: string; + isBeingImproved: string; + isListedForSale: string; + occupancyType: string; + onRentedLot?: string; + annualPropertyTax?: string; + rentPaid?: string; + files?: any[]; + firstMortgageBalanceFiles?: any[]; + isUploading?: boolean; + isUploadingFirstMortgageBalanceFiles?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/equity-member-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/equity-member-information.ts new file mode 100644 index 0000000..636e1b1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/equity-member-information.ts @@ -0,0 +1,10 @@ +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; + +/* + * @TODO Merge this with membership-information.ts and update functions using them or 'any'. + */ +export interface EquityMemberInformation { + purposeOfLoan: LoanPurpose; + termOfLoan: string; + clientMessage?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.html new file mode 100644 index 0000000..7daede7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.html @@ -0,0 +1,49 @@ +
+ +
+
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.spec.ts new file mode 100644 index 0000000..cfc1515 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.spec.ts @@ -0,0 +1,85 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { DataTableService } from '../../../shared/data-table.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { IsRentedLotInfoFormComponent } from './is-rented-lot-info-form.component'; +import { CollateralInformation } from './collateral-information'; +import {TextMaskModule} from 'angular2-text-mask'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.COLLATERAL_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('IsRentedLotInfoFormComponent', () => { + let component: IsRentedLotInfoFormComponent; + let fixture: ComponentFixture; + let service: DataTableService; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: CollateralInformation; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + IsRentedLotInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(IsRentedLotInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + service = TestBed.get(DataTableService); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'onRentedLot': 'yes'}); + expect(changes.onRentedLot).toBe('yes'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'isMailingAddressSame': false}); + expect(changes['isMailingAddressSame']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + expect(status).toBe('INVALID'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.ts new file mode 100644 index 0000000..59194cd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/equity/is-rented-lot-info-form.component.ts @@ -0,0 +1,44 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { Subscription } from 'rxjs'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; + +@Component({ + selector: 'lw-is-rented-lot-info-form', + templateUrl: './is-rented-lot-info-form.component.html' +}) +export class IsRentedLotInfoFormComponent implements OnInit, OnDestroy { + + public localState = { + isRentedLot: false, + showAnnualPropertyTax: false + }; + + @Input() + public group: FormGroup; + + @Input() + public idPrefix: string; + + public numberMaskDecimal = MaskingService.numberMaskWithDecimal; + private subscription: Subscription; + constructor() {} + + public ngOnInit() { + this.subscription = this.group.valueChanges.subscribe(() => { + this.updateLocalState(this.group.value); + }); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public updateLocalState(modelValue): void { + this.localState.isRentedLot = (modelValue.onRentedLot === CreditApplicationValues.CHOICE_YES); + this.localState.showAnnualPropertyTax = (modelValue.onRentedLot === CreditApplicationValues.CHOICE_NO); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.html new file mode 100644 index 0000000..9a84e4b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.html @@ -0,0 +1,66 @@ +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.spec.ts new file mode 100644 index 0000000..e31e200 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.spec.ts @@ -0,0 +1,78 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { EthnicityInfoFormComponent } from './ethnicity-info-form.component'; +import { EthnicityInformation } from './ethnicity-information'; +import {TranslateModule} from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + CommonGroups.ETHNICITY_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + return group; +}; + +describe('EthnicityInfoFormComponent', () => { + let component: EthnicityInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: EthnicityInformation; + const customPrefix = 'testPrefix'; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + EthnicityInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(EthnicityInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + component.idPrefix = customPrefix; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit / allow any and all changes for checkboxes', () => { + const allValuesObject = {'hispanicOrLatino': true, 'mexican': true, 'puertoRican': true, + 'cuban': true, 'otherHispanicLatino': true, 'notHispanicOrLatino': true, 'notProvided': true}; + component.group.patchValue(allValuesObject); + fixture.detectChanges(); + Object.keys(allValuesObject).forEach(key => { + expect(changes[key]).toBe(true); + }); + }); + + it('should emit changes for text-boxes', () => { + component.group.patchValue({'otherHispanicLatinoText': 'Text testing'}); + fixture.detectChanges(); + expect(changes.otherHispanicLatinoText).toBe('Text testing'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.ts new file mode 100644 index 0000000..79baa8b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-info-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-ethnicity-info-form', + templateUrl: './ethnicity-info-form.component.html', + styleUrls: ['./hmda-styles.scss'] +}) +export class EthnicityInfoFormComponent { + @Input() + public group: FormGroup; + + @Input() + public idPrefix: string; + + constructor() {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-information.ts new file mode 100644 index 0000000..45de81f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/ethnicity-information.ts @@ -0,0 +1,10 @@ +export interface EthnicityInformation { + 'hispanicOrLatino'?: boolean; + 'mexican'?: boolean; + 'puertoRican'?: boolean; + 'cuban'?: boolean; + 'otherHispanicLatino'?: boolean; + 'otherHispanicLatinoText'?: string; + 'notHispanicOrLatino'?: boolean; + 'notProvided'?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.html new file mode 100644 index 0000000..9ed6ffa --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.html @@ -0,0 +1,25 @@ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.spec.ts new file mode 100644 index 0000000..943f129 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.spec.ts @@ -0,0 +1,72 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { GenderInfoFormComponent } from './gender-info-form.component'; +import { GenderInformation } from './gender-information'; +import {TranslateModule} from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + CommonGroups.GENDER_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + return group; +}; + +describe('GenderInfoFormComponent', () => { + let component: GenderInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: GenderInformation; + const customPrefix = 'testPrefix'; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + GenderInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(GenderInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + component.idPrefix = customPrefix; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit / allow any and all changes for checkboxes', () => { + const allValuesObject = {'male': true, 'female': true, 'notProvided': true}; + component.group.patchValue(allValuesObject); + fixture.detectChanges(); + Object.keys(allValuesObject).forEach(key => { + expect(changes[key]).toBe(true); + }); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.ts new file mode 100644 index 0000000..71e75c1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-info-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-gender-info-form', + templateUrl: './gender-info-form.component.html', + styleUrls: ['./hmda-styles.scss'] +}) +export class GenderInfoFormComponent { + @Input() + public group: FormGroup; + + @Input() + public idPrefix: string; + + constructor() {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-information.ts new file mode 100644 index 0000000..c0b39f5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/gender-information.ts @@ -0,0 +1,5 @@ +export interface GenderInformation { + 'female'?: boolean; + 'male'?: boolean; + 'notProvided'?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/hmda-styles.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/hmda-styles.scss new file mode 100644 index 0000000..07525de --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/hmda-styles.scss @@ -0,0 +1,3 @@ +.form-group, label{ + margin-bottom: 0 !important; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.html new file mode 100644 index 0000000..76f1c20 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.html @@ -0,0 +1,157 @@ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.spec.ts new file mode 100644 index 0000000..66e15a0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.spec.ts @@ -0,0 +1,86 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { RaceInfoFormComponent } from './race-info-form.component'; +import { RaceInformation } from './race-information'; +import {TranslateModule} from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + CommonGroups.RACE_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + return group; +}; + +describe('RaceInfoFormComponent', () => { + let component: RaceInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: RaceInformation; + const customPrefix = 'testPrefix'; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + RaceInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(RaceInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + component.idPrefix = customPrefix; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit / allow any and all changes for checkboxes', () => { + const allValuesObject = { + 'americanIndianAlaskaNative': true, 'blackOrAfricanAmerican': true, 'asian': true, + 'asianIndian': true, 'chinese': true, 'filipino': true, 'japanese': true, 'korean': true, + 'vietnamese': true, 'otherAsian': true, 'hawaiian': true, 'nativeHawaiian': true, 'guamanianChamorro': true, + 'samoan': true, 'otherPacificIslander': true, 'white': true, 'notProvided': true, + }; + component.group.patchValue(allValuesObject); + fixture.detectChanges(); + Object.keys(allValuesObject).forEach(key => { + expect(changes[key]).toBe(true); + }); + }); + + it('should emit changes for text-boxes', () => { + const allValuesObject = { 'otherPacificIslanderText': 'Text testing', + 'americanIndianAlaskaNativeText': 'Text testing', 'otherAsianText': 'Text testing'}; + component.group.patchValue(allValuesObject); + fixture.detectChanges(); + Object.keys(allValuesObject).forEach(key => { + expect(changes[key]).toBe('Text testing'); + }); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.ts new file mode 100644 index 0000000..d6e34d5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-info-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-race-info-form', + templateUrl: './race-info-form.component.html', + styleUrls: ['./hmda-styles.scss'] +}) +export class RaceInfoFormComponent { + @Input() + public group: FormGroup; + + @Input() + public idPrefix: string; + + constructor() {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-information.ts new file mode 100644 index 0000000..4cd5a05 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/hmda/race-information.ts @@ -0,0 +1,22 @@ +export interface RaceInformation { + 'americanIndianAlaskaNative'?: boolean; + 'americanIndianAlaskaNativeText'?: string; + 'blackOrAfricanAmerican'?: boolean; + 'asian'?: boolean; + 'asianIndian'?: boolean; + 'chinese'?: boolean; + 'filipino'?: boolean; + 'japanese'?: boolean; + 'korean'?: boolean; + 'vietnamese'?: boolean; + 'otherAsian'?: boolean; + 'otherAsianText'?: string; + 'hawaiian'?: boolean; + 'nativeHawaiian'?: boolean; + 'guamanianChamorro'?: boolean; + 'samoan'?: boolean; + 'otherPacificIslander'?: boolean; + 'otherPacificIslanderText'?: string; + 'white'?: boolean; + 'notProvided'?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.html new file mode 100644 index 0000000..cc0a737 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.html @@ -0,0 +1,169 @@ +
+
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+ +
+ + + + +
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.spec.ts new file mode 100644 index 0000000..3cf995a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.spec.ts @@ -0,0 +1,183 @@ +import { TestBed, ComponentFixture, async, tick, fakeAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { MemberInfoFormComponent } from './member-info-form.component'; +import { MembershipInformation } from './membership-information'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TranslateModule } from '@ngx-translate/core'; +import { MemberInfoFormService } from './member-info-form.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; +import { PatriotActComponent } from 'src/app/shared/patriot-act/patriot-act.component'; +import { ElectronicDisclosuresFormComponent } from '../disclosures/electronic-disclosures-form.component'; + +const createRegularTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + CommonGroups.MEMBER_INFO_PURPOSE_OF_LOAN_SIGNATURE.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +const createEquityTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + CommonGroups.MEMBER_INFO_TERM_OF_LOAN.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + CommonGroups.MEMBER_INFO_PURPOSE_OF_LOAN.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + + +describe('MemberInfoFormComponent', () => { + let component: MemberInfoFormComponent; + let fixture: ComponentFixture; + let applicationWizardService: ApplicationWizardService; + let formBuilder: FormBuilder; + let formGroupEquity: FormGroup; + let formGroupRegular: FormGroup; + let changes: MembershipInformation; + let status: string; + let service: MemberInfoFormService; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + MemberInfoFormComponent, + PatriotActComponent, + ElectronicDisclosuresFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroupRegular = createRegularTestFormGroup(formBuilder); + formGroupEquity = createEquityTestFormGroup(formBuilder); + fixture = TestBed.createComponent(MemberInfoFormComponent); + service = TestBed.get(MemberInfoFormService); + + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + component = fixture.componentInstance; + component.group = formGroupRegular; + + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'applicantType': '0', 'limitRequested': '10000', 'livesInMaritalPropertyState': '1'}); + expect(changes.applicantType.toString()).toBe('0'); + expect(changes.limitRequested.toString()).toBe('10000'); + expect(changes.livesInMaritalPropertyState.toString()).toBe('1'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'applicantType': '0', 'limitRequested': '10000', 'creditScore': '650'}); + expect(changes['creditScore']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + component.group.patchValue({'applicantType': '0', 'limitRequested': '10000'}); + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form', () => { + component.group.patchValue({'applicantType': '0', 'limitRequested': '10000', 'purposeOfLoanSignature': 'Refinance', 'livesInMaritalPropertyState': 'yes'}); + expect(status).toBe('VALID'); + }); + + it('should emit changes ( for equity loans ) ', () => { + applicationWizardService.setLoanType(LoanTypeSet[4].id); + component.group = formGroupEquity; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + component.group.patchValue({'purposeOfLoan': '0', 'termOfLoan': 120}); + fixture.detectChanges(); + expect(changes.purposeOfLoan.toString()).toBe('0'); + }); + + it('should emit INVALID for incomplete form ( for equity loans )', () => { + applicationWizardService.setLoanType(LoanTypeSet[4].id); + component.group = formGroupEquity; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + component.group.patchValue({'limitRequested': '10000', 'termOfLoan': -80}); + fixture.detectChanges(); + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form ( for equity loans ) ', () => { + applicationWizardService.setLoanType(LoanTypeSet[5].id); + component.group = formGroupEquity; + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + component.group.patchValue( + {'applicantType': '0', 'limitRequested': '10000', 'livesInMaritalPropertyState': 'yes', 'purposeOfLoan': '1', 'termOfLoan': 120} + ); + fixture.detectChanges(); + expect(status).toBe('VALID'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.ts new file mode 100644 index 0000000..07ed27a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.component.ts @@ -0,0 +1,100 @@ +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { MemberInfoFormService } from './member-info-form.service'; +import { Subscription } from 'rxjs'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import {CreditApplicationValues} from '../../../shared/credit-application-values'; + +@Component({ + selector: 'lw-member-info-form', + templateUrl: './member-info-form.component.html' +}) +export class MemberInfoFormComponent implements OnInit, OnDestroy { + + public numberMask = MaskingService.numberMask; + private subscription: Subscription; + + @Input() + public group: FormGroup; + + @Input() + public amountRequestedName: string; + + @Input() + public hasCoApplicant: boolean; + + @Input() + public askLoanPurpose: boolean; + + @Input() + public askLoanPurposeSignature: boolean; + + @Input() + public askTermDuration: boolean; + + @Input() + public isSpecialLoan: boolean; + + @Input() + public isHelocRateMod: boolean; + + @Input() + public isCreditCard: boolean + + @Input() + public requireAgreement: boolean; + + public purposeOptions = []; + + constructor(private memberService: MemberInfoFormService) {} + + public localState = { + termOptions: [{ value: '', desc: 'Please enter "Loan Amount"'}], + disableDropdown: true + }; + + public ngOnInit() { + if (this.group) { + this.updateLocalState(this.group.value); + } + this.purposeOptions = this.memberService.getPurposeOptions(); + this.askLoanPurposeSignature = this.memberService.requirePurposeOfLoanSignature(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public updateLocalState(modelValues): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + + if (modelValues.limitRequested) { + this.localState.termOptions = this.memberService.getTermOptions(MaskingService.unMaskNumber(modelValues.limitRequested)); + const loanCategory: LoanCategory = this.memberService.getLoanCategory(); + if (!modelValues.termOfLoan && loanCategory === LoanCategory.Equity) { + this.group.patchValue({termOfLoan: this.localState.termOptions[0].value}); + } + } else { + this.group.patchValue({termOfLoan: ''}); + } + + this.localState.disableDropdown = (this.localState.termOptions.length <= 1); + const dropdownControl = this.group.get('termOfLoan'); + + if (dropdownControl) { + if (this.localState.disableDropdown) { + dropdownControl.disable(); + } else { + dropdownControl.enable(); + } + } + + this.isCreditCard = this.memberService.isCreditCard(); + this.subscription = this.group.valueChanges.subscribe(nextValues => this.updateLocalState(nextValues)); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.spec.ts new file mode 100644 index 0000000..f12a010 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.spec.ts @@ -0,0 +1,113 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { MemberInfoFormService } from './member-info-form.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; + +describe('Member Info Form Service', () => { + + let service: MemberInfoFormService; + let applicationWizardService: ApplicationWizardService; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + ConfigurationService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(MemberInfoFormService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[5].id); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should get loan type for term options', () => { + spyOn(service, 'getTermOptions').and.callThrough(); + spyOn(applicationWizardService, 'getFieldValue').and.returnValue(LoanTypeSet[5]); + service.getTermOptions(); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + }); + + it('should return term options when given a correct loanId', () => { + applicationWizardService.setLoanType(LoanTypeSet[6].id); + expect(service.getTermOptions('10000')).toBeTruthy(); + expect(service.getTermOptions('10000').length).toBeGreaterThan(1); + }); + + it('should return term options dynamically based on requested loan amount', () => { + applicationWizardService.setLoanType(LoanTypeSet[7].id); + expect(service.getTermOptions('2010').length).toBeLessThan(service.getTermOptions('2019').length); + }); + + it('should return empty term options when given incorrect loanId', () => { + applicationWizardService.setLoanType(LoanTypeSet[3].id); + spyOn(service, 'getTermOptions').and.callThrough(); + expect(service.getTermOptions()).toEqual([]); + }); + + it('should build a list of term options from an array of numbers (signature)', () => { + const listOfPotentialTermLengths = [48, 63, 84]; + expect(service.buildListByFilteringTermOptions(listOfPotentialTermLengths, LoanCode.SIGNATURE)) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 84, desc: '84 months'} + ]); + }); + + it('should build a list of term options from an array of numbers (heloc)', () => { + const listOfPotentialTermLengths = [48, 63, 84]; + expect(service.buildListByFilteringTermOptions(listOfPotentialTermLengths, LoanCode.EQ_HELOC_Fixed)) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 48, desc: 'Up to 48 months / 4 years'}, + {value: 63, desc: '63 months'}, + {value: 84, desc: '84 months / 7 years'} + ]); + }); + + // tslint:disable-next-line:max-line-length + it('should build a list of term options from an object which has parsable numbers as keys each with a corresponding array of numbers value', () => { + const objectOfListsOfPotentialTermLengths = { + '15150': [48, 63, 84], + '26250': [120, 180] + }; + expect(service.buildListFromRequestedAmountAndTermOptions(objectOfListsOfPotentialTermLengths, '15000')) + .toEqual([ ]); + + expect(service.buildListFromRequestedAmountAndTermOptions(objectOfListsOfPotentialTermLengths, '16000')) + .toEqual([48, 63, 84]); + + expect(service.buildListFromRequestedAmountAndTermOptions(objectOfListsOfPotentialTermLengths, '27000')) + .toEqual([48, 63, 84, 120, 180]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.ts new file mode 100644 index 0000000..86b6c6c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/member-info-form.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { TermOptions } from '../../../shared/options/term-options'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { SelectOptions } from 'src/app/shared/options/select-options'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberInfoFormService { + constructor( + private applicationWizardService: ApplicationWizardService, + private configurationService: ConfigurationService + ) { } + + public getLoanCategory(): LoanCategory { + return this.applicationWizardService.getFieldValue('loanType').category; + } + + public getPurposeOptions(): Array<{ value: any, desc: string }> { + const loanCode: LoanCode = this.applicationWizardService.getFieldValue('loanType').apiCode; + var options = SelectOptions.purposeOptions; + switch (loanCode) { + case LoanCode.EQ_HELOC_Fixed: + case LoanCode.EQ_HELOC_Flex: + return SelectOptions.purposeOptionsAdditionalRefinance; + default: + return SelectOptions.purposeOptions; + } + } + + public requirePurposeOfLoanSignature(): boolean { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + return (loanType.category === LoanCategory.Signature); + } + + public getTermOptions(requestedAmount?: string): Array<{ value: any, desc: string }> { + const loanCode: LoanCode = this.applicationWizardService.getFieldValue('loanType').apiCode; + const uiSettingKey: string = TermOptions.loanCodeToUiSettingKey[loanCode]; + if (!uiSettingKey) { + return []; + } + let termMonthsOptions = this.configurationService.getConfigurationByField(uiSettingKey); + if (!termMonthsOptions) { + return []; + } + + if (!termMonthsOptions.length) { + termMonthsOptions = this.buildListFromRequestedAmountAndTermOptions(termMonthsOptions, requestedAmount); + } + return this.buildListByFilteringTermOptions(termMonthsOptions, loanCode); + } + + public buildListFromRequestedAmountAndTermOptions(termMonthsOptions: any, requestedAmount?: string) { + let uiTermsFromRequestedAmount = []; + Object.keys(termMonthsOptions).forEach(tier => { + if (parseInt(tier, 10) <= parseInt(requestedAmount, 10)) { + uiTermsFromRequestedAmount = uiTermsFromRequestedAmount.concat(termMonthsOptions[tier]); + } + }); + return uiTermsFromRequestedAmount; + } + + public buildListByFilteringTermOptions(termMonthsOptions: Array, loanCode: LoanCode) { + const uiSettingKey = TermOptions.determineTermOptionsUiSettingsField(loanCode); + let filteredTerms = this.configurationService.getConfigurationByField(uiSettingKey).filter(term => { + return termMonthsOptions.indexOf(term.value) > -1; + }); + if (filteredTerms.length) { + filteredTerms = TermOptions.insertTextDecorationsByLoan(filteredTerms, loanCode); + } + filteredTerms.unshift(TermOptions.defaultTermOption); + return filteredTerms; + } + + public isCreditCard(){ + return this.getLoanCategory() === LoanCategory.CreditCard; + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/membership-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/membership-information.ts new file mode 100644 index 0000000..5bedfbe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/member/membership-information.ts @@ -0,0 +1,13 @@ +import {LoanPurpose} from 'src/app/shared/loan-type/loan-purpose'; + +export interface MembershipInformation { + applicantType: string; + limitRequested: number; + livesInMaritalPropertyState: string; + coApplicantLivesInMaritalPropertyState?: string; + confirmJointCredit?: string; + purposeOfLoanSignature?: string; + purposeOfLoan?: LoanPurpose; + termOfLoan?: number; + agreementRequirement?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.html new file mode 100644 index 0000000..cc7c70a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.html @@ -0,0 +1,286 @@ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ + FORMS.PERSONAL.SSN_HINT +
+
+
+ +
+ +
+ + +
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.spec.ts new file mode 100644 index 0000000..2ef915e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.spec.ts @@ -0,0 +1,105 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { PersonalInfoFormComponent } from './personal-info-form.component'; +import { PersonalInformation } from './personal-information'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TcpaComponent } from '../../../shared/legal-message/tcpa.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { FileUploadComponent } from 'src/app/shared/file-upload/file-upload.component'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.PERSONAL_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('PersonalInfoFormComponent', () => { + let component: PersonalInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: PersonalInformation; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + PersonalInfoFormComponent, + TcpaComponent, + FileUploadComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(PersonalInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'email': 'MantisToboggan@wolf-cola.org'}); + expect(changes.firstName).toBe('Frank'); + expect(changes.lastName).toBe('Reynolds'); + expect(changes.email).toBe('MantisToboggan@wolf-cola.org'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'spouseName': 'Barbara'}); + expect(changes['spouseName']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'email': 'MantisToboggan@wolf-cola.org'}); + expect(status).toBe('INVALID'); + }); + + it('should require a valid email address', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'email': 'MantisTobogganATwolf-cola.org'}); + expect(component.group.controls.email.status).toBe('INVALID'); + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'email': 'MantisToboggan@wolf-cola.org'}); + expect(component.group.controls.email.status).toBe('VALID'); + }); + + it('should emit INVALID if primary and co-applicant email addresses are the same', () => { + component.group.patchValue({'primaryApplicantEmail': 'test@test.test', 'coApplicantEmail': 'test@test.test'}); + expect(component.group.controls.coApplicantEmail.status).toBe('INVALID'); + component.group.patchValue({'primaryApplicantEmail': 'test@test.test', 'coApplicantEmail': 'different@test.test'}); + expect(component.group.controls.coApplicantEmail.status).toBe('VALID'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.ts new file mode 100644 index 0000000..093d939 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-info-form.component.ts @@ -0,0 +1,84 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { Subscription } from 'rxjs'; +import { ErrorMessageMap } from '../../controls/error-display/error-display.service'; +import { SelectOptions } from '../../../shared/options/select-options'; +import {LoanCategory} from '../../../shared/loan-type/loan-category'; +import { BorrowerType } from 'src/app/shared/borrower-type'; + +@Component({ + selector: 'lw-personal-info-form', + templateUrl: './personal-info-form.component.html' +}) +export class PersonalInfoFormComponent implements OnInit, OnDestroy { + + public dobMask = MaskingService.dobMask; + public phoneMask = MaskingService.phoneMask; + public ssnMask = MaskingService.ssnMask; + public emailMask = MaskingService.emailMask; + public middleInitialMask = MaskingService.middleInitialMask; + public nameErrors = { + 'pattern': 'ERRORS.CUSTOM.ALPHAONLY' + }; + private valueSubscription: Subscription; + + public hideSSN = false; + public phoneTypeOptions = SelectOptions.phoneTypeOptions; + public suffixOptions = SelectOptions.suffixOptions; + + @Input() + public borrowerType: string; + + @Input() + public group: FormGroup; + + @Input() + public isHelocRateMod: boolean; + + @Input() + public showProofOfStudentUploadForm: boolean; + + @Input() + public loanId: string; + + constructor() {} + + ngOnInit() { + this.valueSubscription = this.group.controls.secondaryPhoneNumber.valueChanges.subscribe(() => { + if (this.group.controls.secondaryPhoneNumber.dirty) { + this.group.controls.secondaryPhoneNumberType.markAsDirty(); + } + }); + + if (this.group) { + this.updateLocalState(this.group.value); + } + } + + ngOnDestroy() { + if ( this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + } + + public updateLocalState(modelValues): void { + if (this.isHelocRateMod) { + this.group.patchValue({maritalStatus: 'no'}); + } + } + + public commandToggleSSNView() { + this.hideSSN = !this.hideSSN; + } + + public handleFiles(files: any[]): void { + if(this.borrowerType === BorrowerType.PRIMARY_BORROWER && this.showProofOfStudentUploadForm) { + this.group.patchValue({ 'files': files }); + } + } + + public handleUploadingFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploading': isUploading }); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-information.ts new file mode 100644 index 0000000..b40cb69 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/personal/personal-information.ts @@ -0,0 +1,22 @@ +import { PhoneTypeEnum } from 'src/app/shared/api-models/api-enums'; +import { ApiSuffix } from 'src/app/shared/api-models/api-suffix'; + +export interface PersonalInformation { + firstName: string; + middleInitial?: string; + lastName: string; + dateOfBirth: string; + suffix?: ApiSuffix; + socialSecurityNumber: string; + primaryPhoneNumberType: PhoneTypeEnum; + primaryPhoneNumber: string; + secondaryPhoneNumberType?: PhoneTypeEnum; + secondaryPhoneNumber?: string; + email: string; + maritalStatus?: string; + coApplicantSpouse?: string; + primaryApplicantEmail?: string; + coApplicantEmail?: string; + isUploading?: boolean; + files?: any[]; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.html new file mode 100644 index 0000000..54b225f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.html @@ -0,0 +1,20 @@ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.spec.ts new file mode 100644 index 0000000..a49c16a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.spec.ts @@ -0,0 +1,90 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { DataTableService } from '../../../shared/data-table.service'; +import {TranslateModule} from '@ngx-translate/core'; +import { IsFullInfoSpouseInfoFormComponent } from './is-full-info-spouse-info-form.component'; +import { IsFullInfoSpouse } from './is-full-info-spouse-information'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.IS_FULL_INFO_SPOUSE.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +describe('IsFullInfoSpouseInfoFormComponent', () => { + let component: IsFullInfoSpouseInfoFormComponent; + let fixture: ComponentFixture; + let service: DataTableService; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: IsFullInfoSpouse; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + IsFullInfoSpouseInfoFormComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(IsFullInfoSpouseInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + service = TestBed.get(DataTableService); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'isFullInfoSpouse': CreditApplicationValues.CHOICE_YES}); + expect(changes.isFullInfoSpouse).toBe('yes'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'isMailingAddressSame': 'maybe'}); + expect(changes['isMailingAddressSame']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form', () => { + component.group.patchValue({'isFullInfoSpouse': CreditApplicationValues.CHOICE_NO}); + expect(status).toBe('VALID'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.ts new file mode 100644 index 0000000..ea487fd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component.ts @@ -0,0 +1,15 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-is-full-info-spouse-info-form', + templateUrl: './is-full-info-spouse-info-form.component.html' +}) +export class IsFullInfoSpouseInfoFormComponent { + + + @Input() + public group: FormGroup; + + constructor() {} +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-information.ts new file mode 100644 index 0000000..53cd13a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/spouse-type/is-full-info-spouse-information.ts @@ -0,0 +1,3 @@ +export interface IsFullInfoSpouse { + isFullInfoSpouse: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.html new file mode 100644 index 0000000..18f9c5a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.html @@ -0,0 +1,20 @@ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.spec.ts new file mode 100644 index 0000000..90a125e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.spec.ts @@ -0,0 +1,71 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TcpaComponent } from '../../../shared/legal-message/tcpa.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AdditionalInfoFormComponent } from './additional-info-form.component'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.SUMMARY_ADDITIONAL_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('AdditionalInfoFormComponent', () => { + let component: AdditionalInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: any; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + AdditionalInfoFormComponent, + TcpaComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(AdditionalInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.ts new file mode 100644 index 0000000..9cebade --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-additional-info-form', + templateUrl: './additional-info-form.component.html' +}) +export class AdditionalInfoFormComponent { + + @Input() + public group: FormGroup; + @Input() + public modFee: Array<{ "value": any, "desc": string}>; + + constructor() { } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info.ts new file mode 100644 index 0000000..89feb31 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/additional-info.ts @@ -0,0 +1,3 @@ +export interface AdditionalInformation { + modFeeAccount?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.html new file mode 100644 index 0000000..ffe0fb9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.html @@ -0,0 +1,51 @@ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.spec.ts new file mode 100644 index 0000000..1cb767e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.spec.ts @@ -0,0 +1,71 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { NoteFormComponent } from './note-form.component'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TcpaComponent } from '../../../shared/legal-message/tcpa.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.SUMMARY_NOTE.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('NoteFormComponent', () => { + let component: NoteFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: any; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + NoteFormComponent, + TcpaComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(NoteFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.ts new file mode 100644 index 0000000..4143e29 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/summary/note-form.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'lw-note-form', + templateUrl: './note-form.component.html' +}) +export class NoteFormComponent { + + @Input() + public group: FormGroup; + @Input() + public primaryContact: Array; + + constructor() {} + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.html new file mode 100644 index 0000000..3d0b25b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.html @@ -0,0 +1,37 @@ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+ +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.spec.ts new file mode 100644 index 0000000..8fe9e9e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.spec.ts @@ -0,0 +1,88 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { TextMaskModule } from 'angular2-text-mask'; +import { TcpaComponent } from '../../../shared/legal-message/tcpa.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { TattleSpousePersonalInfoFormComponent } from './tattle-spouse-personal-info-form.component'; +import { TattleSpousePersonalInformation } from './tattle-spouse-personal-information'; +import { configureTestSuite } from 'ng-bullet'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({}); + + CommonGroups.PERSONAL_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; +}; + +describe('TattleSpousePersonalInfoFormComponent', () => { + let component: TattleSpousePersonalInfoFormComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: TattleSpousePersonalInformation; + let status: string; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + TattleSpousePersonalInfoFormComponent, + TcpaComponent + ], + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(TattleSpousePersonalInfoFormComponent); + component = fixture.componentInstance; + component.group = formGroup; + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds'}); + expect(changes.firstName).toBe('Frank'); + expect(changes.lastName).toBe('Reynolds'); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'spouseName': 'Barbara'}); + expect(changes['spouseName']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + component.group.patchValue({'firstName': 'Frank', 'lastName': 'Reynolds', 'email': 'MantisToboggan@wolf-cola.org'}); + expect(status).toBe('INVALID'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.ts new file mode 100644 index 0000000..e923301 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { ErrorMessageMap } from '../../controls/error-display/error-display.service'; + +@Component({ + selector: 'lw-tattle-spouse-personal-info-form', + templateUrl: './tattle-spouse-personal-info-form.component.html' +}) +export class TattleSpousePersonalInfoFormComponent { + + public middleInitialMask = MaskingService.middleInitialMask; + public nameErrors = { + 'pattern': 'ERRORS.CUSTOM.ALPHAONLY' + }; + + @Input() + public group: FormGroup; + + constructor() {} + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information.ts new file mode 100644 index 0000000..5222df4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information.ts @@ -0,0 +1,8 @@ +export interface TattleSpousePersonalInformation { + firstName: string; + middleInitial?: string; + lastName: string; + // @TODO: Per Jeff's request this model is subject to change might be changed back. -ZB + // socialSecurityNumber: string; + // email?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.html new file mode 100644 index 0000000..5d49762 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.html @@ -0,0 +1,536 @@ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+
+ + +
+ +
+ +
+ +
+ + + + + + + + + + +
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ + +
+
+
ERRORS.CUSTOM.TITLE_BRANDED
+
+ +
+ +
+
+ + +
+
+ +
ERRORS.CUSTOM.LIEN
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.spec.ts new file mode 100644 index 0000000..6fb5c4f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.spec.ts @@ -0,0 +1,236 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CommonGroups } from '../common-groups'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { VehicleCollateralInfoFormComponent } from './vehicle-collateral-info-form.component'; +import { VehicleCollateralInformation } from './vehicle-collateral-information'; +import { TranslateModule } from '@ngx-translate/core'; +import { VehicleCollateralInfoFormService } from './vehicle-collateral-info-form.service'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { UiSettings } from '../../../shared/configuration/ui-settings'; +import { DefaultSettings } from '../../../shared/configuration/default-settings.mock'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { HttpClientTestingModule } from '../../../../../node_modules/@angular/common/http/testing'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { StatePipe } from '../../../shared/pipes/state.pipe'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + + CommonGroups.VEHICLE_COLLATERAL_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; +}; + +const createPartialTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const memberGroup = formBuilder.group({}); + CommonGroups.VEHICLE_COLLATERAL_INFO_PARTIAL.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, formBuilder.control('', eachField.validation.slice())); + }, this); + return memberGroup; +}; + +describe('VehicleCollateralInfoFormComponent', () => { + let component: VehicleCollateralInfoFormComponent; + let fixture: ComponentFixture; + let applicationWizardService: ApplicationWizardService; + let service: VehicleCollateralInfoFormService; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + let changes: VehicleCollateralInformation; + let configurationService: ConfigurationService; + let status: string; + const defaultSettings: UiSettings = DefaultSettings; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + VehicleCollateralInfoFormComponent, + StatePipe + ], + providers: [ + VehicleCollateralInfoFormService, + ConfigurationService + ], + schemas: [ NO_ERRORS_SCHEMA ] + }).compileComponents(); + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(VehicleCollateralInfoFormComponent); + service = TestBed.get(VehicleCollateralInfoFormService); + + component = fixture.componentInstance; + component.group = formGroup; + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[7].id); + + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + })); + + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit changes', () => { + component.group.patchValue({'year': 2015}); + fixture.detectChanges(); + expect(changes.year).toBe(2015); + }); + + it('should not accept bad fields', () => { + component.group.patchValue({'minivan': false}); + fixture.detectChanges(); + expect(changes['minivan']).toBeUndefined(); + }); + + it('should emit INVALID for incomplete form', () => { + fixture.detectChanges(); + expect(status).toBe('INVALID'); + }); + + it('should emit VALID for complete form', () => { + component.group.patchValue({ + value: 16850, + type: 'Automobile', + condition: 'New', + year: 2012, + make: 'Subaru', + model: 'Impreza', + mileage: 57000, + term: 36, + loanPurpose: 'Refinance', + seller: 'Private Party', + stateTitled: 'WI', + countyTitled: 'Washington', + files: [ + { + description: "DNR Registration Card", + name: "dnrRegistrationCard.PNG", + }, + { + description: "Boat Title", + name: "boatTitle.PNG" + } + ] + }); + fixture.detectChanges(); + expect(status).toBe('VALID'); + }); + + it('should emit VALID for complete form that requires partial collateral', () => { + component.requireFullCollateral = false; + component.group = createPartialTestFormGroup(formBuilder); + component.group.valueChanges.subscribe(state => { + changes = state; + }); + component.group.statusChanges.subscribe(newStatus => { + status = newStatus; + }); + component.group.patchValue({ + value: 21150, + type: 'Boat', + year: 2015, + make: 'Sun Ray', + model: 'Fishing Boat', + term: 36, + loanPurpose: 'Refinance' + }); + fixture.detectChanges(); + expect(status).toBe('VALID'); + }); + + it('should update local state on new values', () => { + fixture.detectChanges(); + spyOn(component, 'updateTermsBasedOffState').and.callThrough(); + component.group.patchValue({value: 21150}); + fixture.detectChanges(); + expect(component.updateTermsBasedOffState).toHaveBeenCalled(); + }); + + it('should update non changing terms ( valid limit )', () => { + fixture.detectChanges(); + spyOn(component, 'updateNonChangingTerms').and.callThrough(); + spyOn(service, 'getLimitRequested').and.returnValue(30000); + spyOn(service, 'getTerms').and.callThrough(); + component.updateNonChangingTerms(LoanCode.VEHICLE_DEFAULT); + expect(component.updateNonChangingTerms).toHaveBeenCalled(); + expect(service.getLimitRequested).toHaveBeenCalled(); + expect(service.getTerms).toHaveBeenCalled(); + }); + + it('should update non-changing terms ( requires more information )', () => { + applicationWizardService.setLoanType(LoanTypeSet[7].id); + spyOn(component, 'updateNonChangingTerms').and.callThrough(); + spyOn(service, 'getLimitRequested').and.callThrough(); + spyOn(service, 'getTerms').and.callThrough(); + fixture.detectChanges(); + expect(component.localState.termOptions).toEqual([{ value: '', desc: 'Choose One' }]); + expect(component.updateNonChangingTerms).not.toHaveBeenCalled(); + expect(service.getLimitRequested).not.toHaveBeenCalled(); + expect(service.getTerms).toHaveBeenCalled(); + }); + + it('should skip creating a subscription when not default vehicle', () => { + spyOn(component, 'updateTermsBasedOffState').and.callThrough(); + spyOn(component, 'updateNonChangingTerms').and.callThrough(); + spyOn(service, 'getLoanCode').and.returnValue(LoanCode.BOAT); + fixture.detectChanges(); + expect(component.updateTermsBasedOffState).not.toHaveBeenCalled(); + expect(component.updateNonChangingTerms).toHaveBeenCalled(); + }); + + it('should patch previous values for "term" if they are valid (BOAT)', () => { + spyOn(service, 'getLimitRequested').and.returnValue('10000'); + spyOn(service, 'getLoanCode').and.returnValue(LoanCode.BOAT); + spyOn(service, 'getTerms').and.returnValue([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: '36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'} + ]); + fixture.detectChanges(); + expect(component.group.value.term).toBe(''); + component.group.patchValue({term: 63}); + fixture.detectChanges(); + expect(component.group.value.term).toBe(63); + }); + + it('should NOT patch previous values for "term" if they are invalid (BOAT)', () => { + spyOn(service, 'getLimitRequested').and.returnValue('10000'); + spyOn(service, 'getLoanCode').and.returnValue(LoanCode.BOAT); + spyOn(service, 'getTerms').and.returnValue([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: '36 months'}, + {value: 48, desc: '48 months'} + ]); + component.group.patchValue({term: 63}); + fixture.detectChanges(); + expect(component.group.value.term).toBe(''); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.ts new file mode 100644 index 0000000..e3d8c3c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component.ts @@ -0,0 +1,299 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MaskingService } from '../../../shared/masking/masking.service'; +import { Subject, Subscription } from 'rxjs'; +import { VehicleCollateralInfoFormService } from './vehicle-collateral-info-form.service'; +import { TermInputFields } from 'src/app/shared/terms/term-input-fields'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { CreditApplicationValues } from 'src/app/shared/credit-application-values'; +import { AddressInformation } from '../address/address-information'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +@Component({ + selector: 'lw-vehicle-collateral-info-form', + templateUrl: './vehicle-collateral-info-form.component.html' +}) + +export class VehicleCollateralInfoFormComponent implements OnInit, OnDestroy { + + public numberMask = MaskingService.numberMask; + public yearMask = MaskingService.yearMask; + public phoneMask = MaskingService.phoneMask; + private subscription: Subscription; + private pageSubscription: Subscription; + public stateSet = []; + public removePurchaseContractFiles: Subject; + public removeBoatTitleFiles: Subject; + public removeTrailerTitleFiles: Subject; + + @Input() + public loanId: string; + + @Input() + public requireFullCollateral: boolean; + + @Input() + public requireSellerInput: boolean; + + @Input() + public isTransferringPlate: boolean; + + @Input() + public isVehicleDefault: boolean; + + @Input() + public isTrailerMotorhome: boolean; + + @Input() + public isPowerSport: boolean; + + @Input() + public isBoat: boolean; + + @Input() + public requireBoatAddressInput: boolean; + + @Input() + public hasLien: boolean; + + @Input() + public group: FormGroup; + + @Input() + public boatAddressGroup: FormGroup; + + @Input() + public boatAddressVerifyGroup: FormGroup; + + @Output() + public sendBoatAddress: EventEmitter; + + constructor( + private vehicleService: VehicleCollateralInfoFormService, + private configurationService: ConfigurationService, + private cd: ChangeDetectorRef) { + this.sendBoatAddress = new EventEmitter(); + this.removePurchaseContractFiles = new Subject(); + this.removeBoatTitleFiles = new Subject(); + this.removeTrailerTitleFiles = new Subject(); + } + + public vehicleTitle: string; + public showPrivatePartyUploadForm: boolean; + public showDealerUploadForm: boolean; + public purposeIsPurchase: boolean; + public seller: string; + + public localState = { + termOptions: [{value: '', desc: 'No terms available'}], + typeOptions: [{value: '', desc: 'No types available'}], + loanPurposeOptions: [{value: '', desc: 'No purpose available'}], + conditionOptions: [{value: '', desc: 'No condition available'}], + sellerOptions: [{value: '', desc: 'No seller available'}], + boatTrailerTypeOptions: [{value: '', desc: 'No trailer types available'}], + boatMotorTypeOptions: [{value: '', desc: 'No motor types available'}], + disableDropdown: true, + purposeIsPurchase: false, + showBoatFilesUploadForm: false + }; + + public ngOnInit() { + const loanCode: LoanCode = this.vehicleService.getLoanCode(); + this.stateSet = this.configurationService.getConfigurationByField('stateOptions'); + this.loanId = this.vehicleService.getLoanId(); + this.group.patchValue({ 'files': '' }); + if (this.group) { + this.localState.typeOptions = this.vehicleService.getVehicleTypesBasedOnLoan(loanCode); + const vehicleType = this.vehicleService.getTypeFromLoanCode(loanCode); + this.vehicleTitle = vehicleType.type; + if (!this.localState.typeOptions) { + this.group.patchValue(vehicleType); + } + this.initFormStateFromLoanCode(loanCode); + if (!this.group.value.term) { + this.group.patchValue({term: this.localState.termOptions[0].value}); + } + if (!this.group.value.stateTitled && loanCode === LoanCode.VEHICLE_DEFAULT) { + this.group.patchValue({stateTitled: CreditApplicationValues.STATE_REQUIRED_COAPP}) + } + this.purposeIsPurchase = this.group.value.loanPurpose === LoanPurpose.Purchase; + this.seller = this.group.value.seller; + this.localState.showBoatFilesUploadForm = this.group.value.boatLength > 16; + this.checkSellerForms(this.group.value); + } + this.pageSubscription = this.group.valueChanges.subscribe(nextValues => this.updateLocalState(nextValues)); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.pageSubscription) { + this.pageSubscription.unsubscribe(); + } + } + + public updateLocalState(values) { + this.purposeIsPurchase = values.loanPurpose === LoanPurpose.Purchase; + this.seller = values.seller; + if (!this.purposeIsPurchase) { + this.showDealerUploadForm = undefined; + this.showPrivatePartyUploadForm = undefined; + } + if (values.boatLength > 16 && !this.localState.showBoatFilesUploadForm) { + this.localState.showBoatFilesUploadForm = true; + } + if (this.localState.showBoatFilesUploadForm && values.boatLength <= 16) { + this.removeBoatTitleFiles.next(); + } + this.checkSellerForms(values); + } + + public initFormStateFromLoanCode(loanCode: LoanCode) { + this.updateVehicleNonChangingOptions(loanCode); + if (loanCode === LoanCode.VEHICLE_DEFAULT || loanCode === LoanCode.POWERSPORT) { + this.updateTermsBasedOffState(this.group.value, loanCode); + } else if (loanCode === LoanCode.BOAT || loanCode === LoanCode.TRAILER_MOTORHOME) { + this.updateNonChangingTerms(loanCode); + this.patchDefaultIfTermNotAvailable(); + } else { + console.error('This loan type is not supported, please make sure valid loan type.'); + } + } + + public updateNonChangingTerms(loanCode: LoanCode) { + const limitRequested = this.vehicleService.getLimitRequested(); + if (limitRequested) { + const fields: TermInputFields = new TermInputFields(loanCode, 0, limitRequested, 0); + this.localState.termOptions = this.vehicleService.getTerms(fields); + } else { + this.localState.termOptions = [{value: '', desc: 'No terms available'}]; + this.group.patchValue({termOfLoan: this.localState.termOptions[0].value}); + } + } + + public updateVehicleNonChangingOptions(loanCode: LoanCode) { + this.localState.conditionOptions = this.vehicleService.getOptionsByType("vehicleConditionOptions"); + this.localState.loanPurposeOptions = loanCode === LoanCode.VEHICLE_DEFAULT ? this.vehicleService.getOptionsByType("vehicleLoanPurposeOptionsVehicle") : this.vehicleService.getOptionsByType("vehicleLoanPurposeOptions"); + this.localState.sellerOptions = this.vehicleService.getOptionsByType("vehicleSellerOptions"); + if (loanCode === LoanCode.BOAT) { + this.localState.boatTrailerTypeOptions = this.vehicleService.getOptionsByType("boatTrailerTypeOptions"); + this.localState.boatMotorTypeOptions = this.vehicleService.getOptionsByType("boatMotorTypeOptions"); + } + } + + public patchDefaultIfTermNotAvailable() { + if (this.group.value.term) { + let matched = false; + this.localState.termOptions.forEach((option) => { + if (option.value.toString() === this.group.value.term.toString()) { + matched = true; + } + }); + if (!matched) { + this.group.patchValue({term: this.localState.termOptions[0].value}); + } + } + } + + public updateTermsBasedOffState(modelValues, loanCode): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + const fields: TermInputFields = new TermInputFields(loanCode, modelValues.year, modelValues.value, modelValues.mileage, modelValues.type); + this.localState.termOptions = this.vehicleService.getTerms(fields); + + if (!this.localState.termOptions.length) { + this.localState.termOptions = [{value: '', desc: 'Choose One'}]; + } + + if (this.localState.termOptions.length > 1) { + this.localState.disableDropdown = false; + } else { + this.localState.disableDropdown = true; + } + + const dropdownControl = this.group.get('term'); + + if (dropdownControl) { + if (this.localState.disableDropdown) { + dropdownControl.disable(); + } else { + dropdownControl.enable(); + } + } + + this.patchDefaultIfTermNotAvailable(); + this.subscription = this.group.valueChanges.subscribe(nextValues => this.updateTermsBasedOffState(nextValues, loanCode)); + } + + public confirmSendBoatAddress(addressInformation) { + this.sendBoatAddress.emit(addressInformation); + } + + public handleFiles(files: any[], component: string): void { + this.group.patchValue({ [component + 'Files']: files }); + } + + public handleUploading(isUploading: boolean, component: string): void { + this.group.patchValue({ [component + 'Uploading']: isUploading }); + } + + public removedBoatTitleFiles(removedAllFiles: boolean) { + if (removedAllFiles) { + this.removeTrailerTitleFiles.next(); + } + } + + public removedTrailerTitleFiles(removedAllFiles: boolean) { + if (removedAllFiles) { + if (this.seller === 'Dealer') { + this.showDealerUploadForm = this.purposeIsPurchase; + this.showPrivatePartyUploadForm = false; + this.cd.detectChanges(); + } + this.localState.showBoatFilesUploadForm = false; + } + } + + public removedPurchaseContractFiles(removedAllFiles: boolean) { + if (removedAllFiles) { + if (this.seller === 'Private_Party') { + this.showPrivatePartyUploadForm = this.purposeIsPurchase; + this.showDealerUploadForm = false; + this.cd.detectChanges(); + } + } + } + + public lastLoanPurpose: string; + + public checkSellerForms(values) { + if (values.seller == 'Private_Party') { + + if (this.showPrivatePartyUploadForm == undefined + || values.loanPurpose != this.lastLoanPurpose) { + this.showPrivatePartyUploadForm = this.purposeIsPurchase; + } + + this.removePurchaseContractFiles.next(); + + } else if (values.seller == 'Dealer') { + + if (this.showDealerUploadForm == undefined) { + this.showDealerUploadForm = this.purposeIsPurchase; + } + + if (!this.isBoat || (this.isBoat && !this.localState.showBoatFilesUploadForm)) { + this.showPrivatePartyUploadForm = false; + this.showDealerUploadForm = this.purposeIsPurchase; + } + + if (this.isBoat && this.localState.showBoatFilesUploadForm) { + this.removeBoatTitleFiles.next(); + } + + } + this.lastLoanPurpose = values.loanPurpose; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.spec.ts new file mode 100644 index 0000000..65c27fe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.spec.ts @@ -0,0 +1,106 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { VehicleCollateralInfoFormService } from './vehicle-collateral-info-form.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { TermInputFields } from 'src/app/shared/terms/term-input-fields'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; + +describe('Vehicle Collateral Info Form Service', () => { + + let service: VehicleCollateralInfoFormService; + let applicationWizardService: ApplicationWizardService; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + const mockInputBoat: TermInputFields = new TermInputFields(LoanCode.BOAT, 0, 5000, 0); + const mockInputVehicle2019NonFilter: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2019, 30000, 1000); + const mockCreditCard: TermInputFields = new TermInputFields(LoanCode.CC_CashBack, 2019, 30000, 1000); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + ConfigurationService + ] + }).compileComponents(); + + service = TestBed.get(VehicleCollateralInfoFormService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[10].id); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + })); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should get pwc type options', () => { + spyOn(service, 'getLimitRequested').and.callThrough(); + spyOn(applicationWizardService, 'getFieldValue').and.callThrough(); + service.getLimitRequested(); + expect(service.getLimitRequested).toHaveBeenCalled(); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + }); + + it('should get correct vehicle type based off loanCode', () => { + spyOn(service, 'getTypeFromLoanCode').and.callThrough(); + let term = service.getTypeFromLoanCode(LoanCode.BOAT); + expect(term).toEqual({type: VehicleType.BOAT}); + term = service.getTypeFromLoanCode(LoanCode.TRAILER_MOTORHOME); + expect(term).toEqual({type: VehicleType.TRAILER}); + term = service.getTypeFromLoanCode(LoanCode.VEHICLE_PRE_APPROVAL); + expect(term).toEqual({type: VehicleType.AUTO}); + expect(service.getTypeFromLoanCode).toHaveBeenCalled(); + }); + + it('should get correct correct vehicle calculator and terms based off loanCode(boat)', () => { + spyOn(service, 'getTerms').and.callThrough(); + const terms = service.getTerms(mockInputBoat); + expect(terms) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 63, desc: 'Up to 63 months'}, + ]); + expect(service.getTerms).toHaveBeenCalled(); + }); + + it('should get correct correct vehicle calculator and terms based off loanCode(vehicle)', () => { + spyOn(service, 'getTerms').and.callThrough(); + const terms = service.getTerms(mockInputVehicle2019NonFilter); + expect(terms) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + {value: 84, desc: '84 months'}, + ]); + expect(service.getTerms).toHaveBeenCalled(); + }); + + it('should throw error with invalid loanCode (creditCard)', () => { + spyOn(service, 'getTerms').and.callThrough(); + expect(function() { service.getTerms(mockCreditCard); }) + .toThrow(new Error('Cannot construct Available Terms Calculator from UI Settings')); + expect(service.getTerms).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.ts new file mode 100644 index 0000000..ac958a0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.service.ts @@ -0,0 +1,94 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { Injectable } from '@angular/core'; +import { TermOptions } from '../../../shared/options/term-options'; +import { TermInputFields } from 'src/app/shared/terms/term-input-fields'; +import { VehicleTermCalculator } from 'src/app/shared/terms/vehicle-term-calculator'; +import { DefaultTermCalculator } from 'src/app/shared/terms/default-term-calculator'; +import { AvailableTermsCalculator } from 'src/app/shared/terms/available-terms-calculator'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { VehicleOptions } from 'src/app/shared/options/vehicle-options'; +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; + +@Injectable({ + providedIn: 'root' +}) +export class VehicleCollateralInfoFormService { + + private defaultType: string; + + constructor( + private applicationWizardService: ApplicationWizardService, + private configurationService: ConfigurationService + ) {} + + public getLoanCode(): LoanCode { + return this.applicationWizardService.getFieldValue('loanType').apiCode; + } + + public getLoanId() : string { + return this.applicationWizardService.getFieldValue("loanId"); + } + + public getLimitRequested(): number { + return this.applicationWizardService.getFieldValue('limitRequested'); + } + + public getOptionsByType(options: String): Array<{ value: any, desc: string}> { + return VehicleOptions[`${options}`]; + } + + public getVehicleTypesBasedOnLoan(loanCode: LoanCode): Array<{ value: VehicleType, desc: string }> { + return VehicleOptions.getVehicleTypesByLoanCode(loanCode); + } + + public getTypeFromLoanCode(loanCode: LoanCode): { type: VehicleType } { + if (loanCode === LoanCode.BOAT) { + return {type: VehicleType.BOAT}; + } else if (loanCode === LoanCode.TRAILER_MOTORHOME) { + return {type: VehicleType.TRAILER}; + } else if ([LoanCode.VEHICLE_DEFAULT, LoanCode.VEHICLE_PRE_APPROVAL].includes(loanCode)) { + return {type: VehicleType.AUTO}; + } else if (loanCode === LoanCode.POWERSPORT) { + return {type: VehicleType.PWC}; + } + return null; + } + + public getTerms(fields: TermInputFields): Array<{ value: any, desc: string }> { + const uiSettingKey: string = TermOptions.loanCodeToUiSettingKey[fields.type]; + if (!uiSettingKey) { + throw new Error('Cannot construct Available Terms Calculator from UI Settings'); + } + const termMonthsOptions = this.configurationService.getConfigurationByField(uiSettingKey); + if (!termMonthsOptions) { + throw new Error('Cannot construct Available Terms Calculator from UI Settings'); + } + return this.getTermCalculator(fields, termMonthsOptions).determineTerm(); + } + + public getTermCalculator(fields: TermInputFields, termMonthsOptions: any): AvailableTermsCalculator { + switch (fields.type) { + case LoanCode.TRAILER_MOTORHOME: + return new DefaultTermCalculator( + fields, termMonthsOptions, this.configurationService.getConfigurationByField(TermOptions.uiSettingsKeyWithoutYear) + ); + case LoanCode.BOAT: + return new DefaultTermCalculator( + fields, termMonthsOptions, this.configurationService.getConfigurationByField(TermOptions.uiSettingsKeyWithoutYear) + ); + case LoanCode.POWERSPORT: + return new DefaultTermCalculator( + fields, termMonthsOptions, this.configurationService.getConfigurationByField(TermOptions.uiSettingsKeyWithoutYear) + ); + case LoanCode.VEHICLE_DEFAULT: + return new VehicleTermCalculator( + fields, termMonthsOptions, this.configurationService.getConfigurationByField(TermOptions.uiSettingsKeyWithoutYear) + ); + default: + return new DefaultTermCalculator( + fields, termMonthsOptions, this.configurationService.getConfigurationByField(TermOptions.uiSettingsKeyWithoutYear) + ); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-information.ts new file mode 100644 index 0000000..b113a86 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-information.ts @@ -0,0 +1,44 @@ +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; +import { AddressInformation } from '../address/address-information'; + +export interface VehicleCollateralInformation { + value: number; + condition: string; // [New, Used] + type: VehicleType; + year: number; + make: string; + model: string; + mileage: number; + term: string; + loanPurpose: string; // [ Refinance, Purchase, Lease Payoff, Other ] + seller?: string; // [Private Party, Dealership] + vin?: string; + transferring?: string; + licensePlateNumber?: string; + dealerName?: string; + dealerPhone?: string; + stateTitled?: string; + countyTitled?: string; + titleBranded?: boolean; + lien?: boolean; + boatLength?: number; + boatSameAddress?: string; + boatAddress?: AddressInformation; + trailerYear?: number; + trailerMake?: string; + trailerType?: string; // [Single, Tandem, Tri-Axle] + motorYear?: number; + motorMake?: string; + motorModel?: string; + motorType?: number; // [Inboard, Outboard, Inboard/Outboard] + municipalityType?: string; + municipalityWhereKept?: string; + dnrRegistrationCardFiles?: any[]; + purchaseContractFiles?: any[]; + boatTitleFiles?: any[]; + trailerTitleFiles?: any[]; + dnrRegistrationCardUploading?: boolean; + purchaseContractUploading?: boolean; + boatTitleUploading?: boolean; + trailerTitleUploading?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.spec.ts new file mode 100644 index 0000000..8f4b02c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.spec.ts @@ -0,0 +1,53 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { InputIconComponent } from '../../controls/input-icon/input-icon.component'; +import { ErrorDisplayComponent } from '../../controls/error-display/error-display.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { JointCreditFormComponent } from './joint-credit-confirmation.component'; +import { Subject } from 'rxjs'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Joint Credit Confirmation Component', () => { + let component: JointCreditFormComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + InputIconComponent, + ErrorDisplayComponent, + JointCreditFormComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JointCreditFormComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should subscribe to prop changes from parent component', () => { + component.openModal = new Subject(); + spyOn(component, 'open').and.callThrough(); + fixture.detectChanges(); + expect(component.open).not.toHaveBeenCalled(); + component.openModal.next(false); + expect(component.open).not.toHaveBeenCalled(); + component.openModal.next(true); + expect(component.open).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.ts new file mode 100644 index 0000000..9a41dab --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component.ts @@ -0,0 +1,64 @@ +import { Component, ViewChild, Input, OnInit, OnDestroy } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { FormGroup } from '@angular/forms'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { Subject, Subscription } from 'rxjs'; + +@Component({ + selector: 'lw-joint-credit-confirmation', + templateUrl: './joint-credit-confirmation.html' +}) +export class JointCreditFormComponent implements OnInit, OnDestroy { + + @Input() + public commandJointCredit: Function; + @Input() + public commandIndividual: Function; + @Input() + public openModal: Subject; + + private subscription: Subscription; + + constructor(private modalService: NgbModal) {} + + @ViewChild('jointCredit') modalContent: any; + + public open() { + this.modalService.open(this.modalContent, {size: 'sm'}).result.then((res) => { + + if (res === 'Joint Credit') { + // Does this change the response to applicantType? or the response to confirmJointCredit? -ZB + this.handleJointCredit(); + } else if (res === 'Individual') { + this.handleIndividual(); + } + }, () => { + this.handleIndividual(); + }); + } + + ngOnInit() { + if (this.openModal) { + this.subscription = this.openModal.subscribe((shouldOpen: boolean) => { + if (shouldOpen) { + this.modalContent.elementRef.nativeElement.ownerDocument.activeElement.blur(); + this.open(); + } + }); + } + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public handleJointCredit() { + // @TODO: Does closing this modal alert ever do anything else? + } + + public handleIndividual() { + this.commandIndividual(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.html new file mode 100644 index 0000000..09e01d5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/warning-modals/joint-credit-confirmation.html @@ -0,0 +1,16 @@ + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/wizard-validators.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/wizard-validators.ts new file mode 100644 index 0000000..19739e8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/field-groups/wizard-validators.ts @@ -0,0 +1,330 @@ +import { AbstractControl, ValidationErrors, ValidatorFn, Validators, FormGroup, FormControl } from '@angular/forms'; +import { CreditApplicationValues } from '../../shared/credit-application-values'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { MaskingService } from '../../shared/masking/masking.service'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; + +export class WizardValidators { + + // @TODO This must also check for a "FieldDependsOnME" validator on the other field and throw if not there. + // Possibly just do it in a test, which would be better for performance anyways. + public static RequiredIfValidator(otherField: string): ValidatorFn { + return this.RequiredIfMultipleValidator([otherField]); + } + + public static RequiredIfMultipleValidator(otherFields: Array): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + let allFound = true; + otherFields.forEach((eachField) => { + const dependentField = control.parent.get(eachField); + allFound = allFound && dependentField.value; + }, this); + if (allFound) { + return Validators.required(control); + } + } + return null; + }; + } + + public static RentedLotValidator(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return LoanCode.MANUFACTURED_HOME_LOAN === loanType.apiCode ? Validators.required(control) : null; + }; + } + + public static LienPositionValidator(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return LoanCode.MANUFACTURED_HOME_LOAN !== loanType.apiCode ? Validators.required(control) : null; + }; + } + + public static RentPaidValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const isRented = control.parent.get('onRentedLot'); + if (isRented && isRented.value === CreditApplicationValues.CHOICE_YES) { + return Validators.required(control); + } else { + return null; + } + } + }; + } + + public static AnnualPropertyTaxValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const isRented = control.parent.get('onRentedLot'); + if (isRented && isRented.value === CreditApplicationValues.CHOICE_NO) { + return Validators.required(control); + } else { + return null; + } + } + }; + } + + public static CoApplicantValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const coApplicant = control.parent.get('applicantType'); + const coApplicantLivesInMaritalPropertyState = control.parent.get('coApplicantLivesInMaritalPropertyState'); + const confirmJointCredit = control.parent.get('confirmJointCredit'); + const yes = CreditApplicationValues.CHOICE_YES; + if (!confirmJointCredit || !coApplicantLivesInMaritalPropertyState) { + return null; + } + if (coApplicant.value === yes && coApplicantLivesInMaritalPropertyState.value !== yes && confirmJointCredit.value !== yes) { + coApplicantLivesInMaritalPropertyState.setValidators(Validators.required); + confirmJointCredit.setValidators(Validators.required); + const errors = {required: true}; + coApplicantLivesInMaritalPropertyState.setErrors(errors); + confirmJointCredit.setErrors(errors); + } else if (coApplicant.value === CreditApplicationValues.CHOICE_NO && + coApplicantLivesInMaritalPropertyState && + confirmJointCredit) { + const errors = coApplicantLivesInMaritalPropertyState.errors; + if (errors) { + coApplicantLivesInMaritalPropertyState.setErrors({}); + confirmJointCredit.setErrors({}); + } + if (coApplicantLivesInMaritalPropertyState.validator) { + coApplicantLivesInMaritalPropertyState.clearValidators(); + confirmJointCredit.clearValidators(); + } + } + } + return null; + }; + } + + public static FieldDependsOnMe(otherField: string): ValidatorFn { + return this.FieldsDependsOnMe([otherField]); + } + + public static FieldsDependsOnMe(otherFields: Array): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + otherFields.forEach((eachField) => { + const dependentField = control.parent.get(eachField); + if (!dependentField) { + throw new Error('Dependent field not found ' + eachField); + } + dependentField.updateValueAndValidity(); + }, this); + } + return null; + }; + } + + public static RequireSpecificStateForAddressValidator(doesRequireSpecificState: boolean = false, requiredState: string): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + if (control.value !== requiredState && doesRequireSpecificState) { + return {'requiredState': {state: requiredState}}; + } + return null; + }; + } + + public static RequireWisconsinCollateralValidator(enabled: boolean = false, primaryBorrowerState?: string): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + let addressInformationState = ''; + try { + addressInformationState = control.parent.parent.get('addressInformation').get('state').value; + } catch (ex) { + addressInformationState = ''; + } + + if (control.value === CreditApplicationValues.CHOICE_YES && primaryBorrowerState !== 'WI' && enabled) { + return {'requireWisconsinCollateral': true}; + } else if (control.value === CreditApplicationValues.CHOICE_NO && addressInformationState !== 'WI' && enabled) { + return {'requireWisconsinCollateral': true}; + } else { + return null; + } + }; + } + + public static UpdateCollateralMessage(enabled: boolean = false): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + let isAddressSameComponent; + try { + isAddressSameComponent = control.parent.parent.get('collateralInformation').get('isAddressSame'); + } catch (ex) { + isAddressSameComponent = undefined; + } + + if (isAddressSameComponent) { + isAddressSameComponent.updateValueAndValidity(); + } + + return; + }; + } + + // 1. Control is actually a "FormGroup" + // 2. Return any error you want, with any info you want. + // 3. Can use "compose" if needed. + public static PersonalInfoValidator = (control: AbstractControl): { [key: string]: boolean } => { + // This is straight forward for one field, but multiple? + const secondaryPhone = control.get('secondaryPhoneNumber'); + const secondaryType = control.get('secondaryPhoneNumberType'); + if (!secondaryPhone || !secondaryType) { + return null; + } + // Could use a side-effect strategy where we call 'setErrors' on the field and then add/remove the errors ourselves + if (secondaryPhone.value && !secondaryType.value && secondaryType.touched) { + secondaryType.setErrors(Object.assign({}, secondaryType.errors, {required: true})); + } else { + const errors = secondaryType.errors; + if (errors.required) { + delete errors.required; + secondaryType.setErrors(errors); + } + } + // Could also return the error, but it would live at the group level. + return null; + } + + public static HmdaGenderValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const female = control.parent.get('female'); + const male = control.parent.get('male'); + if (female.value === true && male.value === true) { + control.setErrors({incorrect: true}); + return {incorrect: true}; + } else { + control.setErrors( null); + return null; + } + } + }; + } + + public static RequireOneValidator(): ValidatorFn { + return (control: FormGroup): ValidationErrors | null => { + const siblings = control.controls; + let hasValue = false; + Object.keys(siblings).forEach((child: string) => { + if (siblings[child].value) { + hasValue = true; + return null; + } + }); + if (!hasValue) { + return {noneSelected: true}; + } + }; + } + + public static maxCollateralValueValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = MaskingService.unMaskNumber(control.value); + if (value < 1000000000) { + return null; + } else { + return {maxCurrency: {max: 999999999, actual: value}}; + } + }; + } + + public static HelocRateModAppValidator(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return LoanTitle.HELOC_RateMod !== loanType.title ? Validators.required(control) : null; + }; + } + + public static ListedForSaleValidator(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return ![LoanTitle.ManufacturedHome, LoanTitle.HELOC_RateMod].includes(loanType.title) ? Validators.required(control) : null; + }; + } + + public static VehicleDefaultValidator(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return LoanCode.VEHICLE_DEFAULT == loanType.apiCode ? Validators.required(control) : null; + }; + } + + public static RequireTransferring(loanType): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const value = control.parent.get("loanPurpose") ? control.parent.get("loanPurpose").value : ""; + return [LoanCode.VEHICLE_DEFAULT, LoanCode.TRAILER_MOTORHOME].includes(loanType.apiCode) && value === LoanPurpose.Purchase ? Validators.required(control) : null; + } + }; + } + + public static RequireSeller(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const value = control.parent.get("loanPurpose") ? control.parent.get("loanPurpose").value : ""; + return (value === LoanPurpose.Purchase ) ? Validators.required(control) : null; + } + }; + } + + public static ValueEqual(otherControl: string, title: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.parent) { + const otherValue = control.parent.get(otherControl); + if (otherValue.value && otherValue.value.toLowerCase() === control.value.toLowerCase() && control.value) { + return {sameValue: { name: title }}; + } else { + return null; + } + } + }; + } + + public static FileValidator(files: FormControl): ValidationErrors | null{ + if(files.value) { + if (files.value.length > 0) { + return files.value.filter(file => file.invalid == true).length > 0 ? { invalidFiles: true } : null; + } + } + } + + public static MustBeFalse(isUploading: FormControl): ValidationErrors | null{ + if(isUploading.value === true) { + return { invalidFiles: true }; + } + return null; + } + + public static BrandedTitleValidator(): ValidatorFn { + return (group: FormGroup): ValidationErrors | null => { + if (group.controls.titleBranded) { + const titleBranded = group.controls.titleBranded ? group.controls.titleBranded.value : ""; + const loanPurpose = group.controls.loanPurpose ? group.controls.loanPurpose.value : ""; + const seller = group.controls.seller ? group.controls.seller.value : ""; + return titleBranded === CreditApplicationValues.CHOICE_YES + && loanPurpose === LoanPurpose.Purchase + && seller == 'Private_Party' ? { titleBranded: true } : null; + } + }; + } + + public static NumberMaxLengthValidator(max: Number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = MaskingService.unMaskNumber(control.value); + if (value.length > max) { + return { maxlength: {requiredLength: max, actual: value} } + } + }; + } + + public static VINMaxLength(loanType: any): ValidatorFn { + if (loanType.apiCode == LoanCode.BOAT) { + return Validators.maxLength(60) + } else { + return Validators.maxLength(20) + } + } +} + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.html new file mode 100644 index 0000000..5dfbccf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.html @@ -0,0 +1,16 @@ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.spec.ts new file mode 100644 index 0000000..d83d528 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.spec.ts @@ -0,0 +1,53 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { LoanWizardComponent } from './loan-wizard.component'; +import { Component } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; + +@Component({ + selector: 'lw-waypoints', + template: '

Waypoints stub

' +}) +class MockWaypointsComponent { + + constructor() { } + +} + +describe('LoanWizardComponent', () => { + let component: LoanWizardComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + MockWaypointsComponent, + LoanWizardComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanWizardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should have a router outlet', () => { + const outlet = fixture.debugElement.query(By.css('router-outlet')).nativeElement; + expect(outlet).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.ts new file mode 100644 index 0000000..93877d2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/loan-wizard.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-root', + templateUrl: './loan-wizard.component.html', + styleUrls: ['./loan-wizard.component.scss'] +}) +export class LoanWizardComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.html new file mode 100644 index 0000000..83f2762 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.html @@ -0,0 +1,59 @@ +
+
+
+

+

+
+
COPY.RESIDENCE.{{localState.borrowerType}}
+
+
+ + + + + + +
+ + + + + + + + + +
+

HEADINGS.ADDITIONAL_RESIDENCE_A

+

HEADINGS.ADDITIONAL_RESIDENCE_B

+ + +
+
+ +
+ + +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.spec.ts new file mode 100644 index 0000000..9c4e3da --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.spec.ts @@ -0,0 +1,545 @@ +import {TestBed, async, ComponentFixture, fakeAsync, tick} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {By} from '@angular/platform-browser'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {ReactiveFormsModule} from '@angular/forms'; +import {AddressInfoPageComponent} from './address-info-page.component'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {CreditApplicationDaoService} from '../../../../shared/credit-application-dao.service'; +import {AddressInfoPageService} from './address-info-page.service'; +import {MockCreditApplicationDaoService} from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import {AddressInfoFormComponent} from '../../../field-groups/address/address-info-form.component'; +import {ResidenceInfoFormComponent} from '../../../field-groups/address/residence-info-form.component'; +import {TranslateModule} from '@ngx-translate/core'; +import {BorrowerType} from '../../../../shared/borrower-type'; +import {apiTranslatorPreTranslate} from '../../../../testing/shared-mocks/api-translator-pre-post'; +import {StatePipe} from '../../../../shared/pipes/state.pipe'; +import {ConfigurationService} from '../../../../shared/configuration/configuration.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {UiSettings} from '../../../../shared/configuration/ui-settings'; +import {DefaultSettings} from '../../../../shared/configuration/default-settings.mock'; +import {configureTestSuite} from 'ng-bullet'; +import {AddressValidationService} from 'src/app/shared/address-validation/address-validation.service'; +import {AddressInformation} from 'src/app/loan-wizard/field-groups/address/address-information'; +import {CreditApplicationValues} from 'src/app/shared/credit-application-values'; +import {ApplicationWizardService} from 'src/app/loan-wizard/application-wizard.service'; +import {LoanTypeSet} from 'src/app/shared/loan-type'; +import {MemberService} from '../../../../member/member.service'; + +describe('AddressInfoPageComponent', () => { + let component: AddressInfoPageComponent; + let fixture: ComponentFixture; + let service: AddressInfoPageService; + let memberService: MemberService; + let configService: ConfigurationService; + let addressValidationService: AddressValidationService; + let applicationWizardService: ApplicationWizardService; + const defaultSettings: UiSettings = DefaultSettings; + + const mockPrimaryAddress1: AddressInformation = apiTranslatorPreTranslate.fullInfoBorrowers[0].addressInformation; + const mockPrimaryResidence1 = { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '1', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }; + const mockPrimaryResidence2 = { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '2', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }; + const mockPrimaryResidenceMailing = { + 'isMailingAddressSame': false, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '8', + 'residenceTimeMonths': '5', + 'residenceStatus': '0' + }; + const mockPriorAddress1: AddressInformation = { + 'addressLine1': '321 Main St', + 'addressLine2': 'Apt 100', + 'zipcode': '53203', + 'city': 'Milwaukee', + 'state': 'WI' + }; + const mockPriorResidence1 = { + 'residenceTimeYears': '0', + 'residenceTimeMonths': '6', + 'residenceStatus': '0' + }; + const mockPriorAddress2: AddressInformation = { + 'addressLine1': '999 Main St', + 'addressLine2': 'Apt 101', + 'zipcode': '53204', + 'city': 'Milwaukee', + 'state': 'WI' + }; + const mockPriorResidence2 = { + 'residenceTimeYears': '0', + 'residenceTimeMonths': '6', + 'residenceStatus': '0' + }; + + + const mockMailingAddress1 = { + addressLine1: '111 S. Mailing St.', + addressLine2: '', + city: 'mail-city', + state: 'WI', + zipcode: '12354' + }; + const mockIsSameAddressSame = {isAddressSame: 'yes'}; + const mockIsSameAddressDiff = {isAddressSame: 'no'}; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + AddressInfoPageComponent, + AddressInfoFormComponent, + ResidenceInfoFormComponent, + StatePipe + ], + providers: [ + ConfigurationService, + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}, + AddressValidationService + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddressInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(AddressInfoPageService); + memberService = TestBed.get(MemberService); + addressValidationService = TestBed.get(AddressValidationService); + configService = TestBed.get(ConfigurationService); + configService.saveConfiguration(defaultSettings); + spyOn(service, 'getNextTopicMessage').and.callThrough().and.returnValue('foo bar'); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create for primary borrower', () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + expect(service.determineBorrowerType).toHaveBeenCalled(); + expect(service.individualIsKnownSpouse).toHaveBeenCalled(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.isAddressSameGroup).toBeFalsy(); + expect(component.addressInfoGroup).toBeTruthy(); + expect(component.mailingInfoGroup).toBeFalsy(); // group isn't added by default + expect(component.priorAddressInfoGroup).toBeTruthy(); + expect(component.residenceInfoGroup).toBeTruthy(); + expect(component.priorResidenceInfoGroup).toBeTruthy(); + expect(component.localState.showPriorAddressCount).toEqual(0); + }); + + it('should create for known spouse, is same address question un-answered', () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.CO_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(true); + fixture.detectChanges(); + expect(service.determineBorrowerType).toHaveBeenCalled(); + expect(service.individualIsKnownSpouse).toHaveBeenCalled(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.isAddressSameGroup).toBeTruthy(); + // Nothing else present, the question hasn't been answered. + expect(component.addressInfoGroup).toBeFalsy(); + expect(component.mailingInfoGroup).toBeFalsy(); + expect(component.priorAddressInfoGroup).toBeFalsy(); + expect(component.residenceInfoGroup).toBeFalsy(); + expect(component.priorResidenceInfoGroup).toBeFalsy(); + expect(component.localState.showPriorAddressCount).toEqual(0); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should reveal hidden form fields (mailing address)', async () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + + expect(fixture.debugElement.nativeElement.querySelector('#mailingInfo-inputAddress1')).toBeFalsy(); + + const preClickInputs = fixture.debugElement.queryAll(By.css('input')); + const preClickSelects = fixture.debugElement.queryAll(By.css('select')); + + fixture.debugElement.nativeElement.querySelector('#addressInfo-isMailingSameYes').click(); + fixture.detectChanges(); + + const postClickInputs = fixture.debugElement.queryAll(By.css('input')); + const postClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(preClickInputs.length + preClickSelects.length).toBeLessThan(postClickInputs.length + postClickSelects.length); + expect(fixture.debugElement.nativeElement.querySelector('#mailingInfo-inputAddress1')).toBeTruthy(); + + fixture.debugElement.nativeElement.querySelector('#addressInfo-isMailingSameYes').click(); + fixture.detectChanges(); + + const postSecondClickInputs = fixture.debugElement.queryAll(By.css('input')); + const postSecondClickSelects = fixture.debugElement.queryAll(By.css('select')); + + fixture.whenStable().then(() => { + expect(preClickInputs.length + preClickSelects.length).toBe(postSecondClickInputs.length + postSecondClickSelects.length); + expect(fixture.debugElement.nativeElement.querySelector('#mailingInfo-inputAddress1')).toBeFalsy(); + }); + }); + + it('should add and remove previous address sets as needed', async () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + + const preClickInputs = fixture.debugElement.queryAll(By.css('input')); + const preClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(component.localState.showPriorAddressCount).toEqual(0); + component.residenceInfoGroup.patchValue({ + residenceTimeYears: '1', + residenceTimeMonths: '0' + }); + fixture.detectChanges(); + expect(component.localState.showPriorAddressCount).toEqual(1); + + const postClickInputs = fixture.debugElement.queryAll(By.css('input')); + const postClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(preClickInputs.length + preClickSelects.length).toBeLessThan(postClickInputs.length + postClickSelects.length); + + component.residenceInfoGroup.patchValue({ + residenceTimeYears: '2', + residenceTimeMonths: '0' + }); + fixture.detectChanges(); + expect(component.localState.showPriorAddressCount).toEqual(0); + + const postSecondClickInputs = fixture.debugElement.queryAll(By.css('input')); + const postSecondClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(preClickInputs.length + preClickSelects.length).toBe(postSecondClickInputs.length + postSecondClickSelects.length); + }); + + it('should show previously saved addresses, with prior addresses (primary borrower)', () => { + spyOn(service, 'getPrimaryAddressValues').and.returnValue(mockPrimaryAddress1); + spyOn(service, 'getPrimaryResidenceValues').and.returnValue(mockPrimaryResidence1); + spyOn(service, 'getPriorAddressValues').and.returnValue([mockPriorAddress1, mockPriorAddress2]); + spyOn(service, 'getPriorResidenceValues').and.returnValue([mockPriorResidence1, mockPriorResidence2]); + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.addressInfoGroup).toBeTruthy(); + expect(component.addressInfoGroup.valid).toBeTruthy(); + expect(component.residenceInfoGroup).toBeTruthy(); + expect(component.residenceInfoGroup.valid).toBeTruthy(); + expect(component.priorAddressInfoGroup).toBeTruthy(); + expect(component.priorAddressInfoGroup.valid).toBeTruthy(); + expect(component.priorResidenceInfoGroup).toBeTruthy(); + expect(component.priorResidenceInfoGroup.valid).toBeTruthy(); + expect(component.mailingInfoGroup).toBeFalsy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.localState.nextDisabled).toEqual(false); + expect(component.addressInfoGroup.value).toEqual(mockPrimaryAddress1); + expect(component.residenceInfoGroup.value).toEqual(mockPrimaryResidence1); + expect(component.localState.showPriorAddressCount).toEqual(2); + expect(component.priorAddressInfoGroup.value).toEqual([mockPriorAddress1, mockPriorAddress2]); + expect(component.priorResidenceInfoGroup.value).toEqual([mockPriorResidence1, mockPriorResidence2]); + }); + + it('should show member pre-filled address (primary borrower)', () => { + const mockIndividual = { + 'individualId': 71057, + 'individualType': 'Individual', + 'title': '', + 'firstName': 'JOHN', + 'middleName': 'PAUL', + 'lastName': 'DOE', + 'nameSuffix': 'II', + 'taxId': '999-99-9990', + 'gender': 'M', + 'birthDate': '1970-01-01T00:00:00', + 'branch': 1, + 'driversLicense': '123456789', + 'driversLicenseState': 'WI', + 'ident': '1234567', + 'preferredContact': 'No Preference', + 'maritalStatus': 'Single', + 'isUSCitizen': true, + 'primaryAddressId': 3212176, + 'isEmployee': false, + 'employeeNumber': 0, + 'companyName': '', + 'employerName': 'ABC COMPANY', + 'applicationCountLast90Days': 0, + 'applicationsDeclinedLast90Days': 0, + 'applicationsApprovedLast90Days': 0, + 'address': [{ + 'addressId': 722485, + 'line1': '100 MAIN ST', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 100, + 'occupancy': 'Own', + 'startDate': '1999-03-01T00:00:00', + 'endDate': '2001-03-01T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 3212176, + 'line1': '1400A SCHAUER DR', + 'line2': '', + 'line3': '', + 'city': 'HARTFORD', + 'state': 'WI', + 'zipCode': '53027-1981', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': true, + 'isInternational': false, + 'addressVerification': 1400, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': true + }, { + 'addressId': 3222400, + 'line1': '5445 S WESTRIDGE DR', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151-7948', + 'country': 'US', + 'countyName': '', + 'addressType': 'Mailing', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': false + }], + 'email1': 'scottschmick@landmarkcu.com', + 'email2': '', + 'phone': [{ + 'phoneKey': 'ApexApiWrapper.Apex.MemberServices.PhoneKey', + 'isPrimaryForType': false, + 'isInternational': false, + 'countryCode': '', + 'areaCode': '555', + 'phoneNumber': '555-5555', + 'extension': '0' + }, { + 'phoneKey': 'ApexApiWrapper.Apex.MemberServices.PhoneKey', + 'isPrimaryForType': true, + 'isInternational': false, + 'countryCode': '', + 'areaCode': '262', + 'phoneNumber': '796-1100', + 'extension': '0' + }], + 'status': 'Bankruptcy', + 'isHomeOwner': false, + 'isHeadOfHouseHold': false, + 'language': '' + }; + memberService.transformCoreData(mockIndividual); + spyOn(service, 'getPrimaryResidenceValues').and.returnValue(mockPrimaryResidence2); + fixture.detectChanges(); + expect(component.addressInfoGroup.getRawValue().city).toEqual('HARTFORD'); + expect(component.pageForm.valid).toBeTruthy('Page Form valid'); + expect(component.addressInfoGroup).toBeTruthy('address info group not present'); + expect(component.addressInfoGroup.valid).toBeTruthy('addressInfoGroup invalid'); + expect(component.residenceInfoGroup).toBeTruthy('residence info group not present'); + expect(component.residenceInfoGroup.valid).toBeTruthy('resident info group invalid'); + expect(component.mailingInfoGroup).toBeFalsy('mailing info group is present'); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy('button next is disabled'); + expect(component.localState.nextDisabled).toEqual(false, 'Next is marked Disabled'); + }); + + it('should show previously answered is same address question (known spouse)', () => { + spyOn(service, 'getPrimaryAddressValues').and.returnValue(mockPrimaryAddress1); + spyOn(service, 'getPrimaryResidenceValues').and.returnValue(mockPrimaryResidence1); + spyOn(service, 'getIsSameAddressValues').and.returnValue(mockIsSameAddressSame.isAddressSame); + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.CO_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(true); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.isAddressSameGroup).toBeTruthy(); + expect(component.addressInfoGroup).toBeFalsy(); + expect(component.residenceInfoGroup).toBeFalsy(); + expect(component.mailingInfoGroup).toBeFalsy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.isAddressSameGroup.value).toEqual(mockIsSameAddressSame); + }); + + it('should show previously saved addresses (known spouse)', () => { + spyOn(service, 'getPrimaryAddressValues').and.returnValue(mockPrimaryAddress1); + spyOn(service, 'getPrimaryResidenceValues').and.returnValue(mockPrimaryResidence2); + spyOn(service, 'getIsSameAddressValues').and.returnValue(mockIsSameAddressDiff.isAddressSame); + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.CO_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(true); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.isAddressSameGroup).toBeTruthy(); + expect(component.addressInfoGroup).toBeTruthy(); + expect(component.residenceInfoGroup).toBeTruthy(); + expect(component.mailingInfoGroup).toBeFalsy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.isAddressSameGroup.value).toEqual(mockIsSameAddressDiff); + expect(component.addressInfoGroup.value).toEqual(mockPrimaryAddress1); + expect(component.residenceInfoGroup.value).toEqual(mockPrimaryResidence2); + }); + + it('should show previously saved mailing address (primary borrower)', () => { + spyOn(service, 'getPrimaryAddressValues').and.returnValue(mockPrimaryAddress1); + spyOn(service, 'getMailingAddressValue').and.returnValue(mockMailingAddress1); + spyOn(service, 'getPrimaryResidenceValues').and.returnValue(mockPrimaryResidenceMailing); + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.addressInfoGroup).toBeTruthy(); + expect(component.residenceInfoGroup).toBeTruthy(); + expect(component.mailingInfoGroup).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.residenceInfoGroup.value).toEqual(mockPrimaryResidenceMailing); + expect(component.mailingInfoGroup.value).toEqual(mockMailingAddress1); + }); + + it('should correctly default fields for current and prior addresses (primary borrower)', () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'individualIsKnownSpouse').and.returnValue(false); + fixture.detectChanges(); + + const preClickInputs = fixture.debugElement.queryAll(By.css('input')); + const preClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(component.localState.showPriorAddressCount).toEqual(0); + component.residenceInfoGroup.patchValue({ + residenceTimeYears: '1', + residenceTimeMonths: '0' + }); + fixture.detectChanges(); + expect(component.localState.showPriorAddressCount).toEqual(1); + + const postClickInputs = fixture.debugElement.queryAll(By.css('input')); + const postClickSelects = fixture.debugElement.queryAll(By.css('select')); + + expect(preClickInputs.length + preClickSelects.length).toBeLessThan(postClickInputs.length + postClickSelects.length); + + const baseValue = component.residenceInfoGroup.value; + expect(baseValue).toEqual({ + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': null, + 'residenceTimeYears': '1', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }); + const defaultValue = component.priorResidenceInfoGroup.at(0).value; + expect(defaultValue).toEqual(service.createDefaultPriorResidenceInfo()); + }); + + it('should submit the USPS-Verified Primary Address Value if the match was saved', () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'saveAndNavigate'); + fixture.detectChanges(); + component.addressInfoGroup.patchValue(mockPriorAddress1); + const mockUSPSAddress: AddressInformation = { + addressLine1: mockPriorAddress1.addressLine1.toLocaleUpperCase(), + addressLine2: mockPriorAddress1.addressLine2.toLocaleUpperCase(), + city: mockPriorAddress1.city.toLocaleUpperCase(), + state: mockPriorAddress1.state.toLocaleUpperCase(), + zipcode: mockPriorAddress1.zipcode, + zipcodeExt: '5446' + }; + component.confirmPrimaryAddressInformation(mockUSPSAddress); + const fields = component.generateFieldsFromGroups(); + expect(fields['addressInfo']).toEqual(mockUSPSAddress); + }); + + it('should submit the USPS-Verified Mailing Address Value if the match was saved', () => { + spyOn(service, 'determineBorrowerType').and.returnValue(BorrowerType.PRIMARY_BORROWER); + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'requiresMailingAddress').and.returnValue(true); + fixture.detectChanges(); + component.mailingInfoGroup.patchValue(mockPriorAddress1); + component.residenceInfoGroup.patchValue({isMailingAddressSame: CreditApplicationValues.CHOICE_YES}); + fixture.detectChanges(); + // component. + const mockUSPSAddress: AddressInformation = { + addressLine1: mockPriorAddress1.addressLine1.toLocaleUpperCase(), + addressLine2: mockPriorAddress1.addressLine2.toLocaleUpperCase(), + city: mockPriorAddress1.city.toLocaleUpperCase(), + state: mockPriorAddress1.state.toLocaleUpperCase(), + zipcode: mockPriorAddress1.zipcode, + zipcodeExt: '5446' + }; + component.confirmMailingAddressInformation(mockUSPSAddress); + const fields = component.generateFieldsFromGroups(); + expect(fields['mailingInfo']).toEqual(mockUSPSAddress); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.ts new file mode 100644 index 0000000..9dcc0b1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.component.ts @@ -0,0 +1,343 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormArray, FormGroup } from '@angular/forms'; +import { AddressInfoPageService } from './address-info-page.service'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { + AddressCompareInformation, AddressDiffInformation +} from 'src/app/loan-wizard/field-groups/address-compare/address-compare-information'; +import { AddressInformation } from 'src/app/loan-wizard/field-groups/address/address-information'; +import { AddressFormBuilderService } from 'src/app/shared/address-validation/address-form-builder.service'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; + +@Component({ + selector: 'lw-address-info-page', + templateUrl: './address-info-page.component.html' +}) +export class AddressInfoPageComponent implements OnInit, OnDestroy { + private addressStatusSubscription: Subscription; + private pageStatusSubscription: Subscription; + private pageValueSubscription: Subscription; + + public localState = { + showPriorAddressCount: 0, + previousDisabled: false, + nextDisabled: true, + borrowerType: BorrowerType.PRIMARY_BORROWER, + borrowerName: 'Applicant', + nextTopic: '', + primaryAddressInformation: null, + mailingAddressInformation: null, + addressVerifyStatus: null, + isSigningSpouse: false, + isHelocRateMod: false + }; + + public savingData = null; + + public isAddressSameGroup: FormGroup; + public addressInfoGroup: FormGroup; + public primaryAddressVerifyInfoGroup: FormGroup; + public mailingAddressVerifyInfoGroup: FormGroup; + + public residenceInfoGroup: FormGroup; + public mailingInfoGroup: FormGroup; + public priorAddressInfoGroup: FormArray; + public priorResidenceInfoGroup: FormArray; + public compareAddressInfoGroup: FormGroup; + + public addressCompareInformation: AddressCompareInformation; + + public mailingInfoGroupName = 'mailingInfo'; + public addressInfoGroupName = 'addressInfo'; + + public COMPARE_ADDRESS_FIELD = 'compareAddress'; + public USER_INPUT_FIELD = 'userInput'; + public ADDRESS_SELECT_FIELD = 'addressSelection'; + + public pageForm: FormGroup; + public isHelocRateMod: boolean; + private readonly FORM_STATE_INVALID = 'INVALID'; + + constructor( + private addressInfoPageService: AddressInfoPageService, + private route: ActivatedRoute) { + } + + public ngOnInit() { + this.localState.borrowerType = this.addressInfoPageService.determineBorrowerType(this.route); + this.pageForm = this.addressInfoPageService.createPageGroup(); + this.pageStatusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.determineIsSameAddressShown(); + this.updateLocalState(this.pageForm.value); + } + + public ngOnDestroy() { + if (this.pageStatusSubscription) { + this.pageStatusSubscription.unsubscribe(); + } + if (this.addressStatusSubscription) { + this.addressStatusSubscription.unsubscribe(); + } + if (this.pageValueSubscription) { + this.pageValueSubscription.unsubscribe(); + } + } + + public determineIsSameAddressShown() { + if (this.addressInfoPageService.individualIsKnownSpouse(this.localState.borrowerType)) { + this.addressInfoPageService.addIsAddressSameGroupIfMissing(this.pageForm); + this.isAddressSameGroup = this.pageForm.get(AddressInfoPageService.IS_ADDRESS_SAME_FORM_NAME); + const previousIsSame = this.addressInfoPageService.getIsSameAddressValues(this.localState.borrowerType); + if (typeof previousIsSame === 'string') { + this.isAddressSameGroup.patchValue({isAddressSame: previousIsSame}); + } else { + this.isAddressSameGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_YES}); + } + } + } + + public updateLocalState(modelValue: any): void { + if (this.pageValueSubscription) { + this.pageValueSubscription.unsubscribe(); + } + + if (this.isAddressDifferentFromPreviouslyEntered()) { + this.addAllAddressGroupsIfMissing(); + } else { + this.removeAllAddressGroupsIfPresent(); + } + + this.localState = { + showPriorAddressCount: this.addressInfoPageService + .getRequiredAddressCount(this.pageForm.value.residenceInformation, this.pageForm.value.priorResidenceInformation), + previousDisabled: this.localState.previousDisabled, + nextDisabled: this.localState.nextDisabled, + borrowerType: this.localState.borrowerType, + borrowerName: this.addressInfoPageService.getApplicantName(this.localState.borrowerType), + nextTopic: this.addressInfoPageService.getNextTopicMessage(), + primaryAddressInformation: this.localState.primaryAddressInformation ? + this.localState.primaryAddressInformation : + this.addressInfoPageService.getPrimaryAddressValues(this.localState.borrowerType), + mailingAddressInformation: this.localState.mailingAddressInformation ? + this.localState.mailingAddressInformation : + this.addressInfoPageService.getMailingAddressValue(this.localState.borrowerType), + addressVerifyStatus: this.localState.addressVerifyStatus, + isSigningSpouse: this.addressInfoPageService.individualIsSigningSpouse(this.localState.borrowerType), + isHelocRateMod: this.addressInfoPageService.isHelocRateMod() + }; + + this.updateOptionalMailingForm(); + this.addAdditionalPriorAddressGroups(); + this.pageValueSubscription = this.pageForm.valueChanges.subscribe(() => { + this.updateLocalState(this.pageForm.value); + }); + } + + public isAddressDifferentFromPreviouslyEntered(): boolean { + return (!this.isAddressSameGroup || this.isAddressSameGroup.value.isAddressSame === CreditApplicationValues.CHOICE_NO); + } + + public removeAllAddressGroupsIfPresent() { + this.addressInfoPageService.removeAddressResidenceGroups(this.pageForm); + this.addressInfoGroup = null; + this.primaryAddressVerifyInfoGroup = null; + this.residenceInfoGroup = null; + this.priorAddressInfoGroup = null; + this.priorResidenceInfoGroup = null; + this.mailingInfoGroup = null; + if (this.addressStatusSubscription) { + this.addressStatusSubscription.unsubscribe(); + this.addressCompareInformation = null; + this.compareAddressInfoGroup = null; + } + } + + public addAllAddressGroupsIfMissing() { + if (!this.addressInfoGroup) { + this.addressInfoPageService.addAddressResidenceGroupsIfMissing(this.pageForm); + this.addressInfoGroup = this.pageForm.get(AddressInfoPageService.ADDRESS_FORM_NAME); + this.residenceInfoGroup = this.pageForm.get(AddressInfoPageService.RESIDENCE_FORM_NAME); + this.priorAddressInfoGroup = this.pageForm.get(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME); + this.priorResidenceInfoGroup = this.pageForm.get(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME); + const savedAddressInfoValues = this.addressInfoPageService.getPrimaryAddressValues(this.localState.borrowerType); + if (savedAddressInfoValues) { + this.addressInfoGroup.patchValue(savedAddressInfoValues); + this.localState.primaryAddressInformation = savedAddressInfoValues; + } + this.residenceInfoGroup.patchValue( + this.addressInfoPageService.getPrimaryResidenceValues(this.localState.borrowerType)); + const priorAddressSet = this.addressInfoPageService.getPriorAddressValues(this.localState.borrowerType); + const priorResidenceSet = this.addressInfoPageService.getPriorResidenceValues(this.localState.borrowerType); + if (priorAddressSet && priorAddressSet.length > 0) { + this.pageForm.removeControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME); + this.pageForm.removeControl(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME); + // create prior address groups here. + for (let priorLoop = 0; priorLoop < priorAddressSet.length; priorLoop++) { + this.priorAddressInfoGroup.push(this.addressInfoPageService.createAddressInformationGroup()); + this.priorAddressInfoGroup.at(priorLoop).patchValue(priorAddressSet[priorLoop]); + this.priorResidenceInfoGroup.push(this.addressInfoPageService.createResidenceInformationGroup(false)); + this.priorResidenceInfoGroup.at(priorLoop).patchValue(priorResidenceSet[priorLoop]); + } + this.pageForm.addControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME, this.priorAddressInfoGroup); + this.pageForm.addControl(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME, this.priorResidenceInfoGroup); + } + } + } + + public confirmPrimaryAddressInformation(addressInformation: AddressInformation) { + if (!this.primaryAddressVerifyInfoGroup) { + AddressFormBuilderService.addAddressCompareGroupIfMissing(this.pageForm, AddressInfoPageService.PRIMARY_ADDRESS_VERIFY_FORM_NAME); + this.primaryAddressVerifyInfoGroup = this.pageForm.get(AddressInfoPageService.PRIMARY_ADDRESS_VERIFY_FORM_NAME); + } + if (!addressInformation) { + this.primaryAddressVerifyInfoGroup.patchValue({addressVerifyStatus: false}); + } else { + this.primaryAddressVerifyInfoGroup.patchValue({addressVerifyStatus: true}); + this.patchPrimaryAddressIfFieldsConfirmedAreDifferent(addressInformation); + } + this.localState.primaryAddressInformation = addressInformation; + } + + public patchPrimaryAddressIfFieldsConfirmedAreDifferent(addressInformation: AddressInformation) { + const addressDiff: AddressDiffInformation = AddressValidationFormatConverter.getAddressValidationDiff( + AddressValidationFormatConverter.makeApiAddress(this.addressInfoGroup.value), + addressInformation + ); + if (!addressDiff.allFieldsSame) { + this.addressInfoGroup.patchValue(addressInformation); + } + } + + public confirmMailingAddressInformation(addressInformation: AddressInformation) { + if (!this.mailingAddressVerifyInfoGroup) { + AddressFormBuilderService.addAddressCompareGroupIfMissing(this.pageForm, AddressInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME); + this.mailingAddressVerifyInfoGroup = this.pageForm.get(AddressInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME); + } + if (!addressInformation) { + this.mailingAddressVerifyInfoGroup.patchValue({addressVerifyStatus: false}); + } else { + this.mailingAddressVerifyInfoGroup.patchValue({addressVerifyStatus: true}); + this.patchMailingAddressIfFieldsConfirmedAreDifferent(addressInformation); + } + this.localState.mailingAddressInformation = addressInformation; + } + + public patchMailingAddressIfFieldsConfirmedAreDifferent(addressInformation: AddressInformation) { + const addressDiff: AddressDiffInformation = AddressValidationFormatConverter.getAddressValidationDiff( + AddressValidationFormatConverter.makeApiAddress(this.mailingInfoGroup.value), + addressInformation + ); + if (!addressDiff.allFieldsSame) { + this.mailingInfoGroup.patchValue(addressInformation); + } + } + + public updateOptionalMailingForm(): void { + const showMailingAddress = this.addressInfoPageService.requiresMailingAddress(this.pageForm.value.residenceInformation); + if (showMailingAddress && !this.mailingInfoGroup) { + this.mailingInfoGroup = this.addressInfoPageService.createAddressInformationGroup(); + this.pageForm.addControl(AddressInfoPageService.MAIL_FORM_NAME, this.mailingInfoGroup); + const mailingInfo = this.addressInfoPageService.getMailingAddressValue(this.localState.borrowerType); + if (mailingInfo) { + this.mailingInfoGroup.patchValue(mailingInfo); + } + } else if (!showMailingAddress && this.mailingInfoGroup) { + this.pageForm.removeControl(AddressInfoPageService.MAIL_FORM_NAME); + this.pageForm.removeControl(AddressInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME); + this.mailingInfoGroup = null; + this.mailingAddressVerifyInfoGroup = null; + } + } + + public addAdditionalPriorAddressGroups(): void { + if (this.priorAddressInfoGroup) { + if (this.priorResidenceInfoGroup.length < this.localState.showPriorAddressCount) { + // It appears to only re-validate the form if we remove and add the control back. Calling updateValudAndValidity does nothing. + this.pageForm.removeControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME); + const priorResidenceGroup = this.addressInfoPageService.createResidenceInformationGroup(false); + priorResidenceGroup.patchValue(this.addressInfoPageService.createDefaultPriorResidenceInfo()); + this.priorResidenceInfoGroup.push(priorResidenceGroup); + this.priorAddressInfoGroup.push(this.addressInfoPageService.createAddressInformationGroup()); + this.pageForm.addControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME, this.priorAddressInfoGroup); + } else if (this.priorAddressInfoGroup.length > this.localState.showPriorAddressCount) { + this.priorResidenceInfoGroup.removeAt(this.priorResidenceInfoGroup.length - 1); + this.priorAddressInfoGroup.removeAt(this.priorAddressInfoGroup.length - 1); + } + } + } + + public formatPriorInfo() { + return { + priorAddressInfo: this.priorAddressInfoGroup.value, + priorResidenceInfo: this.priorResidenceInfoGroup.value + }; + } + + public formatIsAddressSameInfo() { + return { + hasSameAddressAsSpouse: this.isAddressSameGroup.value.isAddressSame + }; + } + + public formatPrimaryCompareAddress() { + return { + addressInfo: this.localState.primaryAddressInformation + }; + } + + public formatMailingCompareAddress() { + return { + mailingInfo: this.localState.mailingAddressInformation + }; + } + + public generateFieldsFromGroups() { + const fields = {}; + if (this.pageForm.get(AddressInfoPageService.IS_ADDRESS_SAME_FORM_NAME)) { + Object.assign(fields, this.formatIsAddressSameInfo()); + } + if (this.pageForm.get(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME)) { + Object.assign(fields, this.formatPriorInfo()); + } + + if (this.pageForm.get(AddressInfoPageService.PRIMARY_ADDRESS_VERIFY_FORM_NAME)) { + Object.assign(fields, this.formatPrimaryCompareAddress(), {residenceInfo: this.residenceInfoGroup.value}); + } else if (this.addressInfoGroup) { + Object.assign(fields, + { + addressInfo: this.addressInfoGroup.value, + residenceInfo: this.residenceInfoGroup.value + } + ); + } + + if (this.pageForm.get(AddressInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME)) { + Object.assign(fields, this.formatMailingCompareAddress()); + } else if (this.pageForm.get(AddressInfoPageService.MAIL_FORM_NAME)) { + Object.assign(fields, {mailingInfo: this.mailingInfoGroup.value}); + } + + return fields; + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + const fields = this.generateFieldsFromGroups(); + this.addressInfoPageService.saveAndNavigate(fields, this.route, this.localState.borrowerType); + this.savingData = { + isActive: true + }; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.addressInfoPageService.cancelAndNavigate(this.route); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.spec.ts new file mode 100644 index 0000000..aa698ef --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.spec.ts @@ -0,0 +1,171 @@ +import { AddressInfoPageService } from './address-info-page.service'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { AddressInformation } from '../../../field-groups/address/address-information'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { ResidenceInformation } from '../../../field-groups/address/residence-information'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + + +describe('Address Info Page Service', () => { + + let service: AddressInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(AddressInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create an empty top level form group regardless of type', () => { + spyOn(service, 'createAddressInformationGroup').and.callThrough(); + spyOn(service, 'createIsAddressSameGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createAddressInformationGroup).not.toHaveBeenCalled(); + expect(service.createIsAddressSameGroup).not.toHaveBeenCalled(); + }); + + it('should create an address info group', () => { + const addressGroup = service.createAddressInformationGroup(); + expect(addressGroup).toBeTruthy(); + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + expect(addressGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create a residence info group', () => { + const residenceGroup = service.createResidenceInformationGroup(); + expect(residenceGroup).toBeTruthy(); + CommonGroups.RESIDENCE_INFO.fields.forEach((eachField) => { + expect(residenceGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(applicationWizardService, 'getIndividualGroupValues').and.returnValue(true); + service.getApplicationGroupValues(testGroupName, BorrowerType.PRIMARY_BORROWER); + expect(applicationWizardService.getIndividualGroupValues).toHaveBeenCalledWith(testGroupName, BorrowerType.PRIMARY_BORROWER); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveAddressAndResidenceInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const info = {}; + const resInfo = {}; + const activatedRoute = {}; + const fields = {addressInfo: info, mailingInfo: info, priorAddressInfo: [info], residenceInfo: resInfo, priorResidenceInfo: [resInfo]}; + service.saveAndNavigate(fields, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveAddressAndResidenceInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const info = {}; + const resInfo = {}; + const activatedRoute = {}; + const fields = {addressInfo: info, mailingInfo: info, priorAddressInfo: [info], residenceInfo: resInfo, priorResidenceInfo: [resInfo]}; + service.saveAndNavigate(fields, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should correctly count months at residence', () => { + expect(service.countMonthsAtResidence(null)).toBeNaN(); + expect(Number.isNaN(service.countMonthsAtResidence({ + residenceTimeYears: '', + residenceTimeMonths: '', + residenceStatus: null, + isMailingAddressSame: true + }))).toEqual(true); + + expect(service.countMonthsAtResidence({ + residenceTimeYears: '1', + residenceTimeMonths: '1', + residenceStatus: null, + isMailingAddressSame: true + })).toEqual(13); + }); + + it('should calculate a required address count', () => { + expect(service.getRequiredAddressCount({ + residenceTimeYears: '1', + residenceTimeMonths: '11', + residenceStatus: null, + isMailingAddressSame: true + })).toEqual(1); + + expect(service.getRequiredAddressCount({ + residenceTimeYears: '', + residenceTimeMonths: '', + residenceStatus: null, + isMailingAddressSame: true + })).toEqual(0); + + expect(service.getRequiredAddressCount({ + residenceTimeYears: '2', + residenceTimeMonths: '0', + residenceStatus: null, + isMailingAddressSame: true + })).toEqual(0); + }); + + it('should not return address information for an incomplete return', () => { + const undefinedStateInformation: AddressInformation = { + addressLine1: '', + city: '', + state: CreditApplicationValues.UNDEFINED, + zipcode: '' + }; + spyOn(applicationWizardService, 'getIndividualGroupValues').and.returnValue(undefinedStateInformation); + expect(service.getPrimaryAddressValues(BorrowerType.PRIMARY_BORROWER)).toBeFalsy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.ts new file mode 100644 index 0000000..19fed7a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/address-info-page.service.ts @@ -0,0 +1,266 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder, FormArray, ValidatorFn } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { AddressInformation } from '../../../field-groups/address/address-information'; +import { ResidenceInformation } from '../../../field-groups/address/residence-information'; +import { PageService } from '../../page-service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AddressInfoPageService implements PageService { + + public static readonly ADDRESS_FORM_NAME = 'addressInformation'; + public static readonly MAIL_FORM_NAME = 'mailingAddressInformation'; + public static readonly PRIOR_ADDRESS_FORM_NAME = 'priorAddressInformation'; + public static readonly RESIDENCE_FORM_NAME = 'residenceInformation'; + public static readonly PRIOR_RESIDENCE_FORM_NAME = 'priorResidenceInformation'; + public static readonly IS_ADDRESS_SAME_FORM_NAME = 'isAddressSameInformation'; + public static readonly PRIMARY_ADDRESS_VERIFY_FORM_NAME = 'primaryAddressVerifyInformation'; + public static readonly MAILING_ADDRESS_VERIFY_FORM_NAME = 'mailingAddressVerifyInformation'; + + private CURRENT_ROUTE = 'step-3'; + public readonly REQUIRE_PRIOR_MONTHS_TOTAL = 24; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private formatService: FormatService, + private router: Router, + private formBuilder: FormBuilder) { + } + + public createPageGroup(): FormGroup { + return this.formBuilder.group({}); + } + + public addIsAddressSameGroupIfMissing(group: FormGroup) { + const isAddressSameGroup = this.createIsAddressSameGroup(); + group.addControl(AddressInfoPageService.IS_ADDRESS_SAME_FORM_NAME, isAddressSameGroup); + } + + public addAddressResidenceGroupsIfMissing(group: FormGroup) { + if (group.get(AddressInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + const memberGroup = this.createAddressInformationGroup(); + const residenceGroup = this.createResidenceInformationGroup(); + + group.addControl(AddressInfoPageService.ADDRESS_FORM_NAME, memberGroup); + group.addControl(AddressInfoPageService.RESIDENCE_FORM_NAME, residenceGroup); + group.addControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME, this.formBuilder.array([])); + group.addControl(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME, this.formBuilder.array([])); + residenceGroup.patchValue({isMailingAddressSame: true}); + } + + public removeAddressResidenceGroups(group: FormGroup) { + if (!group.get(AddressInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + group.removeControl(AddressInfoPageService.ADDRESS_FORM_NAME); + group.removeControl(AddressInfoPageService.RESIDENCE_FORM_NAME); + group.removeControl(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME); + group.removeControl(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME); + group.removeControl(AddressInfoPageService.PRIMARY_ADDRESS_VERIFY_FORM_NAME); + } + + public createIsAddressSameGroup(): FormGroup { + const isAddressSameGroup = this.formBuilder.group({}); + CommonGroups.IS_ADDRESS_SAME_INFO.fields.forEach((eachField) => { + isAddressSameGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation)); + }); + return isAddressSameGroup; + } + + public createAddressInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('')); + }, this); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return memberGroup; + } + + public createResidenceInformationGroup(isPrimaryResidenceInfo: boolean = true): FormGroup { // add logic here + const memberGroup = this.formBuilder.group({}); + + CommonGroups.RESIDENCE_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('')); + }, this); + + CommonGroups.RESIDENCE_INFO.fields.forEach((eachField) => { + memberGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + if (isPrimaryResidenceInfo) { + CommonGroups.PRIMARY_RESIDENCE_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(); + validators.push(validatorFn); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + + return memberGroup; + } + + public getMailingAddressValue(borrowerType: BorrowerType): any { + const mailingInfo = this.getApplicationGroupValues(AddressInfoPageService.MAIL_FORM_NAME, borrowerType); + if (mailingInfo.state) { + return mailingInfo; + } + } + + public getIsSameAddressValues(borrowerType: BorrowerType): any { + return this.getApplicationGroupValues('hasSameAddressAsSpouse', borrowerType); + } + + public getPrimaryAddressValues(borrowerType: BorrowerType): AddressInformation { + const primaryAddress = Object.assign({}, this.getApplicationGroupValues(AddressInfoPageService.ADDRESS_FORM_NAME, borrowerType)); + if (primaryAddress.state === CreditApplicationValues.UNDEFINED) { + primaryAddress.state = ''; + } + if (primaryAddress.state) { + return primaryAddress; + } + } + + public getPriorAddressValues(borrowerType: BorrowerType): Array { + return this.getApplicationGroupValues(AddressInfoPageService.PRIOR_ADDRESS_FORM_NAME, borrowerType); + } + + public getPrimaryResidenceValues(borrowerType: BorrowerType): any { + return this.getApplicationGroupValues(AddressInfoPageService.RESIDENCE_FORM_NAME, borrowerType); + } + + public getPriorResidenceValues(borrowerType: BorrowerType): Array { + return this.getApplicationGroupValues(AddressInfoPageService.PRIOR_RESIDENCE_FORM_NAME, borrowerType); + } + + public getApplicationGroupValues(groupName: string, individual: BorrowerType): any { + return this.applicationWizardService.getIndividualGroupValues(groupName, individual); + } + + public createDefaultPriorResidenceInfo(): any { + return { + 'residenceTimeYears': '', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }; + } + + public saveAndNavigate(fields: { + addressInfo?: AddressInformation, + mailingInfo?: AddressInformation, + priorAddressInfo?: Array, + residenceInfo?: ResidenceInformation, + priorResidenceInfo?: Array, + hasSameAddressAsSpouse?: string + }, route: ActivatedRoute, individual: BorrowerType) { + this.CURRENT_ROUTE = this.getRouteName(); + this.creditApplicationService.saveAddressAndResidenceInformation(fields, individual).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public cancelAndNavigate(route: ActivatedRoute) { + const nextRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public requiresMailingAddress(residenceInformation: ResidenceInformation): boolean { + if (!residenceInformation) { + return false; + } + return !residenceInformation.isMailingAddressSame; + } + + public individualIsSigningSpouse(borrowerType: BorrowerType): boolean { + return borrowerType === BorrowerType.PRIMARY_BORROWER_SPOUSE || borrowerType === BorrowerType.CO_BORROWER_SPOUSE; + } + + public individualIsKnownSpouse(borrowerType: BorrowerType): boolean { + if (borrowerType === BorrowerType.PRIMARY_BORROWER_SPOUSE || borrowerType === BorrowerType.CO_BORROWER_SPOUSE) { + return true; + } + return this.applicationWizardService.isCoApplicantSpouse() && borrowerType === BorrowerType.CO_BORROWER; + } + + public getRequiredAddressCount(residenceInformation: ResidenceInformation, priorResidenceInfo: Array = []): number { + let monthCount = this.countMonthsAtResidence(residenceInformation); + if (isNaN(monthCount)) { + return 0; + } + let residenceCount = 0; + let residenceIndex = 0; + while (monthCount < this.REQUIRE_PRIOR_MONTHS_TOTAL && residenceIndex <= priorResidenceInfo.length) { + residenceCount++; + monthCount += this.countMonthsAtResidence(priorResidenceInfo[residenceIndex++]); + } + return residenceCount; + } + + public countMonthsAtResidence(residenceInfo: ResidenceInformation): number { + if (residenceInfo) { + const monthsStr = residenceInfo.residenceTimeMonths; + const yearStr = residenceInfo.residenceTimeYears; // These are temporary while we decide if we'll change the type. + if (monthsStr === '' || yearStr === '') { + return Number.NaN; + } + return parseInt(monthsStr, 10) + (parseInt(yearStr, 10) * 12); + } + return Number.NaN; + } + + public getRouteName(): string { + return this.applicationWizardService.getSimpleRouteName(this.router.url); + } + + public getNextTopicMessage(): string { + this.CURRENT_ROUTE = this.getRouteName(); + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public determineBorrowerType(route: ActivatedRoute): BorrowerType { + if (route.snapshot.data[0]) { + return route.snapshot.data[0].individual; + } + return BorrowerType.PRIMARY_BORROWER; + } + + public getApplicantName(borrowerType: BorrowerType): string { + return this.formatService.formatTitleCase( + this.applicationWizardService.getApplicantName(borrowerType) + ); + } + + public isHelocRateMod(): boolean { + return this.creditApplicationService.isHelocRateMod(this.applicationWizardService.getGroupValues('loanType').title); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.spec.ts new file mode 100644 index 0000000..1b5e61b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.spec.ts @@ -0,0 +1,61 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerAddressInfoPage } from './co-borrower-address-info-page'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('CoBorrower Address Info Page', () => { + + let applicationWizardService: ApplicationWizardService; + let page: CoBorrowerAddressInfoPage; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + page = new CoBorrowerAddressInfoPage( + 'PAGETITLES.COMMON.COAPP_ADDRESS_INFO', + 'co-applicant-residential', + [ + CommonGroups.ADDRESS_INFO + ]); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(true); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(false); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.ts new file mode 100644 index 0000000..e2540fc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-address-info-page.ts @@ -0,0 +1,14 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { PrimaryAddressInfoPage } from './primary-address-info-page'; +import { BorrowerType } from 'src/app/shared/borrower-type'; + +export class CoBorrowerAddressInfoPage extends PrimaryAddressInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.isCoApplicant(); + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.spec.ts new file mode 100644 index 0000000..387f7e0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.spec.ts @@ -0,0 +1,70 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerSpouseAddressInfoPage } from './co-borrower-spouse-address-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('CoBorrower Spouse Address Info Page', () => { + + let applicationWizardService: ApplicationWizardService; + let page: CoBorrowerSpouseAddressInfoPage; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + page = new CoBorrowerSpouseAddressInfoPage( + 'PAGETITLES.COMMON.COAPPSPOUSE_ADDRESS_INFO', + 'co-applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.ts new file mode 100644 index 0000000..80d4bb7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/co-borrower-spouse-address-info-page.ts @@ -0,0 +1,19 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { PrimaryAddressInfoPage } from './primary-address-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; + +export class CoBorrowerSpouseAddressInfoPage extends PrimaryAddressInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.spec.ts new file mode 100644 index 0000000..0e04b51 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.spec.ts @@ -0,0 +1,63 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { PrimaryAddressInfoPage } from './primary-address-info-page'; +import { TranslateModule } from '@ngx-translate/core'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + + +describe('Primary Address Info Page', () => { + + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + let page: PrimaryAddressInfoPage; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + creditApplicationService.setLoanType(LoanTypeSet[0].id); + page = new PrimaryAddressInfoPage('PAGETITLES.COMMON.ADDRESS_INFO', 'step-3', + [ + CommonGroups.ADDRESS_INFO + ]); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should always return primary borrower type', () => { + spyOn(page, 'getBorrowerType').and.callThrough(); + const borrowerType = page.getBorrowerType(); + expect(page.getBorrowerType).toHaveBeenCalled(); + expect(borrowerType).toBe(BorrowerType.PRIMARY_BORROWER); + }); + + it('should correctly determine if page is valid ( default invalid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.ts new file mode 100644 index 0000000..221f33a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-address-info-page.ts @@ -0,0 +1,33 @@ +import { ApplicationPage } from '../../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { AddressInformation } from '../../../../field-groups/address/address-information'; +import { ResidenceInformation } from '../../../../field-groups/address/residence-information'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +export class PrimaryAddressInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const borrowerType = this.getBorrowerType(); + const addressInfo: [AddressInformation | ResidenceInformation] = applicationWizardService + .getIndividualGroupValues('addressInformation', BorrowerType.PRIMARY_BORROWER); + Object.assign(addressInfo, + applicationWizardService.getIndividualGroupValues('residenceInformation', borrowerType) + ); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = addressInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.spec.ts new file mode 100644 index 0000000..2d5cea4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.spec.ts @@ -0,0 +1,69 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimarySpouseAddressInfoPage } from './primary-spouse-address-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('Primary Spouse Address Info Page', () => { + + let applicationWizardService: ApplicationWizardService; + let page: PrimarySpouseAddressInfoPage; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + page = new PrimarySpouseAddressInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_ADDRESS_INFO', + 'applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.ts new file mode 100644 index 0000000..c03c3e5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/address/pages/primary-spouse-address-info-page.ts @@ -0,0 +1,19 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { PrimaryAddressInfoPage } from './primary-address-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; + +export class PrimarySpouseAddressInfoPage extends PrimaryAddressInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.html new file mode 100644 index 0000000..ee13cad --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.html @@ -0,0 +1,20 @@ +
+
+

HEADINGS.DECISION

+
+
+
+ + + + +
+
+ Loan Id: {{loanId}} +
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.scss @@ -0,0 +1 @@ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.spec.ts new file mode 100644 index 0000000..977ae71 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { DecisionPageComponent } from './decision-page.component'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { DecisionPageService } from './decision-page.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { ConfigurationService } from '../../../../shared/configuration/configuration.service'; +import { MockConfigurationService } from '../../../../testing/shared-mocks/configuration.mock.service'; +import { DecisionHeroComponent } from '../../../../layout/decision-hero/decision-hero.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('DecisionPageComponent', () => { + + let component: DecisionPageComponent; + let fixture: ComponentFixture; + let decisionPageService: DecisionPageService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + DecisionPageComponent, + DecisionHeroComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}, + {provide: ConfigurationService, useValue: MockConfigurationService} + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DecisionPageComponent); + component = fixture.componentInstance; + decisionPageService = TestBed.get(DecisionPageService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + spyOn(component, 'updateLocalState').and.returnValue(null); + fixture.detectChanges(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.ts new file mode 100644 index 0000000..e47614e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { DecisionPageService } from './decision-page.service'; +import { ConfigurationService } from '../../../../shared/configuration/configuration.service'; +import { HeroType } from '../../../../layout/decision-hero/decision-hero.component'; +import { environment } from 'src/environments/environment'; +import { Stipulation } from '../../../../shared/decision/decision-status'; + +@Component({ + selector: 'lw-decision-page', + templateUrl: './decision-page.component.html', + styleUrls: ['./decision-page.component.scss'] +}) +export class DecisionPageComponent implements OnInit { + + public localState = { + firstName: '', + phoneNumber: '', + heroType: HeroType.SUCCESS, + showLoanId: false, + decisionKey: 'TEXT' + }; + public stipulations: Array = []; + public loanId: String; + + constructor( + private decisionPageService: DecisionPageService, + private configurationService: ConfigurationService + ) { } + + public ngOnInit() { + this.updateLocalState(); + } + + public updateLocalState() { + const creditApplication = this.decisionPageService.getCreditApplication(); + this.localState.firstName = creditApplication.fullInfoBorrowers[0].personalInformation.firstName; + this.localState.decisionKey = creditApplication.loanType.decisionReviewKey; + this.localState.phoneNumber = this.configurationService.getConfigurationByField('contact')['HelpPhone']; + this.localState.heroType = this.decisionPageService.determineDecisionHeroType(); + if (environment.displayLoanId) { + this.localState.showLoanId = environment.displayLoanId; + this.loanId = creditApplication.loanId; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.spec.ts new file mode 100644 index 0000000..2b76788 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.spec.ts @@ -0,0 +1,93 @@ +import { Router } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { DecisionPageService } from './decision-page.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { CreditApplication } from '../../../../shared/credit-application'; +import { HeroType } from '../../../../layout/decision-hero/decision-hero.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { DecisionResult, DecisionState, DecisionStatus } from '../../../../shared/decision/decision-status'; +import { DecisionService } from '../../../../shared/decision/decision.service'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Decision Page Service', () => { + + let service: DecisionPageService; + let creditApplicationService: CreditApplicationService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let decisionService: DecisionService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(DecisionPageService); + creditApplicationService = TestBed.get(CreditApplicationService); + creditApplicationService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + decisionService = TestBed.get(DecisionService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should determine an decision hero type when there is an error', () => { + expect(service).toBeTruthy(); + const application = new CreditApplication(LoanTypeSet[0]); + spyOn(service, 'getDecision') + .and.returnValue(decisionService.createErrorDecisionResult(application.loanType.shouldDecision, DecisionState.ErrorDecide)); + const type = service.determineDecisionHeroType(); + expect(service.getDecision).toHaveBeenCalled(); + expect(type).toEqual(HeroType.ERROR_DECISION); + }); + + it('should determine an decision hero type when there is no decision', () => { + spyOn(service, 'getDecision').and.returnValue(decisionService.createEmptyDecisionResult(true)); + const type = service.determineDecisionHeroType(); + expect(service.getDecision).toHaveBeenCalled(); + expect(type).toEqual(HeroType.REVIEW); + }); + + it('should determine an decision hero type when there is a ready decision', () => { + expect(service).toBeTruthy(); + const resultReadyEmpty = {result: DecisionResult.DecisionStatus_None, state: DecisionState.Ready, stipulations: []}; + spyOn(service, 'getDecision').and.returnValue(resultReadyEmpty); + const type = service.determineDecisionHeroType(); + expect(service.getDecision).toHaveBeenCalled(); + expect(type).toEqual(HeroType.REVIEW); + }); + + it('should determine an decision hero type when there is a ready decision (2)', () => { + expect(service).toBeTruthy(); + const resultReadySuccess = { + result: DecisionResult.DecisionStatus_Approved, + state: DecisionState.Ready, stipulations: [] + }; + spyOn(service, 'getDecision').and.returnValue(resultReadySuccess); + const type = service.determineDecisionHeroType(); + expect(service.getDecision).toHaveBeenCalled(); + expect(type).toEqual(HeroType.SUCCESS); + + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.ts new file mode 100644 index 0000000..2bc2d48 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { CreditApplication } from '../../../../shared/credit-application'; +import { HeroType } from '../../../../layout/decision-hero/decision-hero.component'; +import { DecisionResult, DecisionState, DecisionStatus } from '../../../../shared/decision/decision-status'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +@Injectable({ + providedIn: 'root' +}) +export class DecisionPageService { + + constructor( + private creditApplicationService: CreditApplicationService) { + } + + public getCreditApplication(): CreditApplication { + return this.creditApplicationService.getFullCreditApplication(); + } + + public getDecision(): DecisionStatus { + return this.getCreditApplication().decision; + } + + public determineDecisionHeroType(): HeroType { + const decision = this.getDecision(); + if (!decision || decision.state === DecisionState.ErrorSubmit) { + return HeroType.ERROR_SUBMIT; + } else if (decision.state === DecisionState.ErrorDecide) { + return HeroType.ERROR_DECISION; + } else if (decision.state === DecisionState.NotStarted || decision.state === DecisionState.Requested) { + return HeroType.REVIEW; + } else if (decision.state === DecisionState.Ready) { + if (decision.result === DecisionResult.DecisionStatus_Approved) { + return HeroType.SUCCESS; + } else if (decision.result === DecisionResult.DecisionStatus_Declined) { + return HeroType.DENIED; + } else { + return HeroType.REVIEW; + } + } + return HeroType.ERROR_SUBMIT; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.spec.ts new file mode 100644 index 0000000..65f7d6c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.spec.ts @@ -0,0 +1,53 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { DecisionPage } from './decision-page'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Decision Page', () => { + + let page: DecisionPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new DecisionPage('PAGETITLES.CC.INTRO', 'decision', []); + + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should be invalid if there is no credit application in state', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(null); + const isValid = page.isValidPage(applicationWizardService, creditApplicationService); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(isValid.length).toBeGreaterThan(0); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.ts new file mode 100644 index 0000000..72149aa --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/decision/decision-page.ts @@ -0,0 +1,15 @@ +import { ApplicationPage } from '../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +export class DecisionPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + if (applicationWizardService.getFullCreditApplication()) { + return []; + } else { + return ['ERROR']; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.html new file mode 100644 index 0000000..be31428 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.html @@ -0,0 +1,49 @@ +
+
+
+

+

+
+
COPY.EMPLOYMENT.{{localState.borrowerType}}
+
+
+ + + + + + + + + + + + + +
+
+ + {{ isCCApplication ? "COPY.EMPLOYMENT.OTHER_INCOME_BSA_CC" : "COPY.EMPLOYMENT.OTHER_INCOME_BSA" }} + +
+
+ + + + + + +
+
Click NEXT if you have no income
+
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.spec.ts new file mode 100644 index 0000000..76b0279 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.spec.ts @@ -0,0 +1,528 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { EmploymentInfoPageComponent } from './employment-info-page.component'; +import { EmploymentInfoPageService } from './employment-info-page.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { EmploymentInformation } from '../../../field-groups/employment/employment-information'; +import { OtherIncomeFlagFormComponent } from '../../../field-groups/employment/financial/other-income-flag-form.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { AlimonyFlagFormComponent } from '../../../field-groups/employment/financial/alimony-flag-form.component'; +import { EmploymentInfoFormComponent } from '../../../field-groups/employment/employment-info-form.component'; +import { AlimonyInfoFormComponent } from '../../../field-groups/employment/alimony-info-form.component'; +import { OtherIncomeInfoFormComponent } from '../../../field-groups/employment/other-income-info-form.component'; +import { EmploymentFlagFormComponent } from '../../../field-groups/employment/financial/employment-flag-form.component'; +import { AlimonyInformation } from '../../../field-groups/employment/alimony-information'; +import { IncomeInformation } from '../../../field-groups/employment/income-information'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { of } from 'rxjs'; +import { configureTestSuite } from 'ng-bullet'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { MockConfigurationService } from 'src/app/testing/shared-mocks/configuration.mock.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + +describe('EmploymentInfoPageComponent', () => { + let component: EmploymentInfoPageComponent; + let fixture: ComponentFixture; + let service: EmploymentInfoPageService; + let wizardService: ApplicationWizardService; + let configService: ConfigurationService; + const otherIncome = {amount: 1000, incomeName: 'tree', frequency: '', files: [], isUploading: ''}; + const otherIncomeTwo = {amount: 2000, incomeName: 'bush', frequency: '', files: [], isUploading: ''}; + const otherIncomeThree = {amount: 3000, incomeName: 'leaf', frequency: '', files: [], isUploading: ''}; + const employmentInformation = { + employmentLengthMonths: 2, + employerName: 'test', + employmentLengthYears: 1, + employmentType: 'PartTime', + jobTitle: 'tester', + salary: 100, + schedule: 'Hourly', + weeklyHours: 25, + workPhoneNumber: '', + isUploading: '', + files: [] + }; + const employmentInformationTwo = { + employmentLengthMonths: 4, + employerName: 'test', + employmentLengthYears: 3, + employmentType: 'PartTime', + jobTitle: 'driver', + salary: 10, + schedule: 'Hourly', + weeklyHours: 5, + workPhoneNumber: '', + isUploading: '', + files: [] + }; + const employmentInformationThree = { + employmentLengthMonths: 3, + employerName: 'test', + employmentLengthYears: 2, + employmentType: 'PartTime', + jobTitle: 'planner', + salary: 15, + schedule: 'Hourly', + weeklyHours: 10, + workPhoneNumber: '', + isUploading: '', + files: [] + }; + const alimony = {childSupportAmount: 1000}; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + EmploymentInfoPageComponent, + EmploymentFlagFormComponent, + EmploymentInfoFormComponent, + OtherIncomeFlagFormComponent, + OtherIncomeInfoFormComponent, + AlimonyFlagFormComponent, + AlimonyInfoFormComponent + ], + providers: [ + {provide: ConfigurationService, useValue: MockConfigurationService}, + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EmploymentInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(EmploymentInfoPageService); + configService = TestBed.get(MockConfigurationService); + wizardService = TestBed.get(ApplicationWizardService); + configService.saveConfiguration(defaultSettings); + spyOn(service, 'getNextTopicMessage').and.callThrough().and.returnValue('foo bar'); + wizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.financialInfoGroup).toBeTruthy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(0); + expect(component.otherIncomeInfoArray.length).toEqual(0); + expect(component.alimonyInfoGroup).toBeFalsy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + const buttonAlimony = fixture.debugElement.query(By.css('#isPayingChildSupport')).nativeElement; + expect(buttonAlimony).toBeTruthy(); + const buttonOtherIncome = fixture.debugElement.query(By.css('#hasOtherIncome0')).nativeElement; + expect(buttonOtherIncome).toBeTruthy(); + const buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + expect(buttonEmploymentIncome).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should have an initially valid page, with only a form group for choosing financials', () => { + fixture.detectChanges(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.financialInfoGroup).toBeTruthy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(0); + expect(component.otherIncomeInfoArray.length).toEqual(0); + expect(component.alimonyInfoGroup).toBeFalsy(); + }); + + it('should show and provide an alimony group when selected.', () => { + fixture.detectChanges(); + const buttonAlimony = fixture.debugElement.query(By.css('#isPayingChildSupport')).nativeElement; + buttonAlimony.click(); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeFalsy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(0); + expect(component.otherIncomeInfoArray.length).toEqual(0); + expect(component.alimonyInfoGroup).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + buttonAlimony.click(); + fixture.detectChanges(); + expect(component.alimonyInfoGroup).toBeFalsy(); + expect(buttonNext.disabled).toBeFalsy(); + }); + + it('should determine an expected other income count', () => { + const financialValues = { + 'hasOtherIncome0': false, + 'hasOtherIncome1': false, + 'hasOtherIncome2': false + }; + expect(component.determineExpectedOtherIncomeCount(undefined)).toEqual(0); + expect(component.determineExpectedOtherIncomeCount(null)).toEqual(0); + expect(component.determineExpectedOtherIncomeCount(financialValues)).toEqual(0); + financialValues.hasOtherIncome0 = true; + expect(component.determineExpectedOtherIncomeCount(financialValues)).toEqual(1); + financialValues.hasOtherIncome0 = false; + financialValues.hasOtherIncome1 = true; + financialValues.hasOtherIncome2 = true; + expect(component.determineExpectedOtherIncomeCount(financialValues)).toEqual(2); + }); + + it('should determine an expected other income removal index', () => { + const financialValues = { + 'hasOtherIncome0': false, + 'hasOtherIncome1': false, + 'hasOtherIncome2': false + }; + + financialValues.hasOtherIncome0 = true; + financialValues.hasOtherIncome1 = false; + financialValues.hasOtherIncome2 = true; + expect(component.determineOtherIncomeRemoveIndex(financialValues)).toEqual(1); + financialValues.hasOtherIncome0 = true; + financialValues.hasOtherIncome1 = true; + financialValues.hasOtherIncome2 = false; + expect(component.determineOtherIncomeRemoveIndex(financialValues)).toEqual(2); + financialValues.hasOtherIncome0 = false; + financialValues.hasOtherIncome1 = true; + financialValues.hasOtherIncome2 = true; + expect(component.determineOtherIncomeRemoveIndex(financialValues)).toEqual(0); + }); + + it('should determine an expected employment income removal index', () => { + const financialValues = { + 'hasEmploymentIncome0': false, + 'hasEmploymentIncome1': false, + 'hasEmploymentIncome2': false + }; + + financialValues.hasEmploymentIncome0 = true; + financialValues.hasEmploymentIncome1 = false; + financialValues.hasEmploymentIncome2 = true; + expect(component.determineEmploymentIncomeRemoveIndex(financialValues)).toEqual(1); + financialValues.hasEmploymentIncome0 = true; + financialValues.hasEmploymentIncome1 = true; + financialValues.hasEmploymentIncome2 = false; + expect(component.determineEmploymentIncomeRemoveIndex(financialValues)).toEqual(2); + financialValues.hasEmploymentIncome0 = false; + financialValues.hasEmploymentIncome1 = true; + financialValues.hasEmploymentIncome2 = true; + expect(component.determineEmploymentIncomeRemoveIndex(financialValues)).toEqual(0); + }); + + it('should show and provide an other income group when selected.', () => { + fixture.detectChanges(); + const buttonOtherIncome0 = fixture.debugElement.query(By.css('#hasOtherIncome0')).nativeElement; + buttonOtherIncome0.click(); + fixture.detectChanges(); + expect(component.financialInfoGroup.value.hasOtherIncome0).toBeTruthy(); + expect(component.determineExpectedOtherIncomeCount(component.financialInfoGroup.value)).toEqual(1); + expect(component.pageForm.valid).toBeFalsy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(0); + expect(component.otherIncomeInfoArray.length).toEqual(1); + expect(component.alimonyInfoGroup).toBeFalsy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + const buttonOtherIncome1 = fixture.debugElement.query(By.css('#hasOtherIncome1')).nativeElement; + buttonOtherIncome1.click(); + fixture.detectChanges(); + expect(component.otherIncomeInfoArray.length).toEqual(2); + const buttonOtherIncome2 = fixture.debugElement.query(By.css('#hasOtherIncome2')).nativeElement; + buttonOtherIncome2.click(); + fixture.detectChanges(); + expect(component.otherIncomeInfoArray.length).toEqual(3); + }); + + it('should hide and remove an other income group when de-selected.', () => { + fixture.detectChanges(); + const buttonOtherIncome = fixture.debugElement.query(By.css('#hasOtherIncome0')).nativeElement; + buttonOtherIncome.click(); + fixture.detectChanges(); + expect(component.otherIncomeInfoArray.length).toEqual(1); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + const buttonOtherIncomeAgain = fixture.debugElement.query(By.css('#hasOtherIncome0')).nativeElement; + buttonOtherIncomeAgain.click(); + fixture.detectChanges(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.otherIncomeInfoArray.length).toEqual(0); + expect(buttonNext.disabled).toBeFalsy(); + }); + + it('should hide and SHIFT the other income groups when de-selected.', () => { + fixture.detectChanges(); + const buttonOtherIncome0 = fixture.debugElement.query(By.css('#hasOtherIncome0')).nativeElement; + buttonOtherIncome0.click(); + fixture.detectChanges(); + let buttonOtherIncome1 = fixture.debugElement.query(By.css('#hasOtherIncome1')).nativeElement; + buttonOtherIncome1.click(); + fixture.detectChanges(); + const buttonOtherIncome2 = fixture.debugElement.query(By.css('#hasOtherIncome2')).nativeElement; + buttonOtherIncome2.click(); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeFalsy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.otherIncomeInfoArray.length).toEqual(3); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + buttonOtherIncome1 = fixture.debugElement.query(By.css('#hasOtherIncome1')).nativeElement; + buttonOtherIncome1.click(); + fixture.detectChanges(); + expect(component.otherIncomeInfoArray.length).toEqual(2); + expect(component.financialInfoGroup.value.hasOtherIncome0).toEqual(true); + expect(component.financialInfoGroup.value.hasOtherIncome1).toEqual(true); + expect(component.financialInfoGroup.value.hasOtherIncome2).toEqual(false); + }); + + it('should show and provide an employment income group when selected.', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + fixture.detectChanges(); + let buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(1); + expect(component.employmentIncomeInfoArray.value[0].employmentLengthMonths).toEqual('0'); // should be defaulted + expect(component.otherIncomeInfoArray).toBeTruthy(); + expect(component.alimonyInfoGroup).toBeFalsy(); + expect(buttonNext.disabled).toBeTruthy(); + buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome1')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(2); + buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome2')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(3); + }); + + it('should hide and remove an employment income group when de-selected.', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + fixture.detectChanges(); + let buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(1); + buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(0); + expect(buttonNext.disabled).toBeFalsy(); + }); + + it('should hide and SHIFT employment income groups when de-selected.', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + fixture.detectChanges(); + const buttonEmploymentIncome0 = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + buttonEmploymentIncome0.click(); + fixture.detectChanges(); + let buttonEmploymentIncome1 = fixture.debugElement.query(By.css('#hasEmploymentIncome1')).nativeElement; + buttonEmploymentIncome1.click(); + fixture.detectChanges(); + const buttonEmploymentIncome2 = fixture.debugElement.query(By.css('#hasEmploymentIncome2')).nativeElement; + buttonEmploymentIncome2.click(); + fixture.detectChanges(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(3); + buttonEmploymentIncome1 = fixture.debugElement.query(By.css('#hasEmploymentIncome1')).nativeElement; + buttonEmploymentIncome1.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(2); + expect(component.financialInfoGroup.value.hasEmploymentIncome0).toEqual(true); + expect(component.financialInfoGroup.value.hasEmploymentIncome1).toEqual(true); + expect(component.financialInfoGroup.value.hasEmploymentIncome2).toEqual(false); + }); + + it('should show and provide a "weekly hours" field when hourly and not full time ', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + fixture.detectChanges(); + const buttonEmploymentIncome = fixture.debugElement.query(By.css('#hasEmploymentIncome0')).nativeElement; + buttonEmploymentIncome.click(); + fixture.detectChanges(); + + const form = component.employmentIncomeInfoArray.at(0); + expect(form.get('weeklyHours')).toBeFalsy(); + form.patchValue({ 'schedule': 'Hourly', 'employmentType': 'FullTime' }); + fixture.detectChanges(); + expect(form.get('weeklyHours')).toBeFalsy(); + form.patchValue({ 'schedule': 'Hourly', 'employmentType': 'PartTime' }); + fixture.detectChanges(); + expect(form.get('weeklyHours')).toBeTruthy(); + form.patchValue({ 'schedule': 'Hourly', 'employmentType': 'FullTime' }); + fixture.detectChanges(); + expect(form.get('weeklyHours')).toBeFalsy(); + }); + + it('should render existing alimony data', () => { + spyOn(service, 'getAlimonyValue').and.returnValue(alimony); + fixture.detectChanges(); + expect(service.getAlimonyValue).toHaveBeenCalledTimes(2); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.alimonyInfoGroup).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.alimonyInfoGroup.value).toEqual(alimony); + }); + + it('should render existing other income data', () => { + spyOn(service, 'getOtherIncomeValue').and.returnValue([otherIncome]); + fixture.detectChanges(); + // expect(service.getOtherIncomeValue).toHaveBeenCalledTimes(2); // initial, fill + expect(component.pageForm.valid).toBeTruthy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.otherIncomeInfoArray.value).toEqual([otherIncome]); + }); + + it('should render up to three existing other income data', () => { + spyOn(service, 'getOtherIncomeValue').and.returnValue([otherIncome, otherIncomeTwo, otherIncomeThree]); + fixture.detectChanges(); + // expect(service.getOtherIncomeValue).toHaveBeenCalledTimes(2); // initial, fill + expect(component.pageForm.valid).toBeTruthy(); + expect(component.otherIncomeInfoArray).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.otherIncomeInfoArray.value).toEqual([otherIncome, otherIncomeTwo, otherIncomeThree]); + }); + + it('should render existing employment income data', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + spyOn(service, 'getEmploymentIncomeValue').and.returnValue([employmentInformation]); + fixture.detectChanges(); + expect(service.getEmploymentIncomeValue).toHaveBeenCalledTimes(3); // initial, fill, weeklyHours + expect(component.pageForm.valid).toBeTruthy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(1); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.employmentIncomeInfoArray.value).toEqual([employmentInformation]); + }); + + it('should render up to three existing employment income data', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + spyOn(service, 'getEmploymentIncomeValue').and + .returnValue([employmentInformation, employmentInformationTwo, employmentInformationThree]); + fixture.detectChanges(); + expect(service.getEmploymentIncomeValue).toHaveBeenCalledTimes(7); // initial, fill, weeklyHours + expect(component.pageForm.valid).toBeTruthy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(3); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.employmentIncomeInfoArray.value) + .toEqual([employmentInformation, employmentInformationTwo, employmentInformationThree]); + }); + + it('should remove the correct data when shifting employer data after de-select', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + spyOn(service, 'getEmploymentIncomeValue').and + .returnValue([employmentInformation, employmentInformationTwo, employmentInformationThree]); + fixture.detectChanges(); + expect(service.getEmploymentIncomeValue).toHaveBeenCalledTimes(7); // initial, fill, weeklyHours + expect(component.pageForm.valid).toBeTruthy(); + expect(component.employmentIncomeInfoArray).toBeTruthy(); + expect(component.employmentIncomeInfoArray.length).toEqual(3); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.employmentIncomeInfoArray.value) + .toEqual([employmentInformation, employmentInformationTwo, employmentInformationThree]); + const buttonEmploymentIncome1 = fixture.debugElement.query(By.css('#hasEmploymentIncome1')).nativeElement; + buttonEmploymentIncome1.click(); + fixture.detectChanges(); + expect(component.employmentIncomeInfoArray.length).toEqual(2); + expect(component.financialInfoGroup.value.hasEmploymentIncome0).toEqual(true); + expect(component.financialInfoGroup.value.hasEmploymentIncome1).toEqual(true); + expect(component.financialInfoGroup.value.hasEmploymentIncome2).toEqual(false); + expect(component.employmentIncomeInfoArray.value) + .toEqual([employmentInformation, employmentInformationThree]); + }); + + it('should accurately submit all data on NEXT', () => { + spyOn(service, 'getAlimonyValue').and.returnValue(alimony); + spyOn(service, 'getOtherIncomeValue').and.returnValue([otherIncome]); + spyOn(service, 'getEmploymentIncomeValue').and.returnValue([employmentInformation]); + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + spyOn(service, 'saveAndNavigate').and.returnValue(true); + buttonNext.click(); + + expect(service.saveAndNavigate).toHaveBeenCalledWith( + alimony, + [employmentInformation], + [otherIncome], + component.route, 'PRIMARY_BORROWER'); + }); + + it('should accurately submit an empty form on next', () => { + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + spyOn(service, 'saveAndNavigate').and.returnValue(of(true)); + buttonNext.click(); + expect(service.saveAndNavigate).toHaveBeenCalledWith( + null, + [], + [], component.route, 'PRIMARY_BORROWER'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.ts new file mode 100644 index 0000000..e5fb79c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.component.ts @@ -0,0 +1,338 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormGroup, FormArray } from '@angular/forms'; +import { EmploymentInfoPageService } from './employment-info-page.service'; +import { ActivatedRoute } from '@angular/router'; +import { Subject, Subscription } from 'rxjs'; +import { BorrowerType } from 'src/app/shared/borrower-type'; + +@Component({ + selector: 'lw-employment-info-page', + templateUrl: './employment-info-page.component.html' +}) +export class EmploymentInfoPageComponent implements OnInit, OnDestroy { + private valueSubscription: Subscription; + private statusSubscription: Subscription; + public savingData = null; + + public localState = { + previousDisabled: false, + nextDisabled: false, + borrowerType: BorrowerType.PRIMARY_BORROWER, + borrowerName: 'Applicant', + nextTopic: '', + isSigningSpouse: false, + MAX_INCOME_EMPLOY: 2, + MAX_INCOME_OTHER: 2 + }; + + public pageForm: FormGroup; + public financialInfoGroup: FormGroup; + public employmentIncomeInfoArray: FormArray; + public otherIncomeInfoArray: FormArray; + public alimonyInfoGroup: FormGroup; + public isCCApplication: boolean; + public loanId: string; + public borrowerType: string; + public showProofOfIncomeUploadForm: boolean; + + public removeEmploymentIncomeFiles: Array>; + public removeOtherIncomeFiles: Array>; + + private readonly FORM_STATE_INVALID = 'INVALID'; + + constructor( + private employmentInfoPageService: EmploymentInfoPageService, + public route: ActivatedRoute + ) { + this.removeEmploymentIncomeFiles = [new Subject(),new Subject(),new Subject()]; + this.removeOtherIncomeFiles = [new Subject(),new Subject(),new Subject()]; + } + + public ngOnInit() { + this.initializeFormGroups(); + this.updateLocalState(this.pageForm.value); + this.loanId = this.employmentInfoPageService.getLoanId(); + this.borrowerType = this.formatBorrowerType(this.employmentInfoPageService.determineBorrowerType(this.route)); + this.showProofOfIncomeUploadForm = this.employmentInfoPageService.showProofOfIncomeUploadForm(); + this.isCCApplication = this.employmentInfoPageService.isCCApplication(); + } + + public initializeFormGroups() { + this.pageForm = this.employmentInfoPageService.createPageGroup(); + this.statusSubscription = this.pageForm.statusChanges.subscribe( + newStatus => { + this.localState.nextDisabled = newStatus === this.FORM_STATE_INVALID; + } + ); + this.employmentIncomeInfoArray = this.pageForm.get(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME) as FormArray; + this.otherIncomeInfoArray = this.pageForm.get(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME) as FormArray; + this.financialInfoGroup = this.pageForm.get(this.employmentInfoPageService.FINANCIAL_FORM_NAME) as FormGroup; + const borrowerType = this.employmentInfoPageService.determineBorrowerType(this.route); + this.financialInfoGroup.patchValue(this.employmentInfoPageService.getFinancialSelection(borrowerType)); + } + + public updateLocalState(nextValues) { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + + // Because it is used in the assignment. + const borrowerType = this.employmentInfoPageService.determineBorrowerType(this.route); + this.localState = { + previousDisabled: false, + nextDisabled: this.localState.nextDisabled, + borrowerType: borrowerType, + borrowerName: this.employmentInfoPageService.getApplicantName(borrowerType), + nextTopic: this.employmentInfoPageService.getNextTopicMessage(), + isSigningSpouse: this.employmentInfoPageService.individualIsSigningSpouse(borrowerType), + MAX_INCOME_EMPLOY: this.localState.MAX_INCOME_EMPLOY, + MAX_INCOME_OTHER: this.localState.MAX_INCOME_OTHER + }; + + this.updateEmploymentIncomeGroups(this.financialInfoGroup.value); + this.updateConditionallyShownEmploymentFields(); + this.updateOtherIncomeGroups(this.financialInfoGroup.value); + this.updateAlimonyGroup(this.financialInfoGroup.value.isPayingChildSupport); + this.valueSubscription = this.pageForm.valueChanges.subscribe(async value => { + this.updateLocalState(value); + }); + } + + public updateConditionallyShownEmploymentFields(): void { + for (let index = 0; index < this.employmentIncomeInfoArray.length; index++) { + const group = this.employmentIncomeInfoArray.at(index) as FormGroup; + const value = group.value; + if (value.schedule === 'Hourly' && value.employmentType !== 'FullTime') { + this.employmentInfoPageService.addWeeklyHoursIfNeeded( + group, + index, + this.localState.borrowerType + ); + } else { + this.employmentInfoPageService.removeWeeklyHoursIfPresent(group); + } + } + } + + public updateAlimonyGroup(isPayingChildSupport: boolean): void { + if (!this.alimonyInfoGroup && isPayingChildSupport) { + this.pageForm.addControl( + this.employmentInfoPageService.ALIMONY_FORM_NAME, + this.employmentInfoPageService.createAlimonyInformationGroup() + ); + this.alimonyInfoGroup = this.pageForm.get(this.employmentInfoPageService.ALIMONY_FORM_NAME) as FormGroup; + this.alimonyInfoGroup.patchValue(this.employmentInfoPageService.getAlimonyValue(this.localState.borrowerType)); + } else if (this.alimonyInfoGroup && !isPayingChildSupport) { + this.pageForm.removeControl( + this.employmentInfoPageService.ALIMONY_FORM_NAME + ); + this.alimonyInfoGroup = undefined; + } + } + + // This is "any" because of the dubious "hasEmploymentIncome0", etc. + public updateOtherIncomeGroups(otherValue: any) { + let showingCount = this.otherIncomeInfoArray.length; + const expectedCount = this.determineExpectedOtherIncomeCount(otherValue); + if (showingCount > expectedCount) { + this.removeAdditionalOtherIncome(this.determineOtherIncomeRemoveIndex(otherValue)); + // may have unchecked a non-last income, so shift the check boxes. + this.shiftFinancialOtherIncome(otherValue); + showingCount = this.otherIncomeInfoArray.length; + // We patch this back because we shifted the booleans. + this.financialInfoGroup.patchValue(otherValue); + } + while (showingCount < expectedCount) { + this.addAdditionalOtherIncome(); + showingCount = this.otherIncomeInfoArray.length; + } + } + + // This is "any" because of the dubious "hasEmploymentIncome0", etc. + public updateEmploymentIncomeGroups(otherValue: any) { + let showingCount = this.employmentIncomeInfoArray.length; + const expectedCount = this.determineExpectedEmploymentIncomeCount(otherValue); + if (showingCount > expectedCount) { + this.removeAdditionalEmploymentIncome(this.determineEmploymentIncomeRemoveIndex(otherValue)); + // may have unchecked a non-last income, so shift the check boxes. + this.shiftFinancialEmploymentIncome(otherValue); + showingCount = this.employmentIncomeInfoArray.length; + // We patch this back because we shifted the booleans. + this.financialInfoGroup.patchValue(otherValue); + } + while (showingCount < expectedCount) { + this.addAdditionalEmploymentIncome(); + showingCount = this.employmentIncomeInfoArray.length; + } + } + + public removeAdditionalOtherIncome(index: number) { + // Check if the upload form is shown and page contains the form + // if there is no income form, remove the form + // This is because the upload component emits when all files are removed + if(this.showProofOfIncomeUploadForm && this.pageForm.contains(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME)) { + this.removeOtherIncomeFiles[index].next(); + } else if(!this.showProofOfIncomeUploadForm && this.pageForm.contains(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME)) { + this.removedOtherIncomeFiles(true, index); + } + } + + public removedOtherIncomeFiles(removedAllFiles, index) { + if(removedAllFiles) { + this.pageForm.removeControl(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME); + this.otherIncomeInfoArray.removeAt(index); + this.pageForm.addControl(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME, this.otherIncomeInfoArray); + } + } + + public removeAdditionalEmploymentIncome(index: number) { + // Check if the upload form is shown and page contains the form + // if there is no income form, remove the form + // This is because the upload component emits when all files are removed + if(this.showProofOfIncomeUploadForm && this.pageForm.contains(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME)) { + this.removeEmploymentIncomeFiles[index].next(); + } else if(!this.showProofOfIncomeUploadForm && this.pageForm.contains(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME)) { + this.removedEmploymentIncomeFiles(true, index); + } + } + + public removedEmploymentIncomeFiles(removedAllFiles, index) { + if(removedAllFiles) { + this.pageForm.removeControl(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME); + this.employmentIncomeInfoArray.removeAt(index); + this.pageForm.addControl(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME, this.employmentIncomeInfoArray); + } + } + + public addAdditionalOtherIncome() { + const data = this.employmentInfoPageService.getOtherIncomeValue(this.localState.borrowerType); + const group = this.employmentInfoPageService.createOtherIncomeInformationGroup(); + this.pageForm.removeControl(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME); + this.otherIncomeInfoArray.push(group); + if (data && data.length >= this.otherIncomeInfoArray.length) { + this.otherIncomeInfoArray.at(this.otherIncomeInfoArray.length - 1).patchValue(data[this.otherIncomeInfoArray.length - 1]); + } + this.pageForm.addControl(this.employmentInfoPageService.OTHER_INCOME_FORM_NAME, this.otherIncomeInfoArray); + } + + public addAdditionalEmploymentIncome() { + const data = this.employmentInfoPageService.getEmploymentIncomeValue(this.localState.borrowerType); + const group = this.employmentInfoPageService.createEmploymentInformationGroup(); + this.pageForm.removeControl(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME); + this.employmentIncomeInfoArray.push(group); + if (data && data.length >= this.employmentIncomeInfoArray.length) { + this.employmentIncomeInfoArray.at(this.employmentIncomeInfoArray.length - 1) + .patchValue(data[this.employmentIncomeInfoArray.length - 1]); + } + this.pageForm.addControl(this.employmentInfoPageService.EMPLOYMENT_FORM_NAME, this.employmentIncomeInfoArray); + } + + public shiftFinancialOtherIncome(otherValues: any): void { + if (otherValues) { + // Has to be less than so we don't go off the end with the +1 + for (let index = 0; index < this.localState.MAX_INCOME_OTHER; index++) { + if (!otherValues['hasOtherIncome' + index] && otherValues['hasOtherIncome' + (index + 1)]) { + otherValues['hasOtherIncome' + index] = true; + otherValues['hasOtherIncome' + (index + 1)] = false; + } + } + } + } + + public shiftFinancialEmploymentIncome(employmentValues: any): void { + if (employmentValues) { + // Has to be less than so we don't go off the end with the +1 + for (let index = 0; index < this.localState.MAX_INCOME_EMPLOY; index++) { + if (!employmentValues['hasEmploymentIncome' + index] && employmentValues['hasEmploymentIncome' + (index + 1)]) { + employmentValues['hasEmploymentIncome' + index] = true; + employmentValues['hasEmploymentIncome' + (index + 1)] = false; + } + } + } + } + + public determineExpectedOtherIncomeCount(otherValues: any): number { + let count = 0; + if (!otherValues) { + return count; + } + for (let index = 0; index <= this.localState.MAX_INCOME_OTHER; index++) { + if (otherValues['hasOtherIncome' + index]) { + count++; + } + } + return count; + } + + public determineExpectedEmploymentIncomeCount(employmentValues: any): number { + let count = 0; + if (!employmentValues) { + return count; + } + for (let index = 0; index <= this.localState.MAX_INCOME_EMPLOY; index++) { + if (employmentValues['hasEmploymentIncome' + index]) { + count++; + } + } + return count; + } + + public determineOtherIncomeRemoveIndex(otherValues: any): number { + if (otherValues) { + for (let index = 0; index <= this.localState.MAX_INCOME_OTHER; index++) { + if (!otherValues['hasOtherIncome' + index]) { + return index; + } + } + } + throw new Error('Other values expected'); + } + + public determineEmploymentIncomeRemoveIndex(employmentValues: any): number { + if (employmentValues) { + for (let index = 0; index <= this.localState.MAX_INCOME_EMPLOY; index++) { + if (!employmentValues['hasEmploymentIncome' + index]) { + return index; + } + } + } + throw new Error('Other values expected'); + } + + public ngOnDestroy() { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + + const alimonyGroup = this.alimonyInfoGroup ? this.alimonyInfoGroup.value : null; + const employmentIncomes = this.employmentIncomeInfoArray.value; + const otherIncomes = this.otherIncomeInfoArray.value; + this.employmentInfoPageService + .saveAndNavigate(alimonyGroup, employmentIncomes, otherIncomes, this.route, this.localState.borrowerType); + this.savingData = { + isActive: true + }; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.employmentInfoPageService.cancelAndNavigate(this.route); + } + + // Returns a formatted string so that ACTion has additional details for file type + public formatBorrowerType(borrowerType: BorrowerType) { + let formattedBorrowerType = borrowerType.toString(); + formattedBorrowerType = formattedBorrowerType.split("_").join(" ").toLowerCase(); + formattedBorrowerType = formattedBorrowerType.charAt(0).toUpperCase() + formattedBorrowerType.slice(1) + return formattedBorrowerType; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.spec.ts new file mode 100644 index 0000000..9ecdebe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.spec.ts @@ -0,0 +1,127 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ReactiveFormsModule, FormGroup } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { EmploymentInfoPageService } from './employment-info-page.service'; +import { EmploymentInformation } from '../../../field-groups/employment/employment-information'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { MockConfigurationService } from 'src/app/testing/shared-mocks/configuration.mock.service'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { IncomeInformation } from 'src/app/loan-wizard/field-groups/employment/income-information'; +import { AlimonyInformation } from '../../../field-groups/employment/alimony-information'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + + +describe('Employment Info Page Service', () => { + + let service: EmploymentInfoPageService; + let applicationWizardService: ApplicationWizardService; + let configService: ConfigurationService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: ConfigurationService, useValue: MockConfigurationService}, + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(EmploymentInfoPageService); + configService = TestBed.get(MockConfigurationService); + configService.saveConfiguration(defaultSettings); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createFinancialInformationGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createFinancialInformationGroup).toHaveBeenCalled(); + }); + + it('should create an employment info group', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + const employmentGroup = service.createEmploymentInformationGroup(); + expect(employmentGroup).toBeTruthy(); + CommonGroups.EMPLOYMENT_INFO.fields.forEach((eachField) => { + expect(employmentGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(applicationWizardService, 'getIndividualGroupValues').and.returnValue(true); + service.getApplicationGroupValues(testGroupName, BorrowerType.PRIMARY_BORROWER); + expect(applicationWizardService.getIndividualGroupValues).toHaveBeenCalledWith(testGroupName, BorrowerType.PRIMARY_BORROWER); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveAllEmploymentInformation').and.callThrough().and.returnValue(of(true)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const info = {}; + const employmentInfo = Array(); + const otherIncomeInfo = Array(); + const activatedRoute = {}; + service.saveAndNavigate(info, employmentInfo, otherIncomeInfo, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveAllEmploymentInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const alimonyInfo = {}; + const employmentInfo = Array(); + const otherIncomeInfo = Array(); + const activatedRoute = {}; + service.saveAndNavigate(alimonyInfo, employmentInfo, otherIncomeInfo, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + spyOn(service, 'getRouteName').and.returnValue('step-4'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should default the number of months to "0"', () => { + spyOn(service, 'getIncomeLimits').and.returnValue(defaultSettings.incomeLimits); + const employmentGroup: FormGroup = service.createEmploymentInformationGroup(); + expect(employmentGroup.value.employmentLengthMonths).toBe('0'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.ts new file mode 100644 index 0000000..f98b2c7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/employment-info-page.service.ts @@ -0,0 +1,248 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder, FormArray } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { EmploymentInformation } from '../../../field-groups/employment/employment-information'; +import { PageService } from '../../page-service'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { AlimonyInformation } from '../../../field-groups/employment/alimony-information'; +import { IncomeInformation } from '../../../field-groups/employment/income-information'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { IncomeLimits } from 'src/app/shared/configuration/ui-settings'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; +import { LoanType } from 'src/app/shared/loan-type'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; + +@Injectable({ + providedIn: 'root' +}) +export class EmploymentInfoPageService implements PageService { + + public readonly EMPLOYMENT_FORM_NAME = 'employmentInformation'; + public readonly FINANCIAL_FORM_NAME = 'financialInformation'; + public readonly OTHER_INCOME_FORM_NAME = 'otherIncomeInformation'; + public readonly ALIMONY_FORM_NAME = 'alimonyInformation'; + public readonly INCOME_LIMIT_CONFIG_FIELD = 'incomeLimits'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private configurationService: ConfigurationService, + private formatService: FormatService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const financialGroup = this.createFinancialInformationGroup(); + const otherIncomeArray = this.createEmptyFormArray(); + const employmentIncomeArray = this.createEmptyFormArray(); + group.addControl(this.FINANCIAL_FORM_NAME, financialGroup); + group.addControl(this.OTHER_INCOME_FORM_NAME, otherIncomeArray); + group.addControl(this.EMPLOYMENT_FORM_NAME, employmentIncomeArray); + return group; + } + + public createEmptyFormArray(): FormArray { + return this.formBuilder.array([]); + } + + public createFinancialInformationGroup(): FormGroup { + const financialGroup = this.formBuilder.group({}); + + CommonGroups.FINANCIAL_INFORMATION.fields.forEach((eachField) => { + financialGroup.addControl(eachField.field, this.formBuilder.control('')); + financialGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return financialGroup; + } + + public createEmploymentInformationGroup(): FormGroup { + const employmentGroup = this.formBuilder.group({}); + + CommonGroups.EMPLOYMENT_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(this.getIncomeLimits())); + }, this); + } + employmentGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + employmentGroup.patchValue(this.generateEmploymentInformationDefaults()); + return employmentGroup; + } + + public generateEmploymentInformationDefaults() { + return { + employmentLengthMonths: '0' + }; + } + + public createEmploymentIncomeInformationGroup(): FormGroup { + const employmentIncomeGroup = this.formBuilder.group({}); + CommonGroups.EMPLOYMENT_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(this.getIncomeLimits())); + }, this); + } + employmentIncomeGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + employmentIncomeGroup.patchValue(this.generateEmploymentInformationDefaults()); + return employmentIncomeGroup; + } + + public createOtherIncomeInformationGroup(): FormGroup { + const otherIncomeGroup = this.formBuilder.group({}); + + CommonGroups.INCOME_INFO.fields.forEach((eachField) => { + otherIncomeGroup.addControl(eachField.field, this.formBuilder.control('')); + otherIncomeGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + return otherIncomeGroup; + } + + public createAlimonyInformationGroup(): FormGroup { + const alimonyGroup = this.formBuilder.group({}); + + CommonGroups.ALIMONY_INFO.fields.forEach((eachField) => { + alimonyGroup.addControl(eachField.field, this.formBuilder.control('')); + alimonyGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return alimonyGroup; + } + + public getFinancialSelection(borrowerType: BorrowerType): any { + const isPayingChildSupport = this.getAlimonyValue(borrowerType); + const hasOtherIncomes: Array = this.getOtherIncomeValue(borrowerType); + const hasEmploymentIncomes: Array = this.getEmploymentIncomeValue(borrowerType); + return { + isPayingChildSupport: isPayingChildSupport && isPayingChildSupport.childSupportAmount, + hasOtherIncome0: hasOtherIncomes.length > 0, + hasOtherIncome1: hasOtherIncomes.length > 1, + hasOtherIncome2: hasOtherIncomes.length > 2, + hasEmploymentIncome0: hasEmploymentIncomes.length > 0, + hasEmploymentIncome1: hasEmploymentIncomes.length > 1, + hasEmploymentIncome2: hasEmploymentIncomes.length > 2, + }; + } + + public getAlimonyValue(borrowerType: BorrowerType): AlimonyInformation { + return this.applicationWizardService.getIndividualGroupValues(this.ALIMONY_FORM_NAME, borrowerType); + } + + public getOtherIncomeValue(borrowerType: BorrowerType): Array { + const otherIncomeData = >this.applicationWizardService + .getIndividualGroupValues(this.OTHER_INCOME_FORM_NAME, borrowerType); + + if (otherIncomeData.length) { + return otherIncomeData; + } + return []; + } + + public getEmploymentIncomeValue(borrowerType: BorrowerType): Array { + const employmentIncomeData = >this.applicationWizardService + .getIndividualGroupValues(this.EMPLOYMENT_FORM_NAME, borrowerType); + + if (employmentIncomeData.length) { + return employmentIncomeData; + } + return []; + } + + public getApplicationGroupValues(groupName: string, individual: BorrowerType): any { + return this.applicationWizardService.getIndividualGroupValues(groupName, individual); + } + + public saveAndNavigate(alimonyInfo: AlimonyInformation, employmentIncomeInfo: Array, + otherIncomeInfo: Array, route: ActivatedRoute, individual: BorrowerType) { + this.creditApplicationService.saveAllEmploymentInformation(employmentIncomeInfo, otherIncomeInfo, alimonyInfo, individual) + .subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.getRouteName()); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.getRouteName()); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public individualIsSigningSpouse(borrowerType: BorrowerType): boolean { + return borrowerType === BorrowerType.PRIMARY_BORROWER_SPOUSE || borrowerType === BorrowerType.CO_BORROWER_SPOUSE; + } + + public cancelAndNavigate(route: ActivatedRoute) { + const nextRoute = this.applicationWizardService.getPreviousRoute(this.getRouteName()); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public getIncomeLimits(): IncomeLimits { + return this.configurationService.getConfigurationByField(this.INCOME_LIMIT_CONFIG_FIELD); + } + + public getRouteName(): string { + return this.applicationWizardService.getSimpleRouteName(this.router.url); + } + + public getNextTopicMessage() { + return this.applicationWizardService.getNextTopicMessage(this.getRouteName()); + } + + public getApplicantName(borrowerType: BorrowerType): string { + return this.formatService.formatTitleCase( + this.applicationWizardService.getApplicantName(borrowerType) + ); + } + + public determineBorrowerType(route: ActivatedRoute): BorrowerType { + if (route.snapshot.data[0]) { + return route.snapshot.data[0].individual; + } + return BorrowerType.PRIMARY_BORROWER; + } + + public addWeeklyHoursIfNeeded(group: FormGroup, index: number, borrowerType: BorrowerType): void { + if (group.get('weeklyHours')) { return; } + const existingValue = this.getEmploymentIncomeValue(borrowerType)[index] || {}; + CommonGroups.WEEKLY_HOURS_INFO.fields.forEach((eachField) => { + group.addControl(eachField.field, this.formBuilder.control(existingValue[eachField.field], eachField.validation)); + }, this); + } + + public removeWeeklyHoursIfPresent(group: FormGroup): void { + if (!group.get('weeklyHours')) { return; } + CommonGroups.WEEKLY_HOURS_INFO.fields.forEach((eachField) => { + group.removeControl(eachField.field); + }, this); + } + + public getLoanId() : string { + return this.applicationWizardService.getFieldValue("loanId"); + } + + public showProofOfIncomeUploadForm(): boolean { + return this.applicationWizardService.showProofOfIncomeUploadForm(); + } + + public isCCApplication(): boolean { + let loanType = this.applicationWizardService.getFieldValue('loanType') as LoanType; + return loanType.category === LoanCategory.CreditCard; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.spec.ts new file mode 100644 index 0000000..e25ccfd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.spec.ts @@ -0,0 +1,60 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerEmploymentInfoPage } from './co-borrower-employment-info-page'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CoBorrower Employment Info Page', () => { + + let page: CoBorrowerEmploymentInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new CoBorrowerEmploymentInfoPage('PAGETITLES.COMMON.COAPP_EMPLOYMENT_INFO', + 'co-applicant-financial', + [ + CommonGroups.FINANCIAL_INFORMATION + ]); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(true); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(false); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.ts new file mode 100644 index 0000000..3f2b21d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-employment-info-page.ts @@ -0,0 +1,15 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { PrimaryEmploymentInfoPage } from './primary-employment-info-page'; + +export class CoBorrowerEmploymentInfoPage extends PrimaryEmploymentInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.isCoApplicant(); + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.spec.ts new file mode 100644 index 0000000..d11bb97 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.spec.ts @@ -0,0 +1,68 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerSpouseEmploymentInfoPage } from './co-borrower-spouse-employment-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CoBorrower Spouse Employment Info Page', () => { + + let page: CoBorrowerSpouseEmploymentInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new CoBorrowerSpouseEmploymentInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_EMPLOYMENT_INFO', + 'co-applicant-spouse-financial', + [ + CommonGroups.FINANCIAL_INFORMATION + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.ts new file mode 100644 index 0000000..26685f4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/co-borrower-spouse-employment-info-page.ts @@ -0,0 +1,18 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { PrimaryEmploymentInfoPage } from './primary-employment-info-page'; +import { SpouseType } from 'src/app/shared/credit-application'; + +export class CoBorrowerSpouseEmploymentInfoPage extends PrimaryEmploymentInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.spec.ts new file mode 100644 index 0000000..22975e9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.spec.ts @@ -0,0 +1,62 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed, async } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { PrimaryEmploymentInfoPage } from './primary-employment-info-page'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +describe('Primary Employment Info Page', () => { + + let page: PrimaryEmploymentInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new PrimaryEmploymentInfoPage('PAGETITLES.COMMON.EMPLOYMENT_INFO', 'step-4', + [ + CommonGroups.EMPLOYMENT_INFO + ]); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should always return primary borrower type', () => { + spyOn(page, 'getBorrowerType').and.callThrough(); + const borrowerType = page.getBorrowerType(); + expect(page.getBorrowerType).toHaveBeenCalled(); + expect(borrowerType).toBe(BorrowerType.PRIMARY_BORROWER); + }); + + it('should correctly determine if page is valid ( default valid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors).toEqual([]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.ts new file mode 100644 index 0000000..f592a9a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-employment-info-page.ts @@ -0,0 +1,54 @@ +import { ApplicationPage } from '../../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; +import {LoanTitle} from '../../../../../shared/loan-type/loan-title'; + +export class PrimaryEmploymentInfoPage extends ApplicationPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.getGroupValues('loanType').title !== LoanTitle.HELOC_RateMod; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + let currentGroup; + const errors = []; + const borrowerType = this.getBorrowerType(); + + const financialGroups = { + employmentInformation: applicationWizardService + .getIndividualGroupValues('employmentInformation', borrowerType), + otherIncomeInformation: applicationWizardService + .getIndividualGroupValues('otherIncomeInformation', borrowerType), + alimonyInformation: applicationWizardService + .getIndividualGroupValues('alimonyInformation', borrowerType) + }; + this.fieldgroup.forEach((pageFieldGroup) => { + currentGroup = financialGroups[pageFieldGroup.group]; + if (currentGroup && Object.keys(currentGroup).length) { + if (currentGroup.length) { + currentGroup.forEach((group) => { + pageFieldGroup.fields.forEach((pageField) => { + value = group[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }); + }); + } else { + pageFieldGroup.fields.forEach((pageField) => { + value = currentGroup[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + } + } + }, this); + return errors; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.spec.ts new file mode 100644 index 0000000..2412a45 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.spec.ts @@ -0,0 +1,68 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimarySpouseEmploymentInfoPage } from './primary-spouse-employment-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Primary Spouse Employment Info Page', () => { + + let page: PrimarySpouseEmploymentInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new PrimarySpouseEmploymentInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_EMPLOYMENT_INFO', + 'applicant-spouse-financial', + [ + CommonGroups.FINANCIAL_INFORMATION + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.ts new file mode 100644 index 0000000..60fdf73 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/employment/pages/primary-spouse-employment-info-page.ts @@ -0,0 +1,19 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { PrimaryEmploymentInfoPage } from './primary-employment-info-page'; +import { SpouseType } from 'src/app/shared/credit-application'; + +export class PrimarySpouseEmploymentInfoPage extends PrimaryEmploymentInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.html new file mode 100644 index 0000000..e313c77 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.html @@ -0,0 +1,37 @@ +
+
+
+

HEADINGS.HMDA.HMDA

+
+
COPY.HMDA
+
+

HMDA.HMDA_INTRO

+
+
+ +
+
+
+
+

HEADINGS.HMDA.ETHNICITY.{{localState.borrowerType}}

+ +
+
+

HEADINGS.HMDA.RACE.{{localState.borrowerType}}

+ +
+
+

HEADINGS.HMDA.SEX.{{localState.borrowerType}}

+ +
+
+
+
+
+
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.spec.ts new file mode 100644 index 0000000..6b893a9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.spec.ts @@ -0,0 +1,141 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { HmdaInfoPageComponent } from './hmda-info-page.component'; +import { HmdaInfoPageService } from './hmda-info-page.service'; +import { EthnicityInfoFormComponent } from '../../../field-groups/hmda/ethnicity-info-form.component'; +import { GenderInfoFormComponent} from '../../../field-groups/hmda/gender-info-form.component'; +import { RaceInfoFormComponent } from '../../../field-groups/hmda/race-info-form.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { apiTranslatorPreTranslate } from '../../../../testing/shared-mocks/api-translator-pre-post'; +import { ErrorDisplayComponent } from '../../../controls/error-display/error-display.component'; +import { configureTestSuite } from 'ng-bullet'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('HmdaInfoPageComponent', () => { + let component: HmdaInfoPageComponent; + let fixture: ComponentFixture; + let service: HmdaInfoPageService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + HmdaInfoPageComponent, + EthnicityInfoFormComponent, + GenderInfoFormComponent, + RaceInfoFormComponent, + ErrorDisplayComponent + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[5].id); + fixture = TestBed.createComponent(HmdaInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(HmdaInfoPageService); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.ethnicityInfoGroup).toBeTruthy(); + expect(component.raceInfoGroup).toBeTruthy(); + expect(component.genderInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should render existing HMDA data', () => { + const raceInfo = apiTranslatorPreTranslate.fullInfoBorrowers[0].raceInformation; + const genderInfo = apiTranslatorPreTranslate.fullInfoBorrowers[0].genderInformation; + const ethnicityInfo = apiTranslatorPreTranslate.fullInfoBorrowers[0].ethnicityInformation; + spyOn(service, 'getRaceFormValues').and.returnValue(raceInfo); + spyOn(service, 'getGenderFormValues').and.returnValue(genderInfo); + spyOn(service, 'getEthnicityFormValues').and.returnValue(ethnicityInfo); + fixture.detectChanges(); + component.raceInfoGroup.updateValueAndValidity(); + component.genderInfoGroup.updateValueAndValidity(); + component.ethnicityInfoGroup.updateValueAndValidity(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.raceInfoGroup.valid).toBeTruthy(); + expect(component.genderInfoGroup.valid).toBeTruthy(); + expect(component.ethnicityInfoGroup.valid).toBeTruthy(); + fixture.detectChanges(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.raceInfoGroup.value).toEqual(raceInfo); + expect(component.genderInfoGroup.value).toEqual(genderInfo); + expect(component.ethnicityInfoGroup.value).toEqual(ethnicityInfo); + }); + + it('should have an initially invalid page', () => { + fixture.detectChanges(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeTruthy(); + expect(component.pageForm.valid).toBeFalsy(); + }); + + it('should make page invalid if both male and female are selected', () => { + fixture.detectChanges(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + component.genderInfoGroup.patchValue({male: true, female: true}); + fixture.detectChanges(); + expect(buttonNext.disabled).toBeTruthy(); + expect(component.pageForm.valid).toBeFalsy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.ts new file mode 100644 index 0000000..68978a5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.component.ts @@ -0,0 +1,82 @@ +import { Component, OnDestroy, OnInit, AfterContentChecked, AfterContentInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { HmdaInfoPageService } from './hmda-info-page.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'lw-hmda-info-page', + templateUrl: './hmda-info-page.component.html' +}) +export class HmdaInfoPageComponent implements OnInit, OnDestroy, AfterContentInit { + public raceInfoGroup: FormGroup; + public ethnicityInfoGroup: FormGroup; + public genderInfoGroup: FormGroup; + private readonly FORM_STATE_INVALID = 'INVALID'; + private statusSubscription: Subscription; + + public pageForm: FormGroup; + public readonly applicantGroupName = 'applicant'; + + public savingData = null; + + public localState = { + previousDisabled: false, + nextDisabled: true, + nextTopic: '', + borrowerType: BorrowerType.PRIMARY_BORROWER, + borrowerName: '', + }; + + constructor( + private hmdaInfoPageService: HmdaInfoPageService, + private route: ActivatedRoute) { } + + public ngOnInit() { + this.localState.borrowerType = this.hmdaInfoPageService.determineBorrowerType(this.route); + this.localState.borrowerName = this.hmdaInfoPageService.getApplicantName(this.localState.borrowerType); + this.pageForm = this.hmdaInfoPageService.createPageGroup(); + this.raceInfoGroup = this.pageForm.get(HmdaInfoPageService.RACE_FORM_NAME); + this.genderInfoGroup = this.pageForm.get(HmdaInfoPageService.GENDER_FORM_NAME); + this.ethnicityInfoGroup = this.pageForm.get(HmdaInfoPageService.ETHNICITY_FORM_NAME); + this.patchInfoGroupValues(); + this.localState.nextDisabled = !this.pageForm.valid; + this.localState.nextTopic = this.hmdaInfoPageService.getNextTopicMessage(); + } + + public ngAfterContentInit() { + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + } + + public ngOnDestroy() { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public patchInfoGroupValues() { + this.raceInfoGroup.patchValue( + this.hmdaInfoPageService.getRaceFormValues(this.localState.borrowerType)); + this.genderInfoGroup.patchValue( + this.hmdaInfoPageService.getGenderFormValues(this.localState.borrowerType)); + this.ethnicityInfoGroup.patchValue( + this.hmdaInfoPageService.getEthnicityFormValues(this.localState.borrowerType)); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + + this.hmdaInfoPageService.saveAndNavigate(this.pageForm.value, this.route, this.localState.borrowerType); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.hmdaInfoPageService.cancelAndNavigate(this.route); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.spec.ts new file mode 100644 index 0000000..acc53b9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.spec.ts @@ -0,0 +1,111 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { HmdaInfoPageService } from './hmda-info-page.service'; +import { EthnicityInformation } from '../../../field-groups/hmda/ethnicity-information'; +import { RaceInformation } from '../../../field-groups/hmda/race-information'; +import { GenderInformation } from '../../../field-groups/hmda/gender-information'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + + +describe('HMDA Info Page Service', () => { + + let service: HmdaInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(HmdaInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[4].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + spyOn(service, 'getNextTopicMessage').and.callThrough().and.returnValue('foo bar'); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level page group', () => { + spyOn(service, 'createPageGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createPageGroup).toHaveBeenCalled(); + }); + + it('should create an ethnicity info group', () => { + const ethnicityGroup = service.createEthnicityInfoGroup(); + expect(ethnicityGroup).toBeTruthy(); + CommonGroups.ETHNICITY_INFO.fields.forEach((eachField) => { + expect(ethnicityGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create a race info group', () => { + const raceGroup = service.createRaceInfoGroup(); + expect(raceGroup).toBeTruthy(); + CommonGroups.RACE_INFO.fields.forEach((eachField) => { + expect(raceGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create a gender info group', () => { + const genderGroup = service.createGenderInfoGroup(); + expect(genderGroup).toBeTruthy(); + CommonGroups.GENDER_INFO.fields.forEach((eachField) => { + expect(genderGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveHMDAInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const ethnicityInfo = {}; + const raceInfo = {}; + const genderInfo = {}; + const infoArray = [ethnicityInfo, genderInfo, raceInfo]; + const activatedRoute = {}; + service.saveAndNavigate(infoArray, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getPreviousRoute').and.returnValue('foo bar'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.ts new file mode 100644 index 0000000..005a18f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/hmda-info-page.service.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { PageService } from '../../page-service'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { WizardValidators } from '../../../field-groups/wizard-validators'; +import { HMDA_ETHNICITIES } from '../../../../shared/api-models/api-hmda-enums'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; + +@Injectable({ + providedIn: 'root' +}) +export class HmdaInfoPageService implements PageService { + + public static readonly ETHNICITY_FORM_NAME = 'ethnicityInformation'; + public static readonly GENDER_FORM_NAME = 'genderInformation'; + public static readonly RACE_FORM_NAME = 'raceInformation'; + + private CURRENT_ROUTE = 'eq-hmda'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private formatService: FormatService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + group.addControl(HmdaInfoPageService.ETHNICITY_FORM_NAME, this.createEthnicityInfoGroup()); + group.addControl(HmdaInfoPageService.RACE_FORM_NAME, this.createRaceInfoGroup()); + group.addControl(HmdaInfoPageService.GENDER_FORM_NAME, this.createGenderInfoGroup()); + return group; + } + + public createEthnicityInfoGroup(): FormGroup { + const groupValidator = CommonGroups.ETHNICITY_INFO.groupValidator(); + const memberGroup = this.formBuilder.group({}, {validator: groupValidator}); + CommonGroups.ETHNICITY_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('', null)); + }, this); + return memberGroup; + } + + public createGenderInfoGroup(): FormGroup { + const groupValidator = CommonGroups.ETHNICITY_INFO.groupValidator(); + const memberGroup = this.formBuilder.group({}, {validator: groupValidator}); + CommonGroups.GENDER_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + // This code is left here due to a suggestion on ticket #1116. + // if (eachField.validatorFn) { + // eachField.validatorFn.forEach((validationFunction) => { + // validators.push(validationFunction()); + // }, this); + // } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + return memberGroup; + } + + public createRaceInfoGroup(): FormGroup { + const groupValidator = CommonGroups.ETHNICITY_INFO.groupValidator(); + const memberGroup = this.formBuilder.group({}, {validator: groupValidator} ); + CommonGroups.RACE_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('', null)); + }, this); + return memberGroup; + } + + public saveAndNavigate(pageData, route: ActivatedRoute, individual: BorrowerType) { + this.CURRENT_ROUTE = this.getRouteName(); + this.creditApplicationService.saveHMDAInformation(pageData, individual).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public cancelAndNavigate(route: ActivatedRoute) { + this.CURRENT_ROUTE = this.getRouteName(); + const nextRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public getApplicationGroupValues(groupName: string, borrowerType: BorrowerType): any { + return this.applicationWizardService.getIndividualGroupValues(groupName, borrowerType); + } + + public getRouteName(): string { + return this.applicationWizardService.getSimpleRouteName(this.router.url); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public getApplicantName(borrowerType: BorrowerType): string { + return this.formatService.formatTitleCase( + this.applicationWizardService.getApplicantName(borrowerType) + ); + } + + public getRaceFormValues(borrowerType: BorrowerType): any { + return this.getApplicationGroupValues(HmdaInfoPageService.RACE_FORM_NAME, borrowerType); + } + + public getGenderFormValues(borrowerType: BorrowerType): any { + return this.getApplicationGroupValues(HmdaInfoPageService.GENDER_FORM_NAME, borrowerType); + } + + public getEthnicityFormValues(borrowerType: BorrowerType): any { + return this.getApplicationGroupValues(HmdaInfoPageService.ETHNICITY_FORM_NAME, borrowerType); + } + + public determineBorrowerType(route: ActivatedRoute): BorrowerType { + if (route.snapshot.data[0]) { + return route.snapshot.data[0].individual; + } else { + return BorrowerType.PRIMARY_BORROWER; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.spec.ts new file mode 100644 index 0000000..d4d8ae9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.spec.ts @@ -0,0 +1,74 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed, async } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerHmdaInfoPage } from './co-borrower-hmda-info-page'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +describe('CoBorrower HMDA Info Page', () => { + + let page: CoBorrowerHmdaInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }).compileComponents(); + + page = new CoBorrowerHmdaInfoPage('PAGETITLES.EQUITY.COAPP_HMDA', 'co-applicant-eq-hmda', + [ + CommonGroups.RACE_INFO, + CommonGroups.ETHNICITY_INFO, + CommonGroups.GENDER_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + + + })); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getFieldValue').and.callThrough().and.returnValue({ category: 'Equity' }); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getFieldValue').and.callThrough().and.returnValue({ category: 'Credit Card' }); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + + it('should correctly determine if page is valid ( default valid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors.length).toBeLessThanOrEqual(0); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.ts new file mode 100644 index 0000000..9280f86 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/co-borrower-hmda-info-page.ts @@ -0,0 +1,22 @@ +import { ApplicationPage } from '../../../../application-page'; +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +export class CoBorrowerHmdaInfoPage extends ApplicationPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const isCoapplicant = applicationWizardService.isCoApplicant(); + return loanCategory === LoanCategory.Equity && isCoapplicant; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + if (applicationWizardService.getFullCreditApplication()) { + return []; + } else { + return ['ERROR']; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.spec.ts new file mode 100644 index 0000000..47d5a58 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.spec.ts @@ -0,0 +1,71 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed, async } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimaryHmdaInfoPage } from './primary-hmda-info-page'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +describe('Primary HMDA Info Page', () => { + + let page: PrimaryHmdaInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }).compileComponents(); + + page = new PrimaryHmdaInfoPage('PAGETITLES.EQUITY.HMDA', 'eq-hmda', + [ + CommonGroups.RACE_INFO, + CommonGroups.ETHNICITY_INFO, + CommonGroups.GENDER_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + + + })); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'getFieldValue').and.callThrough().and.returnValue({ category: 'Equity' }); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'getFieldValue').and.callThrough().and.returnValue({ category: 'Credit Card' }); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.getFieldValue).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + + it('should correctly determine if page is valid ( default valid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors.length).toBeLessThanOrEqual(0); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.ts new file mode 100644 index 0000000..fd1e297 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/hmda/pages/primary-hmda-info-page.ts @@ -0,0 +1,22 @@ +import { ApplicationPage } from '../../../../application-page'; +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; +import {LoanTitle} from '../../../../../shared/loan-type/loan-title'; + +export class PrimaryHmdaInfoPage extends ApplicationPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.getFieldValue('loanType').category === LoanCategory.Equity + && applicationWizardService.getGroupValues('loanType').title !== LoanTitle.HELOC_RateMod; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + if (applicationWizardService.getFullCreditApplication()) { + return []; + } else { + return ['ERROR']; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.html new file mode 100644 index 0000000..f949586 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.html @@ -0,0 +1,36 @@ +
+
+
+
+

HEADINGS.GET_STARTED

+
+
+
+
+
+ + + + + + +
+
+ +
+ +
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.scss new file mode 100644 index 0000000..307957b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.scss @@ -0,0 +1,7 @@ +.lets-get-started-text { + text-align: left; +} + +.cc-image { + border-radius: 6%; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.spec.ts new file mode 100644 index 0000000..2e7acb8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.spec.ts @@ -0,0 +1,240 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule, FormGroup } from '@angular/forms'; +import { Component, Input, NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { MemberInfoPageService } from './member-info-page.service'; +import { MemberInfoPageComponent } from './member-info-page.component'; +import { PatriotActComponent } from '../../../../shared/patriot-act/patriot-act.component'; +import { WindowService } from '../../../../shared/window.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { CreditApplicationService } from 'src/app/shared/credit-application.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; +import { MembershipInformation } from 'src/app/loan-wizard/field-groups/member/membership-information'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; + +@Component({ + selector: 'lw-member-info-form', + template: '

employment info form stub

' +}) +class MockMemberInfoFormComponent { + + @Input() + public group: FormGroup; + + constructor() { } + +} + +describe('MemberInfoPageComponent', () => { + let component: MemberInfoPageComponent; + let fixture: ComponentFixture; + let service: MemberInfoPageService; + let windowService: WindowService; + let creditApplicationService: CreditApplicationService; + let configurationService: ConfigurationService; + let applicationWizardService: ApplicationWizardService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + MemberInfoPageComponent, + MockMemberInfoFormComponent, + PatriotActComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MemberInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(MemberInfoPageService); + creditApplicationService = TestBed.get(CreditApplicationService); + windowService = TestBed.get(WindowService); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + applicationWizardService = TestBed.get(ApplicationWizardService); + const nonCreditLoan = LoanTypeSet.find(loan => loan.title === LoanTitle.HomeEquityFixed) || LoanTypeSet[0]; + applicationWizardService.setLoanType(nonCreditLoan.id); + }); + + it('should create', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.memberInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + component.localState.nextDisabled = false; + spyOn(service, 'saveAndNavigate'); + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + }); + + it('should use the window service to print.', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + spyOn(windowService, 'showWindowForId').and.returnValue(undefined); + component.commandShowPage(); + expect(windowService.showWindowForId).toHaveBeenCalled(); + }); + + it('should require add/remove controls to memberInfoGroup if "yes" is chosen for co applicant', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + expect(component.memberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + component.updateLocalState({ + applicantType: CreditApplicationValues.CHOICE_YES + }); + fixture.detectChanges(); + expect(component.memberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeTruthy(); + component.updateLocalState({ + applicantType: CreditApplicationValues.CHOICE_NO + }); + fixture.detectChanges(); + expect(component.memberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + }); + + it('should require addtl controls if "yes" is chosen for co applicant', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + expect(component.memberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + const data = { + applicantType: CreditApplicationValues.CHOICE_NO, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + agreementRequirement: true + }; + component.updateLocalState(data); + component.memberInfoGroup.patchValue(data); + fixture.detectChanges(); + expect(component.memberInfoGroup.valid).toEqual(true); + data.applicantType = CreditApplicationValues.CHOICE_YES; + component.updateLocalState(data); + component.memberInfoGroup.patchValue(data); + component.memberInfoGroup.controls.coApplicantLivesInMaritalPropertyState.updateValueAndValidity(); + fixture.detectChanges(); + expect(component.memberInfoGroup.valid).toEqual(false); + const expandedData = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_YES, + agreementRequirement: true + }; + component.updateLocalState(expandedData); + component.memberInfoGroup.patchValue(expandedData); + component.memberInfoGroup.controls.coApplicantLivesInMaritalPropertyState.updateValueAndValidity(); + fixture.detectChanges(); + expect(component.memberInfoGroup.valid).toEqual(true); + }); + + it('should command the modal to open when applicant fails to confirm joint credit', async(done) => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + component.openModal.subscribe(nextValues => { + expect(nextValues).toBe(true); + done(); + }); + const data = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_NO + }; + component.updateLocalState(data); + fixture.detectChanges(); + }); + + it('should NOT command the modal to open when applicant confirms joint credit', async(done) => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + component.openModal.subscribe(nextValues => { + expect(nextValues).toBe(false); + done(); + }); + const data = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_YES + }; + component.updateLocalState(data); + fixture.detectChanges(); + }); + + it('should render existing member data (maximum)', () => { + const memberData: MembershipInformation = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_NO, + agreementRequirement: true + }; + spyOn(service, 'getRouteName').and.returnValue('step-1'); + spyOn(service, 'getCommonMemberInfoGroupValues').and.returnValue(memberData); + fixture.detectChanges(); + const controlsLength = Object.keys(component.memberInfoGroup.controls).length; + expect(controlsLength).toEqual(6); + expect(component.pageForm.valid).toBe(true); + }); + + it('should render existing member data (minimum)', () => { + const memberData: MembershipInformation = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_NO, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_NO, + agreementRequirement: true + }; + spyOn(service, 'getRouteName').and.returnValue('step-1'); + spyOn(service, 'getCommonMemberInfoGroupValues').and.returnValue(memberData); + fixture.detectChanges(); + const controlsLength = Object.keys(component.memberInfoGroup.controls).length; + expect(controlsLength).toEqual(4); + expect(component.pageForm.valid).toBe(true); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.ts new file mode 100644 index 0000000..3e659e7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.component.ts @@ -0,0 +1,158 @@ +import { Component, OnInit, AfterContentChecked, OnDestroy, AfterViewInit, AfterContentInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MemberInfoPageService } from './member-info-page.service'; +import { ActivatedRoute } from '@angular/router'; +import { WindowService } from '../../../../shared/window.service'; +import { CreditApplication } from '../../../../shared/credit-application'; +import { MembershipInformation } from '../../../field-groups/member/membership-information'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { Subscription, Subject } from 'rxjs'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; + +@Component({ + selector: 'lw-member-info-page', + templateUrl: './member-info-page.component.html', + styleUrls: ['./member-info-page.component.scss'] +}) +export class MemberInfoPageComponent implements OnInit, AfterContentInit, OnDestroy { + + private valueSubscription: Subscription; + private statusSubscription: Subscription; + + public openModal: Subject; + + public pageForm: FormGroup; + public memberInfoGroup: FormGroup; + private readonly FORM_STATE_INVALID = 'INVALID'; + + public amountRequestedName = 'Credit Limit'; + public loanTypeMessageTranslated = ''; + + public askLoanPurposeSignature = false; + + public savingData = null; + + public showCCRewardsCashBackDisclaimer = null; + + public localState = { + nextDisabled: true, + typeMessage: '', + imagePath: null, + nextTopic: '', + hasCoApplicant: false, + isModalOpen: false, + beforeYouBeginMessage: null, + requireAgreement: false + }; + + constructor( + private memberInfoPageService: MemberInfoPageService, + private route: ActivatedRoute, + private applicationWizardService: ApplicationWizardService, + private windowService: WindowService) { } + + public ngOnInit() { + this.openModal = new Subject(); + this.pageForm = this.memberInfoPageService.createPageGroup(); + this.patchMemberInfoGroup(); + this.showRewardsCashBackDisclaimer(); + this.memberInfoPageService.getLoanTypeMessage().subscribe((newMessage) => { + this.localState.typeMessage = newMessage; + }); + } + + public ngAfterContentInit() { + this.pageForm.addControl(MemberInfoPageService.MEMBER_FORM_NAME, this.memberInfoGroup); + this.localState.nextDisabled = this.pageForm.status === this.FORM_STATE_INVALID; + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + } + + public showRewardsCashBackDisclaimer() { + const loanType = this.applicationWizardService.getGroupValues('loanType').apiCode; + if (loanType == LoanCode.CC_CashBack || loanType == LoanCode.CC_Rewards) { + this.showCCRewardsCashBackDisclaimer = {isActive: true}; + } + + return this.showCCRewardsCashBackDisclaimer; + } + + public ngOnDestroy() { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public updateLocalState(pageValues): void { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + + this.updateCoApplicantFields(pageValues); + this.localState.hasCoApplicant = pageValues.applicantType === CreditApplicationValues.CHOICE_YES; + this.localState.imagePath = this.memberInfoPageService.getImagePath(); + this.localState.nextTopic = this.memberInfoPageService.getNextTopicMessage(); + this.localState.beforeYouBeginMessage = this.memberInfoPageService.getBeforeYouBeginMessage(); + this.localState.requireAgreement = this.memberInfoPageService.requireAgreement(); + + if (pageValues.applicantType === CreditApplicationValues.CHOICE_YES && + pageValues.confirmJointCredit === CreditApplicationValues.CHOICE_NO && + !this.localState.isModalOpen) { + this.localState.isModalOpen = true; + this.openModal.next(true); + } else { + this.localState.isModalOpen = false; + this.openModal.next(false); + } + + this.valueSubscription = this.memberInfoGroup.valueChanges.subscribe((pageValue) => { + this.updateLocalState(pageValue); + }); + } + + public updateCoApplicantFields(pageValues) { + if (pageValues.applicantType === CreditApplicationValues.CHOICE_YES && !this.memberInfoGroup.get('confirmJointCredit')) { + this.memberInfoPageService.addCoApplicantFieldsToMemberGroup(this.memberInfoGroup); + this.memberInfoGroup.updateValueAndValidity(); + } else if (pageValues.applicantType === CreditApplicationValues.CHOICE_NO && this.memberInfoGroup.get('confirmJointCredit')) { + this.memberInfoPageService.removeCoApplicantFieldsFromMemberGroup(this.memberInfoGroup); + this.memberInfoGroup.updateValueAndValidity(); + } + } + + public patchMemberInfoGroup() { + this.memberInfoGroup = this.memberInfoPageService.createMemberInformationGroup(); + const memberInfo: MembershipInformation = this.memberInfoPageService.getCommonMemberInfoGroupValues(); + + if (memberInfo.applicantType !== CreditApplicationValues.CHOICE_YES) { + this.memberInfoPageService.removeCoApplicantFieldsFromMemberGroup(this.memberInfoGroup); + + } + + this.memberInfoGroup.patchValue(memberInfo); + this.updateLocalState(memberInfo); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + this.memberInfoPageService.saveAndNavigate(this.memberInfoGroup.value, this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + + } + + public commandShowPage(): void { + this.windowService.showWindowForId('electronic-disclosures-agreement-container'); + } + + public changeApplicantTypeToIndividual(): void { + this.localState.hasCoApplicant = false; + this.memberInfoPageService.removeCoApplicantFieldsFromMemberGroup(this.memberInfoGroup); + this.memberInfoGroup.patchValue({applicantType: CreditApplicationValues.CHOICE_NO}); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.spec.ts new file mode 100644 index 0000000..7632f9d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.spec.ts @@ -0,0 +1,102 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { MemberInfoPageService } from './member-info-page.service'; +import { MembershipInformation } from '../../../field-groups/member/membership-information'; +import { TranslateModule } from '@ngx-translate/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; + + +describe('Member Info Page Service', () => { + + let service: MemberInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + let router: Router; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(MemberInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + const nonCreditLoan = LoanTypeSet.find(loan => loan.title === LoanTitle.HomeEquityFixed) || LoanTypeSet[0]; + applicationWizardService.setLoanType(nonCreditLoan.id); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create a member info group', () => { + const memberGroup = service.createMemberInformationGroup(); + expect(memberGroup).toBeTruthy(); + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + expect(memberGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(creditApplicationService, 'getMemberInformation').and.returnValue(true); + service.getCommonMemberInfoGroupValues(); + expect(creditApplicationService.getMemberInformation).toHaveBeenCalled(); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveMemberInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = {}; + const activatedRoute = { routeConfig: { + path: 'step-1' + }}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveMemberInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = {}; + const activatedRoute = {routeConfig: { + path: 'step-1' + }}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.ts new file mode 100644 index 0000000..a7c0b58 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.service.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, FormGroup, ValidatorFn } from '@angular/forms'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MembershipInformation } from '../../../field-groups/member/membership-information'; +import { PageService } from '../../page-service'; +import { ConfigurationService } from '../../../../shared/configuration/configuration.service'; +import { Observable } from 'rxjs'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; +import { MemberInfoFormService } from 'src/app/loan-wizard/field-groups/member/member-info-form.service'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberInfoPageService implements PageService { + + public static readonly MEMBER_FORM_NAME = 'memberInformation'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private configurationService: ConfigurationService, + private router: Router, + private formBuilder: FormBuilder) { + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + return group; + } + + public createMemberInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + const loanType = this.applicationWizardService.getGroupValues('loanType'); + const loanCategory = this.applicationWizardService.getFieldValue('loanType').category; + + if(loanCategory !== LoanCategory.CreditCard){ + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(loanCategory, loanType, this.configurationService)); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + }else{ + CommonGroups.MEMBER_INFO_CC.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + this.addCoApplicantFieldsToMemberGroup(memberGroup); + this.addAgreementRequirement(memberGroup); + return memberGroup; + } + + public addCoApplicantFieldsToMemberGroup(formGroup: FormGroup) { + CommonGroups.CO_APP_MEMBER_INFO.fields.forEach((eachField) => { + formGroup.addControl(eachField.field, this.formBuilder.control('', [])); + }, this); + } + + public addAgreementRequirement(formGroup: FormGroup) { + if (this.requireAgreement()) { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + CommonGroups.MEMBER_AGREEMENT_REQUIREMENT.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(loanType, this.configurationService)); + }, this); + } + formGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + } + + public removeCoApplicantFieldsFromMemberGroup(formGroup: FormGroup) { + formGroup.removeControl('confirmJointCredit'); + formGroup.removeControl('coApplicantLivesInMaritalPropertyState'); + } + + public getCommonMemberInfoGroupValues(): MembershipInformation { + return this.creditApplicationService.getMemberInformation(); + } + + public saveAndNavigate(memberInfoData: MembershipInformation, route: ActivatedRoute) { + + this.creditApplicationService.saveMemberInformation(memberInfoData).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(route.routeConfig.path); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(route.routeConfig.path); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public getImagePath(): string { + return this.applicationWizardService.getImagePath(); + } + + public getLoanTypeMessage(): Observable { + return this.applicationWizardService.getLoanMessage(); + } + + public getRouteName(): string { + return this.applicationWizardService.getSimpleRouteName(this.router.url); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.getRouteName()); + } + + public getBeforeYouBeginMessage(): object { + return this.applicationWizardService.getBeforeYouBeginMessage(); + } + + public requireAgreement(): boolean { + var loanTitle = this.applicationWizardService.getGroupValues('loanType').title; + return loanTitle && ![LoanTitle.Vehicle, LoanTitle.VehiclePreApproval, LoanTitle.HELOC_RateMod].includes(loanTitle); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.ts new file mode 100644 index 0000000..7f17505 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/member/member-info-page.ts @@ -0,0 +1,22 @@ +import { ApplicationPage } from '../../../application-page'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { MembershipInformation } from '../../../field-groups/member/membership-information'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +export class MemberInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const memberInfo: MembershipInformation = creditApplicationService.getMemberInformation(); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = memberInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.spec.ts new file mode 100644 index 0000000..d071a04 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.spec.ts @@ -0,0 +1,61 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerPersonalInfoPage } from './co-borrower-personal-info-page'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CoBorrower Personal Info Page', () => { + + let page: CoBorrowerPersonalInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new CoBorrowerPersonalInfoPage('PAGETITLES.COMMON.COAPP_PERSONAL_INFO', + 'co-applicant-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(true); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isCoApplicant').and.callThrough().and.returnValue(false); + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isCoApplicant).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.ts new file mode 100644 index 0000000..b0f30a8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-personal-info-page.ts @@ -0,0 +1,15 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { PrimaryPersonalInfoPage } from './primary-personal-info-page'; + +export class CoBorrowerPersonalInfoPage extends PrimaryPersonalInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.isCoApplicant(); + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.spec.ts new file mode 100644 index 0000000..30b90c2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.spec.ts @@ -0,0 +1,68 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed, async } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerSpousePersonalInfoPage } from './co-borrower-spouse-personal-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CoBorrower Spouse Personal Info Page', () => { + + let page: CoBorrowerSpousePersonalInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new CoBorrowerSpousePersonalInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_PERSONAL_INFO', + 'co-applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.ts new file mode 100644 index 0000000..bf3938e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/co-borrower-spouse-personal-info-page.ts @@ -0,0 +1,18 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { PrimaryPersonalInfoPage } from './primary-personal-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; + +export class CoBorrowerSpousePersonalInfoPage extends PrimaryPersonalInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.spec.ts new file mode 100644 index 0000000..f950b14 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.spec.ts @@ -0,0 +1,63 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimaryPersonalInfoPage } from './primary-personal-info-page'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +describe('Primary Personal Info Page', () => { + + let page: PrimaryPersonalInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new PrimaryPersonalInfoPage('PAGETITLES.COMMON.PERSONAL_INFO', 'step-2', + [ + CommonGroups.PERSONAL_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should always return primary borrower type', () => { + spyOn(page, 'getBorrowerType').and.callThrough(); + const borrowerType = page.getBorrowerType(); + expect(page.getBorrowerType).toHaveBeenCalled(); + expect(borrowerType).toBe(BorrowerType.PRIMARY_BORROWER); + }); + + it('should correctly determine if page is valid ( default invalid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.ts new file mode 100644 index 0000000..f6cf025 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-personal-info-page.ts @@ -0,0 +1,30 @@ +import { ApplicationPage } from '../../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { PersonalInformation } from '../../../../field-groups/personal/personal-information'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +export class PrimaryPersonalInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const borrowerType = this.getBorrowerType(); + + const personalInfo: PersonalInformation = applicationWizardService + .getIndividualGroupValues('personalInformation', borrowerType); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = personalInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.spec.ts new file mode 100644 index 0000000..a751e96 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.spec.ts @@ -0,0 +1,68 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimarySpousePersonalInfoPage } from './primary-spouse-personal-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Primary Spouse Personal Info Page', () => { + + let page: PrimarySpousePersonalInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new PrimarySpousePersonalInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_PERSONAL_INFO', + 'applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.ts new file mode 100644 index 0000000..2d1aaba --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/pages/primary-spouse-personal-info-page.ts @@ -0,0 +1,19 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { PrimaryPersonalInfoPage } from './primary-personal-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; + +export class PrimarySpousePersonalInfoPage extends PrimaryPersonalInfoPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (applicationWizardService.isApplicantNonSpouse(this.getBorrowerType())) { + return applicationWizardService.getSpouseType(this.getBorrowerType()) === SpouseType.FullInfoSpouse; + } + return false; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.html new file mode 100644 index 0000000..6f8b500 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.html @@ -0,0 +1,29 @@ +
+
+
+

HEADINGS.PERSONAL.{{localState.borrowerType}}

+
+
COPY.PERSONAL.{{localState.borrowerType}}
+
+
+ + + + +
+ + +
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.spec.ts new file mode 100644 index 0000000..7708db6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.spec.ts @@ -0,0 +1,206 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { PersonalInfoPageComponent } from './personal-info-page.component'; +import { PersonalInfoPageService } from './personal-info-page.service'; +import { PersonalInfoFormComponent } from '../../../field-groups/personal/personal-info-form.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { PersonalInformation } from '../../../field-groups/personal/personal-information'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationValues } from 'src/app/shared/credit-application-values'; +import { PhoneTypeEnum } from 'src/app/shared/api-models/api-enums'; +import { ApiSuffix } from 'src/app/shared/api-models/api-suffix'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + + +describe('PersonalInfoPageComponent', () => { + let component: PersonalInfoPageComponent; + let fixture: ComponentFixture; + let service: PersonalInfoPageService; + let applicationWizardService: ApplicationWizardService; + const personalInformationNoMarriage: PersonalInformation = { + firstName: 'John', + middleInitial: 'J', + lastName: 'Doe', + dateOfBirth: '12/02/1945', + suffix: ApiSuffix.IV, + socialSecurityNumber: '999-99-0000', + primaryPhoneNumberType: PhoneTypeEnum.Home, + primaryPhoneNumber: '414-888-9999', + email: 'noone@nowhere.org', + secondaryPhoneNumber: '', + secondaryPhoneNumberType: null, + primaryApplicantEmail: null, + coApplicantEmail: null, + files: null, + isUploading: null + }; + + const personalInformationMarriage: PersonalInformation = { + firstName: 'John', + middleInitial: 'J', + lastName: 'Doe', + dateOfBirth: '12/02/1945', + suffix: null, + socialSecurityNumber: '999-99-0000', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + primaryPhoneNumber: '414-888-9999', + email: 'noone@nowhere.org', + secondaryPhoneNumber: '', + secondaryPhoneNumberType: null, + maritalStatus: CreditApplicationValues.CHOICE_YES, + primaryApplicantEmail: null, + coApplicantEmail: null, + files: null, + isUploading: null + }; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + PersonalInfoPageComponent, + PersonalInfoFormComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PersonalInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(PersonalInfoPageService); + spyOn(service, 'getNextTopicMessage').and.callThrough().and.returnValue('foo bar'); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + spyOn(service, 'getRouteName').and.returnValue('step-2'); + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.personalInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + spyOn(service, 'getRouteName').and.returnValue('step-2'); + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should require secondary phone type if phone is present.', () => { + spyOn(service, 'getRouteName').and.returnValue('step-1'); + fixture.detectChanges(); + component.personalInfoGroup.patchValue({ + 'secondaryPhoneNumber': '', + 'secondaryPhoneNumberType': '' + }); + fixture.detectChanges(); + expect(component.personalInfoGroup.get('secondaryPhoneNumberType').valid).toEqual(true, 'Failed on both empty'); + component.personalInfoGroup.patchValue({ + 'secondaryPhoneNumber': '(414) 222-3333', + 'secondaryPhoneNumberType': '' + }); + expect(component.personalInfoGroup.get('secondaryPhoneNumberType').valid).toEqual(false, 'Failed on phone without type'); + component.personalInfoGroup.patchValue({ + 'secondaryPhoneNumber': '(414) 555-6666', + 'secondaryPhoneNumberType': '2' + }); + expect(component.personalInfoGroup.get('secondaryPhoneNumberType').valid).toEqual(true, 'Failed on phone with type'); + component.personalInfoGroup.patchValue({ + 'secondaryPhoneNumber': '', + 'secondaryPhoneNumberType': '2' + }); + expect(component.personalInfoGroup.get('secondaryPhoneNumberType').valid).toEqual(true, 'Failed on empty secondary phone number'); + }); + + it('Should render existing personal data (no marriage)', () => { + spyOn(service, 'getApplicationGroupValues').and.returnValue(personalInformationNoMarriage); + spyOn(service, 'shouldAskMaritalStatus').and.returnValue(false); + spyOn(service, 'hasCoApplicant').and.returnValue(false); + + fixture.detectChanges(); + expect(service.getApplicationGroupValues).toHaveBeenCalled(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.personalInfoGroup).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.personalInfoGroup.value).toEqual(personalInformationNoMarriage); + }); + + it('should render existing personal data (marriage)', () => { + spyOn(service, 'getApplicationGroupValues').and.returnValue(personalInformationMarriage); + spyOn(service, 'shouldAskMaritalStatus').and.returnValue(true); + spyOn(service, 'hasCoApplicant').and.returnValue(true); + spyOn(service, 'getIsCoApplicantSpouse').and.returnValue(CreditApplicationValues.CHOICE_YES); + fixture.detectChanges(); + expect(service.getApplicationGroupValues).toHaveBeenCalled(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.personalInfoGroup).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.personalInfoGroup.value).toEqual( + Object.assign(personalInformationMarriage, {coApplicantSpouse: CreditApplicationValues.CHOICE_YES}) + ); + }); + + it('should not have form fields for marital and co-applicant in some cases', () => { + spyOn(service, 'getApplicationGroupValues').and.returnValue(personalInformationMarriage); + spyOn(service, 'shouldAskMaritalStatus').and.returnValue(false); + spyOn(service, 'hasCoApplicant').and.returnValue(false); + fixture.detectChanges(); + expect(service.getApplicationGroupValues).toHaveBeenCalled(); + expect(component.pageForm.valid).toBeTruthy(); + expect(component.personalInfoGroup).toBeTruthy(); + expect(component.personalInfoGroup.get('maritalStatus')).toBeFalsy(); + expect(component.personalInfoGroup.get('coApplicantSpouse')).toBeFalsy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.ts new file mode 100644 index 0000000..bdd3f5d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.component.ts @@ -0,0 +1,126 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { PersonalInfoPageService } from './personal-info-page.service'; +import { ActivatedRoute } from '@angular/router'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'lw-personal-info-page', + templateUrl: './personal-info-page.component.html' +}) +export class PersonalInfoPageComponent implements OnInit, OnDestroy { + + public pageForm: FormGroup; + public personalInfoGroup: FormGroup; + private readonly FORM_STATE_INVALID = 'INVALID'; + private valueSubscription: Subscription; + private statusSubscription: Subscription; + + public savingData = null; + + public showProofOfStudentUploadForm = false; + public borrowerType: string; + public loanId: string; + + public localState = { + nextDisabled: true, + previousDisabled: false, + borrowerType: BorrowerType.PRIMARY_BORROWER, + nextTopic: '', + shouldAskMaritalStatus: false, + // This 'hasCoApplicant' bool is only relevant to the PRIMARY_BORROWER + hasCoApplicant: false, + showSpouseDisclosure: true, + isHelocRateMod: false + }; + + constructor( + private personalInfoPageService: PersonalInfoPageService, + private route: ActivatedRoute) {} + + public ngOnInit() { + this.initializeFormGroups(); + this.updateLocalState(this.pageForm.value); + this.showProofOfStudentUploadForm = this.personalInfoPageService.isStudent(); + this.borrowerType = this.personalInfoPageService.determineBorrowerType(this.route).toString(); + this.loanId = this.personalInfoPageService.getLoanId(); + } + + public initializeFormGroups() { + const borrowerType = this.personalInfoPageService.determineBorrowerType(this.route); + this.pageForm = this.personalInfoPageService.createPageGroup(); + this.personalInfoGroup = this.pageForm.get(PersonalInfoPageService.PERSONAL_FORM_NAME); + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.personalInfoGroup.patchValue( + this.personalInfoPageService.getApplicationGroupValues(PersonalInfoPageService.PERSONAL_FORM_NAME, borrowerType)); + } + + public updateLocalState(nextValues: any) { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + const borrowerType = this.personalInfoPageService.determineBorrowerType(this.route); + this.localState = { + nextDisabled: this.localState.nextDisabled, + previousDisabled: false, + borrowerType: borrowerType, + nextTopic: this.personalInfoPageService.getNextTopicMessage(), + shouldAskMaritalStatus: this.personalInfoPageService.shouldAskMaritalStatus(borrowerType), + hasCoApplicant: this.personalInfoPageService.hasCoApplicant() && borrowerType === BorrowerType.PRIMARY_BORROWER, // move to service + showSpouseDisclosure: borrowerType === BorrowerType.PRIMARY_BORROWER_SPOUSE || borrowerType === BorrowerType.CO_BORROWER_SPOUSE, + isHelocRateMod: this.personalInfoPageService.isHelocRateMod() + }; + this.adjustPersonalForms(this.personalInfoGroup.value); + this.valueSubscription = this.pageForm.valueChanges.subscribe((value) => { + this.updateLocalState(value); + }); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + + this.personalInfoPageService.saveAndNavigate(this.personalInfoGroup.value, this.route, this.localState.borrowerType); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.personalInfoPageService.cancelAndNavigate(this.route); + } + + public adjustPersonalForms(value: any): void { + if (this.localState.shouldAskMaritalStatus) { + this.personalInfoPageService.addMarriageControlsIfMissing(this.personalInfoGroup, this.localState.borrowerType); + } else { + this.personalInfoPageService.removeMarriageControls(this.personalInfoGroup); + } + if (this.shouldShowSpouseQuestions()) { + this.personalInfoPageService.addSpouseControlsIfMissing(this.personalInfoGroup, this.localState.borrowerType); + } else { + this.personalInfoPageService.removeSpouseControls(this.personalInfoGroup); + } + } + + public shouldShowSpouseQuestions() { + return this.localState.shouldAskMaritalStatus && this.localState.hasCoApplicant && this.hasAnsweredMarried(); + } + + public hasAnsweredMarried() { + return this.personalInfoGroup.value.maritalStatus === CreditApplicationValues.CHOICE_YES; + } + + public ngOnDestroy() { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.spec.ts new file mode 100644 index 0000000..67150a6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.spec.ts @@ -0,0 +1,121 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { MockActivatedRoute } from '../../../../testing/shared-mocks/activated-route.mock'; +import { PersonalInfoPageService } from './personal-info-page.service'; +import { PersonalInformation } from '../../../field-groups/personal/personal-information'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + + +describe('Personal Info Page Service', () => { + + let service: PersonalInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(PersonalInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createPersonalInformationGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createPersonalInformationGroup).toHaveBeenCalled(); + }); + + it('should create a personal info group', () => { + const personalGroup = service.createPersonalInformationGroup(); + expect(personalGroup).toBeTruthy(); + CommonGroups.PERSONAL_INFO.fields.forEach((eachField) => { + expect(personalGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(applicationWizardService, 'getIndividualGroupValues').and.returnValue(true); + service.getApplicationGroupValues(testGroupName, BorrowerType.PRIMARY_BORROWER); + expect(applicationWizardService.getIndividualGroupValues).toHaveBeenCalledWith(testGroupName, BorrowerType.PRIMARY_BORROWER); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'savePersonalInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const info = {}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'savePersonalInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('foo bar'); + const info = {}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, BorrowerType.PRIMARY_BORROWER); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should mark waypoint complete upon successful save', async() => { + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('../step-3'); + spyOn(creditApplicationService, 'savePersonalInformation').and.returnValue(of(true)); + spyOn(creditApplicationDaoService, 'saveApplication').and.returnValue(of(true)); + const info = {}; + const activatedRoute = new MockActivatedRoute({}); + service.saveAndNavigate(info, activatedRoute, BorrowerType.PRIMARY_BORROWER); + }); + + it('should always ask marital status for PRIMARY and CO borrowers', () => { + expect(service.shouldAskMaritalStatus(BorrowerType.CO_BORROWER)).toBe(true); + expect(service.shouldAskMaritalStatus(BorrowerType.PRIMARY_BORROWER)).toBe(true); + expect(service.shouldAskMaritalStatus(BorrowerType.CO_BORROWER_SPOUSE)).toBe(false); + expect(service.shouldAskMaritalStatus(BorrowerType.PRIMARY_BORROWER_SPOUSE)).toBe(false); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.ts new file mode 100644 index 0000000..e558f70 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/personal/personal-info-page.service.ts @@ -0,0 +1,152 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { PersonalInformation } from '../../../field-groups/personal/personal-information'; +import { PageService } from '../../page-service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import {LoanTitle} from '../../../../shared/loan-type/loan-title'; + +@Injectable({ + providedIn: 'root' +}) +export class PersonalInfoPageService implements PageService { + + public static readonly PERSONAL_FORM_NAME = 'personalInformation'; + + private CURRENT_ROUTE = 'step-2'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + const personalGroup = this.createPersonalInformationGroup(); + group.addControl(PersonalInfoPageService.PERSONAL_FORM_NAME, personalGroup); + + return group; + } + + public createPersonalInformationGroup(): FormGroup { + const personalGroup = this.formBuilder.group({}); + + CommonGroups.PERSONAL_INFO.fields.forEach((eachField) => { + personalGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return personalGroup; + } + + public getApplicationGroupValues(groupName: string, individual: BorrowerType): any { + return this.applicationWizardService.getIndividualGroupValues(groupName, individual); + } + + public getIsCoApplicantSpouse(): string { + return this.applicationWizardService.getFullCreditApplication().hasCoApplicantSpouse; + } + + public isHelocRateMod(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.HELOC_RateMod; + } + + public isStudent(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.Student; + } + + public getLoanId(): string { + return this.applicationWizardService.getGroupValues('loanId'); + } + + public saveAndNavigate(personalInfoData: PersonalInformation, route: ActivatedRoute, individual: BorrowerType) { + this.CURRENT_ROUTE = this.getRouteName(); + this.creditApplicationService.savePersonalInformation(personalInfoData, individual).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public cancelAndNavigate(route: ActivatedRoute) { + const nextRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public getRouteName(): string { + return this.applicationWizardService.getSimpleRouteName(this.router.url); + } + + public getNextTopicMessage() { + this.CURRENT_ROUTE = this.getRouteName(); + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public shouldAskMaritalStatus(individual: BorrowerType): boolean { + return individual === BorrowerType.PRIMARY_BORROWER || individual === BorrowerType.CO_BORROWER; + } + + public hasCoApplicant(): boolean { + return this.applicationWizardService.getGroupValues('hasCoApplicant') === CreditApplicationValues.CHOICE_YES; + } + + public determineBorrowerType(route: ActivatedRoute): BorrowerType { + if (route.snapshot.data[0]) { + return route.snapshot.data[0].individual; + } + return BorrowerType.PRIMARY_BORROWER; + } + + public addMarriageControlsIfMissing(personalInfoGroup: FormGroup, individual: BorrowerType) { + if (personalInfoGroup.get(CommonGroups.IS_MARRIED.fields[0].field)) { + return; + } + const existingValue = this.getApplicationGroupValues(PersonalInfoPageService.PERSONAL_FORM_NAME, individual); + CommonGroups.IS_MARRIED.fields.forEach((eachField) => { + personalInfoGroup.addControl(eachField.field, this.formBuilder.control(existingValue[eachField.field], eachField.validation.slice())); + }, this); + } + + public removeMarriageControls(personalInfoGroup: FormGroup): void { + if (!personalInfoGroup.get(CommonGroups.IS_MARRIED.fields[0].field)) { + return; + } + CommonGroups.IS_MARRIED.fields.forEach((eachField) => { + personalInfoGroup.removeControl(eachField.field); + }, this); + } + + public addSpouseControlsIfMissing(personalInfoGroup: FormGroup, individual: BorrowerType) { + if (personalInfoGroup.get(CommonGroups.IS_CO_APPLICANT_SPOUSE.fields[0].field)) { + return; + } + + const existingValue = this.getIsCoApplicantSpouse(); + CommonGroups.IS_CO_APPLICANT_SPOUSE.fields.forEach((eachField) => { + personalInfoGroup.addControl(eachField.field, this.formBuilder.control(existingValue, eachField.validation.slice())); + }, this); + } + + public removeSpouseControls(personalInfoGroup: FormGroup): void { + if (!personalInfoGroup.get(CommonGroups.IS_CO_APPLICANT_SPOUSE.fields[0].field)) { + return; + } + CommonGroups.IS_CO_APPLICANT_SPOUSE.fields.forEach((eachField) => { + personalInfoGroup.removeControl(eachField.field); + }, this); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.html new file mode 100644 index 0000000..3d34db3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.html @@ -0,0 +1,19 @@ +
+
+

HEADINGS.PROCESSING

+
+
+
+ +
+
+
+
+
+ + {{dStep.getMessage()}} +
+
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.scss new file mode 100644 index 0000000..8b35f22 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.scss @@ -0,0 +1,25 @@ + +$baseColor : #e74c3c; +$fontColor : #fff; +$max-width: 770px; + +.decision-row { + margin-bottom: 0.25em; + color: #DFDFDF; + font-size: 44px; +} + +.active { + color: #005B9A; +} + +.done { + color: #7ca550; +} + +/*** Mobile view settings ***/ +@media screen and (max-width: $max-width) { + .decision-row { + font-size: 24px; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.spec.ts new file mode 100644 index 0000000..5a2e6b7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.spec.ts @@ -0,0 +1,127 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { ProcessingPageComponent } from './processing-page.component'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { ProcessingPageService } from './processing-page.service'; +import { DecisionAnimationComponent } from '../../../../layout/decision-animation/decision-animation.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ConfigurationService } from '../../../../shared/configuration/configuration.service'; +import { MockConfigurationService } from '../../../../testing/shared-mocks/configuration.mock.service'; +import { configureTestSuite } from 'ng-bullet'; +import { of } from 'rxjs'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + +describe('ProcessingPageComponent', () => { + + let component: ProcessingPageComponent; + let fixture: ComponentFixture; + let processingPageService: ProcessingPageService; + let applicationWizardService: ApplicationWizardService; + let configurationService: ConfigurationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ProcessingPageComponent, + DecisionAnimationComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}, + {provide: ConfigurationService, useValue: MockConfigurationService} + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProcessingPageComponent); + component = fixture.componentInstance; + configurationService = TestBed.get(ConfigurationService); + processingPageService = TestBed.get(ProcessingPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + spyOn(component, 'reviewApplication').and.returnValue(null); + spyOn(component, 'getPhoneNumber').and.returnValue('414-555-5309'); + fixture.detectChanges(); + expect(component.reviewApplication).toHaveBeenCalled(); + }); + + it('should review the application and start the next step', () => { + spyOn(component, 'getPhoneNumber').and.returnValue('414-555-5309'); + spyOn(processingPageService, 'reviewApplication').and.returnValue(of(true)); + spyOn(component, 'transmitApplication').and.returnValue(null); + fixture.detectChanges(); + expect(processingPageService.reviewApplication).toHaveBeenCalled(); + expect(component.transmitApplication).toHaveBeenCalled(); + // component.reviewApplication(); + expect(component.processingSteps[component.STEP_REVIEW].isComplete).toEqual(true); + }); + + it('should transmit the application and start the next step', () => { + spyOn(component, 'getPhoneNumber').and.returnValue('414-555-5309'); + spyOn(processingPageService, 'reviewApplication').and.returnValue(of(true)); + spyOn(processingPageService, 'submitApplication').and.returnValue(of(true)); + spyOn(component, 'submitDecision').and.returnValue(null); + + fixture.detectChanges(); + expect(processingPageService.reviewApplication).toHaveBeenCalled(); + expect(processingPageService.submitApplication).toHaveBeenCalled(); + expect(component.submitDecision).toHaveBeenCalled(); + expect(component.processingSteps[component.STEP_REVIEW].isComplete).toEqual(true); + expect(component.processingSteps[component.STEP_TRANSMIT].isComplete).toEqual(true); + }); + + it('should submit for decision and start the next step', () => { + spyOn(component, 'getPhoneNumber').and.returnValue('414-555-5309'); + spyOn(processingPageService, 'reviewApplication').and.returnValue(of(true)); + spyOn(processingPageService, 'submitApplication').and.returnValue(of(true)); + spyOn(processingPageService, 'submitForDecisioning').and.returnValue(of(true)); + spyOn(component, 'pollDecision').and.returnValue(null); + + fixture.detectChanges(); + expect(processingPageService.reviewApplication).toHaveBeenCalled(); + expect(processingPageService.submitApplication).toHaveBeenCalled(); + expect(processingPageService.submitForDecisioning).toHaveBeenCalled(); + expect(component.pollDecision).toHaveBeenCalled(); + expect(component.processingSteps[component.STEP_REVIEW].isComplete).toEqual(true); + expect(component.processingSteps[component.STEP_TRANSMIT].isComplete).toEqual(true); + expect(component.processingSteps[component.STEP_SUBMIT_DECIDE].isComplete).toEqual(true); + }); + + it('should handle skip Decision', () => { + spyOn(component, 'getPhoneNumber').and.returnValue('414-555-5309'); + spyOn(processingPageService, 'reviewApplication').and.returnValue(of(true)); + spyOn(processingPageService, 'submitApplication').and.returnValue(of(true)); + spyOn(processingPageService, 'setAsSkippedDecision').and.returnValue(of(true)); + spyOn(processingPageService, 'shouldRequestDecision').and.returnValue(false); + spyOn(component, 'pollDecision').and.returnValue(null); + + fixture.detectChanges(); + expect(processingPageService.reviewApplication).toHaveBeenCalled(); + expect(processingPageService.submitApplication).toHaveBeenCalled(); + expect(processingPageService.setAsSkippedDecision).toHaveBeenCalled(); + expect(component.pollDecision).toHaveBeenCalled(); + expect(component.processingSteps[component.STEP_REVIEW].isComplete).toEqual(true); + expect(component.processingSteps[component.STEP_TRANSMIT].isComplete).toEqual(true); + expect(component.processingSteps[component.STEP_SUBMIT_DECIDE].isComplete).toEqual(true); + }); + + it('should provide the decision step array', () => { + const steps = component.createProcessingSteps(); + expect(steps).toBeTruthy(); + expect(steps.length).toEqual(4); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.ts new file mode 100644 index 0000000..a6d0856 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.component.ts @@ -0,0 +1,128 @@ +import { Component, ErrorHandler, OnInit } from '@angular/core'; +import { ProcessingPageService } from './processing-page.service'; +import { ProcessingStep } from './processing-step'; +import { ConfigurationService } from '../../../../shared/configuration/configuration.service'; +import { ActivatedRoute } from '@angular/router'; +import { DecisionState } from '../../../../shared/decision/decision-status'; + +@Component({ + selector: 'lw-processing-page', + templateUrl: './processing-page.component.html', + styleUrls: ['./processing-page.component.scss'] +}) +export class ProcessingPageComponent implements OnInit { + + public STEP_REVIEW = 0; + public STEP_TRANSMIT = 1; + public STEP_SUBMIT_DECIDE = 2; + public STEP_POLL_DECIDE = 3; + + public localState = { + firstName: '', + phoneNumber: '' + }; + public processingSteps: Array = []; + + constructor( + private processingPageService: ProcessingPageService, + private configurationService: ConfigurationService, + private route: ActivatedRoute, + private eHandler: ErrorHandler + ) { } + + public ngOnInit(): void { + this.processingSteps = this.createProcessingSteps(); + this.localState.firstName = this.processingPageService.getBorrowerFirstName(); + this.localState.phoneNumber = this.getPhoneNumber(); + this.reviewApplication(); + } + + public getPhoneNumber(): string { + return this.configurationService.getConfigurationByField('contact')['HelpPhone']; + } + + public setCurrentStep(newStep: number): void { + this.processingSteps.forEach((step, stepIndex) => { + step.setCurrent(stepIndex === newStep); + }, this); + } + + public reviewApplication(): void { + this.setCurrentStep(this.STEP_REVIEW); + this.processingPageService.reviewApplication().subscribe( + (successFlag) => { + if (successFlag) { + this.processingSteps[this.STEP_REVIEW].setComplete(true); + this.transmitApplication(); + } else { + this.handleFailure('Failed to verify application.', DecisionState.NotStarted); + } + } + ); + } + + public transmitApplication(): void { + this.setCurrentStep(this.STEP_TRANSMIT); + this.processingPageService.submitApplication().subscribe( + (successFlag) => { + if (successFlag) { + this.processingSteps[this.STEP_TRANSMIT].setComplete(true); + this.submitDecision(); + } else { + this.handleFailure('Failed to transmit application.', DecisionState.ErrorSubmit); + } + } + ); + } + + public submitDecision(): void { + this.setCurrentStep(this.STEP_SUBMIT_DECIDE); + let decisionObservable = null; + if (this.processingPageService.shouldRequestDecision()) { + decisionObservable = this.processingPageService.submitForDecisioning(); + } else { + decisionObservable = this.processingPageService.setAsSkippedDecision(); + } + decisionObservable.subscribe( + (successFlag) => { + if (successFlag) { + this.processingSteps[this.STEP_SUBMIT_DECIDE].setComplete(true); + this.pollDecision(); + } else { + this.handleFailure('Failed submitting decision.', DecisionState.ErrorDecide); + } + } + ); + } + + public pollDecision(): void { + this.setCurrentStep(this.STEP_POLL_DECIDE); + this.processingPageService.requestDecisionResultFromCRIF().subscribe( + (successFlag) => { + if (successFlag) { + this.processingSteps[this.STEP_POLL_DECIDE].setComplete(true); + this.processingPageService.saveAndNavigate(this.route); + } else { + this.handleFailure('Failure polling decision.', DecisionState.ErrorDecide); + } + }, (error) => { + this.handleFailure('Error polling decision.', DecisionState.ErrorDecide, error); + } + ); + } + + public handleFailure(failure: string, decisionState: DecisionState, error?: any): void { + // @TODO ERROR_NOTIFY + this.processingPageService.setAsErrorDecision(decisionState); + this.processingPageService.saveAndNavigate(this.route); + } + + public createProcessingSteps(): Array { + return [ + new ProcessingStep('Checking Application', 'Application Completed'), + new ProcessingStep('Transmitting Application', 'Application Transmitted'), + new ProcessingStep('Processing Application', 'Application Processed'), + new ProcessingStep('Waiting on Status', 'Decision Received') + ]; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.spec.ts new file mode 100644 index 0000000..2b25889 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.spec.ts @@ -0,0 +1,139 @@ +import { ActivatedRoute, Router } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProcessingPageService } from './processing-page.service'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { DecisionState } from '../../../../shared/decision/decision-status'; +import { of } from 'rxjs'; +import { DecisionService } from '../../../../shared/decision/decision.service'; + +describe('Processing Page Service', () => { + + let service: ProcessingPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let decisionService: DecisionService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(ProcessingPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + decisionService = TestBed.get(DecisionService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should save and continue', () => { + spyOn(applicationWizardService, 'getNextRoute').and.returnValue('testRoute'); + spyOn(router, 'navigate').and.returnValue(true); + const activatedRoute = { + routeConfig: { + path: 'testPath' + } + }; + service.saveAndNavigate(activatedRoute); + expect(applicationWizardService.getNextRoute).toHaveBeenCalledWith('testPath'); + expect(router.navigate).toHaveBeenCalled(); + expect(() => { service.saveAndNavigate(null); }).toThrow(); + }); + + it('should review an application and mark complete', () => { + spyOn(service, 'markApplicationCompleted').and.returnValue(true); + service.reviewApplication().subscribe( + (result) => { + expect(result).toEqual(true); + } + ); + expect(service.markApplicationCompleted).toHaveBeenCalled(); + }); + + it('should submit application', () => { + spyOn(applicationWizardService, 'submitCreditApplication').and.returnValue(of(true)); + service.submitApplication().subscribe( + (result) => { + expect(result).toEqual(true); + } + ); + expect(applicationWizardService.submitCreditApplication).toHaveBeenCalled(); + }); + + it('should submit for decisioning', () => { + spyOn(applicationWizardService, 'submitApplicationForDecision').and.returnValue(of(true)); + service.submitForDecisioning().subscribe( + (result) => { + expect(result).toEqual(true); + } + ); + expect(applicationWizardService.submitApplicationForDecision).toHaveBeenCalled(); + }); + + it('should set as skipped decision', () => { + const existingDecision = service.getCreditApplication().decision; + spyOn(decisionService, 'createEmptyDecisionResult').and.callThrough(); + service.setAsSkippedDecision(); + expect(service.getCreditApplication().decision).not.toEqual(existingDecision); + expect(decisionService.createEmptyDecisionResult).toHaveBeenCalled(); + }); + + it('should request decision result from CRIF FAIL', () => { + const responses = [of(false), of(false), of(false), of(false), of(false), of(true)]; + spyOn(applicationWizardService, 'updateApplicationDecisionStatus').and.returnValue(of(false)); + let decisionFromCRIF = false; + service.requestDecisionResultFromCRIF().subscribe((result) => { + decisionFromCRIF = result; + + }); + expect(decisionFromCRIF).toEqual(false); + expect(applicationWizardService.updateApplicationDecisionStatus).toHaveBeenCalled(); + }); + + it('should request decision', () => { + spyOn(service, 'shouldRequestDecision').and.callThrough(); + const decision = service.shouldRequestDecision(); + expect(decision).toBeTruthy(); + expect(service.shouldRequestDecision).toHaveBeenCalled(); + }); + + it('should set as error decision', () => { + spyOn(service, 'setAsErrorDecision').and.callThrough(); + spyOn(decisionService, 'createErrorDecisionResult'); + service.setAsErrorDecision(DecisionState.ErrorSubmit); + expect(service.setAsErrorDecision).toHaveBeenCalled(); + expect(decisionService.createErrorDecisionResult).toHaveBeenCalled(); + }); + + it('should handle post application events', () => { + spyOn(service, 'handlePostApplicationEvents').and.callThrough(); + spyOn(applicationWizardService, 'handlePostApplicationEvents'); + service.handlePostApplicationEvents(); + expect(service.handlePostApplicationEvents).toHaveBeenCalled(); + expect(applicationWizardService.handlePostApplicationEvents).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.ts new file mode 100644 index 0000000..41fc321 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@angular/core'; +import { interval, Observable, of, throwError } from 'rxjs'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CreditApplication } from '../../../../shared/credit-application'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { DecisionState } from '../../../../shared/decision/decision-status'; +import { delay, flatMap, map, mapTo, retryWhen } from 'rxjs/operators'; +import { DecisionService } from '../../../../shared/decision/decision.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProcessingPageService { + + private STANDARD_DELAY = 4000; + private STANDARD_RETRY = 29; // for a total of 30 + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private formatService: FormatService, + private decisionService: DecisionService, + private router: Router) { } + + public saveAndNavigate(route: ActivatedRoute): void { + if (!route) { + throw new Error('ProcessingPageService: saveAndNavigate called with null route'); + } + const nextRoute = this.applicationWizardService.getNextRoute(route.routeConfig.path); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public reviewApplication(): Observable { + this.markApplicationCompleted(); + const obs = of(null).pipe( + mapTo(true), + delay(this.STANDARD_DELAY) + ); + return obs; + } + + public submitApplication(): Observable { + return this.applicationWizardService.submitCreditApplication(this.getCreditApplication()); + } + + public submitForDecisioning(): Observable { + return this.applicationWizardService.submitApplicationForDecision(this.getCreditApplication()); + } + + public setAsSkippedDecision(): Observable { + const decision = this.decisionService.createEmptyDecisionResult(this.getCreditApplication().loanType.shouldDecision); + this.getCreditApplication().decision = decision; + const obs = of(null).pipe( + mapTo(true), + delay(this.STANDARD_DELAY) + ); + return obs; + } + + public getRetryStrategy(maxRetryCount: number = 29, delayInMils: number = 4000): any { + return (src: Observable) => src.pipe( + retryWhen(_ => { + return interval(delayInMils).pipe( + flatMap(count => count === maxRetryCount ? throwError('Reached maximum tries.') : of(count)) + ); + }) + ); + } + + public requestDecisionResultFromCRIF(): Observable { + return this.applicationWizardService.updateApplicationDecisionStatus().pipe( + map((val) => { + if (val === false) { + throw val; + } + return val; + }), + this.getRetryStrategy(this.STANDARD_RETRY, this.STANDARD_DELAY) + ); + } + + public getCreditApplication(): CreditApplication { + return this.creditApplicationService.getFullCreditApplication(); + } + + public shouldRequestDecision(): boolean { + return this.getCreditApplication().loanType.shouldDecision; + } + + public setAsErrorDecision(decisionState: DecisionState): void { + const decision = this.decisionService.createErrorDecisionResult(this.getCreditApplication().loanType.shouldDecision, decisionState); + this.getCreditApplication().decision = decision; + } + + public handlePostApplicationEvents(): void { + this.applicationWizardService.handlePostApplicationEvents(); + } + + public markApplicationCompleted(): any { + this.getCreditApplication().hasBeenCompleted = true; + } + + public getBorrowerFirstName(): string { + return this.formatService.formatTitleCase( + this.getCreditApplication().fullInfoBorrowers[0].personalInformation.firstName + ); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.spec.ts new file mode 100644 index 0000000..40c30bd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.spec.ts @@ -0,0 +1,51 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { ProcessingPage } from './processing-page'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Processing Page', () => { + + let page: ProcessingPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new ProcessingPage('PAGETITLES.CC.INTRO', 'decision', []); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should be invalid if there is no credit application in state', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(null); + const isValid = page.isValidPage(applicationWizardService, creditApplicationService); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(isValid.length).toBeGreaterThan(0); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.ts new file mode 100644 index 0000000..0c938fd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-page.ts @@ -0,0 +1,15 @@ +import { ApplicationPage } from '../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +export class ProcessingPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + if (applicationWizardService.getFullCreditApplication()) { + return []; + } else { + return ['ERROR']; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-step.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-step.ts new file mode 100644 index 0000000..74f79cc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/processing/processing-step.ts @@ -0,0 +1,33 @@ +export class ProcessingStep { + + public isComplete: boolean; + public isActive: boolean; + hasStatus: boolean; + preMessage: string; + postMessage: string; + + constructor(preMsg: string, postMsg: string, hasStatus = true) { + this.preMessage = preMsg; + this.postMessage = postMsg; + this.hasStatus = hasStatus; + } + + public setCurrent(isCurrent: boolean): void { + this.isActive = isCurrent; + } + + public setComplete(isComplete: boolean) { + this.isComplete = isComplete; + if (this.isComplete) { + this.isActive = false; + } + } + + public getMessage(): string { + return this.isComplete ? this.postMessage : this.preMessage; + } + + public setPostMessage(postMsg: string): void { + this.postMessage = postMsg; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.html new file mode 100644 index 0000000..7eeaf48 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.html @@ -0,0 +1,16 @@ +
+

HEADINGS.SPOUSE.{{localState.borrowerType}}

+
+ + +
+
+ + +
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.spec.ts new file mode 100644 index 0000000..1f27839 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.spec.ts @@ -0,0 +1,124 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { SpouseTypeInfoPageComponent } from './spouse-type-info-page.component'; +import { SpouseTypeInfoPageService } from './spouse-type-info-page.service'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { SpouseType } from '../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + +describe('SpouseInfoPageComponent', () => { + let component: SpouseTypeInfoPageComponent; + let fixture: ComponentFixture; + let service: SpouseTypeInfoPageService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + SpouseTypeInfoPageComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SpouseTypeInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(SpouseTypeInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.spouseTypeGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('Should render existing spouse answer', () => { + const existingAnswer = {isFullInfoSpouse: CreditApplicationValues.CHOICE_YES}; + spyOn(service, 'getCurrentSpouseTypeValue').and.returnValue(existingAnswer); + fixture.detectChanges(); + expect(service.getCurrentSpouseTypeValue).toHaveBeenCalled(); + expect(component.pageForm.valid).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + expect(component.spouseTypeGroup.value).toEqual(existingAnswer); + }); + + it('should accurately submit all data on NEXT', () => { + const existingAnswer = {isFullInfoSpouse: CreditApplicationValues.CHOICE_YES}; + spyOn(service, 'getCurrentSpouseTypeValue').and.returnValue(existingAnswer); + fixture.detectChanges(); + expect(component.pageForm.valid).toBeTruthy(); + const buttonNext = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + expect(buttonNext.disabled).toBeFalsy(); + spyOn(service, 'saveAndNavigate').and.returnValue(true); + buttonNext.click(); + expect(service.saveAndNavigate).toHaveBeenCalledWith(existingAnswer, component.route, component.localState.borrowerType); + }); + + it('should patch previous data onInit', () => { + spyOn(service, 'getSpouseType').and.returnValue(SpouseType.FullInfoSpouse); + fixture.detectChanges(); + expect(component.spouseTypeGroup.value).toEqual({isFullInfoSpouse: CreditApplicationValues.CHOICE_YES}); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.ts new file mode 100644 index 0000000..dba3940 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { SpouseTypeInfoPageService } from './spouse-type-info-page.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { SpouseType } from '../../../../shared/credit-application'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; + +@Component({ + selector: 'lw-spouse-type-info-page', + templateUrl: './spouse-type-info-page.component.html' +}) +export class SpouseTypeInfoPageComponent implements OnInit { + + public pageForm: FormGroup; + public spouseTypeGroup: FormGroup; + private readonly FORM_STATE_INVALID = 'INVALID'; + private subscription: Subscription; + + public savingData = null; + + public localState = { + nextDisabled: true, + previousDisabled: false, + nextTopic: '', + borrowerType: BorrowerType.PRIMARY_BORROWER, + savingStep: null + }; + + constructor( + private spouseTypeInfoPageService: SpouseTypeInfoPageService, + public route: ActivatedRoute) { } + + public ngOnInit() { + this.initializeFormGroups(); + this.updateLocalState(this.pageForm.value); + } + + public initializeFormGroups() { + this.pageForm = this.spouseTypeInfoPageService.createPageGroup(); + this.spouseTypeGroup = this.pageForm.get(SpouseTypeInfoPageService.SPOUSE_TYPE_FORM_NAME); + this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.spouseTypeGroup.patchValue(this.spouseTypeInfoPageService.getCurrentSpouseTypeValue(this.route)); + } + + public updateLocalState(modelValue: any): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.localState.borrowerType = this.spouseTypeInfoPageService.determineBorrowerType(this.route); + this.localState.nextTopic = this.spouseTypeInfoPageService.getNextTopicMessage(this.localState.borrowerType); + this.subscription = this.pageForm.valueChanges.subscribe(() => { + this.updateLocalState(this.pageForm.value); + }); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + const isFullInfoSpouse = this.pageForm.value[SpouseTypeInfoPageService.SPOUSE_TYPE_FORM_NAME]; + this.spouseTypeInfoPageService.saveAndNavigate(isFullInfoSpouse, this.route, this.localState.borrowerType); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.spouseTypeInfoPageService.cancelAndNavigate(this.route); + } + + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.spec.ts new file mode 100644 index 0000000..0927162 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.spec.ts @@ -0,0 +1,113 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { SpouseTypeInfoPageService } from './spouse-type-info-page.service'; +import { IsFullInfoSpouse } from '../../../field-groups/spouse-type/is-full-info-spouse-information'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Spouse Type Info Page Service', () => { + + let service: SpouseTypeInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(SpouseTypeInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createSpouseTypeInformationGroup').and.callThrough(); + const pageGroup = service.createPageGroup(); + const isSameGroup = pageGroup.get(SpouseTypeInfoPageService.SPOUSE_TYPE_FORM_NAME); + expect(service.createSpouseTypeInformationGroup).toHaveBeenCalled(); + CommonGroups.IS_FULL_INFO_SPOUSE.fields.forEach((eachField) => { + expect(isSameGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create an address info group', () => { + const addressGroup = service.createSpouseTypeInformationGroup(); + expect(addressGroup).toBeTruthy(); + CommonGroups.IS_FULL_INFO_SPOUSE.fields.forEach((eachField) => { + expect(addressGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should provide the spouse type', () => { + spyOn(applicationWizardService, 'getSpouseType').and.callThrough(); + service.getSpouseType(BorrowerType.PRIMARY_BORROWER); + expect(applicationWizardService.getSpouseType).toHaveBeenCalledWith(BorrowerType.PRIMARY_BORROWER); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveSpouseTypeInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = {}; + info.isFullInfoSpouse = CreditApplicationValues.CHOICE_YES; + const borrower = BorrowerType.PRIMARY_BORROWER_SPOUSE; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, borrower); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveSpouseTypeInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = {}; + info.isFullInfoSpouse = CreditApplicationValues.CHOICE_YES; + const borrower = BorrowerType.PRIMARY_BORROWER_SPOUSE; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, borrower); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.ts new file mode 100644 index 0000000..3a3bcb1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { PageService } from '../../page-service'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { TattleSpousePersonalInformation } from '../../../field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { Individual, SpouseType } from '../../../../shared/credit-application'; +import { IsFullInfoSpouse } from '../../../field-groups/spouse-type/is-full-info-spouse-information'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +@Injectable({ + providedIn: 'root' +}) +export class SpouseTypeInfoPageService implements PageService { + + public static readonly SPOUSE_TYPE_FORM_NAME = 'spouseTypeGroup'; + private CURRENT_ROUTE = 'applicant-spouse-type'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + const spouseTypeGroup = this.createSpouseTypeInformationGroup(); + group.addControl(SpouseTypeInfoPageService.SPOUSE_TYPE_FORM_NAME, spouseTypeGroup); + + return group; + } + + public createSpouseTypeInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + + CommonGroups.IS_FULL_INFO_SPOUSE.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; + } + + public getSpouseType(individual: BorrowerType): SpouseType { + return this.applicationWizardService.getSpouseType(individual); + } + + public saveAndNavigate(spouseTypeInformation: IsFullInfoSpouse, route: ActivatedRoute, individual: BorrowerType) { + this.creditApplicationService.saveSpouseTypeInformation(spouseTypeInformation, individual).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public determineCurrentRoute(individual: BorrowerType): string { + switch (individual) { + case BorrowerType.PRIMARY_BORROWER: + return 'applicant-spouse-type'; + case BorrowerType.CO_BORROWER: + return 'co-applicant-spouse-type'; + } + } + + public cancelAndNavigate(route: ActivatedRoute) { + const nextRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public getNextTopicMessage(individual: BorrowerType): string { + this.CURRENT_ROUTE = this.determineCurrentRoute(individual); + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public determineBorrowerType(route: ActivatedRoute): BorrowerType { + if (route.snapshot.data[0]) { + return route.snapshot.data[0].individual; + } + return BorrowerType.PRIMARY_BORROWER; + } + + public getCurrentSpouseTypeValue(route: ActivatedRoute): any { + const borrowerType = this.determineBorrowerType(route); + const spouseType = this.getSpouseType(borrowerType); + if (spouseType === SpouseType.FullInfoSpouse) { + return {isFullInfoSpouse: CreditApplicationValues.CHOICE_YES}; + } else if (spouseType === SpouseType.TattleSpouse) { + return {isFullInfoSpouse: CreditApplicationValues.CHOICE_NO}; + } + return { isFullInfoSpouse: null }; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.ts new file mode 100644 index 0000000..a66ea95 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/spouse-type/spouse-type-info-page.ts @@ -0,0 +1,23 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ApplicationPage } from '../../../application-page'; +import { BorrowerType } from '../../../../shared/borrower-type'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +export class SpouseTypeInfoPage extends ApplicationPage { + + private INDIVIDUAL_REF: string; + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + if (this.route === 'applicant-spouse-type') { + return applicationWizardService.isApplicantNonSpouse(BorrowerType.PRIMARY_BORROWER); + } else if (this.route === 'co-applicant-spouse-type') { + return applicationWizardService.isApplicantNonSpouse(BorrowerType.CO_BORROWER); + } + return false; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + // @TODO: #565 removes this page + return []; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.html new file mode 100644 index 0000000..696cad9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.html @@ -0,0 +1,20 @@ +
+
+
SUMMARY.GENERAL.{{title}} + +
+
+
SUMMARY.GENERAL.{{loanCategory}}
+
{{limitRequested | currency}}
+
SUMMARY.GENERAL.PURPOSE
+
{{purposeOfLoanSignature}}
+
SUMMARY.GENERAL.PURPOSE
+
{{purposeOfLoan}}
+
SUMMARY.GENERAL.LOAN_TERM
+
{{loanTerms}} Months
+
SUMMARY.GENERAL.HELOC_RATE_MOD
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.spec.ts new file mode 100644 index 0000000..7d42b02 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.spec.ts @@ -0,0 +1,70 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { SummaryPageService } from './summary-page.service'; +import { ApplicationSummaryComponent } from './application-summary.component'; +import { CreditApplication } from '../../../../shared/credit-application'; +import { apiTranslatorPreTranslate } from 'src/app/testing/shared-mocks/api-translator-pre-post'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('ApplicationSummaryComponent', () => { + let component: ApplicationSummaryComponent; + let fixture: ComponentFixture; + let creditApplication: CreditApplication; + let service: SummaryPageService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ApplicationSummaryComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ApplicationSummaryComponent); + component = fixture.componentInstance; + service = TestBed.get(SummaryPageService); + creditApplication = apiTranslatorPreTranslate; + + component.creditApplication = creditApplication; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')); + expect(button).toBeTruthy(); + button.nativeElement.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); + + it('should display loan term when termOfLoan exists', () => { + expect(component.hasLoanTerms).toEqual(false); + fixture.detectChanges(); + expect(component.hasLoanTerms).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.ts new file mode 100644 index 0000000..4e50072 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/application-summary.component.ts @@ -0,0 +1,71 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {CreditApplication} from '../../../../shared/credit-application'; +import {LoanCategory} from 'src/app/shared/loan-type/loan-category'; +import {LoanTitle} from '../../../../shared/loan-type/loan-title'; +import { SelectOptions } from 'src/app/shared/options/select-options'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; + +@Component({ + selector: 'lw-application-summary', + templateUrl: './application-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class ApplicationSummaryComponent implements OnInit { + + public title: string; + public limitRequested: string; + public purposeOfLoanSignature: string; + public purposeOfLoan: string; + public loanCategory: string; + public loanTerms: string; + public hasLoanTerms: Boolean = false; + public isHelocRateMod: Boolean = false; + public askPurposeOfLoanSignature: Boolean = false; + public askPurposeOfLoan: Boolean = false; + + @Input() + public creditApplication: CreditApplication; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + ngOnInit(): void { + this.loanCategory = this.creditApplication.loanType.category === LoanCategory.CreditCard ? '' : 'LOAN_LIMIT'; + this.title = this.creditApplication.loanType.category === LoanCategory.CreditCard ? '' : 'LOAN_TITLE'; + this.isHelocRateMod = this.creditApplication.loanType.title === LoanTitle.HELOC_RateMod; + this.askPurposeOfLoanSignature = this.creditApplication.loanType.category === LoanCategory.Signature; + this.askPurposeOfLoan = this.creditApplication.loanType.category === LoanCategory.Equity; + this.purposeOfLoanSignature = this.creditApplication.purposeOfLoanSignature; + + if (this.creditApplication.purposeOfLoan) { + if (this.askPurposeOfLoan) { + this.purposeOfLoan = this.creditApplication.purposeOfLoan.replace(/_/g, ' '); + } else { + this.purposeOfLoan = this.creditApplication.purposeOfLoan; + } + } + + if (this.creditApplication && this.creditApplication.limitRequested) { + this.limitRequested = this.creditApplication.limitRequested.toString(); + if (this.creditApplication.termOfLoan) { + this.loanTerms = this.creditApplication.termOfLoan.toString(); + this.hasLoanTerms = true; + } + } else { + this.limitRequested = '0'; + } + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } + + public isCreditCard(){ + return this.loanCategory == LoanCategory.CreditCard; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.html new file mode 100644 index 0000000..3cd95e9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.html @@ -0,0 +1,55 @@ + +
+
+
{{title}} + +
+
+
SUMMARY.COLLATERAL.ADDRESS_SAME
+
{{collateralInformation.isAddressSame | booleanString}}
+
SUMMARY.COLLATERAL.ESTIMATED_VALUE
+
{{collateralInformation.estimatedValue | currency}}
+
Address
+
{{collateralInformation.addressLine1}} +
+ {{collateralInformation.addressLine2}} +
+
+ {{collateralInformation.city}}, {{collateralInformation.state}} {{collateralInformation.zipcode}} +
+
SUMMARY.COLLATERAL.OCCUPANCY_TYPE
+
{{collateralInformation.occupancyType | occupancyType}}
+
SUMMARY.COLLATERAL.PROPERTY_TYPE
+
{{collateralInformation.propertyType | propertyType}}
+
SUMMARY.COLLATERAL.PROPERTY_TYPE
+
{{collateralInformation.propertyType | mfgHomePropertyType}}
+ +
{{ collateralInformation.files[0].description }} document(s):
+
{{collateralInformation.files | uploadedFiles}}
+ +
SUMMARY.COLLATERAL.FIRST_MORTGAGE
+
{{collateralInformation.hasFirstMortgage | booleanString}}
+
{{ collateralInformation.firstMortgageBalanceFiles[0].description }} document(s):
+
{{collateralInformation.firstMortgageBalanceFiles | uploadedFiles}}
+ +
SUMMARY.COLLATERAL.PAYOFF_FIRST_MORTGAGE
+
{{collateralInformation.payoffFirstMortgage ? 'Yes' : 'No'}}
+
SUMMARY.COLLATERAL.SECOND_MORTGAGE
+
{{collateralInformation.hasSecondMortgage | booleanString}}
+
SUMMARY.COLLATERAL.PAYOFF_SECOND_MORTGAGE
+
{{collateralInformation.payoffSecondMortgage ? 'Yes' : 'No'}}
+
SUMMARY.COLLATERAL.IS_BEING_IMPROVED
+
{{collateralInformation.isBeingImproved | booleanString}}
+
SUMMARY.COLLATERAL.IS_LISTED
+
{{collateralInformation.isListedForSale | booleanString}}
+
SUMMARY.COLLATERAL.ON_RENTED_LOT
+
{{collateralInformation.onRentedLot | booleanString}}
+
SUMMARY.COLLATERAL.RENT_PAID
+
{{collateralInformation.rentPaid}}
+
SUMMARY.COLLATERAL.ANNUAL_PROPERTY_TAX
+
{{collateralInformation.annualPropertyTax | currency}}
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.spec.ts new file mode 100644 index 0000000..455aa6b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.spec.ts @@ -0,0 +1,84 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { CollateralSummaryComponent } from './collateral-summary.component'; +import { OccupancyTypePipe } from '../../../../shared/pipes/occupancy-type.pipe'; +import { BooleanStringPipe } from '../../../../shared/pipes/boolean-string.pipe'; +import { UploadedFilesPipe } from 'src/app/shared/pipes/uploaded-files.pipe'; +import { PropertyTypePipe, MfgHomePropertyTypePipe } from '../../../../shared/pipes/property-type.pipe'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CollateralSummaryComponent', () => { + let component: CollateralSummaryComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + CollateralSummaryComponent, + OccupancyTypePipe, + BooleanStringPipe, + PropertyTypePipe, + MfgHomePropertyTypePipe, + UploadedFilesPipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CollateralSummaryComponent); + component = fixture.componentInstance; + component.collateralInformation = { + estimatedValue: 5446, + propertyType: 'Vacant', + hasFirstMortgage: CreditApplicationValues.CHOICE_YES, + hasSecondMortgage: CreditApplicationValues.CHOICE_YES, + payoffFirstMortgage: false, + payoffSecondMortgage: false, + isBeingImproved: CreditApplicationValues.CHOICE_YES, + isListedForSale: CreditApplicationValues.CHOICE_NO, + occupancyType: 'Owner Occupied', + isAddressSame: CreditApplicationValues.CHOICE_YES, + addressLine1: '', + city: '', + state: '', + zipcode: '', + files: [], + firstMortgageBalanceFiles: [], + isUploadingFirstMortgageBalanceFiles: false, + isUploading: false + }; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.ts new file mode 100644 index 0000000..fe56264 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/collateral-summary.component.ts @@ -0,0 +1,40 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { CollateralInformationGroup } from '../../../../shared/credit-application'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; + +@Component({ + selector: 'lw-collateral-summary', + templateUrl: './collateral-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class CollateralSummaryComponent implements OnInit { + + public isMfgHome: boolean; + + @Input() + public title: string; + + @Input() + public collateralInformation: CollateralInformationGroup; + + @Input() + public loanCode: LoanCode; + + @Input() + public isHelocRateMod: boolean; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + public ngOnInit() { + this.isMfgHome = this.loanCode === LoanCode.MANUFACTURED_HOME_LOAN; + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.html new file mode 100644 index 0000000..eeed60e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.html @@ -0,0 +1,64 @@ +
+
+
{{title}} + +
+
+
SUMMARY.EMPLOYER.CURRENT
+
SUMMARY.EMPLOYER.OTHER #{{i}}
+
SUMMARY.EMPLOYER.EMPLOYERNAME
+
{{employer.employerName}}
+
SUMMARY.EMPLOYER.EMPLOYERTYPE
+
{{employer.employmentType | employmentType}}: {{employer.jobTitle}}
+
SUMMARY.EMPLOYER.SALARY
+
{{employer.salary | currency}}
+
Proof of Income document(s)
+
{{employer.files | uploadedFiles}}
+
SUMMARY.EMPLOYER.SCHEDULE
+
{{employer.schedule}} + + ({{employer.weeklyHours}} weekly hours) + +
+
SUMMARY.EMPLOYER.LENGTH
+
{{employer.employmentLengthYears}} year(s), {{employer.employmentLengthMonths}} + month(s) +
+
SUMMARY.EMPLOYER.WORKPHONE
+
{{employer.workPhoneNumber}}
+


+
+ +
+
SUMMARY.EMPLOYER.OTHERINCOME
+
{{otherIncomeInformation[0].amount | currency}}
+
SUMMARY.EMPLOYER.SOURCE
+
{{otherIncomeInformation[0].incomeName}}
+
Proof of Income document(s)
+
{{otherIncomeInformation[0].files | uploadedFiles}}
+


+
SUMMARY.EMPLOYER.OTHERINCOME
+
{{otherIncomeInformation[1].amount | currency}}
+
SUMMARY.EMPLOYER.SOURCE
+
{{otherIncomeInformation[1].incomeName}}
+
Proof of Income document(s)
+
{{otherIncomeInformation[1].files | uploadedFiles}}
+


+
SUMMARY.EMPLOYER.OTHERINCOME
+
{{otherIncomeInformation[2].amount | currency}}
+
SUMMARY.EMPLOYER.SOURCE
+
{{otherIncomeInformation[2].incomeName}}
+
Proof of Income document(s)
+
{{otherIncomeInformation[2].files | uploadedFiles}}
+
+ +


+ +
+
SUMMARY.EMPLOYER.CHILDSUPPORT
+
{{alimonyInformation.childSupportAmount}}
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.spec.ts new file mode 100644 index 0000000..4f50875 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.spec.ts @@ -0,0 +1,92 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { IncomeSummaryComponent } from './income-summary.component'; +import { EmploymentTypePipe } from '../../../../shared/pipes/employment-type.pipe'; +import { PhoneNumberPipe } from '../../../../shared/pipes/phone-number.pipe'; +import { configureTestSuite } from 'ng-bullet'; +import { UploadedFilesPipe } from 'src/app/shared/pipes/uploaded-files.pipe'; + +describe('IncomeSummaryComponent', () => { + let component: IncomeSummaryComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + IncomeSummaryComponent, + EmploymentTypePipe, + PhoneNumberPipe, + UploadedFilesPipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IncomeSummaryComponent); + component = fixture.componentInstance; + component.employmentInformation = [{ + 'employerName': 'NVISIA LLC', + 'employmentType': 'Full time', + 'jobTitle': 'Senior Technical Architect', + 'salary': 23434, + 'schedule': 'Annually', + 'employmentLengthYears': 2, + 'employmentLengthMonths': 2, + 'workPhoneNumber': '1112223333' + }, + { + 'employerName': 'LYFT', + 'employmentType': 'Part time', + 'jobTitle': 'Driver', + 'salary': 15, + 'schedule': 'Hourly', + 'weeklyHours': 12, + 'employmentLengthYears': 4, + 'employmentLengthMonths': 2, + 'workPhoneNumber': '2622225555' + }, + { + 'employerName': 'UBER', + 'employmentType': 'Part time', + 'jobTitle': 'Driver', + 'salary': 20, + 'schedule': 'Hourly', + 'weeklyHours': 10, + 'employmentLengthYears': 5, + 'employmentLengthMonths': 2, + 'workPhoneNumber': '2625554433' + }]; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.ts new file mode 100644 index 0000000..e334fc7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/income-summary.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { EmploymentInformation } from '../../../field-groups/employment/employment-information'; +import { IncomeInformation } from '../../../field-groups/employment/income-information'; +import { AlimonyInformation } from '../../../field-groups/employment/alimony-information'; + +@Component({ + selector: 'lw-income-summary', + templateUrl: './income-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class IncomeSummaryComponent { + + @Input() + public title: string; + + @Input() + public employmentInformation: Array; + + @Input() + public otherIncomeInformation: Array; + + @Input() + public alimonyInformation: AlimonyInformation; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.html new file mode 100644 index 0000000..95d30f1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.html @@ -0,0 +1,30 @@ + +
+
+
{{title}} + +
+
+
SUMMARY.PERSONAL.FULL_NAME
+
{{personalInformation.firstName}} {{personalInformation.middleInitial}} {{personalInformation.lastName}}
+
SUMMARY.PERSONAL.DOB
+
{{personalInformation.dateOfBirth}}
+
SUMMARY.PERSONAL.SSN
+
{{personalInformation.socialSecurityNumber | ssn}}
+
Proof of being a student document(s)
+
{{personalInformation.files | uploadedFiles}}
+
SUMMARY.PERSONAL.PRIMARY_PHONE
+
{{personalInformation.primaryPhoneNumber}} ({{personalInformation.primaryPhoneNumberType | phoneType}})
+
SUMMARY.PERSONAL.SECONDARY_PHONE
+
{{personalInformation.secondaryPhoneNumber}} ({{personalInformation.secondaryPhoneNumberType | phoneType}})
+
SUMMARY.PERSONAL.EMAIL
+
{{personalInformation.email}}
+
SUMMARY.PERSONAL.PRIMARY_APPLICANT_EMAIL
+
{{personalInformation.primaryApplicantEmail}}
+
SUMMARY.PERSONAL.CO_APPLICANT_EMAIL
+
{{personalInformation.coApplicantEmail}}
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.spec.ts new file mode 100644 index 0000000..3841240 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.spec.ts @@ -0,0 +1,73 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { PersonalSummaryComponent } from './personal-summary.component'; +import { SsnPipe } from '../../../../shared/pipes/ssn.pipe'; +import { PhoneTypePipe } from '../../../../shared/pipes/phone-type.pipe'; +import { PhoneNumberPipe } from '../../../../shared/pipes/phone-number.pipe'; +import { configureTestSuite } from 'ng-bullet'; +import { PhoneTypeEnum } from 'src/app/shared/api-models/api-enums'; +import { UploadedFilesPipe } from 'src/app/shared/pipes/uploaded-files.pipe'; + +describe('PersonalSummaryComponent', () => { + let component: PersonalSummaryComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + PersonalSummaryComponent, + SsnPipe, + PhoneTypePipe, + PhoneNumberPipe, + UploadedFilesPipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PersonalSummaryComponent); + component = fixture.componentInstance; + component.personalInformation = { + 'firstName': 'John', + 'middleInitial': 'J', + 'lastName': 'Doe', + 'dateOfBirth': '12021980', + 'socialSecurityNumber': '333-44-5555', + 'primaryPhoneNumberType': PhoneTypeEnum.Mobile, + 'primaryPhoneNumber': '(414) 732-1111', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'john.do@gmail.com' + }; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.ts new file mode 100644 index 0000000..8bd0165 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/personal-summary.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, Output, EventEmitter, Pipe, PipeTransform } from '@angular/core'; +import { PersonalInformation } from '../../../field-groups/personal/personal-information'; + +@Component({ + selector: 'lw-personal-summary', + templateUrl: './personal-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class PersonalSummaryComponent { + + @Input() + public title: string; + + @Input() + public personalInformation: PersonalInformation; + + @Input() + public isHelocRateMod: boolean; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.html new file mode 100644 index 0000000..ba1e322 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.html @@ -0,0 +1,54 @@ +
+
+
{{title}} + +
+
{{title}}
+ + + + +
+
SUMMARY.ADDRESS.ADDRESS_SAME
+
BUTTONS.YES
+
+
+ +
+
SUMMARY.ADDRESS.STREET_ADDRESS
+
{{addressInformation.addressLine1}} +
+ {{addressInformation.addressLine2}} +
+
+ {{addressInformation.city}}, {{addressInformation.state}} {{addressInformation.zipcode}} +
+
+
+
SUMMARY.ADDRESS.TENANT_STATUS
+
{{residenceInformation.residenceStatus | addressStatus}}
+
SUMMARY.ADDRESS.MONTHLY_PAYMENT
+
{{residenceInformation.residenceMonthlyPayment | currency}}
+
SUMMARY.ADDRESS.LENGTH
+
{{residenceInformation.residenceTimeYears}} year(s), {{residenceInformation.residenceTimeMonths}} + month(s) +
+
+ +
+
SUMMARY.ADDRESS.MAILING_ADDRESS
+
{{mailAddressInformation.addressLine1}} +
+ {{mailAddressInformation.addressLine2}} +
+
+ {{mailAddressInformation.city}}, {{mailAddressInformation.state}} {{mailAddressInformation.zipcode}} +
+
+
+ + +
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.spec.ts new file mode 100644 index 0000000..d6e2dfd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.spec.ts @@ -0,0 +1,75 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { ResidenceSummaryComponent } from './residence-summary.component'; +import { AddressStatusPipe } from '../../../../shared/pipes/address-status.pipe'; +import { configureTestSuite } from 'ng-bullet'; + +describe('ResidenceSummaryComponent', () => { + let component: ResidenceSummaryComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + ResidenceSummaryComponent, + AddressStatusPipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ResidenceSummaryComponent); + component = fixture.componentInstance; + component.addressInformation = { + 'addressLine1': '111 E Franklin St', + 'addressLine2': '', + 'zipcode': '53207', + 'city': 'Milwaukee', + 'state': 'WI' + }; + component.residenceInformation = { + 'residenceStatus': '1', + 'residenceMonthlyPayment': 2343, + 'residenceTimeYears': '3', + 'residenceTimeMonths': '6', + 'isMailingAddressSame': true, + }; + component.mailAddressInformation = { + 'addressLine1': '', + 'addressLine2': '', + 'zipcode': '', + 'city': '', + 'state': '' + }; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.ts new file mode 100644 index 0000000..64dd4c6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/residence-summary.component.ts @@ -0,0 +1,49 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { AddressInformation } from '../../../field-groups/address/address-information'; +import {ResidenceInformation} from '../../../field-groups/address/residence-information'; + +@Component({ + selector: 'lw-residence-summary', + templateUrl: './residence-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class ResidenceSummaryComponent { + + @Input() + public title: string; + + @Input() + public addressInformation: AddressInformation; + + @Input() + public priorAddress: boolean; + + @Input() + public mailAddressInformation: AddressInformation; + + @Input() + public residenceInformation: ResidenceInformation; + + @Input() + public priorAddressInformation: AddressInformation; + + @Input() + public priorResidenceInformation: ResidenceInformation; + + @Input() + public hasSameAddressAsSpouse: boolean; + + @Input() + public isHelocRateMod: boolean; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.spec.ts new file mode 100644 index 0000000..0f0162d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.spec.ts @@ -0,0 +1,50 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed, async } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { SummaryInfoPage } from './summary-info-page'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Summary Info Page', () => { + let page: SummaryInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new SummaryInfoPage('PAGETITLES.COMMON.SUMMARY', 'cc-summary', []), + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should be invalid if there is no credit application in state', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(null); + const isValid = page.isValidPage(applicationWizardService, creditApplicationService); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect (isValid.length).toBeGreaterThan(0); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.ts new file mode 100644 index 0000000..a2b540f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-info-page.ts @@ -0,0 +1,15 @@ +import { ApplicationPage } from '../../../application-page'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +export class SummaryInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + if (applicationWizardService.getFullCreditApplication()) { + return []; + } else { + return ['ERROR']; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.html new file mode 100644 index 0000000..5006a1e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.html @@ -0,0 +1,197 @@ +

SUMMARY.PRIMARY_HEADING

+
+
COPY.SUMMARY
+ + + + + + + + + + + + + + + + + + + +

SUMMARY.PRIMARY_SPOUSE_HEADING

+
+ + + + +
+ + +

SUMMARY.PRIMARY_SPOUSE_HEADING

+
+ + + + + + +
+ + +

SUMMARY.CO_APP_HEADING

+
+ + + + +
+ + +
+
+ + +
+ + +

SUMMARY.CO_APP_SPOUSE_HEADING

+
+ + + + +
+ + +

SUMMARY.CO_APP_SPOUSE_HEADING

+
+ + + + + + +
+ + +

SUMMARY.ADDITIONAL_INFO

+
+
+ + +
+
+
+ +

SUMMARY.NOTE_HEADING

+
+
+ +
+ +
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.scss new file mode 100644 index 0000000..44d8df5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.scss @@ -0,0 +1,3 @@ +.summary-card-field-title { + font-weight: bold; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.spec.ts new file mode 100644 index 0000000..3b77a1d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.spec.ts @@ -0,0 +1,120 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SummaryPageComponent } from './summary-page.component'; +import { SummaryPageService } from './summary-page.service'; +import { ApplicationSummaryComponent } from './application-summary.component'; +import { PersonalSummaryComponent } from './personal-summary.component'; +import { ResidenceSummaryComponent } from './residence-summary.component'; +import { SsnPipe } from '../../../../shared/pipes/ssn.pipe'; +import { EmploymentTypePipe } from '../../../../shared/pipes/employment-type.pipe'; +import { AddressStatusPipe } from '../../../../shared/pipes/address-status.pipe'; +import { PhoneNumberPipe } from '../../../../shared/pipes/phone-number.pipe'; +import { PhoneTypePipe } from '../../../../shared/pipes/phone-type.pipe'; +import { CollateralSummaryComponent } from './collateral-summary.component'; +import { BooleanStringPipe } from '../../../../shared/pipes/boolean-string.pipe'; +import { OccupancyTypePipe } from '../../../../shared/pipes/occupancy-type.pipe'; +import { PropertyTypePipe, MfgHomePropertyTypePipe } from '../../../../shared/pipes/property-type.pipe'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; +import { NoteFormComponent } from '../../../field-groups/summary/note-form.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { UploadedFilesPipe } from 'src/app/shared/pipes/uploaded-files.pipe'; + +describe('SummaryPageComponent', () => { + let component: SummaryPageComponent; + let fixture: ComponentFixture; + let service: SummaryPageService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientModule, + TranslateModule.forRoot() + ], + declarations: [ + ApplicationSummaryComponent, + PersonalSummaryComponent, + CollateralSummaryComponent, + SummaryPageComponent, + SsnPipe, + EmploymentTypePipe, + AddressStatusPipe, + PhoneNumberPipe, + PhoneTypePipe, + BooleanStringPipe, + OccupancyTypePipe, + PropertyTypePipe, + MfgHomePropertyTypePipe, + UploadedFilesPipe, + NoteFormComponent + ], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SummaryPageComponent); + component = fixture.componentInstance; + service = TestBed.get(SummaryPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show all additional residences', async () => { + fixture.detectChanges(); + const preChangeResidence = fixture.debugElement.queryAll(By.css('lw-residence-summary')); + expect(preChangeResidence).toBeTruthy(); + expect(preChangeResidence.length).toEqual(1); + expect(component.creditApplication.fullInfoBorrowers[0].priorAddressInformation).toBeFalsy(); + const newApplication = Object.assign({}, component.creditApplication); + newApplication.fullInfoBorrowers[0].priorAddressInformation = [{ + 'addressLine1': '526 E Lincoln Ave', + 'addressLine2': '', + 'zipcode': '53207', + 'city': 'Milwaukee', + 'state': 'WI' + }]; + newApplication.fullInfoBorrowers[0].priorResidenceInformation = [{ + 'isMailingAddressSame': true, + 'residenceStatus': '1', + 'residenceMonthlyPayment': 2343, + 'residenceTimeYears': '1', + 'residenceTimeMonths': '6', + }]; + component.creditApplication = newApplication; + fixture.detectChanges(); + // Logged bug #919. When that is resolved this test should pass. + // const postChangeResidence = fixture.debugElement.queryAll(By.css('lw-residence-summary')); + // expect(postChangeResidence.length).toEqual(2); + expect(component.creditApplication.fullInfoBorrowers[0].priorAddressInformation.length).toEqual(1); + }); + + it('should not allow user to submit if the note has invalid symbols/text', () => { + const input = fixture.debugElement.query(By.css('#inputNote')).nativeElement; + const invalidString = 'string that is invalid !@#$%^&*()'; + input.value = invalidString; + expect(component.localState.nextDisabled).toBe(true); + const inputEvent = document.createEvent('Event'); + inputEvent.initEvent('input', false, true); + input.dispatchEvent(inputEvent); + fixture.detectChanges(); + expect(component.localState.nextDisabled).toBe(true); + expect(component.noteGroup.value.note).toBe(invalidString); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.ts new file mode 100644 index 0000000..4f9f403 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.component.ts @@ -0,0 +1,115 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { SummaryPageService } from './summary-page.service'; +import { ActivatedRoute } from '@angular/router'; +import { CreditApplication, Individual, TattleIndividual } from '../../../../shared/credit-application'; +import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { SelectOptions } from '../../../../shared/options/select-options'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; + +@Component({ + selector: 'lw-summary-page', + templateUrl: './summary-page.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class SummaryPageComponent implements OnInit, OnDestroy { + + public creditApplication: CreditApplication; + public primaryBorrower: Individual; + public primaryBorrowerTattleSpouse?: TattleIndividual; + public primaryBorrowerFullInfoSpouse?: Individual; + public coBorrower?: Individual; + public coBorrowerTattleSpouse?: TattleIndividual; + public coBorrowerFullInfoSpouse?: Individual; + public primaryContact: Array = []; + public modFee: Array<{"value" : any, "desc" : string}> = []; + public pageForm: FormGroup; + public noteGroup: FormGroup; + public additionalInfoGroup: FormGroup; + public statusSubscription: Subscription; + public readonly FORM_STATE_INVALID = 'INVALID'; + + public savingData = null; + + public localState = { + nextDisabled: true, + hasCoApplicant: false, + hasCoApplicantTattleSpouse: false, + hasCoApplicantFullInfoSpouse: false, + hasPrimaryTattleSpouse: false, + hasPrimaryFullInfoSpouse: false, + isHelocRateMod: false, + isTrailerMotorhome: false, + isPowerSport: false, + isVehicleDefault: false, + requireFullCollateral: false, + isBoat: false + }; + + constructor( + private summaryPageService: SummaryPageService, + private route: ActivatedRoute) { } + + public ngOnInit() { + this.pageForm = this.summaryPageService.createPageGroup(); + this.noteGroup = this.pageForm.get(SummaryPageService.NOTE_FORM_NAME); + this.additionalInfoGroup = this.pageForm.get(SummaryPageService.ADDITIONAL_INFO_FORM_NAME); + this.creditApplication = this.summaryPageService.getCreditApplication(); + this.updateIndividuals(this.creditApplication); + this.primaryContact = this.summaryPageService.buildPrimaryContactMethodList(this.primaryBorrower); + this.modFee = SelectOptions.modFeeOptions; + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.localState.isHelocRateMod = this.summaryPageService.equalsLoanType(LoanTitle.HELOC_RateMod); + this.localState.isTrailerMotorhome = this.summaryPageService.equalsLoanType(LoanTitle.TrailerMotorhome); + this.localState.isPowerSport = this.summaryPageService.equalsLoanType(LoanTitle.PowerSport); + this.localState.isVehicleDefault = this.summaryPageService.equalsLoanType(LoanTitle.Vehicle); + this.localState.isBoat = this.summaryPageService.equalsLoanType(LoanTitle.Boat); + this.localState.requireFullCollateral = this.summaryPageService.requireFullCollateral(); + } + + public ngOnDestroy() { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public updateIndividuals(creditApplication: CreditApplication) { + this.primaryBorrower = creditApplication.fullInfoBorrowers[0]; + if (this.primaryBorrower.fullInfoSpouse) { + this.localState.hasPrimaryFullInfoSpouse = true; + this.primaryBorrowerFullInfoSpouse = this.primaryBorrower.fullInfoSpouse; + } else if (this.primaryBorrower.tattleSpouse) { + this.localState.hasPrimaryTattleSpouse = true; + this.primaryBorrowerTattleSpouse = this.primaryBorrower.tattleSpouse; + } + if (creditApplication.fullInfoBorrowers.length === 2) { + this.localState.hasCoApplicant = true; + this.coBorrower = creditApplication.fullInfoBorrowers[1]; + if (this.coBorrower.fullInfoSpouse) { + this.localState.hasCoApplicantFullInfoSpouse = true; + this.coBorrowerFullInfoSpouse = this.coBorrower.fullInfoSpouse; + } else if (this.coBorrower.tattleSpouse) { + this.localState.hasCoApplicantTattleSpouse = true; + this.coBorrowerTattleSpouse = this.coBorrower.tattleSpouse; + } + } + } + + public commandGoBack(newLoc: string) { + this.summaryPageService.navigateBack(newLoc, this.route); + } + + public commandSubmit() { + if (this.localState.nextDisabled) { + return; + } + this.summaryPageService.saveAndNavigate(this.noteGroup.value, this.additionalInfoGroup.value, this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.spec.ts new file mode 100644 index 0000000..468673a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.spec.ts @@ -0,0 +1,80 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { SummaryPageService } from './summary-page.service'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { Individual } from 'src/app/shared/credit-application'; +import { apiTranslatorPreTranslate } from 'src/app/testing/shared-mocks/api-translator-pre-post'; +import { vehicleCreditApplication } from 'src/app/testing/shared-mocks/vehicle-credit-application.mock'; + +describe('Summary Page Service', () => { + + let service: SummaryPageService; + let applicationWizardService: ApplicationWizardService; + const mockIndividual: Individual = apiTranslatorPreTranslate.fullInfoBorrowers[0]; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(SummaryPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should get credit application information from application wizard service', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.callThrough(); + service.getCreditApplication(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + }); + + it('should navigate back to a previous route', () => { + spyOn(router, 'navigate').and.callThrough().and.returnValue(true); + const route = new ActivatedRoute(); + service.navigateBack('residence', route); + expect(router.navigate).toHaveBeenCalledWith(['../step-3'], { relativeTo: route }); + }); + + it('should build primary contact method list', () => { + const list = service.buildPrimaryContactMethodList(mockIndividual); + expect(list).toEqual(['primary@example.com', '(555) 555-0134']); + }); + + it('should NOT show vehicle condition when loantype is NOT vehicle', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const condition = service.showVehicleCondition(); + expect(condition).toBe(false); + }); + + it('should show vehicle condition when vehicle type is automobile', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(vehicleCreditApplication); + const condition = service.showVehicleCondition(); + expect(condition).toBe(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.ts new file mode 100644 index 0000000..e13f19a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/summary-page.service.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CreditApplication, Individual } from '../../../../shared/credit-application'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import { FormatService } from 'src/app/shared/pipes/format.service'; + +@Injectable({ + providedIn: 'root' +}) + +export class SummaryPageService { + + public static readonly NOTE_FORM_NAME = 'noteGroup'; + public static readonly ADDITIONAL_INFO_FORM_NAME = 'additionalInfoGroup'; + public summaryLinks = { + loan: '../step-1', + personal: '../step-2', + residence: '../step-3', + employment: '../step-4', + coApplicantPersonal: '../co-applicant-personal', + coApplicantResidence: '../co-applicant-residential', + coApplicantFinancial: '../co-applicant-financial', + collateral: '../eq-collateral', + vehicle: '../ve-collateral', + applicantSpouse: '../applicant-spouse', + coApplicantSpouse: '../co-applicant-spouse', + applicantSpousePersonal: '../applicant-spouse-personal', + applicantSpouseResidential: '../applicant-spouse-residential', + applicantSpouseFinancial: '../applicant-spouse-financial', + coApplicantSpousePersonal: '../co-applicant-spouse-personal', + coApplicantSpouseResidential: '../co-applicant-spouse-residential', + coApplicantSpouseFinancial: '../co-applicant-spouse-financial' + }; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private router: Router, + private formBuilder: FormBuilder, + private formatService: FormatService) { } + + public getCreditApplication(): CreditApplication { + return this.applicationWizardService.getFullCreditApplication(); + } + + public saveAndNavigate(noteData: any, additionalInfoData: any, route: ActivatedRoute) { + this.creditApplicationService.saveSummaryInformation(additionalInfoData, noteData).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(route.routeConfig.path); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(route.routeConfig.path); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public navigateBack(newLoc: string, route: ActivatedRoute) { + const targetRoute = this.summaryLinks[newLoc]; + if (!targetRoute) { + throw new Error('Unknown location key ' + newLoc); + } + this.router.navigate([targetRoute], { relativeTo: route }); + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const noteGroup = this.formBuilder.group({}); + const additionalInfoGroup = this.formBuilder.group({}); + CommonGroups.SUMMARY_NOTE.fields.forEach((eachField) => { + noteGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + CommonGroups.SUMMARY_ADDITIONAL_INFO.fields.forEach((eachField) => { + additionalInfoGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + group.addControl(SummaryPageService.NOTE_FORM_NAME, noteGroup); + group.addControl(SummaryPageService.ADDITIONAL_INFO_FORM_NAME, additionalInfoGroup); + return group; + } + + public showVehicleCondition(): boolean { + const loanInfo = this.getCreditApplication(); + if (loanInfo.loanType.category === LoanCategory.Vehicle) { + switch (loanInfo.vehicleInformation.type) { + case VehicleType.AUTO: + case VehicleType.CYCLE: { + return true; + } + default: { + return false; + } + } + } + return false; + } + + public buildPrimaryContactMethodList(primaryBorrower: Individual): Array { + const primaryContactMethod: Array = []; + primaryContactMethod.push(primaryBorrower.personalInformation.email); + primaryContactMethod.push(primaryBorrower.personalInformation.primaryPhoneNumber); + + if (primaryBorrower.personalInformation.secondaryPhoneNumber) { + primaryContactMethod.push(primaryBorrower.personalInformation.secondaryPhoneNumber); + } + + if (primaryBorrower.employmentInformation) { + for (let i = 0; i < primaryBorrower.employmentInformation.length; i++) { + if (primaryBorrower.employmentInformation[i].workPhoneNumber) { + primaryContactMethod.push(primaryBorrower.employmentInformation[i].workPhoneNumber); + } + } + } + return primaryContactMethod; + } + + public equalsLoanType(loanType): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === loanType; + } + + public requireFullCollateral(): boolean { + const loanInfo = this.getCreditApplication(); + const loanId = loanInfo.loanType.id; + return !( loanId === 'boat' || loanId === 'pwc' || loanId === 'tmh'); + } + + public formatState(state: string): string { + return this.formatService.formatState(state); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.html new file mode 100644 index 0000000..106a373 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.html @@ -0,0 +1,14 @@ + +
+
+
SUMMARY.TITLES.TATTLE + +
+
+
SUMMARY.PERSONAL.FULL_NAME
+
{{personalInformation.firstName}} {{personalInformation.middleInitial}} {{personalInformation.lastName}}
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.spec.ts new file mode 100644 index 0000000..a3dcaa3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.spec.ts @@ -0,0 +1,57 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { TattleSpouseSummaryComponent } from './tattle-spouse-summary.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Tattle Spouse Summary Component', () => { + let component: TattleSpouseSummaryComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + TattleSpouseSummaryComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TattleSpouseSummaryComponent); + component = fixture.componentInstance; + component.personalInformation = { + firstName: 'John', + middleInitial: 'J', + lastName: 'Doe', + }; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.ts new file mode 100644 index 0000000..24b5b8b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/tattle-spouse-summary.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { TattleIndividual } from '../../../../shared/credit-application'; +import { TattleSpousePersonalInformation } from '../../../field-groups/tattle-spouse/tattle-spouse-personal-information'; + +@Component({ + selector: 'lw-tattle-spouse-summary', + templateUrl: './tattle-spouse-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) +export class TattleSpouseSummaryComponent { + + @Input() + public personalInformation: TattleSpousePersonalInformation; + + @Output() + public requestToEdit: EventEmitter; + + constructor() { + this.requestToEdit = new EventEmitter(); + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.html new file mode 100644 index 0000000..6a5effb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.html @@ -0,0 +1,110 @@ + +
+
+
SUMMARY.TITLES.VEHICLE + +
+
+
SUMMARY.VEHICLE.TYPE
+ +
+
SUMMARY.VEHICLE.CONDITION
+
{{vehicleInformation.condition}}
+
SUMMARY.VEHICLE.MODEL
+
+ {{vehicleInformation.year}} {{vehicleInformation.make}} {{vehicleInformation.model}} +
+
SUMMARY.VEHICLE.MILEAGE
+
{{vehicleInformation.mileage}}
+ +
SUMMARY.VEHICLE.VALUE
+
{{vehicleInformation.value | currency}}
+ +
{{isPowerSport ? "FORMS.VEHICLE.VIN_POWERSPORT" : isBoat ? "FORMS.VEHICLE.BOAT_HULL" : "FORMS.VEHICLE.VIN"}}
+
{{vehicleInformation.vin}}
+ +
FORMS.VEHICLE.BOAT_LENGTH
+
{{vehicleInformation.boatLength}} feet
+ +
FORMS.VEHICLE.STATE_TITLED
+
{{localState.stateTitled}}
+ +
FORMS.VEHICLE.COUNTY_TITLED
+
{{vehicleInformation.countyTitled}}
+ +
FORMS.VEHICLE.LOAN_PURPOSE
+
{{localState.loanPurpose}}
+ +
SUMMARY.VEHICLE.TERM
+
{{vehicleInformation.term}} months
+ +
DNR Registration Card document(s)
+
{{vehicleInformation.dnrRegistrationCardFiles | uploadedFiles}}
+ +
FORMS.VEHICLE.BOAT_SAME_ADDRESS
+
{{vehicleInformation.boatSameAddress}}
+ +
SUMMARY.VEHICLE.BOAT_ADDRESS
+
{{vehicleInformation.boatAddress.addressLine1}} +
+ {{vehicleInformation.boatAddress.addressLine2}} +
+
+ {{vehicleInformation.boatAddress.city}}, {{vehicleInformation.boatAddress.state}} {{vehicleInformation.boatAddress.zipcode}} +
+ +
FORMS.VEHICLE.TRANSFERRING
+
{{vehicleInformation.transferring}}
+ +
FORMS.VEHICLE.LICENSE_PLATE_NUMBER
+
{{vehicleInformation.licensePlateNumber}}
+ +
{{isBoat ? "FORMS.VEHICLE.BOAT_SELLER" : "SUMMARY.VEHICLE.SELLER"}}
+
{{vehicleInformation.seller | vehicleSeller}}
+ +
FORMS.VEHICLE.TITLE_BRANDED
+
{{vehicleInformation.titleBranded}}
+ +
FORMS.VEHICLE.LIEN
+
{{vehicleInformation.lien}}
+ +
FORMS.VEHICLE.DEALER_NAME
+
{{vehicleInformation.dealerName}}
+ +
FORMS.VEHICLE.DEALER_PHONE
+
{{vehicleInformation.dealerPhone}}
+ +
Boat Title document(s)
+
{{vehicleInformation.boatTitleFiles | uploadedFiles}}
+ +
Purchase Contract document(s)
+
{{vehicleInformation.purchaseContractFiles | uploadedFiles}}
+ +
FORMS.VEHICLE.TRAILER_YEAR
+
{{vehicleInformation.trailerYear}}
+ +
FORMS.VEHICLE.TRAILER_MAKE
+
{{vehicleInformation.trailerMake}}
+ +
FORMS.VEHICLE.TRAILER_TYPE
+
{{localState.trailerType}}
+ +
Trailer Title document(s)
+
{{vehicleInformation.trailerTitleFiles | uploadedFiles}}
+ +
FORMS.VEHICLE.MOTOR_YEAR
+
{{vehicleInformation.motorYear}}
+ +
FORMS.VEHICLE.MOTOR_MAKE
+
{{vehicleInformation.motorMake}}
+ +
FORMS.VEHICLE.MOTOR_MODEL
+
{{vehicleInformation.motorModel}}
+ +
FORMS.VEHICLE.MOTOR_TYPE
+
{{localState.motorType}}
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.spec.ts new file mode 100644 index 0000000..87e2649 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.spec.ts @@ -0,0 +1,88 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { OccupancyTypePipe } from '../../../../shared/pipes/occupancy-type.pipe'; +import { BooleanStringPipe } from '../../../../shared/pipes/boolean-string.pipe'; +import { PropertyTypePipe } from '../../../../shared/pipes/property-type.pipe'; +import { VehicleSummaryComponent } from './vehicle-summary.component'; +import { VehicleSellerPipe } from '../../../../shared/pipes/vehicle-collateral.pipe'; +import { VehicleTypePipe } from '../../../../shared/pipes/vehicle-type.pipe'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; +import { UploadedFilesPipe } from 'src/app/shared/pipes/uploaded-files.pipe'; + +describe('VehicleSummaryComponent', () => { + let component: VehicleSummaryComponent; + let fixture: ComponentFixture; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + VehicleSummaryComponent, + OccupancyTypePipe, + BooleanStringPipe, + PropertyTypePipe, + VehicleSellerPipe, + VehicleTypePipe, + UploadedFilesPipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VehicleSummaryComponent); + component = fixture.componentInstance; + component.vehicleInformation = { + value: 5446, + condition: 'New', + type: VehicleType.AUTO, + year: 2015, + make: 'Honda', + model: 'Fit', + mileage: 65000, + term: '36', + loanPurpose: LoanPurpose.Refinance_Vehicle, + seller: 'Dealer', + vin: '00000000' + }; + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should emit an event when the edit button is clicked', () => { + fixture.detectChanges(); + let eventHappened = false; + component.requestToEdit.subscribe((event) => { + eventHappened = true; + }); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(eventHappened).toEqual(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.ts new file mode 100644 index 0000000..bf1912c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/summary/vehicle-summary.component.ts @@ -0,0 +1,63 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { VehicleOptions } from 'src/app/shared/options/vehicle-options'; +import { VehicleCollateralInformation } from '../../../field-groups/vehicle/vehicle-collateral-information'; +import { SummaryPageService } from './summary-page.service'; + +@Component({ + selector: 'lw-vehicle-summary', + templateUrl: './vehicle-summary.component.html', + styleUrls: ['./summary-page.component.scss'] +}) + +export class VehicleSummaryComponent implements OnInit { + + public localState = { + showVehicleCondition: false, + motorType: '', + trailerType: '', + stateTitled: '', + loanPurpose: '' + }; + + @Input() + public vehicleInformation: VehicleCollateralInformation; + + @Output() + public requestToEdit: EventEmitter; + + @Input() + public isTrailerMotorhome: boolean; + + @Input() + public isPowerSport: boolean; + + @Input() + public isVehicleDefault: boolean; + + @Input() + public isBoat: boolean; + + @Input() + public requireFullCollateral: boolean; + + constructor(private summaryPageService: SummaryPageService) { + this.requestToEdit = new EventEmitter(); + } + + public ngOnInit() { + this.updateLocalState(); + this.localState.motorType = this.vehicleInformation.motorType ? VehicleOptions.boatMotorTypeOptions.filter(x => x.value == this.vehicleInformation.motorType)[0].desc : ''; + this.localState.trailerType = this.vehicleInformation.trailerType ? VehicleOptions.boatTrailerTypeOptions.filter(x => x.value == this.vehicleInformation.trailerType)[0].desc : ''; + this.localState.loanPurpose = this.vehicleInformation.loanPurpose ? VehicleOptions.vehicleLoanPurposeOptionsVehicle.filter(x => x.value == this.vehicleInformation.loanPurpose)[0].desc : ''; + this.localState.stateTitled = this.vehicleInformation.stateTitled ? this.summaryPageService.formatState(this.vehicleInformation.stateTitled) : ''; + } + + public commandRequestEdit() { + this.requestToEdit.emit('edit'); // @TODO This may be an id later. + } + + public updateLocalState() { + this.localState.showVehicleCondition = this.summaryPageService.showVehicleCondition(); + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.spec.ts new file mode 100644 index 0000000..8d70e24 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.spec.ts @@ -0,0 +1,68 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoBorrowerTattleSpouseInfoPage } from './co-borrower-tattle-spouse-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CoBorrower Tattle Spouse Info Page', () => { + + let page: CoBorrowerTattleSpouseInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new CoBorrowerTattleSpouseInfoPage('PAGETITLES.COMMON.COAPP_TATTLESPOUSE', + 'co-applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO, + CommonGroups.IS_ADDRESS_SAME_INFO + ]); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.ts new file mode 100644 index 0000000..bd4b4a4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page.ts @@ -0,0 +1,15 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { PrimaryTattleSpouseInfoPage } from './primary-tattle-spouse-info-page'; +import { SpouseType } from 'src/app/shared/credit-application'; + +export class CoBorrowerTattleSpouseInfoPage extends PrimaryTattleSpouseInfoPage { + + public getBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER; + } + + public getSpouseBorrowerType(): BorrowerType { + return BorrowerType.CO_BORROWER_SPOUSE; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.spec.ts new file mode 100644 index 0000000..b04087d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.spec.ts @@ -0,0 +1,93 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../../../shared/loan-type'; +import { CommonGroups } from '../../../../field-groups/common-groups'; +import { TranslateModule } from '@ngx-translate/core'; +import { PrimaryTattleSpouseInfoPage } from './primary-tattle-spouse-info-page'; +import { SpouseType } from '../../../../../shared/credit-application'; +import { BorrowerType } from '../../../../../shared/borrower-type'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +describe('Primary Tattle Spouse Info Page', () => { + + let page: PrimaryTattleSpouseInfoPage; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new PrimaryTattleSpouseInfoPage('PAGETITLES.COMMON.PRIMARY_TATTLESPOUSE', + 'applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO, + CommonGroups.IS_ADDRESS_SAME_INFO + ]); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + + it('should determine that a page is required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.TattleSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(true); + }); + + it('should determine that a page is NOT required', () => { + spyOn(applicationWizardService, 'isApplicantNonSpouse').and.callThrough().and.returnValue(true); + spyOn(applicationWizardService, 'getSpouseType').and.callThrough().and.returnValue(SpouseType.FullInfoSpouse); + + const isRequired: boolean = page.determinePageRequired(applicationWizardService); + expect(applicationWizardService.isApplicantNonSpouse).toHaveBeenCalled(); + expect(applicationWizardService.getSpouseType).toHaveBeenCalled(); + expect(isRequired).toBe(false); + }); + + + it('should always return primary borrower type', () => { + spyOn(page, 'getBorrowerType').and.callThrough(); + const borrowerType = page.getBorrowerType(); + expect(page.getBorrowerType).toHaveBeenCalled(); + expect(borrowerType).toBe(BorrowerType.PRIMARY_BORROWER); + }); + + it('should always return spouse borrower type', () => { + spyOn(page, 'getSpouseBorrowerType').and.callThrough(); + const borrowerType = page.getSpouseBorrowerType(); + expect(page.getSpouseBorrowerType).toHaveBeenCalled(); + expect(borrowerType).toBe(BorrowerType.PRIMARY_BORROWER_SPOUSE); + }); + + it('should correctly determine if page is valid ( default invalid )', () => { + spyOn(page, 'isValidPage').and.callThrough(); + const errors = page.isValidPage(applicationWizardService, creditApplicationService); + expect(page.isValidPage).toHaveBeenCalled(); + expect(errors.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.ts new file mode 100644 index 0000000..90fe301 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/pages/primary-tattle-spouse-info-page.ts @@ -0,0 +1,44 @@ +import { ApplicationWizardService } from '../../../../application-wizard.service'; +import { ApplicationPage } from '../../../../application-page'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { TattleSpousePersonalInformation } from '../../../../field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { Individual, SpouseType } from '../../../../../shared/credit-application'; +import { CreditApplicationService } from '../../../../../shared/credit-application.service'; + +export class PrimaryTattleSpouseInfoPage extends ApplicationPage { + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + const spouseType = applicationWizardService.getSpouseType(this.getBorrowerType()); + const needsSpouse = applicationWizardService.isApplicantNonSpouse(this.getBorrowerType()); + if (!spouseType && needsSpouse) { + // This condition gives us a nice default topic message for the NEXT button + return true; + } + return needsSpouse && spouseType === SpouseType.TattleSpouse; + } + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const borrowerType = this.getSpouseBorrowerType(); + + const personalInfo: TattleSpousePersonalInformation = + applicationWizardService.getIndividualGroupValues('personalInformation', borrowerType); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = personalInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + + public getBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER; + } + + public getSpouseBorrowerType(): BorrowerType { + return BorrowerType.PRIMARY_BORROWER_SPOUSE; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.html new file mode 100644 index 0000000..92140f4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.html @@ -0,0 +1,35 @@ +
+
+
+

HEADINGS.SPOUSE.{{localState.borrowerType}}

+
+ +
COPY.SPOUSE.{{localState.borrowerType}}
+
+
+
+ + + + + + + + +
+
+ + +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.spec.ts new file mode 100644 index 0000000..645287a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.spec.ts @@ -0,0 +1,153 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { TattleSpouseInfoPageComponent } from './tattle-spouse-info-page.component'; +import { TattleSpouseInfoPageService } from './tattle-spouse-info-page.service'; +import { TattleIndividual } from '../../../../shared/credit-application'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressInformation } from 'src/app/loan-wizard/field-groups/address/address-information'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + +describe('TattleSpouseInfoPageComponent', () => { + let component: TattleSpouseInfoPageComponent; + let fixture: ComponentFixture; + let service: TattleSpouseInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + TattleSpouseInfoPageComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TattleSpouseInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(TattleSpouseInfoPageService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.isAddressSameGroup).toBeTruthy(); + expect(component.addressInfoGroup).not.toBeTruthy(); + }); + + it('should not allow progress if invalid', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.localState.nextDisabled).toEqual(true); + spyOn(service, 'saveAndNavigate').and.callThrough(); + component.commandNext(); + expect( service.saveAndNavigate).not.toHaveBeenCalled(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should patch previous values onInit', () => { + const tattleSpouseInfo: TattleIndividual = creditApplicationService.generateDefaultTattleSpouse(); + spyOn(service, 'getApplicationGroupValues').and.returnValue(tattleSpouseInfo); + fixture.detectChanges(); + expect(component.personalInfoGroup.value).toEqual(tattleSpouseInfo.personalInformation); + expect(component.isAddressSameGroup.value).toEqual({isAddressSame: CreditApplicationValues.CHOICE_YES}); + }); + + it('should add address group', () => { + const tattleSpouseInfo: TattleIndividual = creditApplicationService.generateDefaultTattleSpouse(); + tattleSpouseInfo.hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_NO; + spyOn(service, 'getApplicationGroupValues').and.returnValue(tattleSpouseInfo); + fixture.detectChanges(); + expect(component.addressInfoGroup.value).toEqual(tattleSpouseInfo.addressInformation); + expect(component.personalInfoGroup.value).toEqual(tattleSpouseInfo.personalInformation); + expect(component.isAddressSameGroup.value).toEqual({isAddressSame: CreditApplicationValues.CHOICE_NO}); + }); + + it('should submit the USPS Verified Address Value if the match was saved', () => { + spyOn(service, 'saveAndNavigate'); + fixture.detectChanges(); + component.isAddressSameGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Alston', + state: 'MA', + zipcode: '54545', + }; + const mockUSPSAddress: AddressInformation = { + addressLine1: mockAddressInput.addressLine1.toLocaleUpperCase(), + addressLine2: mockAddressInput.addressLine2.toLocaleUpperCase(), + city: mockAddressInput.city.toLocaleUpperCase(), + state: mockAddressInput.state.toLocaleUpperCase(), + zipcode: mockAddressInput.zipcode, + zipcodeExt: '5446' + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + component.confirmMailingAddressInformation(mockUSPSAddress); + const fields = component.makeTattleIndividual(); + expect(fields.addressInformation).toEqual(mockUSPSAddress); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.ts new file mode 100644 index 0000000..20e5ca9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component.ts @@ -0,0 +1,172 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { TattleSpouseInfoPageService } from './tattle-spouse-info-page.service'; +import { TattleIndividual } from '../../../../shared/credit-application'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { TattleSpousePersonalInformation } from '../../../field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { AddressInformation } from '../../../field-groups/address/address-information'; +import { AddressFormBuilderService } from 'src/app/shared/address-validation/address-form-builder.service'; +import { AddressDiffInformation } from 'src/app/loan-wizard/field-groups/address-compare/address-compare-information'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; + +@Component({ + selector: 'lw-tattle-spouse-info-page', + templateUrl: './tattle-spouse-info-page.component.html' +}) +export class TattleSpouseInfoPageComponent implements OnInit, OnDestroy { + + public pageForm: FormGroup; + public isAddressSameGroup: FormGroup; + public addressInfoGroup: FormGroup; + public personalInfoGroup: FormGroup; + public mailingAddressVerifyInfoGroup: FormGroup; + private INDIVIDUAL_REF: BorrowerType; + private readonly FORM_STATE_INVALID = 'INVALID'; + private valueSubscription: Subscription; + private statusSubscription: Subscription; + + public savingData = null; + + public localState = { + showAddress: false, + nextDisabled: true, + previousDisabled: false, + nextTopic: '', + borrowerType: '', + mailingAddressInformation: null, + showSpouseDisclosure: true + }; + + constructor( + private tattleSpouseInfoPageService: TattleSpouseInfoPageService, + private route: ActivatedRoute) { } + + public ngOnDestroy() { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public ngOnInit() { + if (this.route.snapshot.data[0]) { + this.INDIVIDUAL_REF = this.route.snapshot.data[0].individual; + } else { + this.INDIVIDUAL_REF = BorrowerType.PRIMARY_BORROWER_SPOUSE; + } + this.localState.borrowerType = this.INDIVIDUAL_REF; + this.pageForm = this.tattleSpouseInfoPageService.createPageGroup(); + this.isAddressSameGroup = this.pageForm.get(TattleSpouseInfoPageService.IS_SAME_ADDRESS_FORM_NAME); + this.personalInfoGroup = this.pageForm.get(TattleSpouseInfoPageService.PERSONAL_FORM_NAME); + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.patchPreviousValues(); + } + + public patchPreviousValues() { + const previousValues: TattleIndividual = this.tattleSpouseInfoPageService.getApplicationGroupValues(this.INDIVIDUAL_REF); + if (previousValues.hasSameAddressAsSpouse) { + this.isAddressSameGroup.patchValue({isAddressSame: previousValues.hasSameAddressAsSpouse}); + } + this.personalInfoGroup.patchValue(previousValues.personalInformation); + this.updateLocalState(this.pageForm.value); + } + + public updateLocalState(modelValue: any): void { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + + const value = modelValue[TattleSpouseInfoPageService.IS_SAME_ADDRESS_FORM_NAME]; + this.localState = { + showAddress: this.tattleSpouseInfoPageService.getRequiredAddress(value.isAddressSame), + previousDisabled: this.localState.previousDisabled, + nextDisabled: this.localState.nextDisabled, + nextTopic: this.tattleSpouseInfoPageService.getNextTopicMessage(this.INDIVIDUAL_REF), + borrowerType: this.localState.borrowerType, + mailingAddressInformation: this.localState.mailingAddressInformation ? + this.localState.mailingAddressInformation : + this.tattleSpouseInfoPageService.getApplicationGroupValues( + this.INDIVIDUAL_REF)['addressInformation'], + showSpouseDisclosure: this.localState.borrowerType === BorrowerType.PRIMARY_BORROWER_SPOUSE || + this.localState.borrowerType === BorrowerType.CO_BORROWER_SPOUSE + }; + this.updateAddressesForm(); + this.valueSubscription = this.pageForm.valueChanges.subscribe(() => { + this.updateLocalState(this.pageForm.value); + }); + } + + public confirmMailingAddressInformation(addressInformation: AddressInformation) { + if (!this.mailingAddressVerifyInfoGroup) { + AddressFormBuilderService.addAddressCompareGroupIfMissing( + this.pageForm, TattleSpouseInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME + ); + this.mailingAddressVerifyInfoGroup = this.pageForm.get(TattleSpouseInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME); + } + if (!addressInformation) { + this.mailingAddressVerifyInfoGroup.patchValue({addressVerifyStatus: false}); + } else { + this.mailingAddressVerifyInfoGroup.patchValue({addressVerifyStatus: true}); + const addressDiff: AddressDiffInformation = AddressValidationFormatConverter.getAddressValidationDiff( + AddressValidationFormatConverter.makeApiAddress(this.addressInfoGroup.value), + addressInformation + ); + if (!addressDiff.allFieldsSame) { + this.addressInfoGroup.patchValue(addressInformation); + } + } + this.localState.mailingAddressInformation = addressInformation; + } + + public makeTattleIndividual(): TattleIndividual { + const personalInformation: TattleSpousePersonalInformation = this.pageForm.value[TattleSpouseInfoPageService.PERSONAL_FORM_NAME]; + let addressInformation: AddressInformation; + if (this.localState.mailingAddressInformation) { + addressInformation = this.localState.mailingAddressInformation; + } else if (this.addressInfoGroup) { + addressInformation = this.addressInfoGroup.value; + } + return { + personalInformation: personalInformation, + addressInformation: addressInformation, + hasSameAddressAsSpouse: this.isAddressSameGroup.value.isAddressSame + }; + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + this.tattleSpouseInfoPageService.saveAndNavigate(this.makeTattleIndividual(), this.route, this.INDIVIDUAL_REF); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.tattleSpouseInfoPageService.cancelAndNavigate(this.route); + } + + public updateAddressesForm(): void { + if (this.localState.showAddress && !this.addressInfoGroup) { + this.tattleSpouseInfoPageService.addAddressGroupsIfMissing(this.pageForm); + this.addressInfoGroup = this.pageForm.get(TattleSpouseInfoPageService.ADDRESS_FORM_NAME); + this.pageForm.addControl(TattleSpouseInfoPageService.ADDRESS_FORM_NAME, this.addressInfoGroup); + const prevValue: AddressInformation = this.tattleSpouseInfoPageService.getApplicationGroupValues( + this.INDIVIDUAL_REF)['addressInformation']; + if (prevValue.state) { + this.addressInfoGroup.patchValue(prevValue); + this.localState.mailingAddressInformation = prevValue; + } + } else if (!this.localState.showAddress && this.addressInfoGroup) { + this.addressInfoGroup = null; + this.mailingAddressVerifyInfoGroup = null; + this.tattleSpouseInfoPageService.removeAddressGroups(this.pageForm); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.spec.ts new file mode 100644 index 0000000..d6d7a6d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.spec.ts @@ -0,0 +1,105 @@ +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../../testing/shared-mocks/credit-application-dao.mock.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import { LoanTypeSet } from '../../../../shared/loan-type'; +import { TattleSpouseInfoPageService } from './tattle-spouse-info-page.service'; + +import { BorrowerType } from '../../../../shared/borrower-type'; +import { TattleIndividual } from '../../../../shared/credit-application'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +describe('Tattle Spouse\'s Info Page Service', () => { + + let service: TattleSpouseInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(TattleSpouseInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createTattleSpousePersonalInformationGroup').and.callThrough(); + const pageGroup = service.createPageGroup(); + const personalGroup = pageGroup.get(TattleSpouseInfoPageService.PERSONAL_FORM_NAME); + expect(service.createTattleSpousePersonalInformationGroup).toHaveBeenCalled(); + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO.fields.forEach((eachField) => { + expect(personalGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create an address info group', () => { + const addressGroup = service.createAddressInformationGroup(); + expect(addressGroup).toBeTruthy(); + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + expect(addressGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveTattleSpouseInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = {}; + const borrower = BorrowerType.PRIMARY_BORROWER_SPOUSE; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, borrower); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveTattleSpouseInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = {}; + const borrower = BorrowerType.PRIMARY_BORROWER_SPOUSE; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute, borrower); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.ts new file mode 100644 index 0000000..6f4dfc0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.service.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ApplicationWizardService } from '../../../application-wizard.service'; +import { CommonGroups } from '../../../field-groups/common-groups'; +import { PageService } from '../../page-service'; +import { CreditApplicationValues } from '../../../../shared/credit-application-values'; +import { TattleSpousePersonalInformation } from '../../../field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { BorrowerType } from 'src/app/shared/borrower-type'; +import { AddressInformation } from '../../../field-groups/address/address-information'; +import { TattleIndividual } from '../../../../shared/credit-application'; +import { AddressStatusPipe } from '../../../../shared/pipes/address-status.pipe'; +import { CreditApplicationService } from '../../../../shared/credit-application.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TattleSpouseInfoPageService implements PageService { + + public static readonly ADDRESS_FORM_NAME = 'spouseAddressInformationGroup'; + public static readonly IS_SAME_ADDRESS_FORM_NAME = 'isAddressSameGroup'; + public static readonly PERSONAL_FORM_NAME = 'spousePersonalInformationGroup'; + public static readonly MAILING_ADDRESS_VERIFY_FORM_NAME = 'mailingAddressCompareInformationGroup'; + private CURRENT_ROUTE = 'applicant-spouse'; + public readonly MODEL_REF = 'nonApplicantAddressSpouseInformation'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + const memberGroup = this.createIsSameAddressInformationGroup(); + group.addControl(TattleSpouseInfoPageService.IS_SAME_ADDRESS_FORM_NAME, memberGroup); + const personalGroup = this.createTattleSpousePersonalInformationGroup(); + group.addControl(TattleSpouseInfoPageService.PERSONAL_FORM_NAME, personalGroup); + + return group; + } + + public createIsSameAddressInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + + CommonGroups.IS_ADDRESS_SAME_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; + } + + public createTattleSpousePersonalInformationGroup(): FormGroup { + const personalGroup = this.formBuilder.group({}); + + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO.fields.forEach((eachField) => { + personalGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return personalGroup; + } + + public addAddressGroupsIfMissing(group: FormGroup) { + if (group.get(TattleSpouseInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + const addressGroup = this.createAddressInformationGroup(); + + group.addControl(TattleSpouseInfoPageService.ADDRESS_FORM_NAME, addressGroup); + } + + public removeAddressGroups(group: FormGroup) { + if (!group.get(TattleSpouseInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + group.removeControl(TattleSpouseInfoPageService.ADDRESS_FORM_NAME); + group.removeControl(TattleSpouseInfoPageService.MAILING_ADDRESS_VERIFY_FORM_NAME); + } + + public createAddressInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return memberGroup; + } + + public getApplicationGroupValues(individual: BorrowerType): TattleIndividual { + return { + personalInformation: this.applicationWizardService.getIndividualGroupValues('personalInformation', individual), + addressInformation: this.applicationWizardService.getIndividualGroupValues('addressInformation', individual), + hasSameAddressAsSpouse: this.applicationWizardService.getIndividualGroupValues('hasSameAddressAsSpouse', individual) + }; + } + + public saveAndNavigate(tattleSpouseInformation: TattleIndividual, route: ActivatedRoute, individual: BorrowerType) { + this.CURRENT_ROUTE = this.determineCurrentRoute(individual); + this.creditApplicationService.saveTattleSpouseInformation(tattleSpouseInformation, individual).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public determineCurrentRoute(individual: BorrowerType): string { + switch (individual) { + case BorrowerType.PRIMARY_BORROWER_SPOUSE: + return 'applicant-spouse'; + case BorrowerType.CO_BORROWER_SPOUSE: + return 'co-applicant-spouse'; + } + } + + public cancelAndNavigate(route: ActivatedRoute) { + const nextRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + + public getNextTopicMessage(individual: BorrowerType): string { + this.CURRENT_ROUTE = this.determineCurrentRoute(individual); + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public getRequiredAddress(isAddressSame: string): boolean { + return isAddressSame === CreditApplicationValues.CHOICE_NO; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-info-page.ts new file mode 100644 index 0000000..c8c4dd8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-info-page.ts @@ -0,0 +1,24 @@ +import { ApplicationPage } from '../../application-page'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { DisclosureInformation } from '../../field-groups/disclosures/credit-card-disclosure'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + +export class CreditCardDisclosuresInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + + const disclosureInfo: DisclosureInformation = applicationWizardService.getGroupValues('disclosureInformation'); + + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = disclosureInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.html new file mode 100644 index 0000000..196cf58 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.html @@ -0,0 +1,31 @@ +
+
+
+

HEADINGS.DISCLOSURES

+
+
+ + + + + +
+

DISCLOSURE.HELOC.CONFIRM

+
+
+
+ +
+ + +
+
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.spec.ts new file mode 100644 index 0000000..9adc0d0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.spec.ts @@ -0,0 +1,85 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditCardDisclosuresPageComponent } from './credit-card-disclosures-page.component'; +import { AddressInfoFormComponent } from '../../field-groups/address/address-info-form.component'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { AddressInfoPageService } from '../common/address/address-info-page.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { StatePipe } from '../../../shared/pipes/state.pipe'; +import { DisclosureInformation } from '../../field-groups/disclosures/credit-card-disclosure'; +import { configureTestSuite } from 'ng-bullet'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + + +describe('CreditCardDisclosuresPageComponent', () => { + let component: CreditCardDisclosuresPageComponent; + let fixture: ComponentFixture; + let service: AddressInfoPageService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + CreditCardDisclosuresPageComponent, + AddressInfoFormComponent, + StatePipe + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CreditCardDisclosuresPageComponent); + component = fixture.componentInstance; + service = TestBed.get(AddressInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should bind the previous button to the correct command function', () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should patch page data onInit', () => { + const disclosures: DisclosureInformation = { + truthful: false, + contact: true + }; + spyOn(service, 'getApplicationGroupValues').and.returnValue(disclosures); + fixture.detectChanges(); + expect(component.disclosureGroup.value).toEqual(disclosures); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.ts new file mode 100644 index 0000000..518a592 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { CreditCardDisclosuresPageService } from './credit-card-disclosures-page.service'; +import { ApplicationWizardService } from '../../application-wizard.service'; + +@Component({ + selector: 'lw-credit-card-disclosures-page', + templateUrl: './credit-card-disclosures-page.component.html' +}) +export class CreditCardDisclosuresPageComponent implements OnInit { + + public savingData = null; + + public localState = { + previousDisabled: false, + nextDisabled: true, + cardName: '', + nextTopic: '', + showMarriedDisclosure: false, + isHeloc: false + }; + public pageForm: FormGroup; + public disclosureGroup: FormGroup; + private readonly FORM_STATE_INVALID = 'INVALID'; + + constructor( + private creditCardDisclosuresPageService: CreditCardDisclosuresPageService, + private applicationWizardService: ApplicationWizardService, + private route: ActivatedRoute) { } + + public ngOnInit() { + this.pageForm = this.creditCardDisclosuresPageService.createPageGroup(); + this.disclosureGroup = this.pageForm.get(CreditCardDisclosuresPageService.DISCLOSURE_FORM_NAME); + this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.disclosureGroup.patchValue(this.creditCardDisclosuresPageService + .getApplicationGroupValues(CreditCardDisclosuresPageService.DISCLOSURE_FORM_NAME)); + this.localState.nextTopic = this.creditCardDisclosuresPageService.getNextTopicMessage(); + this.localState.showMarriedDisclosure = this.creditCardDisclosuresPageService.determineShowMarriedDisclosure(); + this.localState.isHeloc = this.creditCardDisclosuresPageService.isHeloc(); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + this.creditCardDisclosuresPageService.saveAndNavigate(this.pageForm.value.disclosureInformation, this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.creditCardDisclosuresPageService.cancelAndNavigate(this.route); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.spec.ts new file mode 100644 index 0000000..606173c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.spec.ts @@ -0,0 +1,58 @@ +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CreditCardDisclosuresPageService } from './credit-card-disclosures-page.service'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('Credit Card Disclosures Page Service', () => { + + let service: CreditCardDisclosuresPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(CreditCardDisclosuresPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.ts new file mode 100644 index 0000000..fc5bf2b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { AddressInformation } from '../../field-groups/address/address-information'; +import { DisclosureInformation } from '../../field-groups/disclosures/credit-card-disclosure'; +import { PageService } from '../page-service'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; +import {LoanTitle} from '../../../shared/loan-type/loan-title'; +import { environment } from 'src/environments/environment'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + + +@Injectable({ + providedIn: 'root' +}) +export class CreditCardDisclosuresPageService implements PageService { + + public static readonly DISCLOSURE_FORM_NAME = 'disclosureInformation'; + private readonly CURRENT_ROUTE = 'cc-disclosures'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private router: Router, + private formBuilder: FormBuilder, + private httpClient: HttpClient) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + const disclosureGroup = this.createDisclosureInformationGroup(); + group.addControl('disclosureInformation', disclosureGroup); + + return group; + } + + public createDisclosureInformationGroup(): FormGroup { + const disclosureGroup = this.formBuilder.group({}); + + CommonGroups.CC_DISCLOSURE_INFO.fields.forEach((eachField) => { + disclosureGroup.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return disclosureGroup; + } + + public getApplicationGroupValues(groupName: string): any { + return this.applicationWizardService.getGroupValues(groupName); + } + + public saveAndNavigate(disclosureInformation: DisclosureInformation, route: ActivatedRoute) { + this.creditApplicationService.saveDisclosureInformation(disclosureInformation).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], { relativeTo: route }); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], { relativeTo: route }); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public cancelAndNavigate(route: ActivatedRoute) { + const prevRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([prevRoute], { relativeTo: route }); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public determineShowMarriedDisclosure(): boolean { + return this.creditApplicationService.isMarriedApplicant(); + } + + public isHeloc(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.HELOC; + } + + public buildGetPdfImportantTermsURL(): string { + return environment.urlBase + '/GetPdf/ImportantTerms'; + } + + public buildGetPdfWhatYouShouldKnowURL(): string { + return environment.urlBase + '/GetPdf/WhatYouShouldKnow'; + } + + public requestImportantTermsPdf(): Observable { + return this.httpClient.get(this.buildGetPdfImportantTermsURL(), {responseType: "blob" as "json"}) + } + + public requestWhatYouShouldKnowPdf(): Observable { + return this.httpClient.get(this.buildGetPdfWhatYouShouldKnowURL(), {responseType: "blob" as "json"}) + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-outline.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-outline.ts new file mode 100644 index 0000000..ce1271b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-outline.ts @@ -0,0 +1,144 @@ +import { ApplicationPage } from '../../application-page'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { MemberInfoPage } from '../common/member/member-info-page'; +import { PrimaryEmploymentInfoPage } from '../common/employment/pages/primary-employment-info-page'; +import { PrimaryPersonalInfoPage } from '../common/personal/pages/primary-personal-info-page'; +import { PrimaryAddressInfoPage } from '../common/address/pages/primary-address-info-page'; +import { CreditCardDisclosuresInfoPage } from './credit-card-disclosures-info-page'; +import { PrimaryTattleSpouseInfoPage } from '../common/tattle-spouse/pages/primary-tattle-spouse-info-page'; +import { SpouseTypeInfoPage } from '../common/spouse-type/spouse-type-info-page'; +import { PrimarySpousePersonalInfoPage } from '../common/personal/pages/primary-spouse-personal-info-page'; +import { CoBorrowerPersonalInfoPage } from '../common/personal/pages/co-borrower-personal-info-page'; +import { CoBorrowerSpousePersonalInfoPage } from '../common/personal/pages/co-borrower-spouse-personal-info-page'; +import { PrimarySpouseAddressInfoPage } from '../common/address/pages/primary-spouse-address-info-page'; +import { CoBorrowerAddressInfoPage } from '../common/address/pages/co-borrower-address-info-page'; +import { CoBorrowerSpouseAddressInfoPage } from '../common/address/pages/co-borrower-spouse-address-info-page'; +import { PrimarySpouseEmploymentInfoPage } from '../common/employment/pages/primary-spouse-employment-info-page'; +import { CoBorrowerEmploymentInfoPage } from '../common/employment/pages/co-borrower-employment-info-page'; +import { CoBorrowerSpouseEmploymentInfoPage } from '../common/employment/pages/co-borrower-spouse-employment-info-page'; +import { CoBorrowerTattleSpouseInfoPage } from '../common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page'; +import { SummaryInfoPage } from '../common/summary/summary-info-page'; +import { DecisionPage } from '../common/decision/decision-page'; +import { ProcessingPage } from '../common/processing/processing-page'; + +export enum PageInfo { + PRIMARY, + PRIMARY_SPOUSE, + CO_APPLICANT, + CO_APPLICANT_SPOUSE +} + +export const CreditCardPages: Array = [ + /* PRIMARY */ + new MemberInfoPage('PAGETITLES.COMMON.INTRO', + 'step-1', + [ + CommonGroups.MEMBER_INFO_CC + ]), + new PrimaryPersonalInfoPage('PAGETITLES.COMMON.PERSONAL_INFO', + 'step-2', + [ + CommonGroups.PERSONAL_INFO + ]), + new PrimaryAddressInfoPage('PAGETITLES.COMMON.ADDRESS_INFO', + 'step-3', + [ + CommonGroups.ADDRESS_INFO + ]), + new PrimaryEmploymentInfoPage('PAGETITLES.COMMON.EMPLOYMENT_INFO', + 'step-4', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* PRIMARY_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.PRIMARY_SPOUSETYPE', + 'applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ]), + new PrimaryTattleSpouseInfoPage('PAGETITLES.COMMON.PRIMARY_TATTLESPOUSE', + 'applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ]), + new PrimarySpousePersonalInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_PERSONAL_INFO', + 'applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimarySpouseAddressInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_ADDRESS_INFO', + 'applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimarySpouseEmploymentInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_EMPLOYMENT_INFO', + 'applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT */ + new CoBorrowerPersonalInfoPage('PAGETITLES.COMMON.COAPP_PERSONAL_INFO', + 'co-applicant-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerAddressInfoPage('PAGETITLES.COMMON.COAPP_ADDRESS_INFO', + 'co-applicant-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerEmploymentInfoPage('PAGETITLES.COMMON.COAPP_EMPLOYMENT_INFO', + 'co-applicant-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.COAPP_SPOUSETYPE', + 'co-applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ]), + new CoBorrowerTattleSpouseInfoPage('PAGETITLES.COMMON.COAPP_TATTLESPOUSE', + 'co-applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ]), + new CoBorrowerSpousePersonalInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_PERSONAL_INFO', + 'co-applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerSpouseAddressInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_ADDRESS_INFO', + 'co-applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerSpouseEmploymentInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_EMPLOYMENT_INFO', + 'co-applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* DISCLOSURE / SUMMARY */ + new CreditCardDisclosuresInfoPage('PAGETITLES.COMMON.DISCLOSURES', + 'cc-disclosures', + [ + CommonGroups.CC_DISCLOSURE_INFO + ]), + new SummaryInfoPage('PAGETITLES.COMMON.SUMMARY', 'cc-summary', []), + new ProcessingPage('PAGETITLES.CC.PROCESS', 'process', []), + new DecisionPage('PAGETITLES.COMMON.DECISION', 'decision', []) +]; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-routing.module.ts new file mode 100644 index 0000000..e47c3f7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-routing.module.ts @@ -0,0 +1,64 @@ +import { Route} from '@angular/router'; +import { MemberInfoPageComponent } from '../common/member/member-info-page.component'; +import { PersonalInfoPageComponent } from '../common/personal/personal-info-page.component'; +import { AddressInfoPageComponent } from '../common/address/address-info-page.component'; +import { EmploymentInfoPageComponent } from '../common/employment/employment-info-page.component'; +import { CreditCardDisclosuresPageComponent } from './credit-card-disclosures-page.component'; +import { SummaryPageComponent } from '../common/summary/summary-page.component'; +import { DecisionPageComponent } from '../common/decision/decision-page.component'; +import { LoanRouteGuard } from '../../../shared/guards/loan-route.guard'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { TattleSpouseInfoPageComponent } from '../common/tattle-spouse/tattle-spouse-info-page.component'; +import { SpouseTypeInfoPageComponent } from '../common/spouse-type/spouse-type-info-page.component'; +import { ProcessingPageComponent } from '../common/processing/processing-page.component'; +import { StepperControllerGuard } from 'src/app/shared/guards/stepper-controller-guard'; +import { MemberCoreResolver } from 'src/app/member/member-core.resolver'; +import { environment } from 'src/environments/environment'; + + +export const ccRoutes: Route = { + path: 'cc/:type', + resolve: environment.coreResolver ? { CoreIndividual: MemberCoreResolver} : {}, + canActivate: [StepperControllerGuard], + canActivateChild: [LoanRouteGuard], + children: [ + { path: 'step-1', component: MemberInfoPageComponent }, + { path: 'step-2', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-3', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-4', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'co-applicant-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'cc-disclosures', component: CreditCardDisclosuresPageComponent }, + { path: 'cc-summary', component: SummaryPageComponent }, + { path: 'process', component: ProcessingPageComponent }, + { path: 'decision', component: DecisionPageComponent }, + { path: '', redirectTo: 'step-1', pathMatch: 'prefix' } + ] +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.html new file mode 100644 index 0000000..586597e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.html @@ -0,0 +1 @@ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.spec.ts new file mode 100644 index 0000000..2838a9c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.spec.ts @@ -0,0 +1,49 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditCardSchumerBoxComponent } from './credit-card-schumer-box.component'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('CreditCardSchumerBoxComponent', () => { + let component: CreditCardSchumerBoxComponent; + let fixture: ComponentFixture; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + CreditCardSchumerBoxComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + fixture = TestBed.createComponent(CreditCardSchumerBoxComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.ts new file mode 100644 index 0000000..6412501 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/credit-card/credit-card-schumer-box.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { ApplicationWizardService } from '../../application-wizard.service'; + +@Component({ + selector: 'lw-credit-card-schumer-box', + templateUrl: './credit-card-schumer-box.component.html' +}) +export class CreditCardSchumerBoxComponent implements OnInit { + + public localState = { + boxType: null, + showSchumer: false + }; + + constructor( + private applicationWizardService: ApplicationWizardService + ) { } + + ngOnInit() { + this.localState.boxType = this.applicationWizardService.getFieldValue('loanType').apiCode; + this.localState.showSchumer = this.applicationWizardService.getFieldValue('loanType').category === 'Credit Card'; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.html new file mode 100644 index 0000000..931ce8e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.html @@ -0,0 +1,43 @@ +
+
+
+

+
+
COPY.COLLATERAL.PRIMARY_BORROWER
+
+
+ + + + + + + + + + + +
+ +
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.spec.ts new file mode 100644 index 0000000..141684b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.spec.ts @@ -0,0 +1,457 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { AddressInfoFormComponent } from '../../field-groups/address/address-info-form.component'; +import { IsAddressSameInfoFormComponent } from '../../field-groups/address/is-address-same-info-form.component'; +import { CollateralInfoPageComponent } from './collateral-info-page.component'; +import { CollateralInfoPageService } from './collateral-info-page.service'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { StatePipe } from '../../../shared/pipes/state.pipe'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { UiSettings } from '../../../shared/configuration/ui-settings'; +import { DefaultSettings } from '../../../shared/configuration/default-settings.mock'; +import { ConfigurationService } from '../../../shared/configuration/configuration.service'; +import { CollateralInformationGroup} from '../../../shared/credit-application'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressInformation } from '../../field-groups/address/address-information'; +import { BorrowerType } from '../../../shared/borrower-type'; + +class ApplicationWizardServiceMock { + private readonly loanType = LoanTypeSet.find((loan) => loan.id === 'hef') || LoanTypeSet[0]; + + private readonly borrowerAddress: AddressInformation = { + addressLine1: '123 Mockingbird Ln', + addressLine2: '', + city: 'Madison', + state: 'WI', + zipcode: '53703' + }; + + private readonly collateralInformation: CollateralInformationGroup = { + estimatedValue: 0, + propertyType: '', + occupancyType: '', + hasFirstMortgage: CreditApplicationValues.CHOICE_NO, + hasSecondMortgage: CreditApplicationValues.CHOICE_NO, + payoffFirstMortgage: false, + payoffSecondMortgage: false, + isBeingImproved: CreditApplicationValues.CHOICE_NO, + isListedForSale: CreditApplicationValues.CHOICE_NO, + addressLine1: '', + addressLine2: '', + city: '', + state: '', + zipcode: '', + isAddressSame: CreditApplicationValues.CHOICE_YES, + onRentedLot: CreditApplicationValues.CHOICE_NO, + rentPaid: '', + annualPropertyTax: '', + files: [], + firstMortgageBalanceFiles: [], + isUploadingFirstMortgageBalanceFiles: false, + isUploading: false + }; + + private readonly fullCreditApplication: any = { + loanId: 'eq-collateral', + loanType: this.loanType, + collateralInformation: this.collateralInformation, + hasCoApplicant: CreditApplicationValues.CHOICE_NO, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + hasBeenCompleted: false, + limitRequested: 0, + primaryContactMethod: 'email', + fullInfoBorrowers: [{ + personalInformation: { firstName: 'Alex', maritalStatus: CreditApplicationValues.CHOICE_NO }, + addressInformation: this.borrowerAddress, + residenceInformation: {} + }] + }; + + public getNextTopicMessage(): string { + return 'Next topic message'; + } + + public getNextRoute(): string { + return '../cc-disclosures'; + } + + public getPreviousRoute(): string { + return '../co-applicant-spouse-financial'; + } + + public getFieldValue(field: string, group?: string): any { + if (group) { + const groupedValues = this.fullCreditApplication[group]; + return groupedValues ? groupedValues[field] : null; + } + return this.fullCreditApplication[field] || null; + } + + public getGroupValues(group: string): any { + return this.fullCreditApplication[group] || {}; + } + + public getFullCreditApplication(): any { + return this.fullCreditApplication; + } + + public getIndividualGroupValues(group: string, borrowerType: BorrowerType): any { + const individual = this.getIndividualByBorrowerType(borrowerType); + if (individual && individual[group]) { + return individual[group]; + } + return {}; + } + + public getIndividualByBorrowerType(borrowerType: BorrowerType): any { + return this.fullCreditApplication.fullInfoBorrowers[0]; + } + + public getApplicantName(): string { + return this.fullCreditApplication.fullInfoBorrowers[0].personalInformation.firstName; + } + + public isRentedLotInfoRequired(): boolean { + return true; + } + + public isLienPositionInfoRequired(): boolean { + return true; + } + + public needsCollateral(): boolean { + return true; + } +} + + +describe('CollateralInfoPageComponent', () => { + let component: CollateralInfoPageComponent; + let fixture: ComponentFixture; + let service: CollateralInfoPageService; + let configService: ConfigurationService; + let applicationWizardService: ApplicationWizardService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + CollateralInfoPageComponent, + IsAddressSameInfoFormComponent, + AddressInfoFormComponent, + StatePipe + ], + providers: [ + ConfigurationService, + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}, + {provide: ApplicationWizardService, useClass: ApplicationWizardServiceMock} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + fixture = TestBed.createComponent(CollateralInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(CollateralInfoPageService); + configService = TestBed.get(ConfigurationService); + configService.saveConfiguration(defaultSettings); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.collateralInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', async () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should update on new values', () => { + fixture.detectChanges(); + spyOn(component, 'updateCollateralFormGroupControls').and.callThrough(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + fixture.detectChanges(); + expect(component.updateCollateralFormGroupControls).toHaveBeenCalled(); + }); + + it('should use the service to add address controls to the form group', () => { + spyOn(service, 'addAddressGroupsIfMissing').and.callThrough(); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + fixture.detectChanges(); + expect(service.addAddressGroupsIfMissing).toHaveBeenCalled(); + expect(component.addressInfoGroup.value.addressLine1).toBeDefined(); + }); + + it('should use the service to remove address controls from the form group', () => { + fixture.detectChanges(); + spyOn(service, 'addAddressGroupsIfMissing').and.callThrough(); + spyOn(service, 'removeAddressGroups').and.callThrough(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_YES}); + fixture.detectChanges(); + expect(service.removeAddressGroups).toHaveBeenCalled(); + expect(component.addressInfoGroup).toBeFalsy(); + }); + + it('should be require rent paid if the Property is a manufactured home', () => { + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({ + isAddressSame: CreditApplicationValues.CHOICE_YES, + estimatedValue: 10000, + propertyType: 'Double', + isBeingImproved: CreditApplicationValues.CHOICE_NO, + isListedForSale: CreditApplicationValues.CHOICE_YES, + occupancyType: 'Vacant', + onRentedLot: CreditApplicationValues.CHOICE_YES + }); + fixture.detectChanges(); + expect(component.collateralInfoGroup.valid).toBe(false); + component.collateralInfoGroup.patchValue({ + rentPaid: 350 + }); + fixture.detectChanges(); + expect(component.collateralInfoGroup.valid).toBe(true); + }); + + it('should patch page data onInit', () => { + const collateralData: CollateralInformationGroup = { + estimatedValue: 5446, + propertyType: 'Condo', + occupancyType: 'OwnerOccupied', + hasFirstMortgage: CreditApplicationValues.CHOICE_YES, + hasSecondMortgage: CreditApplicationValues.CHOICE_YES, + payoffFirstMortgage: false, + payoffSecondMortgage: false, + isBeingImproved: CreditApplicationValues.CHOICE_YES, + isListedForSale: CreditApplicationValues.CHOICE_NO, + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Alston', + state: 'MA', + zipcode: '54545', + isAddressSame: CreditApplicationValues.CHOICE_NO, + onRentedLot: '', + rentPaid: '', + annualPropertyTax: '5000.00', + files: [], + firstMortgageBalanceFiles: [], + isUploadingFirstMortgageBalanceFiles: false, + isUploading: false + }; + + spyOn(service, 'getApplicationGroupValues').and.returnValue(collateralData); + fixture.componentInstance.ngOnInit(); + expect(Object.assign(component.collateralInfoGroup.value, component.addressInfoGroup.value)).toEqual(collateralData); + }); + + it('should submit the USPS Verified Address Value if the match was saved', () => { + spyOn(service, 'saveAndNavigate'); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Alston', + state: 'MA', + zipcode: '54545', + }; + const mockUSPSAddress: AddressInformation = { + addressLine1: mockAddressInput.addressLine1.toLocaleUpperCase(), + addressLine2: mockAddressInput.addressLine2.toLocaleUpperCase(), + city: mockAddressInput.city.toLocaleUpperCase(), + state: mockAddressInput.state.toLocaleUpperCase(), + zipcode: mockAddressInput.zipcode, + zipcodeExt: '5446' + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + component.confirmRealEstateAddressInformation(mockUSPSAddress); + const fields = component.generateFieldsFromGroups(); + expect(fields[CollateralInfoPageService.ADDRESS_FORM_NAME]).toEqual(mockUSPSAddress); + }); + + it('Should Require WI collateral for address info if is HELOC Fixed Or Flex', () => { + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(true); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Washington', + state: 'DC', + zipcode: '25000', + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + fixture.detectChanges(); + const stateStatus = component.addressInfoGroup.get('state').status; + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('INVALID'); + expect(stateStatus).toBe('INVALID'); + }); + + it('Should Not Display Error Text for WI collateral if loan is HELOC Fixed Or Flex and AddressInfo is Wisconsin', () => { + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(true); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Milwaukee', + state: 'WI', + zipcode: '53211', + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + fixture.detectChanges(); + const stateStatus = component.addressInfoGroup.get('state').status; + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('VALID'); + expect(stateStatus).toBe('VALID'); + }); + + it('Should be invalid if Addres Same NO if Primary Holder Address is not in WI and loan is HELOC fix or flex ', () => { + spyOn(applicationWizardService, 'getIndividualGroupValues').and.callFake((arg1, arg2) => { + if (arg1 === 'addressInformation' && arg2 === 'PRIMARY_BORROWER') { + return {state: 'DC'}; + } + }); + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(true); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Washington', + state: 'DC', + zipcode: '25000', + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + fixture.detectChanges(); + const stateStatus = component.addressInfoGroup.get('state').status; + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('INVALID'); + expect(stateStatus).toBe('INVALID'); + }); + + it('Shound be valid if addres same YES if Primary Holder Address is in WI and Loan is HELOC fix or flex', () => { + spyOn(applicationWizardService, 'getIndividualGroupValues').and.callFake((arg1, arg2) => { + if (arg1 === 'addressInformation' && arg2 === 'PRIMARY_BORROWER') { + return {state: 'WI'}; + } + }); + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(true); + + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_YES}); + fixture.detectChanges(); + + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('VALID'); + }); + + it('Should be valid if Addres Same NO if Primary Holder Address is not in WI when loan is not HELOC fix or flex', () => { + spyOn(applicationWizardService, 'getIndividualGroupValues').and.callFake((arg1, arg2) => { + if (arg1 === 'addressInformation' && arg2 === 'PRIMARY_BORROWER') { + return {state: 'DC'}; + } + }); + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(false); + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_NO}); + const mockAddressInput: AddressInformation = { + addressLine1: '51 Gardner', + addressLine2: '', + city: 'Washington', + state: 'DC', + zipcode: '25000', + }; + + component.addressInfoGroup.patchValue(mockAddressInput); + fixture.detectChanges(); + const stateStatus = component.addressInfoGroup.get('state').status; + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('VALID'); + expect(stateStatus).toBe('VALID'); + }); + + it(' Shound be valid if addres same YES and Primary Holder Address is not in WI when loan is not Heloc Fixed or Flex', () => { + spyOn(applicationWizardService, 'getIndividualGroupValues').and.callFake((arg1, arg2) => { + if (arg1 === 'addressInformation' && arg2 === 'PRIMARY_BORROWER') { + return {state: 'DC'}; + } + }); + spyOn(service, 'saveAndNavigate'); + spyOn(service, 'isHelocOrHomeEquityFixedLoan').and.returnValue(false); + + fixture.detectChanges(); + component.collateralInfoGroup.patchValue({isAddressSame: CreditApplicationValues.CHOICE_YES}); + fixture.detectChanges(); + + const addresSameStatus = component.collateralInfoGroup.get('isAddressSame').status; + expect(addresSameStatus).toBe('VALID'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.ts new file mode 100644 index 0000000..3da1566 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.component.ts @@ -0,0 +1,211 @@ +import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { CollateralInfoPageService } from './collateral-info-page.service'; +import { Subject, Subscription } from 'rxjs'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { AddressInformation } from '../../field-groups/address/address-information'; +import { AddressFormBuilderService } from 'src/app/shared/address-validation/address-form-builder.service'; +import { AddressDiffInformation } from '../../field-groups/address-compare/address-compare-information'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; +import { OccupancyTypeEnum } from 'src/app/shared/api-models/api-enums'; + +@Component({ + selector: 'lw-collateral-info-page', + templateUrl: './collateral-info-page.component.html' +}) +export class CollateralInfoPageComponent implements OnInit, OnDestroy { + + private statusSubscription: Subscription; + private valueSubscription: Subscription; + + public pageForm: FormGroup; + public collateralInfoGroup: FormGroup; + public addressInfoGroup: FormGroup; + public realEstateAddressVerifyInfoGroup: FormGroup; + + private readonly FORM_STATE_INVALID = 'INVALID'; + + public savingData = null; + + public loanId: string; + + public fileUploadValues: any; + + public localState = { + nextDisabled: true, + previousDisabled: false, + nextTopic: '', + sameAddress: true, + isAddressSameType: 'heloc', + askAboutRentedLot: false, + borrowerName: 'Applicant', + realEstateAddressInformation: null, + isHelocRateMod: false, + isMfgHome: false + }; + + constructor( + private collateralInfoPageService: CollateralInfoPageService, + private route: ActivatedRoute, + private cd: ChangeDetectorRef) { + } + + public ngOnInit() { + this.loanId = this.collateralInfoPageService.getLoanId(); + this.fileUploadValues = this.collateralInfoPageService.getFileUploadValues(); + + this.localState.borrowerName = this.collateralInfoPageService.getApplicantName(); + this.pageForm = this.collateralInfoPageService.createPageGroup(); + this.collateralInfoGroup = this.pageForm.get(CollateralInfoPageService.COLLATERAL_FORM_NAME); + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + this.valueSubscription = this.pageForm.valueChanges.subscribe((change) => { + const collateralInfo = change.collateralInformation; + this.updateCollateralFormGroupControls(collateralInfo); + }); + this.localState.nextTopic = this.collateralInfoPageService.getNextTopicMessage(); + this.collateralInfoGroup + .patchValue( + this.collateralInfoPageService + .getApplicationGroupValues(CollateralInfoPageService.COLLATERAL_FORM_NAME) + ); + if (this.collateralInfoGroup.value.isAddressSame === CreditApplicationValues.CHOICE_NO) { + this.updateCollateralFormGroupControls(this.collateralInfoGroup.value); + const previousValues = this.collateralInfoPageService.getApplicationGroupValues(CollateralInfoPageService.COLLATERAL_FORM_NAME); + + if (previousValues.state) { + this.addressInfoGroup.patchValue(previousValues); + this.localState.realEstateAddressInformation = AddressValidationFormatConverter.filterUiAddressInformation(previousValues); + } + } + + this.localState.askAboutRentedLot = this.collateralInfoPageService.isRentedLotInfoRequired(); + if (!this.localState.askAboutRentedLot) { + this.collateralInfoGroup = this.collateralInfoPageService.clearRentInfoRequirement(this.collateralInfoGroup); + } + + this.localState.isHelocRateMod = this.collateralInfoPageService.isHelocRateMod(); + this.localState.isMfgHome = this.collateralInfoPageService.isMfgHome(); + } + + public ngOnDestroy() { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public updateCollateralFormGroupControls(change) { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + if (!change) { + return; + } + + if (change.isAddressSame === CreditApplicationValues.CHOICE_NO) { + this.localState.sameAddress = false; + this.collateralInfoPageService.addAddressGroupsIfMissing(this.pageForm); + this.addressInfoGroup = this.pageForm.get(CollateralInfoPageService.ADDRESS_FORM_NAME); + } else if (change.isAddressSame === CreditApplicationValues.CHOICE_YES) { + this.localState.sameAddress = true; + this.collateralInfoPageService.removeAddressGroups(this.pageForm); + this.addressInfoGroup = null; + this.realEstateAddressVerifyInfoGroup = null; + this.collateralInfoGroup.patchValue({occupancyType: OccupancyTypeEnum[0]}); + } + + if (change.hasFirstMortgage === CreditApplicationValues.CHOICE_NO) { + const nextCollateralInfoGroup = this.collateralInfoPageService.removeFirstMortgagePayoff(this.collateralInfoGroup); + this.collateralInfoGroup = nextCollateralInfoGroup; + this.collateralInfoGroup.patchValue({payoffFirstMortgage: 'false'}); + } else if (change.hasFirstMortgage === CreditApplicationValues.CHOICE_YES) { + const nextCollateralInfoGroup = this.collateralInfoPageService.addFirstMortgagePayoff(this.collateralInfoGroup); + this.collateralInfoGroup = nextCollateralInfoGroup; + } + + if (change.hasSecondMortgage === CreditApplicationValues.CHOICE_NO) { + const nextCollateralInfoGroup = this.collateralInfoPageService.removeSecondMortgagePayoff(this.collateralInfoGroup); + this.collateralInfoGroup = nextCollateralInfoGroup; + this.collateralInfoGroup.patchValue({payoffSecondMortgage: 'false'}); + } else if (change.hasSecondMortgage === CreditApplicationValues.CHOICE_YES) { + const nextCollateralInfoGroup = this.collateralInfoPageService.addSecondMortgagePayoff(this.collateralInfoGroup); + this.collateralInfoGroup = nextCollateralInfoGroup; + } + + if (this.localState.isHelocRateMod) { + const nextCollateralInfoGroup = this.collateralInfoPageService.removeSecondMortgagePayoff(this.collateralInfoGroup); + this.collateralInfoGroup = nextCollateralInfoGroup; + this.collateralInfoGroup.patchValue({hasFirstMortgage: 'true'}); + this.collateralInfoGroup.patchValue({payoffFirstMortgage: 'false'}); + this.collateralInfoGroup.patchValue({hasSecondMortgage: 'false'}); + this.collateralInfoGroup.patchValue({payoffSecondMortgage: 'false'}); + this.collateralInfoGroup.patchValue({isBeingImproved: 'false'}); + } + + this.valueSubscription = this.pageForm.valueChanges.subscribe((nextValues) => { + const collateralInfo = nextValues.collateralInformation; + this.updateCollateralFormGroupControls(collateralInfo); + }); + this.cd.detectChanges(); + } + + public confirmRealEstateAddressInformation(addressInformation: AddressInformation) { + if (!this.realEstateAddressVerifyInfoGroup) { + AddressFormBuilderService.addAddressCompareGroupIfMissing( + this.pageForm, CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME + ); + this.realEstateAddressVerifyInfoGroup = this.pageForm.get(CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME); + } + if (!addressInformation) { + this.realEstateAddressVerifyInfoGroup.patchValue({addressVerifyStatus: false}); + } else { + this.realEstateAddressVerifyInfoGroup.patchValue({addressVerifyStatus: true}); + const addressDiff: AddressDiffInformation = AddressValidationFormatConverter.getAddressValidationDiff( + AddressValidationFormatConverter.makeApiAddress(this.addressInfoGroup.value), + addressInformation + ); + if (!addressDiff.allFieldsSame) { + this.addressInfoGroup.patchValue(addressInformation); + } + } + this.localState.realEstateAddressInformation = addressInformation; + } + + public getAddressValues(): AddressInformation | null { + if (this.localState.realEstateAddressInformation) { + return this.localState.realEstateAddressInformation; + } else if (this.addressInfoGroup) { + return this.addressInfoGroup.value; + } else { + return null; + } + } + + public generateFieldsFromGroups() { + const fields = Object.assign({}, {[CollateralInfoPageService.COLLATERAL_FORM_NAME]: this.collateralInfoGroup.value}); + const address = this.getAddressValues(); + if (address) { + Object.assign(fields, {[CollateralInfoPageService.ADDRESS_FORM_NAME]: address}); + } + return fields; + } + + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + this.collateralInfoPageService.saveAndNavigate(this.generateFieldsFromGroups(), this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.collateralInfoPageService.cancelAndNavigate(this.route); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.spec.ts new file mode 100644 index 0000000..87f2e2d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.spec.ts @@ -0,0 +1,178 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule, FormGroup } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { AddressInformation } from '../../field-groups/address/address-information'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { IsAddressSameInformation } from '../../field-groups/address/is-address-same-information'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { CollateralInfoPageService } from './collateral-info-page.service'; +import { CollateralInformation } from '../../field-groups/equity/collateral-information'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressFormBuilderService } from 'src/app/shared/address-validation/address-form-builder.service'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + + +describe('Collateral Info Page Service', () => { + + let service: CollateralInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(CollateralInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[5].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createCollateralInformationGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createCollateralInformationGroup).toHaveBeenCalled(); + }); + + it('should create a collateral info group', () => { + const collateralGroup = service.createCollateralInformationGroup(); + expect(collateralGroup).toBeTruthy(); + CommonGroups.IS_ADDRESS_SAME_INFO.fields.forEach((eachField) => { + expect(collateralGroup.get(eachField.field)).toBeTruthy(); + }, this); + CommonGroups.COLLATERAL_INFO.fields.forEach((eachField) => { + expect(collateralGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should remove addressInfoGroup to the page form', () => { + const formGroup = new FormGroup({}); + service.addAddressGroupsIfMissing(formGroup); + AddressFormBuilderService.addAddressCompareGroupIfMissing(formGroup, CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME); + expect(formGroup.get(CollateralInfoPageService.ADDRESS_FORM_NAME)).toBeTruthy(); + expect(formGroup.get(CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME)).toBeTruthy(); + service.removeAddressGroups(formGroup); + expect(formGroup.get(CollateralInfoPageService.ADDRESS_FORM_NAME)).toBeFalsy(); + expect(formGroup.get(CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME)).toBeFalsy(); + }); + + it('should add first mortgage payoff control to the form group', () => { + const collateralGroup = service.createCollateralInformationGroup(); + expect(collateralGroup).toBeTruthy(); + const collateralGroupWithoutFirstMortgagePayoff = service.removeFirstMortgagePayoff(collateralGroup); + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithoutFirstMortgagePayoff.get(eachField.field)).toBeFalsy(); + }, this); + + const collateralGroupWithFirstMortgagePayoff = service.addFirstMortgagePayoff(collateralGroupWithoutFirstMortgagePayoff); + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithFirstMortgagePayoff.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should remove first mortgage payoff control from the form group', () => { + const collateralGroup = service.createCollateralInformationGroup(); + const collateralGroupWithFirstMortgagePayoff = service.addFirstMortgagePayoff(collateralGroup); + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithFirstMortgagePayoff.get(eachField.field)).toBeTruthy(); + }, this); + + const collateralGroupWithoutFirstMortgagePayoff = service.removeFirstMortgagePayoff(collateralGroupWithFirstMortgagePayoff); + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithoutFirstMortgagePayoff.get(eachField.field)).toBeFalsy(); + }, this); + }); + + it('should add second mortgage payoff control to the form group', () => { + const collateralGroup = service.createCollateralInformationGroup(); + expect(collateralGroup).toBeTruthy(); + const collateralGroupWithoutSecondMortgagePayoff = service.removeSecondMortgagePayoff(collateralGroup); + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithoutSecondMortgagePayoff.get(eachField.field)).toBeFalsy(); + }, this); + + const collateralGroupWithSecondMortgagePayoff = service.addSecondMortgagePayoff(collateralGroupWithoutSecondMortgagePayoff); + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithSecondMortgagePayoff.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should remove second mortgage payoff control from the form group', () => { + const collateralGroup = service.createCollateralInformationGroup(); + const collateralGroupWithSecondMortgagePayoff = service.addSecondMortgagePayoff(collateralGroup); + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithSecondMortgagePayoff.get(eachField.field)).toBeTruthy(); + }, this); + + const collateralGroupWithoutSecondMortgagePayoff = service.removeSecondMortgagePayoff(collateralGroupWithSecondMortgagePayoff); + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + expect(collateralGroupWithoutSecondMortgagePayoff.get(eachField.field)).toBeFalsy(); + }, this); + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(applicationWizardService, 'getGroupValues').and.returnValue(true); + service.getApplicationGroupValues(testGroupName); + expect(applicationWizardService.getGroupValues).toHaveBeenCalledWith(testGroupName); + }); + + it('should emit an error from the "Estimated Value" control if it is out of range', () => { + const testCollateralGroup = service.createCollateralInformationGroup(); + testCollateralGroup.patchValue({estimatedValue: 1000000000}); + expect(testCollateralGroup.controls.estimatedValue.errors.maxCurrency).toBeTruthy(); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveCollateralInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = <[IsAddressSameInformation, AddressInformation, CollateralInformation]>{}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveCollateralInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = <[IsAddressSameInformation, AddressInformation, CollateralInformation]>{}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.ts new file mode 100644 index 0000000..e13324f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.service.ts @@ -0,0 +1,261 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, FormGroup, ValidatorFn } from '@angular/forms'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { PageService } from '../page-service'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { LoanType } from '../../../shared/loan-type'; +import { CreditApplicationValues } from 'src/app/shared/credit-application-values'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import {LoanTitle} from '../../../shared/loan-type/loan-title'; + +@Injectable({ + providedIn: 'root' +}) +export class CollateralInfoPageService implements PageService { + + public static readonly COLLATERAL_FORM_NAME = 'collateralInformation'; + public static readonly ADDRESS_FORM_NAME = 'addressInformation'; + public static readonly REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME = 'realEstateCollateralInformation'; + private readonly CURRENT_ROUTE = 'eq-collateral'; + private readonly REQUIRED_STATE = 'WI'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private formatService: FormatService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + const collateralGroup = this.createCollateralInformationGroup(); + group.addControl(CollateralInfoPageService.COLLATERAL_FORM_NAME, collateralGroup); + + return group; + } + + public createCollateralInformationGroup(): FormGroup { + const collateralGroup = this.formBuilder.group({}); + const loanType: LoanType = this.applicationWizardService.getGroupValues('loanType'); + const primaryBorrowerAddress = + this.applicationWizardService.getIndividualGroupValues('addressInformation', BorrowerType.PRIMARY_BORROWER).state; + + + CommonGroups.IS_ADDRESS_SAME_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(this.isHelocOrHomeEquityFixedLoan(), primaryBorrowerAddress)); + }, this); + } + collateralGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + CommonGroups.COLLATERAL_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + collateralGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + CommonGroups.LIEN_POSITION_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + collateralGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + return collateralGroup; + } + + public isHelocOrHomeEquityFixedLoan(): boolean { + return this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.EQ_HELOC_Fixed || + this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.EQ_HELOC_Flex; + } + + public createAddressInformationGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(this.isHelocOrHomeEquityFixedLoan(), this.REQUIRED_STATE)); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + return memberGroup; + } + + public addAddressGroupsIfMissing(group: FormGroup) { + if (group.get(CollateralInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + const addressGroup = this.createAddressInformationGroup(); + + group.addControl(CollateralInfoPageService.ADDRESS_FORM_NAME, addressGroup); + } + + public removeAddressGroups(group: FormGroup) { + if (!group.get(CollateralInfoPageService.ADDRESS_FORM_NAME)) { + return; + } + group.removeControl(CollateralInfoPageService.ADDRESS_FORM_NAME); + group.removeControl(CollateralInfoPageService.REAL_ESTATE_ADDRESS_COMPARE_FORM_NAME); + } + + public addFirstMortgagePayoff(group: FormGroup): FormGroup { + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + group.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }); + + return group; + } + + public removeFirstMortgagePayoff(group: FormGroup): FormGroup { + CommonGroups.LIEN_POSITION_INFO_FIRST_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + group.removeControl(eachField.field); + }); + + return group; + } + + public addSecondMortgagePayoff(group: FormGroup): FormGroup { + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + group.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }); + + return group; + } + + public removeSecondMortgagePayoff(group: FormGroup): FormGroup { + CommonGroups.LIEN_POSITION_INFO_SECOND_MORTGAGE_PAYOFF.fields.forEach((eachField) => { + group.removeControl(eachField.field); + }); + + return group; + } + + public getApplicationGroupValues(groupName: string): any { + return this.applicationWizardService.getGroupValues(groupName); + } + + public saveAndNavigate(values, route: ActivatedRoute) { + this.creditApplicationService.saveCollateralInformation(values).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + console.log(failure); + // notify user, server, log, etc. + } + ); + } + + public cancelAndNavigate(route: ActivatedRoute) { + const prevRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([prevRoute], {relativeTo: route}); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public isRentedLotInfoRequired(): boolean { + return this.applicationWizardService.isRentedLotInfoRequired(); + } + + public isLienPositionInfoRequired(): boolean { + return this.applicationWizardService.isLienPositionInfoRequired(); + } + + public clearRentInfoRequirement(group: FormGroup): FormGroup { + group.removeControl('onRentedLot'); + group.removeControl('annualPropertyTax'); + group.removeControl('rentPaid'); + return group; + } + + public clearLienPositionInfoRequirement(group: FormGroup): FormGroup { + CommonGroups.LIEN_POSITION_INFO.fields.forEach((eachField) => { + group.removeControl(eachField.field); + }); + + return group; + } + + public getApplicantName(): string { + return this.formatService.formatTitleCase( + this.applicationWizardService.getApplicantName(BorrowerType.PRIMARY_BORROWER) + ); + } + + public getFirstMortgagePayoff(): boolean { + return this.applicationWizardService.getFieldValue('payoffFirstMortgage'); + } + + public getSecondMortgagePayoff(): boolean { + return this.applicationWizardService.getFieldValue('payoffSecondMortgage'); + } + + public isHelocRateMod(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.HELOC_RateMod; + } + + public isMfgHome(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.ManufacturedHome; + } + + public getLoanId(): string { + return this.applicationWizardService.getFieldValue('loanId'); + } + + public getFileUploadValues(): any { + + let fileUploadValues = {} + switch(this.applicationWizardService.getGroupValues('loanType').title as LoanTitle){ + case LoanTitle.ManufacturedHome: + fileUploadValues["section"] = ""; + fileUploadValues["sectionId"] = "Collateral"; + fileUploadValues["description"] = "Manufactured Home Title / Purchase Contract"; + break; + case LoanTitle.HELOC: + case LoanTitle.HomeEquityFixed: + case LoanTitle.MortgageRefinanceExpress: + fileUploadValues["section"] = ""; + fileUploadValues["sectionId"] = "Collateral"; + fileUploadValues["description"] = "Copy of homeowners insurance"; + fileUploadValues["label"] = "a copy of the homeowners insurance"; + break; + default: + fileUploadValues = null; + break; + } + + return fileUploadValues; + + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.ts new file mode 100644 index 0000000..4e6fc30 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/collateral-info-page.ts @@ -0,0 +1,22 @@ +import { ApplicationPage } from '../../application-page'; +import { CollateralInformation } from '../../field-groups/equity/collateral-information'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + +export class CollateralInfoPage extends ApplicationPage { + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + + const collateralInfo: CollateralInformation = applicationWizardService.getGroupValues('collateralInformation'); + + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = collateralInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.html new file mode 100644 index 0000000..01f4ead --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.html @@ -0,0 +1,34 @@ +
+
+
+
+

HEADINGS.GET_STARTED

+
+
+
+
+
+
+
+ + + + +
+
+ +
+ +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.spec.ts new file mode 100644 index 0000000..f5d9cbe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.spec.ts @@ -0,0 +1,195 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule, FormGroup } from '@angular/forms'; +import { NO_ERRORS_SCHEMA, Component, Input } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { EquityMemberInfoPageComponent } from './equity-member-info-page.component'; +import { EquityMemberInfoPageService } from './equity-member-info-page.service'; +import { WindowService } from '../../../shared/window.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { LoanTypeSet } from 'src/app/shared/loan-type'; + +@Component({ + selector: 'lw-member-info-form', + template: '

employment info form stub

' +}) +class MockEquityMemberInfoFormComponent { + + @Input() + public group: FormGroup; + + constructor() { } + +} + +describe('EquityMemberInfoPageComponent', () => { + let component: EquityMemberInfoPageComponent; + let fixture: ComponentFixture; + let service: EquityMemberInfoPageService; + let applicationWizardService: ApplicationWizardService; + let windowService: WindowService; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + EquityMemberInfoPageComponent, + MockEquityMemberInfoFormComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EquityMemberInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(EquityMemberInfoPageService); + windowService = TestBed.get(WindowService); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.pageForm).toBeTruthy(); + expect(component.equityMemberInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + }); + + it('should use the window service to print.', () => { + fixture.detectChanges(); + spyOn(windowService, 'showWindowForId').and.returnValue(undefined); + component.commandShowPage(); + expect(windowService.showWindowForId).toHaveBeenCalled(); + }); + + it('should require add/remove controls to memberInfoGroup if "yes" is chosen for co applicant', () => { + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + component.updateLocalState({ + applicantType: CreditApplicationValues.CHOICE_YES + }); + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeTruthy(); + component.updateLocalState({ + applicantType: CreditApplicationValues.CHOICE_NO + }); + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + }); + + it('should require addtl controls if "yes" is chosen for co applicant', () => { + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.get('coApplicantLivesInMaritalPropertyState')).toBeFalsy(); + const data = { + applicantType: CreditApplicationValues.CHOICE_NO, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + termOfLoan: 120, + purposeOfLoan: 'Refinance', + agreementRequirement: true + }; + component.updateLocalState(data); + component.equityMemberInfoGroup.patchValue(data); + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.valid).toEqual(true); + data.applicantType = CreditApplicationValues.CHOICE_YES; + component.updateLocalState(data); + component.equityMemberInfoGroup.patchValue(data); + component.equityMemberInfoGroup.controls.coApplicantLivesInMaritalPropertyState.updateValueAndValidity(); + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.valid).toEqual(false); + const expandedData = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_YES, + termOfLoan: 120, + purposeOfLoan: 'Refinance', + agreementRequirement: true + }; + component.updateLocalState(expandedData); + component.equityMemberInfoGroup.patchValue(expandedData); + component.equityMemberInfoGroup.controls.coApplicantLivesInMaritalPropertyState.updateValueAndValidity(); + fixture.detectChanges(); + expect(component.equityMemberInfoGroup.valid).toEqual(true); + }); + + it('should command the modal to open when applicant fails to confirm joint credit', async(done) => { + fixture.detectChanges(); + component.openModal.subscribe(nextValues => { + expect(nextValues).toBe(true); + done(); + }); + const data = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_NO + }; + component.updateLocalState(data); + fixture.detectChanges(); + }); + + it('should NOT command the modal to open when applicant confirms joint credit', async(done) => { + fixture.detectChanges(); + component.openModal.subscribe(nextValues => { + expect(nextValues).toBe(false); + done(); + }); + const data = { + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + applicantType: CreditApplicationValues.CHOICE_YES, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_NO, + limitRequested: 10000, + confirmJointCredit: CreditApplicationValues.CHOICE_YES + }; + component.updateLocalState(data); + fixture.detectChanges(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.ts new file mode 100644 index 0000000..ff406c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.component.ts @@ -0,0 +1,146 @@ +import { Component, OnInit, OnDestroy, AfterContentInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { EquityMemberInfoPageService } from './equity-member-info-page.service'; +import { CreditApplication } from '../../../shared/credit-application'; +import { WindowService } from '../../../shared/window.service'; +import { CreditApplicationValues } from 'src/app/shared/credit-application-values'; +import { Subscription, Subject } from 'rxjs'; + +@Component({ + selector: 'lw-equity-member-info-page', + templateUrl: './equity-member-info-page.component.html' +}) +export class EquityMemberInfoPageComponent implements OnInit, OnDestroy, AfterContentInit { + + public pageForm: FormGroup; + public equityMemberInfoGroup: FormGroup; + public applicationTypeMessage: string; + public amountRequestedName = 'Loan Amount'; + + public askLoanPurpose: boolean; + public askTermDuration: boolean; + + private valueSubscription: Subscription; + private statusSubscription: Subscription; + public openModal: Subject; + + public savingData = null; + + private readonly FORM_STATE_INVALID = 'INVALID'; + + public localState = { + nextDisabled: true, + nextTopic: '', + hasCoApplicant: false, + askLoanPurpose: false, + askTermDuration: false, + showCycleCopy: false, + isSpecialLoan: false, + isHelocRateMod: false, + isModalOpen: false, + isHelocOrFixedLoan: false, + beforeYouBeginMessage: null, + requireAgreement: false + }; + + + constructor( + private equityMemberInfoPageService: EquityMemberInfoPageService, + private route: ActivatedRoute, + private windowService: WindowService) { } + + public ngOnInit() { + this.openModal = new Subject(); + this.pageForm = this.equityMemberInfoPageService.createPageGroup(); + this.patchMemberInfoGroupValues(); + this.equityMemberInfoPageService.getLoanTypeMessage().subscribe( (newMessage) => { + this.applicationTypeMessage = newMessage; + }); + this.localState.showCycleCopy = this.equityMemberInfoPageService.isCycleLoan(); + this.localState.isSpecialLoan = this.equityMemberInfoPageService.isSpecialLoan(); + this.localState.isHelocRateMod = this.equityMemberInfoPageService.isHelocRateMod(); + this.localState.askLoanPurpose = this.equityMemberInfoPageService.requirePurposeOfLoan(); + this.localState.askTermDuration = this.equityMemberInfoPageService.requireTermOfLoan(); + this.localState.isHelocOrFixedLoan = this.equityMemberInfoPageService.isHelocOrHomeEquityFixedLoan(); + this.localState.requireAgreement = this.equityMemberInfoPageService.requireAgreement(); + } + + public ngAfterContentInit() { + this.pageForm.addControl(EquityMemberInfoPageService.EQUITY_MEMBER_FORM_NAME, this.equityMemberInfoGroup); + this.localState.nextDisabled = this.pageForm.status === this.FORM_STATE_INVALID; + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + } + + public ngOnDestroy() { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public updateLocalState(pageValues): void { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + + this.updateCoApplicantFields(pageValues); + this.localState.hasCoApplicant = pageValues.applicantType === CreditApplicationValues.CHOICE_YES; + this.localState.nextTopic = this.equityMemberInfoPageService.getNextTopicMessage(); + this.localState.beforeYouBeginMessage = this.equityMemberInfoPageService.getBeforeYouBeginMessage(); + + if (pageValues.applicantType === CreditApplicationValues.CHOICE_YES && + pageValues.confirmJointCredit === CreditApplicationValues.CHOICE_NO && + !this.localState.isModalOpen) { + this.localState.isModalOpen = true; + this.openModal.next(true); + } else { + this.localState.isModalOpen = false; + this.openModal.next(false); + } + + this.valueSubscription = this.equityMemberInfoGroup.valueChanges.subscribe((pageValue) => { + this.updateLocalState(pageValue); + }); + } + + public updateCoApplicantFields(pageValues) { + if (pageValues.applicantType === CreditApplicationValues.CHOICE_YES && !this.equityMemberInfoGroup.get('confirmJointCredit')) { + this.equityMemberInfoPageService.addCoApplicantFieldsToMemberGroup(this.equityMemberInfoGroup); + } else if (pageValues.applicantType === CreditApplicationValues.CHOICE_NO && this.equityMemberInfoGroup.get('confirmJointCredit')) { + this.equityMemberInfoPageService.removeCoApplicantFieldsFromMemberGroup(this.equityMemberInfoGroup); + } + } + + public patchMemberInfoGroupValues() { + this.equityMemberInfoGroup = this.equityMemberInfoPageService.createEquityMemberInformationGroup(); + const formatted = Object.assign( + this.equityMemberInfoPageService.getCommonMemberInfoGroupValues(), + this.equityMemberInfoPageService.getEquityMemberInfoValues() + ); + + if (formatted.applicantType !== CreditApplicationValues.CHOICE_YES) { + this.equityMemberInfoPageService.removeCoApplicantFieldsFromMemberGroup(this.equityMemberInfoGroup); + } + this.equityMemberInfoGroup.patchValue(formatted); + this.updateLocalState(formatted); + } + + public changeApplicantTypeToIndividual(): void { + this.equityMemberInfoGroup.patchValue({applicantType: CreditApplicationValues.CHOICE_NO}); + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + this.equityMemberInfoPageService.saveAndNavigate(this.equityMemberInfoGroup.value, this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandShowPage(): void { + this.windowService.showWindowForId('electronic-disclosures-agreement-container'); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.spec.ts new file mode 100644 index 0000000..4f9dd53 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.spec.ts @@ -0,0 +1,149 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { MembershipInformation } from '../../field-groups/member/membership-information'; +import { EquityMemberInfoPageService } from './equity-member-info-page.service'; +import { EquityMemberInformation } from '../../field-groups/equity/equity-member-information'; +import { LoanType, LoanTypeSet } from '../../../shared/loan-type'; +import { LoanCategory } from '../../../shared/loan-type/loan-category'; +import { LoanTitle } from '../../../shared/loan-type/loan-title'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { UiSettings } from 'src/app/shared/configuration/ui-settings'; +import { DefaultSettings } from 'src/app/shared/configuration/default-settings.mock'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + + +describe('Equity Member Info Page Service', () => { + + let service: EquityMemberInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationService: CreditApplicationService; + let router: Router; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(EquityMemberInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + creditApplicationService = TestBed.get(CreditApplicationService); + applicationWizardService.setLoanType(LoanTypeSet[5].id); + router = TestBed.get(Router); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create an equity member info group', () => { + const memberGroup = service.createEquityMemberInformationGroup(); + expect(memberGroup).toBeTruthy(); + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + expect(memberGroup.get(eachField.field)).toBeTruthy(); + }, this); + CommonGroups.MEMBER_INFO_TERM_OF_LOAN.fields.forEach((eachField) => { + expect(memberGroup.get(eachField.field)).toBeTruthy(); + }); + CommonGroups.MEMBER_INFO_TERM_OF_LOAN.fields.forEach((eachField) => { + expect(memberGroup.get(eachField.field)).toBeTruthy(); + }); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveEquityMemberInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = <[MembershipInformation, EquityMemberInformation]>{}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveEquityMemberInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = <[MembershipInformation, EquityMemberInformation]>{}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should determine if term of loan is required', () => { + const originalGetGroupValues = applicationWizardService.getGroupValues; + let currentLoanType: LoanType = { ...LoanTypeSet[4] }; + const getGroupValuesSpy = spyOn(applicationWizardService, 'getGroupValues').and.callFake((groupName: string) => { + if (groupName === 'loanType') { + return currentLoanType; + } + return originalGetGroupValues.call(applicationWizardService, groupName); + }); + + expect(service.requireTermOfLoan()).toBe(false); + + currentLoanType = { ...currentLoanType, title: LoanTitle.HomeEquityFixed, category: LoanCategory.Equity, requiresCollateral: true }; + expect(service.requireTermOfLoan()).toBe(true); + + currentLoanType = { ...currentLoanType, title: LoanTitle.Vehicle, category: LoanCategory.Vehicle, requiresCollateral: true }; + expect(service.requireTermOfLoan()).toBe(false); + + currentLoanType = { ...currentLoanType, title: LoanTitle.VehiclePreApproval, category: LoanCategory.Vehicle, requiresCollateral: false }; + expect(service.requireTermOfLoan()).toBe(true); + + currentLoanType = { ...currentLoanType, title: LoanTitle.Signature, category: LoanCategory.Signature, requiresCollateral: false }; + expect(service.requireTermOfLoan()).toBe(true); + + getGroupValuesSpy.and.callThrough(); + }); + + it('should determine if purpose of loan is required', () => { + const originalGetGroupValues = applicationWizardService.getGroupValues; + let currentLoanType: LoanType = { ...LoanTypeSet[0] }; + const getGroupValuesSpy = spyOn(applicationWizardService, 'getGroupValues').and.callFake((groupName: string) => { + if (groupName === 'loanType') { + return currentLoanType; + } + return originalGetGroupValues.call(applicationWizardService, groupName); + }); + + expect(service.requirePurposeOfLoan()).toBe(false); + + currentLoanType = { ...currentLoanType, category: LoanCategory.Equity }; + expect(service.requirePurposeOfLoan()).toBe(true); + + currentLoanType = { ...currentLoanType, category: LoanCategory.Vehicle }; + expect(service.requirePurposeOfLoan()).toBe(false); + + currentLoanType = { ...currentLoanType, category: LoanCategory.Signature }; + expect(service.requirePurposeOfLoan()).toBe(false); + + getGroupValuesSpy.and.callThrough(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.ts new file mode 100644 index 0000000..8bea279 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.service.ts @@ -0,0 +1,189 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { EquityMemberInformation } from '../../field-groups/equity/equity-member-information'; +import { MembershipInformation } from '../../field-groups/member/membership-information'; +import { PageService } from '../page-service'; +import { LoanType } from '../../../shared/loan-type'; +import { ConfigurationService } from 'src/app/shared/configuration/configuration.service'; +import { Observable } from 'rxjs'; +import { LoanTitle } from 'src/app/shared/loan-type/loan-title'; +import { LoanCategory } from 'src/app/shared/loan-type/loan-category'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; +import { MemberInfoFormService } from '../../field-groups/member/member-info-form.service'; + +@Injectable({ + providedIn: 'root' +}) +export class EquityMemberInfoPageService implements PageService { + + public static readonly EQUITY_MEMBER_FORM_NAME = 'equityMemberInformation'; + public static readonly MEMBER_FORM_NAME = 'memberInformation'; + private readonly CURRENT_ROUTE = 'step-1'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private configurationService: ConfigurationService, + private router: Router, + private formBuilder: FormBuilder, + private memberService: MemberInfoFormService) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + return group; + } + + public createEquityMemberInformationGroup(): FormGroup { + const equityMemberGroup = this.formBuilder.group({}); + const loanType = this.applicationWizardService.getGroupValues('loanType'); + const loanCategory = this.memberService.getLoanCategory(); + CommonGroups.MEMBER_INFO.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(loanCategory, loanType, this.configurationService)); + }, this); + } + equityMemberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + if (this.requireTermOfLoan()) { + CommonGroups.MEMBER_INFO_TERM_OF_LOAN.fields.forEach( ( eachField ) => { + equityMemberGroup.addControl( eachField.field, this.formBuilder.control( '', eachField.validation.slice() ) ); + }, this ); + } + + if (this.requirePurposeOfLoan()) { + CommonGroups.MEMBER_INFO_PURPOSE_OF_LOAN.fields.forEach( ( eachField ) => { + equityMemberGroup.addControl( eachField.field, this.formBuilder.control( '', eachField.validation.slice() ) ); + }, this ); + } + + if (this.requirePurposeOfLoanSignature()) { + CommonGroups.MEMBER_INFO_PURPOSE_OF_LOAN_SIGNATURE.fields.forEach( ( eachField ) => { + equityMemberGroup.addControl( eachField.field, this.formBuilder.control( '', eachField.validation.slice() ) ); + }, this ); + } + + if (this.isSpecialLoan()) { + CommonGroups.SPECIAL.fields.forEach( ( eachField ) => { + equityMemberGroup.addControl( eachField.field, this.formBuilder.control( '', eachField.validation.slice() ) ); + }, this ); + } + + this.addCoApplicantFieldsToMemberGroup(equityMemberGroup); + this.addAgreementRequirement(equityMemberGroup); + + return equityMemberGroup; + } + + public addCoApplicantFieldsToMemberGroup(formGroup: FormGroup) { + CommonGroups.CO_APP_MEMBER_INFO.fields.forEach((eachField) => { + formGroup.addControl(eachField.field, this.formBuilder.control('', [])); + }, this); + } + + public addAgreementRequirement(formGroup: FormGroup) { + + if (this.requireAgreement()) { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + CommonGroups.MEMBER_AGREEMENT_REQUIREMENT.fields.forEach((eachField) => { + const validators = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + validators.push(validationFunction(loanType, this.configurationService)); + }, this); + } + formGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + } + + public removeCoApplicantFieldsFromMemberGroup(formGroup: FormGroup) { + formGroup.removeControl('confirmJointCredit'); + formGroup.removeControl('coApplicantLivesInMaritalPropertyState'); + } + + public getEquityMemberInfoValues(): EquityMemberInformation { + return this.creditApplicationService.getEquityMemberInformation(); + } + + public getCommonMemberInfoGroupValues(): MembershipInformation { + return this.creditApplicationService.getMemberInformation(); + } + + public isHelocOrHomeEquityFixedLoan(): boolean { + return this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.EQ_HELOC_Fixed || + this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.EQ_HELOC_Flex; + } + + public saveAndNavigate(equityMemberInfoData: [MembershipInformation, EquityMemberInformation], route: ActivatedRoute) { + this.creditApplicationService.saveEquityMemberInformation(equityMemberInfoData).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], {relativeTo: route}); + } + }, + (failure) => { + // notify user, server, log, etc. + } + ); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public requireTermOfLoan(): boolean { + const loanType: LoanType = this.applicationWizardService.getGroupValues('loanType'); + if (loanType.title === LoanTitle.HELOC) { + return false; + } + return (loanType.category === LoanCategory.Equity || loanType.category === LoanCategory.Signature + || (loanType.category === LoanCategory.Vehicle && !loanType.requiresCollateral)); + } + + public requirePurposeOfLoan(): boolean { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + return (loanType.category === LoanCategory.Equity); + } + + public requirePurposeOfLoanSignature(): boolean { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + return (loanType.category === LoanCategory.Signature); + } + + public getLoanTypeMessage(): Observable { + return this.applicationWizardService.getLoanMessage(); + } + + public isCycleLoan(): boolean { + return this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.VEHICLE_DEFAULT; + } + + public isSpecialLoan(): boolean { + return this.applicationWizardService.getGroupValues('loanType').apiCode === LoanCode.SPECIAL; + } + + public isHelocRateMod(): boolean { + return this.applicationWizardService.getGroupValues('loanType').title === LoanTitle.HELOC_RateMod; + } + + public requireAgreement(): boolean { + return ![LoanTitle.Vehicle, LoanTitle.VehiclePreApproval, LoanTitle.HELOC_RateMod].includes(this.applicationWizardService.getGroupValues('loanType').title); + } + + public getBeforeYouBeginMessage(): object { + return this.applicationWizardService.getBeforeYouBeginMessage(); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.ts new file mode 100644 index 0000000..1d9463d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-member-info-page.ts @@ -0,0 +1,28 @@ +import { ApplicationPage } from '../../application-page'; +import { MembershipInformation } from 'src/app/loan-wizard/field-groups/member/membership-information'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; +import { EquityMemberInformation } from '../../field-groups/equity/equity-member-information'; +import { CreditApplication } from '../../../shared/credit-application'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + +export class EquityMemberInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const memberInfo = creditApplicationService.getMemberInformation(); + const application: CreditApplication = creditApplicationService.getFullCreditApplication(); + + Object.assign(memberInfo, {purposeOfLoan: application.purposeOfLoan}); + Object.assign(memberInfo, {termOfLoan: application.termOfLoan}); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = memberInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-outline.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-outline.ts new file mode 100644 index 0000000..6af6567 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-outline.ts @@ -0,0 +1,166 @@ +import { ApplicationPage } from '../../application-page'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { PrimaryEmploymentInfoPage } from '../common/employment/pages/primary-employment-info-page'; +import { PrimaryPersonalInfoPage } from '../common/personal/pages/primary-personal-info-page'; +import { PrimaryAddressInfoPage } from '../common/address/pages/primary-address-info-page'; +import { CollateralInfoPage } from './collateral-info-page'; +import { CreditCardDisclosuresInfoPage } from '../credit-card/credit-card-disclosures-info-page'; +import { EquityMemberInfoPage } from './equity-member-info-page'; +import { SpouseTypeInfoPage } from '../common/spouse-type/spouse-type-info-page'; +import { PrimarySpousePersonalInfoPage } from '../common/personal/pages/primary-spouse-personal-info-page'; +import { CoBorrowerPersonalInfoPage } from '../common/personal/pages/co-borrower-personal-info-page'; +import { CoBorrowerSpousePersonalInfoPage } from '../common/personal/pages/co-borrower-spouse-personal-info-page'; +import { PrimarySpouseAddressInfoPage } from '../common/address/pages/primary-spouse-address-info-page'; +import { CoBorrowerAddressInfoPage } from '../common/address/pages/co-borrower-address-info-page'; +import { CoBorrowerSpouseAddressInfoPage } from '../common/address/pages/co-borrower-spouse-address-info-page'; +import { PrimarySpouseEmploymentInfoPage } from '../common/employment/pages/primary-spouse-employment-info-page'; +import { CoBorrowerEmploymentInfoPage } from '../common/employment/pages/co-borrower-employment-info-page'; +import { CoBorrowerSpouseEmploymentInfoPage } from '../common/employment/pages/co-borrower-spouse-employment-info-page'; +import { PrimaryTattleSpouseInfoPage } from '../common/tattle-spouse/pages/primary-tattle-spouse-info-page'; +import { CoBorrowerTattleSpouseInfoPage } from '../common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page'; +import { PrimaryHmdaInfoPage } from '../common/hmda/pages/primary-hmda-info-page'; +import { CoBorrowerHmdaInfoPage } from '../common/hmda/pages/co-borrower-hmda-info-page'; +import { SummaryInfoPage } from '../common/summary/summary-info-page'; +import { DecisionPage } from '../common/decision/decision-page'; +import { ProcessingPage } from '../common/processing/processing-page'; + +export const EquityPages: Array = [ + /* PRIMARY */ + new EquityMemberInfoPage('PAGETITLES.COMMON.INTRO', 'step-1', + [ + CommonGroups.MEMBER_INFO, + CommonGroups.MEMBER_INFO_PURPOSE_OF_LOAN + ] + ), + new PrimaryPersonalInfoPage('PAGETITLES.COMMON.PERSONAL_INFO', 'step-2', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimaryAddressInfoPage('PAGETITLES.COMMON.ADDRESS_INFO', 'step-3', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimaryHmdaInfoPage('PAGETITLES.EQUITY.HMDA', 'eq-hmda', + [ + CommonGroups.RACE_INFO, + CommonGroups.ETHNICITY_INFO, + CommonGroups.GENDER_INFO + ] + ), + new PrimaryEmploymentInfoPage('PAGETITLES.COMMON.EMPLOYMENT_INFO', 'step-4', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + /* PRIMARY_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.PRIMARY_SPOUSETYPE', + 'applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ] + ), + new PrimaryTattleSpouseInfoPage('PAGETITLES.COMMON.PRIMARY_TATTLESPOUSE', + 'applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ] + ), + new PrimarySpousePersonalInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_PERSONAL_INFO', + 'applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimarySpouseAddressInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_ADDRESS_INFO', + 'applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimarySpouseEmploymentInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_EMPLOYMENT_INFO', + 'applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + /* CO_APPLICANT */ + new CoBorrowerPersonalInfoPage('PAGETITLES.COMMON.COAPP_PERSONAL_INFO', + 'co-applicant-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerAddressInfoPage('PAGETITLES.COMMON.COAPP_ADDRESS_INFO', + 'co-applicant-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerHmdaInfoPage('PAGETITLES.EQUITY.COAPP_HMDA', 'co-applicant-eq-hmda', + [ + CommonGroups.RACE_INFO, + CommonGroups.ETHNICITY_INFO, + CommonGroups.GENDER_INFO + ] + ), + new CoBorrowerEmploymentInfoPage('PAGETITLES.COMMON.COAPP_EMPLOYMENT_INFO', + 'co-applicant-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.COAPP_SPOUSETYPE', + 'co-applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ] + ), + new CoBorrowerTattleSpouseInfoPage('PAGETITLES.COMMON.COAPP_TATTLESPOUSE', + 'co-applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ] + ), + new CoBorrowerSpousePersonalInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_PERSONAL_INFO', + 'co-applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerSpouseAddressInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_ADDRESS_INFO', + 'co-applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerSpouseEmploymentInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_EMPLOYMENT_INFO', + 'co-applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + new CollateralInfoPage('PAGETITLES.EQUITY.COLLATERAL', 'eq-collateral', + [ + CommonGroups.COLLATERAL_INFO, + CommonGroups.LIEN_POSITION_INFO + ] + ), + new CreditCardDisclosuresInfoPage('PAGETITLES.COMMON.DISCLOSURES', 'cc-disclosures', + [ + CommonGroups.CC_DISCLOSURE_INFO // TODO: Make a different disclosure group, or change the name. + ] + ), + new SummaryInfoPage('PAGETITLES.COMMON.SUMMARY', 'cc-summary', []), + new ProcessingPage('PAGETITLES.CC.PROCESS', 'process', []), + new DecisionPage('PAGETITLES.CC.INTRO', 'decision', []) +]; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-routing.module.ts new file mode 100644 index 0000000..15ca7c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/equity/equity-routing.module.ts @@ -0,0 +1,71 @@ +import { Route} from '@angular/router'; +import { PersonalInfoPageComponent } from '../common/personal/personal-info-page.component'; +import { AddressInfoPageComponent } from '../common/address/address-info-page.component'; +import { EmploymentInfoPageComponent } from '../common/employment/employment-info-page.component'; +import { CreditCardDisclosuresPageComponent } from '../credit-card/credit-card-disclosures-page.component'; +import { SummaryPageComponent } from '../common/summary/summary-page.component'; +import { DecisionPageComponent } from '../common/decision/decision-page.component'; +import { LoanRouteGuard } from '../../../shared/guards/loan-route.guard'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { TattleSpouseInfoPageComponent } from '../common/tattle-spouse/tattle-spouse-info-page.component'; +import { SpouseTypeInfoPageComponent } from '../common/spouse-type/spouse-type-info-page.component'; +import { ProcessingPageComponent } from '../common/processing/processing-page.component'; +import { EquityMemberInfoPageComponent } from './equity-member-info-page.component'; +import { HmdaInfoPageComponent } from '../common/hmda/hmda-info-page.component'; +import { CollateralInfoPageComponent } from './collateral-info-page.component'; +import { StepperControllerGuard } from 'src/app/shared/guards/stepper-controller-guard'; +import { MemberCoreResolver } from 'src/app/member/member-core.resolver'; +import { environment } from 'src/environments/environment'; + + +export const eqRoutes: Route = { + path: 'eq/:type', + resolve: environment.coreResolver ? { CoreIndividual: MemberCoreResolver} : {}, + canActivate: [StepperControllerGuard], + canActivateChild: [LoanRouteGuard], + children: [ + { path: 'step-1', component: EquityMemberInfoPageComponent }, + { path: 'step-2', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-3', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'eq-hmda', component: HmdaInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-4', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'co-applicant-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-eq-hmda', component: HmdaInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'eq-collateral', component: CollateralInfoPageComponent }, + { path: 'cc-disclosures', component: CreditCardDisclosuresPageComponent }, + { path: 'cc-summary', component: SummaryPageComponent }, + { path: 'process', component: ProcessingPageComponent }, + { path: 'decision', component: DecisionPageComponent }, + { path: '', redirectTo: 'step-1', pathMatch: 'prefix' } + ] +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/page-service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/page-service.ts new file mode 100644 index 0000000..cf6a3e9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/page-service.ts @@ -0,0 +1,7 @@ +import { BorrowerType } from '../../shared/borrower-type'; + +export interface PageService { + + getNextTopicMessage(individual?: BorrowerType): string; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-loan-outline.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-loan-outline.ts new file mode 100644 index 0000000..537e274 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-loan-outline.ts @@ -0,0 +1,138 @@ +import { ApplicationPage } from '../../application-page'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { MemberInfoPage } from '../common/member/member-info-page'; +import { PrimaryEmploymentInfoPage } from '../common/employment/pages/primary-employment-info-page'; +import { PrimaryPersonalInfoPage } from '../common/personal/pages/primary-personal-info-page'; +import { PrimaryAddressInfoPage } from '../common/address/pages/primary-address-info-page'; +import { PrimaryTattleSpouseInfoPage } from '../common/tattle-spouse/pages/primary-tattle-spouse-info-page'; +import { SpouseTypeInfoPage } from '../common/spouse-type/spouse-type-info-page'; +import { PrimarySpousePersonalInfoPage } from '../common/personal/pages/primary-spouse-personal-info-page'; +import { CoBorrowerPersonalInfoPage } from '../common/personal/pages/co-borrower-personal-info-page'; +import { CoBorrowerSpousePersonalInfoPage } from '../common/personal/pages/co-borrower-spouse-personal-info-page'; +import { PrimarySpouseAddressInfoPage } from '../common/address/pages/primary-spouse-address-info-page'; +import { CoBorrowerAddressInfoPage } from '../common/address/pages/co-borrower-address-info-page'; +import { CoBorrowerSpouseAddressInfoPage } from '../common/address/pages/co-borrower-spouse-address-info-page'; +import { PrimarySpouseEmploymentInfoPage } from '../common/employment/pages/primary-spouse-employment-info-page'; +import { CoBorrowerEmploymentInfoPage } from '../common/employment/pages/co-borrower-employment-info-page'; +import { CoBorrowerSpouseEmploymentInfoPage } from '../common/employment/pages/co-borrower-spouse-employment-info-page'; +import { CoBorrowerTattleSpouseInfoPage } from '../common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page'; +import { SummaryInfoPage } from '../common/summary/summary-info-page'; +import { DecisionPage } from '../common/decision/decision-page'; +import { ProcessingPage } from '../common/processing/processing-page'; +import { CreditCardDisclosuresInfoPage } from '../credit-card/credit-card-disclosures-info-page'; +import { EquityMemberInfoPage } from '../equity/equity-member-info-page'; + +export const SignatureLoanPages: Array = [ + /* PRIMARY */ + new EquityMemberInfoPage('PAGETITLES.COMMON.INTRO', + 'step-1', + [ + CommonGroups.MEMBER_INFO + ]), + new PrimaryPersonalInfoPage('PAGETITLES.COMMON.PERSONAL_INFO', + 'step-2', + [ + CommonGroups.PERSONAL_INFO + ]), + new PrimaryAddressInfoPage('PAGETITLES.COMMON.ADDRESS_INFO', + 'step-3', + [ + CommonGroups.ADDRESS_INFO + ]), + new PrimaryEmploymentInfoPage('PAGETITLES.COMMON.EMPLOYMENT_INFO', + 'step-4', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* PRIMARY_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.PRIMARY_SPOUSETYPE', + 'applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ]), + new PrimaryTattleSpouseInfoPage('PAGETITLES.COMMON.PRIMARY_TATTLESPOUSE', + 'applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ]), + new PrimarySpousePersonalInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_PERSONAL_INFO', + 'applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimarySpouseAddressInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_ADDRESS_INFO', + 'applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimarySpouseEmploymentInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_EMPLOYMENT_INFO', + 'applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT */ + new CoBorrowerPersonalInfoPage('PAGETITLES.COMMON.COAPP_PERSONAL_INFO', + 'co-applicant-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerAddressInfoPage('PAGETITLES.COMMON.COAPP_ADDRESS_INFO', + 'co-applicant-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerEmploymentInfoPage('PAGETITLES.COMMON.COAPP_EMPLOYMENT_INFO', + 'co-applicant-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.COAPP_SPOUSETYPE', + 'co-applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ]), + new CoBorrowerTattleSpouseInfoPage('PAGETITLES.COMMON.COAPP_TATTLESPOUSE', + 'co-applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ]), + new CoBorrowerSpousePersonalInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_PERSONAL_INFO', + 'co-applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerSpouseAddressInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_ADDRESS_INFO', + 'co-applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerSpouseEmploymentInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_EMPLOYMENT_INFO', + 'co-applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* DISCLOSURE / SUMMARY */ + new CreditCardDisclosuresInfoPage('PAGETITLES.COMMON.DISCLOSURES', + 'cc-disclosures', + [ + CommonGroups.CC_DISCLOSURE_INFO + ]), + new SummaryInfoPage('PAGETITLES.COMMON.SUMMARY', 'cc-summary', []), + new ProcessingPage('PAGETITLES.CC.PROCESS', 'process', []), + new DecisionPage('PAGETITLES.COMMON.DECISION', 'decision', []) +]; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-routing.module.ts new file mode 100644 index 0000000..d30f83c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/signature/signature-routing.module.ts @@ -0,0 +1,64 @@ +import { Route} from '@angular/router'; +import { PersonalInfoPageComponent } from '../common/personal/personal-info-page.component'; +import { AddressInfoPageComponent } from '../common/address/address-info-page.component'; +import { EmploymentInfoPageComponent } from '../common/employment/employment-info-page.component'; +import { SummaryPageComponent } from '../common/summary/summary-page.component'; +import { DecisionPageComponent } from '../common/decision/decision-page.component'; +import { LoanRouteGuard } from '../../../shared/guards/loan-route.guard'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { TattleSpouseInfoPageComponent } from '../common/tattle-spouse/tattle-spouse-info-page.component'; +import { SpouseTypeInfoPageComponent } from '../common/spouse-type/spouse-type-info-page.component'; +import { ProcessingPageComponent } from '../common/processing/processing-page.component'; +import { CreditCardDisclosuresPageComponent } from '../credit-card/credit-card-disclosures-page.component'; +import { EquityMemberInfoPageComponent } from '../equity/equity-member-info-page.component'; +import { StepperControllerGuard } from 'src/app/shared/guards/stepper-controller-guard'; +import { MemberCoreResolver } from 'src/app/member/member-core.resolver'; +import { environment } from 'src/environments/environment'; + + +export const slRoutes: Route = { + path: 'sl/:type', + resolve: environment.coreResolver ? { CoreIndividual: MemberCoreResolver} : {}, + canActivate: [StepperControllerGuard], + canActivateChild: [LoanRouteGuard], + children: [ + { path: 'step-1', component: EquityMemberInfoPageComponent }, + { path: 'step-2', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-3', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-4', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'co-applicant-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'cc-disclosures', component: CreditCardDisclosuresPageComponent }, + { path: 'cc-summary', component: SummaryPageComponent }, + { path: 'process', component: ProcessingPageComponent }, + { path: 'decision', component: DecisionPageComponent }, + { path: '', redirectTo: 'step-1', pathMatch: 'prefix' } + ] +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.html new file mode 100644 index 0000000..04ea4cf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.html @@ -0,0 +1,30 @@ +
+
+
+

+
+
+
+ + +
+ +
+ + +
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.spec.ts new file mode 100644 index 0000000..64569ea --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.spec.ts @@ -0,0 +1,97 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { VehicleCollateralInfoPageComponent } from './vehicle-collateral-info-page.component'; +import { VehicleCollateralInfoPageService } from './vehicle-collateral-info-page.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('VehicleCollateralInfoPageComponent', () => { + let component: VehicleCollateralInfoPageComponent; + let fixture: ComponentFixture; + let service: VehicleCollateralInfoPageService; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + VehicleCollateralInfoPageComponent + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[10].id); + fixture = TestBed.createComponent(VehicleCollateralInfoPageComponent); + component = fixture.componentInstance; + service = TestBed.get(VehicleCollateralInfoPageService); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.vehicleInfoGroup).toBeTruthy(); + }); + + it('should bind the next button to the correct command function', async(() => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonNext')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).toHaveBeenCalled(); + expect(component.commandPrevious).not.toHaveBeenCalled(); + }); + })); + + it('should use the service to navigate', () => { + fixture.detectChanges(); + spyOn(service, 'saveAndNavigate'); + component.localState.nextDisabled = false; + component.commandNext(); + expect(service.saveAndNavigate).toHaveBeenCalled(); + spyOn(service, 'cancelAndNavigate'); + component.commandPrevious(); + expect(service.cancelAndNavigate).toHaveBeenCalled(); + }); + + it('should bind the previous button to the correct command function', async () => { + spyOn(component, 'commandNext'); + spyOn(component, 'commandPrevious'); + + const button = fixture.debugElement.query(By.css('#buttonPrevious')).nativeElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.commandNext).not.toHaveBeenCalled(); + expect(component.commandPrevious).toHaveBeenCalled(); + }); + }); + + it('should patch in loan term correctly (must convert to "int" for dropdown', () => { + spyOn(applicationWizardService, 'getFieldValue').and.returnValue('48'); + + fixture.detectChanges(); + expect(component.vehicleInfoGroup.value.term).toBe(48); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.ts new file mode 100644 index 0000000..1be8fa2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component.ts @@ -0,0 +1,161 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { VehicleCollateralInfoPageService } from './vehicle-collateral-info-page.service'; +import { Subject, Subscription } from 'rxjs'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +import { CreditApplicationValues } from '../../../shared/credit-application-values'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { AddressFormBuilderService } from 'src/app/shared/address-validation/address-form-builder.service'; +import { AddressInformation } from '../../field-groups/address/address-information'; +import { AddressDiffInformation } from '../../field-groups/address-compare/address-compare-information'; +import { AddressValidationFormatConverter } from 'src/app/shared/address-validation/address-validation-format-converter'; + +@Component({ + selector: 'lw-collateral-vehicle-info-page', + templateUrl: './vehicle-collateral-info-page.component.html' +}) +export class VehicleCollateralInfoPageComponent implements OnInit, OnDestroy { + + public savingData = null; + + public localState = { + previousDisabled: false, + nextDisabled: true, + nextTopic: '', + inputYear: 0, + inputType: '', + borrowerName: 'Applicant', + requireFullCollateral: true, + collateralType: 'vehicle', + requireSellerInput: true, + savingStep: null, + isTrailerMotorhome: false, + isPowerSport: false, + isVehicleDefault: false, + isBoat: false, + isTransferringPlate: false, + requireBoatAddressInput: false, + boatAddressInformation: null, + hasLien: false + }; + private subscription: Subscription; + public vehicleInfoGroup: FormGroup; + public boatAddressGroup: FormGroup; + public boatAddressVerifyGroup: FormGroup; + public loanId: string; + public pageForm: FormGroup; + public loanCode: LoanCode; + + private readonly FORM_STATE_INVALID = 'INVALID'; + + constructor( + private vehicleCollateralInfoPageService: VehicleCollateralInfoPageService, + private route: ActivatedRoute, + private cd: ChangeDetectorRef) {} + + public ngOnInit() { + this.pageForm = this.vehicleCollateralInfoPageService.createPageGroup(); + this.vehicleInfoGroup = this.pageForm.get(VehicleCollateralInfoPageService.VEHICLE_FORM_NAME); + this.loanId = this.vehicleCollateralInfoPageService.getLoanId(); + this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.nextDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + + this.loanCode = this.vehicleCollateralInfoPageService.getLoanCode(); + + this.vehicleInfoGroup.controls.term.patchValue(parseInt(this.vehicleCollateralInfoPageService.getTermLength(), 10)); + + this.vehicleInfoGroup.patchValue( + this.vehicleCollateralInfoPageService.getApplicationGroupValues(VehicleCollateralInfoPageService.VEHICLE_FORM_NAME) + ); + + this.updateLocalState(this.pageForm.value); + } + + public updateLocalState(values) { + this.localState.borrowerName = this.vehicleCollateralInfoPageService.getApplicantName(); + this.localState.nextTopic = this.vehicleCollateralInfoPageService.getNextTopicMessage(); + this.localState.requireFullCollateral = this.vehicleCollateralInfoPageService.requireFullCollateral(); + this.localState.collateralType = this.vehicleCollateralInfoPageService.getCollateralType(); + this.localState.isTrailerMotorhome = this.vehicleCollateralInfoPageService.isLoanId('tmh'); + this.localState.isPowerSport = this.vehicleCollateralInfoPageService.isLoanId('pwc'); + this.localState.isVehicleDefault = this.vehicleCollateralInfoPageService.isLoanId('auto'); + this.localState.isBoat = this.vehicleCollateralInfoPageService.isLoanId('boat'); + this.localState.isTransferringPlate = values.transferring === CreditApplicationValues.CHOICE_YES; + this.localState.requireBoatAddressInput = (this.localState.isBoat && values.boatSameAddress === CreditApplicationValues.CHOICE_NO); + this.localState.hasLien = values.lien === CreditApplicationValues.CHOICE_YES; + this.localState.requireSellerInput = values.loanPurpose === LoanPurpose.Purchase; + + if (this.subscription) { + this.subscription.unsubscribe(); + } + + this.vehicleCollateralInfoPageService.modifyVehicleSellerInfoGroup(this.localState.requireSellerInput, this.vehicleInfoGroup); + this.modifyBoatAddressInfoGroup(); + this.subscription = this.vehicleInfoGroup.valueChanges.subscribe(nextValues => this.updateLocalState(nextValues)); + this.cd.detectChanges(); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public commandNext() { + if (this.localState.nextDisabled) { + return; + } + + this.vehicleCollateralInfoPageService.saveAndNavigate(this.pageForm.value, this.route); + this.savingData = {isActive: true}; + this.localState.nextDisabled = true; + } + + public commandPrevious() { + this.vehicleCollateralInfoPageService.cancelAndNavigate(this.route); + } + + public modifyBoatAddressInfoGroup(): void { + if (this.localState.requireBoatAddressInput && !this.boatAddressGroup) { + this.boatAddressGroup = this.vehicleCollateralInfoPageService.createBoatAddressInfoGroup(); + this.pageForm.addControl(VehicleCollateralInfoPageService.BOAT_ADDRESS_FORM_NAME, this.boatAddressGroup); + this.boatAddressGroup.patchValue({state: CreditApplicationValues.STATE_REQUIRED_COAPP}); + this.patchBoatAddressGroup(); + } else if (!this.localState.requireBoatAddressInput && this.boatAddressGroup) { + this.pageForm.removeControl(VehicleCollateralInfoPageService.BOAT_ADDRESS_FORM_NAME); + this.boatAddressGroup = null; + } + } + + public patchBoatAddressGroup() { + const vehicleInfo = this.vehicleCollateralInfoPageService.getApplicationGroupValues(VehicleCollateralInfoPageService.VEHICLE_FORM_NAME); + if (vehicleInfo.boatAddress) { + this.boatAddressGroup.patchValue(vehicleInfo.boatAddress); + } + } + + public confirmBoatAddressAddressInformation(addressInformation: AddressInformation) { + if (!this.boatAddressVerifyGroup) { + AddressFormBuilderService.addAddressCompareGroupIfMissing(this.pageForm, VehicleCollateralInfoPageService.BOAT_ADDRESS_VERIFY_FORM_NAME); + this.boatAddressVerifyGroup = this.pageForm.get(VehicleCollateralInfoPageService.BOAT_ADDRESS_VERIFY_FORM_NAME); + } + if (!addressInformation) { + this.boatAddressVerifyGroup.patchValue({addressVerifyStatus: false}); + } else { + this.boatAddressVerifyGroup.patchValue({addressVerifyStatus: true}); + this.patchBoatAddressIfFieldsConfirmedAreDifferent(addressInformation); + } + } + + public patchBoatAddressIfFieldsConfirmedAreDifferent(addressInformation: AddressInformation) { + const addressDiff: AddressDiffInformation = AddressValidationFormatConverter.getAddressValidationDiff( + AddressValidationFormatConverter.makeApiAddress(this.boatAddressGroup.value), + addressInformation + ); + if (!addressDiff.allFieldsSame) { + this.boatAddressGroup.patchValue(addressInformation); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.spec.ts new file mode 100644 index 0000000..8a22ef9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.spec.ts @@ -0,0 +1,140 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { VehicleCollateralInfoPageService } from './vehicle-collateral-info-page.service'; +import {VehicleCollateralInformation} from '../../field-groups/vehicle/vehicle-collateral-information'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + + +describe('Vehicle Collateral Info Page Service', () => { + + let service: VehicleCollateralInfoPageService; + let applicationWizardService: ApplicationWizardService; + let creditApplicationDaoService: MockCreditApplicationDaoService; + let creditApplicationService: CreditApplicationService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(VehicleCollateralInfoPageService); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[7].id); + creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService); + creditApplicationService = TestBed.get(CreditApplicationService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create the top level form group', () => { + spyOn(service, 'createPageGroup').and.callThrough(); + service.createPageGroup(); + expect(service.createPageGroup).toHaveBeenCalled(); + }); + + it('should create a vehicle info group', () => { + const collateralGroup = service.createVehicleInformationGroup(); + expect(collateralGroup).toBeTruthy(); + CommonGroups.VEHICLE_COLLATERAL_INFO.fields.forEach((eachField) => { + expect(collateralGroup.get(eachField.field)).toBeTruthy(); + }, this); + + CommonGroups.VEHICLE_SELLER_INFO.fields.forEach((eachField) => { + expect(collateralGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should create a partial vehicle info group', () => { + applicationWizardService.setLoanType(LoanTypeSet[10].id); + const collateralGroup = service.createVehicleInformationGroup(); + expect(collateralGroup).toBeTruthy(); + CommonGroups.VEHICLE_COLLATERAL_INFO_PARTIAL.fields.forEach((eachField) => { + expect(collateralGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); + + it('should determine if full collateral is required', () => { + spyOn(service, 'requireFullCollateral').and.callThrough(); + applicationWizardService.setLoanType(LoanTypeSet[10].id); + let requireFull = service.requireFullCollateral(); + expect(requireFull).toBeFalsy(); + applicationWizardService.setLoanType(LoanTypeSet[7].id); + requireFull = service.requireFullCollateral(); + expect(requireFull).toBeTruthy(); + }); + + it('should grab proper collateral type', () => { + spyOn(service, 'getCollateralType').and.callThrough(); + const boatLoan = LoanTypeSet.find((loanType) => loanType.id === 'boat'); + const vehicleLoan = LoanTypeSet.find((loanType) => loanType.id === 'auto'); + + if (boatLoan) { + applicationWizardService.setLoanType(boatLoan.id); + expect(service.getCollateralType()).toBe(boatLoan.collateralType); + } + + if (vehicleLoan) { + applicationWizardService.setLoanType(vehicleLoan.id); + expect(service.getCollateralType()).toBe(vehicleLoan.collateralType); + } + }); + + it('should provide the value for the groups', () => { + const testGroupName = 'testGroup'; + spyOn(applicationWizardService, 'getGroupValues').and.returnValue(true); + service.getApplicationGroupValues(testGroupName); + expect(applicationWizardService.getGroupValues).toHaveBeenCalledWith(testGroupName); + }); + + it('should navigate the application, on successful save', () => { + spyOn(creditApplicationService, 'saveVehicleCollateralInformation').and.returnValue(of(true)); + spyOn(router, 'navigate'); + const info = {vehicleInformation: {}}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate the application, if failed save', () => { + spyOn(creditApplicationService, 'saveVehicleCollateralInformation').and.returnValue(of(false)); + spyOn(router, 'navigate'); + const info = {vehicleInformation: {}}; + const activatedRoute = {}; + service.saveAndNavigate(info, activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('cancelling should always navigate', () => { + spyOn(router, 'navigate'); + const activatedRoute = {}; + service.cancelAndNavigate(activatedRoute); + expect(router.navigate).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.ts new file mode 100644 index 0000000..0df0cd6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.service.ts @@ -0,0 +1,207 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormBuilder, FormControl, Validators, ValidatorFn } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { VehicleCollateralInformation } from '../../field-groups/vehicle/vehicle-collateral-information'; +import { PageService } from '../page-service'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { LoanType } from '../../../shared/loan-type'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; +import { FormatService } from 'src/app/shared/pipes/format.service'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { AddressInformation } from '../../field-groups/address/address-information'; + +@Injectable({ + providedIn: 'root' +}) +export class VehicleCollateralInfoPageService implements PageService { + + public static readonly VEHICLE_FORM_NAME = 'vehicleInformation'; + public static readonly BOAT_ADDRESS_FORM_NAME = 'boatAddressInformation'; + public static readonly BOAT_ADDRESS_VERIFY_FORM_NAME = 'boatAddressVerifyInformation'; + private readonly CURRENT_ROUTE = 've-collateral'; + + constructor( + private applicationWizardService: ApplicationWizardService, + private creditApplicationService: CreditApplicationService, + private formatService: FormatService, + private router: Router, + private formBuilder: FormBuilder) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const memberGroup = this.createVehicleInformationGroup(); + group.addControl(VehicleCollateralInfoPageService.VEHICLE_FORM_NAME, memberGroup); + return group; + } + + public createVehicleInformationGroup(): FormGroup { + const groupValidator = CommonGroups.VEHICLE_SELLER_INFO.groupValidator(); + const memberGroup = this.formBuilder.group({}, {validator: groupValidator}); + const loanType = this.applicationWizardService.getGroupValues('loanType'); + + if (this.requireFullCollateral()) { + CommonGroups.VEHICLE_COLLATERAL_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } else { + CommonGroups.VEHICLE_COLLATERAL_INFO_PARTIAL.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + + CommonGroups.VEHICLE_SELLER_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + + if (this.isBoatLoan()) { + CommonGroups.VEHICLE_BOAT_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(); + validators.push(validatorFn); + }, this); + } + memberGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + }, this); + } + + return memberGroup; + } + + public createBoatAddressInfoGroup(): FormGroup { + const memberGroup = this.formBuilder.group({}); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.addControl(eachField.field, this.formBuilder.control('')); + }, this); + + CommonGroups.ADDRESS_INFO.fields.forEach((eachField) => { + memberGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return memberGroup; + } + + public modifyVehicleSellerInfoGroup(requireSellerInput, formGroup) { + const loanType = this.applicationWizardService.getGroupValues('loanType'); + + if (requireSellerInput && !formGroup.get('seller')) { + CommonGroups.VEHICLE_SELLER_INFO.fields.forEach((eachField) => { + const validators: Array = eachField.validation.slice(); + if (eachField.validatorFn) { + eachField.validatorFn.forEach((validationFunction) => { + const validatorFn = validationFunction(loanType); + validators.push(validatorFn); + }, this); + } + formGroup.addControl(eachField.field, this.formBuilder.control('', validators)); + formGroup.get(eachField.field).updateValueAndValidity(); + }, this); + this.patchVehicleSellerInfoGroup(formGroup); + } else if (!requireSellerInput && formGroup.get('seller')) { + CommonGroups.VEHICLE_SELLER_INFO.fields.forEach((eachField) => { + formGroup.removeControl(eachField.field); + }, this); + } + } + + public patchVehicleSellerInfoGroup(formGroup) { + const vehicleInfo = this.getApplicationGroupValues(VehicleCollateralInfoPageService.VEHICLE_FORM_NAME); + CommonGroups.VEHICLE_SELLER_INFO.fields.forEach((eachField) => { + if (vehicleInfo[`${eachField.field}`]) { + formGroup.patchValue({[`${eachField.field}`]: vehicleInfo[`${eachField.field}`]}); + } + }, this); + } + + public requireFullCollateral(): boolean { + const loanId = this.applicationWizardService.getGroupValues('loanType').id; + return !( loanId === 'boat' || loanId === 'pwc' || loanId === 'tmh'); + } + + public isBoatLoan(): boolean { + return this.isLoanId('boat'); + } + + public isLoanId(apiCode: string): boolean { + const loanId = this.applicationWizardService.getGroupValues('loanType').id; + return (loanId === apiCode); + } + + public getApplicationGroupValues(groupName: string): any { + return this.applicationWizardService.getGroupValues(groupName); + } + + public saveAndNavigate(fields: { + vehicleInformation: VehicleCollateralInformation, + boatAddressInformation?: AddressInformation}, route: ActivatedRoute) { + this.creditApplicationService.saveVehicleCollateralInformation(fields).subscribe( + (successFlag: boolean) => { + if (successFlag) { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], { relativeTo: route }); + } else { + const nextRoute = this.applicationWizardService.getNextRoute(this.CURRENT_ROUTE); + this.router.navigate([nextRoute], { relativeTo: route }); + } + }, + (failure) => { + // notify user, server, log, etc. + }); + } + + public cancelAndNavigate( route: ActivatedRoute) { + const prevRoute = this.applicationWizardService.getPreviousRoute(this.CURRENT_ROUTE); + this.router.navigate([prevRoute], { relativeTo: route }); + } + + public getNextTopicMessage(): string { + return this.applicationWizardService.getNextTopicMessage(this.CURRENT_ROUTE); + } + + public getApplicantName(): string { + return this.formatService.formatTitleCase( + this.applicationWizardService.getApplicantName(BorrowerType.PRIMARY_BORROWER) + ); + } + + public getCollateralType(): string { + return this.applicationWizardService.getFieldValue('loanType').collateralType; + } + + public getTermLength(): string { + return this.applicationWizardService.getFieldValue('termOfLoan'); + } + + public getLoanCode(): LoanCode { + return this.applicationWizardService.getFieldValue('loanType').apiCode; + } + + public getLoanId() : string { + return this.applicationWizardService.getFieldValue("loanId"); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.spec.ts new file mode 100644 index 0000000..f30d6a2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.spec.ts @@ -0,0 +1,46 @@ +import { ApplicationWizardService } from '../../application-wizard.service'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../../../shared/credit-application-dao.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MockCreditApplicationDaoService } from '../../../testing/shared-mocks/credit-application-dao.mock.service'; +import { LoanTypeSet } from '../../../shared/loan-type'; +import { TranslateModule } from '@ngx-translate/core'; +import { VehicleCollateralInfoPage } from './vehicle-collateral-info-page'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Vehicle Collateral Info Page', () => { + + let page: VehicleCollateralInfoPage; + let applicationWizardService: ApplicationWizardService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + page = new VehicleCollateralInfoPage('PAGETITLES.VEHICLE.COLLATERAL', 've-collateral', + [ + CommonGroups.VEHICLE_COLLATERAL_INFO + ] + ); + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(page).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.ts new file mode 100644 index 0000000..0d4c3e5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-collateral-info-page.ts @@ -0,0 +1,25 @@ +import { ApplicationPage } from '../../application-page'; +import { ApplicationWizardService } from '../../application-wizard.service'; +import { VehicleCollateralInformation } from '../../field-groups/vehicle/vehicle-collateral-information'; +import { CreditApplicationService } from '../../../shared/credit-application.service'; + +export class VehicleCollateralInfoPage extends ApplicationPage { + + public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array { + let value; + const errors = []; + const vehicleCollateralInfo: VehicleCollateralInformation = applicationWizardService.getGroupValues('vehicleInformation'); + this.fieldgroup.forEach((pageFieldGroup) => { + pageFieldGroup.fields.forEach((pageField) => { + value = vehicleCollateralInfo[pageField.field]; + errors.push(...applicationWizardService.getFieldErrors(pageField.field, value, pageField.validation.slice())); + value = undefined; + }, this); + }, this); + return errors; + } + + public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean { + return applicationWizardService.getGroupValues('loanType').id !== 'pauto'; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-outline.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-outline.ts new file mode 100644 index 0000000..7649f11 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-outline.ts @@ -0,0 +1,149 @@ +import { ApplicationPage } from '../../application-page'; +import { CommonGroups } from '../../field-groups/common-groups'; +import { PrimaryEmploymentInfoPage } from '../common/employment/pages/primary-employment-info-page'; +import { PrimaryPersonalInfoPage } from '../common/personal/pages/primary-personal-info-page'; +import { PrimaryAddressInfoPage } from '../common/address/pages/primary-address-info-page'; +import { CreditCardDisclosuresInfoPage } from '../credit-card/credit-card-disclosures-info-page'; +import { VehicleCollateralInfoPage } from './vehicle-collateral-info-page'; +import { SpouseTypeInfoPage } from '../common/spouse-type/spouse-type-info-page'; +import { PrimarySpousePersonalInfoPage } from '../common/personal/pages/primary-spouse-personal-info-page'; +import { CoBorrowerPersonalInfoPage } from '../common/personal/pages/co-borrower-personal-info-page'; +import { CoBorrowerSpousePersonalInfoPage } from '../common/personal/pages/co-borrower-spouse-personal-info-page'; +import { PrimarySpouseAddressInfoPage } from '../common/address/pages/primary-spouse-address-info-page'; +import { CoBorrowerAddressInfoPage } from '../common/address/pages/co-borrower-address-info-page'; +import { CoBorrowerSpouseAddressInfoPage } from '../common/address/pages/co-borrower-spouse-address-info-page'; +import { PrimarySpouseEmploymentInfoPage } from '../common/employment/pages/primary-spouse-employment-info-page'; +import { CoBorrowerEmploymentInfoPage } from '../common/employment/pages/co-borrower-employment-info-page'; +import { CoBorrowerSpouseEmploymentInfoPage } from '../common/employment/pages/co-borrower-spouse-employment-info-page'; +import { PrimaryTattleSpouseInfoPage } from '../common/tattle-spouse/pages/primary-tattle-spouse-info-page'; +import { CoBorrowerTattleSpouseInfoPage } from '../common/tattle-spouse/pages/co-borrower-tattle-spouse-info-page'; +import { SummaryInfoPage } from '../common/summary/summary-info-page'; +import { DecisionPage } from '../common/decision/decision-page'; +import { ProcessingPage } from '../common/processing/processing-page'; +import { EquityMemberInfoPage } from '../equity/equity-member-info-page'; + +export const VehiclePages: Array = [ + /* PRIMARY */ + new EquityMemberInfoPage('PAGETITLES.COMMON.INTRO', 'step-1', + [ + CommonGroups.MEMBER_INFO + ] + ), + new PrimaryPersonalInfoPage('PAGETITLES.COMMON.PERSONAL_INFO', 'step-2', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimaryAddressInfoPage('PAGETITLES.COMMON.ADDRESS_INFO', 'step-3', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimaryEmploymentInfoPage('PAGETITLES.COMMON.EMPLOYMENT_INFO', 'step-4', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + /* PRIMARY_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.PRIMARY_SPOUSETYPE', + 'applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ] + ), + new PrimaryTattleSpouseInfoPage('PAGETITLES.COMMON.PRIMARY_TATTLESPOUSE', + 'applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ] + ), + new PrimarySpousePersonalInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_PERSONAL_INFO', + 'applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new PrimarySpouseAddressInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_ADDRESS_INFO', + 'applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new PrimarySpouseEmploymentInfoPage('PAGETITLES.COMMON.PRIMARYSPOUSE_EMPLOYMENT_INFO', + 'applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + /* CO_APPLICANT */ + new CoBorrowerPersonalInfoPage('PAGETITLES.COMMON.COAPP_PERSONAL_INFO', + 'co-applicant-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerAddressInfoPage('PAGETITLES.COMMON.COAPP_ADDRESS_INFO', + 'co-applicant-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerEmploymentInfoPage('PAGETITLES.COMMON.COAPP_EMPLOYMENT_INFO', + 'co-applicant-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ]), + /* CO_APPLICANT_SPOUSE */ + new SpouseTypeInfoPage('PAGETITLES.COMMON.COAPP_SPOUSETYPE', + 'co-applicant-spouse-type', + [ + CommonGroups.IS_FULL_INFO_SPOUSE + ] + ), + new CoBorrowerTattleSpouseInfoPage('PAGETITLES.COMMON.COAPP_TATTLESPOUSE', + 'co-applicant-spouse', + [ + CommonGroups.TATTLE_SPOUSE_PERSONAL_INFO + ] + ), + new CoBorrowerSpousePersonalInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_PERSONAL_INFO', + 'co-applicant-spouse-personal', + [ + CommonGroups.PERSONAL_INFO + ] + ), + new CoBorrowerSpouseAddressInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_ADDRESS_INFO', + 'co-applicant-spouse-residential', + [ + CommonGroups.ADDRESS_INFO + ] + ), + new CoBorrowerSpouseEmploymentInfoPage('PAGETITLES.COMMON.COAPPSPOUSE_EMPLOYMENT_INFO', + 'co-applicant-spouse-financial', + [ + CommonGroups.EMPLOYMENT_INFO, + CommonGroups.INCOME_INFO, + CommonGroups.ALIMONY_INFO + ] + ), + new VehicleCollateralInfoPage('PAGETITLES.VEHICLE.COLLATERAL', 've-collateral', + [ + CommonGroups.VEHICLE_COLLATERAL_INFO_PARTIAL + ] + ), + new CreditCardDisclosuresInfoPage('PAGETITLES.COMMON.DISCLOSURES', 'cc-disclosures', + [ + CommonGroups.CC_DISCLOSURE_INFO // TODO: Make a different disclosure group, or change the name. + ] + ), + new SummaryInfoPage('PAGETITLES.COMMON.SUMMARY', 'cc-summary', []), + new ProcessingPage('PAGETITLES.CC.PROCESS', 'process', []), + new DecisionPage('PAGETITLES.CC.INTRO', 'decision', []) + +]; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-routing.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-routing.module.ts new file mode 100644 index 0000000..0543086 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/loan-wizard/wizards/vehicle/vehicle-routing.module.ts @@ -0,0 +1,66 @@ +import { Route} from '@angular/router'; +import { PersonalInfoPageComponent } from '../common/personal/personal-info-page.component'; +import { AddressInfoPageComponent } from '../common/address/address-info-page.component'; +import { EmploymentInfoPageComponent } from '../common/employment/employment-info-page.component'; +import { CreditCardDisclosuresPageComponent } from '../credit-card/credit-card-disclosures-page.component'; +import { SummaryPageComponent } from '../common/summary/summary-page.component'; +import { DecisionPageComponent } from '../common/decision/decision-page.component'; +import { LoanRouteGuard } from '../../../shared/guards/loan-route.guard'; +import { BorrowerType } from '../../../shared/borrower-type'; +import { TattleSpouseInfoPageComponent } from '../common/tattle-spouse/tattle-spouse-info-page.component'; +import { SpouseTypeInfoPageComponent } from '../common/spouse-type/spouse-type-info-page.component'; +import { ProcessingPageComponent } from '../common/processing/processing-page.component'; +import { EquityMemberInfoPageComponent } from '../equity/equity-member-info-page.component'; +import { VehicleCollateralInfoPageComponent } from './vehicle-collateral-info-page.component'; +import { StepperControllerGuard } from 'src/app/shared/guards/stepper-controller-guard'; +import { MemberCoreResolver } from 'src/app/member/member-core.resolver'; +import { environment } from 'src/environments/environment'; + + +export const veRoutes: Route = { + path: 've/:type', + resolve: environment.coreResolver ? { CoreIndividual: MemberCoreResolver} : {}, + canActivate: [StepperControllerGuard], + canActivateChild: [LoanRouteGuard], + children: [ + { path: 'step-1', component: EquityMemberInfoPageComponent }, + { path: 'step-2', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-3', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'step-4', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER}] }, + { path: 'applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.PRIMARY_BORROWER_SPOUSE}] }, + { path: 'co-applicant-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse-type', component: SpouseTypeInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER}] }, + { path: 'co-applicant-spouse', component: TattleSpouseInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-personal', component: PersonalInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-residential', component: AddressInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 'co-applicant-spouse-financial', component: EmploymentInfoPageComponent, + data: [{individual: BorrowerType.CO_BORROWER_SPOUSE}] }, + { path: 've-collateral', component: VehicleCollateralInfoPageComponent }, + { path: 'cc-disclosures', component: CreditCardDisclosuresPageComponent }, + { path: 'cc-summary', component: SummaryPageComponent }, + { path: 'process', component: ProcessingPageComponent }, + { path: 'decision', component: DecisionPageComponent }, + { path: '', redirectTo: 'step-1', pathMatch: 'prefix' } + ] +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-core.resolver.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-core.resolver.ts new file mode 100644 index 0000000..cf653c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-core.resolver.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { Resolve } from '@angular/router'; +import { MemberService } from './member.service'; +import { CoreIndividual } from './models/core-individual'; + +@Injectable() +export class MemberCoreResolver implements Resolve { + + constructor(private memberService: MemberService) {} + + public resolve(): Observable | boolean { + if (this.memberService.shouldLoadIndividualData()) { + const isDoneResolvingSubject = new Subject(); + this.memberService.requestIndividual() + .subscribe( + (individual: CoreIndividual) => { + this.memberService.transformCoreData(individual); + isDoneResolvingSubject.next(true); + isDoneResolvingSubject.complete(); + }, + (failure) => { + isDoneResolvingSubject.next(true); + isDoneResolvingSubject.complete(); + } + ); + return isDoneResolvingSubject.asObservable(); + } + return true; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.html new file mode 100644 index 0000000..154882e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.html @@ -0,0 +1,54 @@ +
+
+
+ +
MEMBER.LOGIN.MEMBER_SUBTEXT
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
{{localState.errorMessage | translate}}
+
+
+
+
+
+
+ +
MEMBER.LOGIN.NON_MEMBER_SUBTEXT
+
+ +
+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.scss new file mode 100644 index 0000000..9a32f71 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.scss @@ -0,0 +1,51 @@ +.card { + background-color: #F8F9FA; +} + +.btn-success { + margin-left: 15px; + margin-bottom: 20px; +} + +.btn-disabled { + margin-bottom: 20px; + margin-left: 15px; +} + +.btn-link { + border: 0px; + padding-left: 15px; +} + +.login-header { + font-family: 'Open Sans', sans-serif !important; + margin-left:15px; + margin-bottom:5px; + margin-top: 15px; + font-size: 32px; + font-weight: bold; + color: #005674; +} + +.middle-text { + font-family: Arial, sans-serif !important; + margin-left:15px; + margin-bottom: 15px; +} + +.error-message { + font-size: 80%; + color: #dc3545; + font-style: italic; + margin-left: 15px; +} + +@media (max-width: 767px) { + .card-body { + padding: 0px; + } + + .login-header { + font-size: 24px; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.spec.ts new file mode 100644 index 0000000..2909445 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.spec.ts @@ -0,0 +1,145 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { MemberLoginComponent } from './member-login.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { MemberService } from '../member.service'; +import { CookieUtil } from 'src/app/shared/cookie/cookie-util'; +import { LoginStatus } from '../models/login-status'; + +describe('MemberLoginComponent', () => { + let component: MemberLoginComponent; + let service: MemberService; + let fixture: ComponentFixture; + + const mockSuccessStatus: LoginStatus = { + statusMessage : 'success', + errorInfo : null + }; + + const mockInvalidCredStatus: LoginStatus = { + statusMessage : 'failure', + errorInfo : { + errorCode : '25002', + errorMessage : 'Fake error message', + errorType : 'APP_ERROR' + } + }; + + const mockLockedStatus: LoginStatus = { + statusMessage : 'failure', + errorInfo : { + errorCode : '25100', + errorMessage : 'Fake error message', + errorType : 'APP_ERROR' + } + }; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + MemberLoginComponent, + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MemberLoginComponent); + service = TestBed.get(MemberService); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should intilize local state with default values', () => { + fixture.detectChanges(); + expect(component.localState.error).toBe(false); + expect(component.localState.loginDisabled).toBe(true); + expect(component.localState.errorMessage).toBe(''); + }); + + it('should authenticate member and throw error when user credentials are incorrect', () => { + fixture.detectChanges(); + spyOn(service, 'authenticateMember').and.returnValue(of(mockInvalidCredStatus)); + component.commandLogin(); + expect(service.authenticateMember).toHaveBeenCalled(); + expect(component.localState.errorMessage).toBe('MEMBER.LOGIN.ERROR_USER'); + }); + + it('should authenticate member and throw error when user is locked', () => { + fixture.detectChanges(); + spyOn(service, 'authenticateMember').and.returnValue(of(mockLockedStatus)); + component.commandLogin(); + expect(service.authenticateMember).toHaveBeenCalled(); + expect(component.localState.errorMessage).toBe('MEMBER.LOGIN.ERROR_LOCKED'); + }); + + it('should authenticate member and NOT throw error when authentication passes', () => { + fixture.detectChanges(); + spyOn(service, 'authenticateMember').and.returnValue(of(mockSuccessStatus)); + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(true); + spyOn(component, 'routeLoanApplication'); + component.commandLogin(); + expect(service.authenticateMember).toHaveBeenCalled(); + expect(component.routeLoanApplication).toHaveBeenCalled(); + expect(component.localState.errorMessage).toBe(''); + }); + + it('should authenticate non member and route when cookies exist', () => { + fixture.detectChanges(); + spyOn(service, 'authenticateNonMember').and.returnValue(of(mockSuccessStatus)); + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(true); + spyOn(component, 'routeLoanApplication'); + spyOn(component, 'routeLoanApplicationNoCookies'); + component.commandNonMember(); + expect(service.authenticateNonMember).toHaveBeenCalled(); + expect(component.routeLoanApplication).toHaveBeenCalled(); + expect(component.routeLoanApplicationNoCookies).not.toHaveBeenCalled(); + }); + + it('should authenticate non member and route when cookies dont exist', () => { + fixture.detectChanges(); + spyOn(service, 'authenticateNonMember').and.returnValue(of(mockSuccessStatus)); + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(false); + spyOn(component, 'routeLoanApplication'); + spyOn(component, 'routeLoanApplicationNoCookies'); + component.commandNonMember(); + expect(service.authenticateNonMember).toHaveBeenCalled(); + expect(component.routeLoanApplication).not.toHaveBeenCalled(); + expect(component.routeLoanApplicationNoCookies).toHaveBeenCalled(); + }); + + it('should route user to landmark login page', () => { + fixture.detectChanges(); + expect(component.localState.existingAppIndicator).toBe(false); + spyOn(component, 'routeToLandmarkLogin'); + component.commandExistingApp(); + expect(component.routeToLandmarkLogin).toHaveBeenCalled(); + expect(component.localState.existingAppIndicator).toBe(true); + }); + + it('should throw errors and reset password fields', () => { + fixture.detectChanges(); + spyOn(component, 'resetPasswordField'); + component.throwError('This is a test error'); + expect(component.resetPasswordField).toHaveBeenCalled(); + expect(component.localState.errorMessage).toBe('This is a test error'); + expect(component.localState.error).toBe(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.ts new file mode 100644 index 0000000..68c2d14 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.component.ts @@ -0,0 +1,142 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MemberLoginService } from './member-login.service'; +import { MemberService } from '../member.service'; +import { CookieUtil } from 'src/app/shared/cookie/cookie-util'; +import { LoginStatus } from '../models/login-status'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'lw-member-login', + templateUrl: './member-login.component.html', + styleUrls: ['./member-login.component.scss'] +}) +export class MemberLoginComponent implements OnInit, OnDestroy { + private redirect; + public pageForm: FormGroup; + public loginGroup: FormGroup; + public statusSubscription: Subscription; + public readonly FORM_STATE_INVALID = 'INVALID'; + public localState = { + loginIndicator: false, + nonMemberIndicator: false, + existingAppIndicator: false, + loginDisabled: true, + error: false, + errorMessage: '' + }; + + constructor(private router: Router, private memberLoginService: MemberLoginService, + private memberService: MemberService, private route: ActivatedRoute) { } + + ngOnInit () { + this.getUrlParams(); + this.setupForm(); + this.setupLocalState(); + } + + ngOnDestroy () { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public getUrlParams(): void { + this.route.queryParams.subscribe(params => this.redirect = params['redirect'] || '/select-loan'); + } + + public setupForm(): void { + this.pageForm = this.memberLoginService.createPageGroup(); + this.loginGroup = this.pageForm.get(this.memberLoginService.LOGIN_FORM_NAME); + } + + public setupLocalState(): void { + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.loginDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + } + + public commandLogin(): void { + this.localState.loginDisabled = true; + this.localState.loginIndicator = true; + this.memberService.authenticateMember(this.loginGroup.value.username, this.loginGroup.value.password).subscribe ( + (success: LoginStatus) => { + if (success && success.errorInfo) { + this.checkErrorCode(success.errorInfo.errorCode); + } else { + if (CookieUtil.hasAuthCookie()) { + this.routeLoanApplication(); + } else { + this.throwError('MEMBER.LOGIN.ERROR_COOKIES'); + } + } + }, (error) => { + this.throwError('MEMBER.LOGIN.ERROR_SERVER'); + } + ); + } + + public checkErrorCode(errorCode: string): void { + if (errorCode === '25002') { + this.throwError('MEMBER.LOGIN.ERROR_USER'); + } else if (errorCode === '25100') { + this.throwError('MEMBER.LOGIN.ERROR_LOCKED'); + } else { + this.throwError('MEMBER.LOGIN.ERROR_USER'); + throw new Error(`Unexpected login error code: ${errorCode}`); + } + } + + public commandNonMember(): void { + this.localState.loginDisabled = true; + this.localState.nonMemberIndicator = true; + this.memberService.authenticateNonMember().subscribe ( + (success) => { + if (CookieUtil.hasAuthCookie()) { + this.routeLoanApplication(); + } else { + this.routeLoanApplicationNoCookies(); + } + }, (error) => { + this.routeLoanApplicationNoCookies(); + } + ); + } + + public commandExistingApp(): void { + this.localState.existingAppIndicator = true; + this.routeToLandmarkLogin(); + } + + public routeToLandmarkLogin(): void { + location.href = environment.obsUri; + } + + public routeLoanApplication(): void { + this.router.navigateByUrl(this.redirect); + } + + public routeLoanApplicationNoCookies(): void { + this.router.navigate([this.redirect], { + queryParams: { + noCookie: true, + } + }); + } + + public resetPasswordField(): void { + this.loginGroup.patchValue({password: null}); + this.loginGroup.markAsPristine(); + this.loginGroup.markAsUntouched(); + } + + public throwError(error: string): void { + this.localState.loginIndicator = false; + this.resetPasswordField(); + this.localState.errorMessage = error; + this.localState.error = true; + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.spec.ts new file mode 100644 index 0000000..8efa2d5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.spec.ts @@ -0,0 +1,49 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { MemberLoginService } from './member-login.service'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; + + +describe('MemberLoginService', () => { + + let service: MemberLoginService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + MemberLoginService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(MemberLoginService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create a login group', () => { + const loginGroup = service.createLoginGroup(); + expect(loginGroup).toBeTruthy(); + CommonGroups.LOGIN.fields.forEach((eachField) => { + expect(loginGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.ts new file mode 100644 index 0000000..9ba8364 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-login/member-login.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberLoginService { + + public readonly LOGIN_FORM_NAME = 'loginInformation'; + + constructor(private formBuilder: FormBuilder) { + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const loginGroup = this.createLoginGroup(); + group.addControl(this.LOGIN_FORM_NAME, loginGroup); + return group; + } + + public createLoginGroup(): FormGroup { + const loginGroup = this.formBuilder.group({}); + + CommonGroups.LOGIN.fields.forEach((eachField) => { + loginGroup.addControl(eachField.field, this.formBuilder.control('')); + loginGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return loginGroup; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.html new file mode 100644 index 0000000..90c6b64 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.spec.ts new file mode 100644 index 0000000..600d7ad --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.spec.ts @@ -0,0 +1,36 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { MemberOutletComponent } from './member-outlet.component'; +import { HttpClient } from '@angular/common/http'; + +describe('MemberOutletComponent', () => { + let component: MemberOutletComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + ], + declarations: [ + MemberOutletComponent, + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MemberOutletComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.ts new file mode 100644 index 0000000..887ea46 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-outlet/member-outlet.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-member-outlet', + templateUrl: './member-outlet.component.html', + styleUrls: ['./member-outlet.component.scss'] +}) +export class MemberOutletComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.html new file mode 100644 index 0000000..f160299 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.html @@ -0,0 +1,56 @@ +
+
+
+ +
MEMBER.VERIFICATION.SUBTEXT
+
+
+ +
+ +
+
+
+ +
+ +
+ + + +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ + + + + +
{{localState.errorMessage | translate}} attempt {{attempts.value}} of {{MAX_ATTEMPTS}}.
+
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.scss new file mode 100644 index 0000000..8f514f5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.scss @@ -0,0 +1,36 @@ +.card { + background-color: #F8F9FA; +} + +.btn-success { + margin-left: 15px; + margin-bottom: 15px; +} + +.btn-link { + border: 0px; + padding-left: 15px; +} + +.login-header { + font-family: 'Open Sans', sans-serif !important; + margin-left:15px; + margin-bottom:5px; + margin-top: 15px; + font-size: 32px; + font-weight: bold; + color: #005674; +} + +.middle-text { + font-family: Arial, sans-serif !important; + margin-left:15px; + margin-bottom: 15px; +} + +.error-message { + font-size: 80%; + color: #dc3545; + font-style: italic; + margin-left: 15px; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.spec.ts new file mode 100644 index 0000000..1c824ad --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.spec.ts @@ -0,0 +1,100 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MemberService } from '../member.service'; +import { MemberVerificationComponent } from './member-verification.component'; +import { of } from 'rxjs'; +import { Router } from '@angular/router'; + +describe('MemberVerificationComponent', () => { + let component: MemberVerificationComponent; + let service: MemberService; + let fixture: ComponentFixture; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + declarations: [ + MemberVerificationComponent, + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MemberVerificationComponent); + service = TestBed.get(MemberService); + component = fixture.componentInstance; + router = TestBed.get(Router); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should intilize local state with default values', () => { + fixture.detectChanges(); + expect(component.localState.loginIndicator).toBe(false); + expect(component.localState.loginDisabled).toBe(true); + expect(component.localState.hideSSN).toBe(true); + expect(component.localState.errorMessage).toBe(''); + }); + + it('should identify individual, set individual id, and route upon success', () => { + fixture.detectChanges(); + spyOn(service, 'identifyIndividual').and.returnValue(of('1234A')); + spyOn(router, 'navigateByUrl').and.returnValue( Promise.resolve(true)); + component.commandContinue(); + expect(service.identifyIndividual).toHaveBeenCalled(); + expect(router.navigateByUrl).toHaveBeenCalledWith('/select-loan'); + }); + + it('should route without verification', () => { + fixture.detectChanges(); + expect(component.localState.noVerifyIndicator).toBe(false); + spyOn(component, 'routeLoanApplicationNoCookies'); + spyOn(service, 'authenticateNonMember').and.returnValues( of(true)); + component.commandContinueWithoutVerification(); + expect(component.localState.noVerifyIndicator).toBe(true); + expect(component.routeLoanApplicationNoCookies).toHaveBeenCalled(); + }); + + it('should subscribe to attempts and throw error if attempts exceeds max attempts', () => { + fixture.detectChanges(); + spyOn(component, 'getAttemptsAsObservable').and.returnValue(of(10)); + component.subscribeToAttempts(); + expect(component.localState.loginDisabled).toBe(true); + expect(component.localState.showNoVerifyButton).toBe(true); + expect(component.localState.errorMessage).toBe('MEMBER.VERIFICATION.MAX_ATTEMPTS'); + expect(component.localState.error).toBe(true); + }); + + it('should throw error', () => { + fixture.detectChanges(); + spyOn(component, 'setAttempts'); + component.throwError('This is a test error'); + expect(component.localState.errorMessage).toBe('This is a test error'); + expect(component.localState.error).toBe(true); + expect(component.setAttempts).toHaveBeenCalled(); + }); + + it('should toggle hideSSN state', () => { + fixture.detectChanges(); + expect(component.localState.hideSSN).toBe(true); + component.commandToggleSSNView(); + expect(component.localState.hideSSN).toBe(false); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.ts new file mode 100644 index 0000000..708254e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.component.ts @@ -0,0 +1,155 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Subscription, BehaviorSubject, Observable } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MemberVerificationService } from './member-verification.service'; +import { MaskingService } from 'src/app/shared/masking/masking.service'; +import { MemberService } from '../member.service'; +import { CookieUtil } from '../../shared/cookie/cookie-util'; +import { finalize } from 'rxjs/operators'; + + +@Component({ + selector: 'lw-member-verification', + templateUrl: './member-verification.component.html', + styleUrls: ['./member-verification.component.scss'] +}) +export class MemberVerificationComponent implements OnInit, OnDestroy { + public attempts: BehaviorSubject = new BehaviorSubject(0); + private redirect = '/select-loan'; + public pageForm: FormGroup; + public verificationGroup: FormGroup; + public statusSubscription: Subscription; + public readonly FORM_STATE_INVALID = 'INVALID'; + public dobMask = MaskingService.dobMask; + public readonly MAX_ATTEMPTS = 3; + public localState = { + hideSSN: true, + loginIndicator: false, + loginDisabled: true, + showNoVerifyButton: false, + noVerifyIndicator: false, + error: false, + errorMessage: '' + }; + + constructor(private router: Router, private memberVerificationService: MemberVerificationService, private memberService: MemberService, + private route: ActivatedRoute) { } + + ngOnInit() { + // this.getUrlParams(); + this.setupForm(); + this.setupLocalState(); + this.subscribeToAttempts(); + } + + ngOnDestroy() { + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public getUrlParams(): void { + this.route.queryParams.subscribe((params) => { + this.redirect = params['redirect'] || '/select-loan'; + }); + } + + public setupForm(): void { + this.pageForm = this.memberVerificationService.createPageGroup(); + this.verificationGroup = this.pageForm.get(this.memberVerificationService.VERIFICATION_FORM_NAME); + } + + public setupLocalState(): void { + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.loginDisabled = (newStatus === this.FORM_STATE_INVALID); + }); + } + + public subscribeToAttempts(): void { + this.getAttemptsAsObservable().subscribe((attempts) => { + if (attempts >= this.MAX_ATTEMPTS) { + this.localState.loginDisabled = true; + this.localState.showNoVerifyButton = true; + this.localState.errorMessage = 'MEMBER.VERIFICATION.MAX_ATTEMPTS'; + this.localState.error = true; + } + }); + } + + public commandContinue(): void { + this.localState.loginDisabled = true; + this.localState.loginIndicator = true; + this.memberService.identifyIndividual(this.verificationGroup.value.dob, this.verificationGroup.value.ssn).pipe( + finalize(() => { + this.localState.loginDisabled = false; + this.localState.loginIndicator = false; + }) + ).subscribe( + () => { + const promise = this.router.navigateByUrl(this.redirect); + promise.then((result) => { + if (result === false) { + this.throwError('MEMBER.VERIFICATION.FAILURE'); + } + }); + promise.catch((reason) => { + // @TODO NOTIFY_ERROR + this.throwError('MEMBER.VERIFICATION.FAILURE'); + }); + }, (error) => { + this.throwError('MEMBER.VERIFICATION.ERROR'); + } + ); + } + + public commandContinueWithoutVerification() { + this.localState.noVerifyIndicator = true; + this.memberService.authenticateNonMember().subscribe( + (success) => { + if (CookieUtil.hasAuthCookie()) { + this.routeLoanApplication(); + } else { + this.routeLoanApplicationNoCookies(); + } + }, (error) => { + this.routeLoanApplicationNoCookies(); + } + ); + } + + public commandToggleSSNView(): void { + this.localState.hideSSN = !this.localState.hideSSN; + } + + public routeLoanApplication(): void { + this.router.navigateByUrl(this.redirect); + } + + public routeLoanApplicationNoCookies(): void { + this.router.navigate([this.redirect], { + queryParams: { + noCookie: true, + } + }); + } + + public setAttempts(attempts: number): void { + this.attempts.next(attempts); + } + + public getAttempts(): number { + return this.attempts.value; + } + + public getAttemptsAsObservable(): Observable { + return this.attempts.asObservable(); + } + + public throwError(error: string): void { + this.localState.errorMessage = error; + this.localState.error = true; + this.setAttempts(this.getAttempts() + 1); + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.spec.ts new file mode 100644 index 0000000..41e4d56 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.spec.ts @@ -0,0 +1,51 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; +import { MemberVerificationService } from './member-verification.service'; + + +describe('MemberVerificationService', () => { + + let service: MemberVerificationService; + let httpTestingController: HttpTestingController; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + MemberVerificationService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(MemberVerificationService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create a login group', () => { + const verificationGroup = service.createVerificationGroup(); + expect(verificationGroup).toBeTruthy(); + CommonGroups.MEMBER_VERIFICATION.fields.forEach((eachField) => { + expect(verificationGroup.get(eachField.field)).toBeTruthy(); + }, this); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.ts new file mode 100644 index 0000000..8348097 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member-verification/member-verification.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; +import { of, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberVerificationService { + public readonly VERIFICATION_FORM_NAME = 'memberInformation'; + + constructor(private formBuilder: FormBuilder) { + } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + const verificationGroup = this.createVerificationGroup(); + group.addControl(this.VERIFICATION_FORM_NAME, verificationGroup); + return group; + } + + public createVerificationGroup(): FormGroup { + const verificationGroup = this.formBuilder.group({}); + + CommonGroups.MEMBER_VERIFICATION.fields.forEach((eachField) => { + verificationGroup.addControl(eachField.field, this.formBuilder.control('')); + verificationGroup.controls[eachField.field].setValidators(eachField.validation.slice()); + }, this); + + return verificationGroup; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.spec.ts new file mode 100644 index 0000000..09aee1e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.spec.ts @@ -0,0 +1,107 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { MemberService } from './member.service'; +import { environment } from 'src/environments/environment'; +import { CookieUtil } from '../shared/cookie/cookie-util'; +import { CreditApplicationService } from '../shared/credit-application.service'; + +describe('Member Service', () => { + + let service: MemberService; + let creditApplicationService: CreditApplicationService; + let httpTestingController: HttpTestingController; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + MemberService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(MemberService); + creditApplicationService = TestBed.get(CreditApplicationService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should build authentication url', () => { + const url = service.buildAuthenticateURL(); + expect(url).toBe(environment.authUrlBase + '/SAMLAssertionConsumer/Authenticate'); + }); + + it('should build initsession url', () => { + const url = service.buildInitSessionURL(); + expect(url).toBe(environment.authUrlBase + '/SAMLAssertionConsumer/InitSession'); + }); + + it('should build member meta url', () => { + const url = service.buildMemberMetaURL(); + expect(url).toBe(environment.authUrlBase + '/SAMLAssertionConsumer/MemberInfo'); + }); + + it('should build identify url', () => { + const url = service.buildIdentifyURL(); + expect(url).toBe(environment.authUrlBase + '/SAMLAssertionConsumer/Identify'); + }); + + it('should build individual url', () => { + const url = service.buildIndividualURL(); + expect(url).toBe(environment.urlBase + '/Individual/self'); + }); + + it('should authenticate member', () => { + service.authenticateMember('peter', 'pan').subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildAuthenticateURL()); + expect(req.request.method).toEqual('POST'); + httpTestingController.verify(); + }); + + it('should authenticate non member', () => { + service.authenticateNonMember().subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildInitSessionURL()); + expect(req.request.method).toEqual('GET'); + httpTestingController.verify(); + }); + + it('should identify individual', () => { + service.identifyIndividual('12/12/1999', '2341').subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildIdentifyURL()); + expect(req.request.method).toEqual('POST'); + httpTestingController.verify(); + }); + + it('should request individual', () => { + service.requestIndividual().subscribe(() => {}); + const req = httpTestingController.expectOne(service.buildIndividualURL()); + expect(req.request.method).toEqual('GET'); + httpTestingController.verify(); + }); + + it('should not load individual data if it is already filled', () => { + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(true); + spyOn(CookieUtil, 'allowPrefill').and.returnValue(true); + spyOn(creditApplicationService, 'canAcceptIndividualPrefill').and.returnValue(false); + expect(service.shouldLoadIndividualData()).toEqual(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.ts new file mode 100644 index 0000000..f32d566 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/member.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { CreditApplicationService } from '../shared/credit-application.service'; +import { MemberCredentials } from './models/member-credentials'; +import { MemberMeta } from './models/member-meta'; +import { CookieUtil } from '../shared/cookie/cookie-util'; +import { IndividualCredentials } from './models/individual-credentials'; +import { CoreIndividual } from './models/core-individual'; +import { CoreTranslatorService } from '../shared/core-translator/core-translator.service'; +import { map } from 'rxjs/operators'; + + +@Injectable({ + providedIn: 'root' +}) +export class MemberService { + constructor(private httpClient: HttpClient, private creditApplicationService: CreditApplicationService, + private coreTranslatorService: CoreTranslatorService) { + } + + public buildAuthenticateURL(): string { + return environment.authUrlBase + '/SAMLAssertionConsumer/Authenticate'; + } + + public buildInitSessionURL(): string { + return environment.authUrlBase + '/SAMLAssertionConsumer/InitSession'; + } + + public buildMemberMetaURL(): string { + return environment.authUrlBase + '/SAMLAssertionConsumer/MemberInfo'; + } + + public buildIdentifyURL(): string { + return environment.authUrlBase + '/SAMLAssertionConsumer/Identify'; + } + + public buildIndividualURL(): string { + return environment.urlBase + '/Individual/self'; + } + + public authenticateMember(username: string, password: string): Observable { + const memberCredentials: MemberCredentials = new MemberCredentials(username, password); + return this.httpClient.post(this.buildAuthenticateURL(), memberCredentials, {withCredentials: true}); + } + + public authenticateNonMember(): Observable { + return this.httpClient.get(this.buildInitSessionURL(), {withCredentials: true}); + } + + // make a call to core to get individual on account. return individual id if successful. + public identifyIndividual(dob: string, ssn: string): Observable { + const individualCredentials: IndividualCredentials = new IndividualCredentials(dob, ssn); + return this.httpClient.post(this.buildIdentifyURL(), individualCredentials, {withCredentials: true}); + } + + // make a call to core to retrieve user data. + public requestIndividual(): Observable { + return this.httpClient.get(this.buildIndividualURL(), {withCredentials: true}) + .pipe(map((individualRaw: any) => { + return individualRaw; + })); + } + + public transformCoreData(individual: CoreIndividual): void { + const creditApplication = this.creditApplicationService.getFullCreditApplication(); + this.coreTranslatorService.setCoreData(creditApplication, individual); + } + + public shouldShowVerificationPage() { + return (CookieUtil.hasAuthCookie() && CookieUtil.requiresVerification()); + } + + public shouldLoadIndividualData() { + return (CookieUtil.hasAuthCookie() && CookieUtil.allowPrefill() && this.creditApplicationService.canAcceptIndividualPrefill() ); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-address.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-address.ts new file mode 100644 index 0000000..0535159 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-address.ts @@ -0,0 +1,21 @@ + + +export interface CoreAddress { + addressId: number; + line1: string; + line2: string; + line3: string; + city: string; + state: string; + zipCode: string; + country: string; + countyName: string; + addressType: string; + isPrimary: boolean; + isInternational: boolean; + addressVerification: number; + occupancy: string; + startDate: string; + endDate: string; + uspsVerified: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-individual.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-individual.ts new file mode 100644 index 0000000..140db6b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-individual.ts @@ -0,0 +1,38 @@ +import { CoreAddress } from './core-address'; +import { CorePhone } from './core-phone'; + +export interface CoreIndividual { + individualId: number; + individualType: string; + title: string; + firstName: string; // individual.personalInfo.firstName + middleName: string; // individual.personalInfo.middleInitial (take first initial) + lastName: string; // individual.personalInfo.lastName + nameSuffix: string; // individual.personalInfo.suffix + taxId: string; + gender: string; + birthDate: string; // individual.personalInfo.birthday + branch: number; + driversLicense: string; + driversLicenseState: string; + ident: string; + preferredContact: string; + maritalStatus: string; // individual.personalInfo.martialStatus + isUSCitizen: boolean; + primaryAddressId: number; + isEmployee: boolean; + employeeNumber: number; + companyName: string; + employerName: string; + applicationCountLast90Days: number; + applicationsDeclinedLast90Days: number; + applicationsApprovedLast90Days: number; + address: CoreAddress[]; + email1: string; // individual.personalInfo.email + email2: string; + phone: CorePhone[]; // individual.personalInfo.primaryPhoneNumberType, individual.personalInfo.primaryPhoneNumber + status: string; + isHomeOwner: boolean; + isHeadOfHouseHold: boolean; + language: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-phone.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-phone.ts new file mode 100644 index 0000000..c45e418 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/core-phone.ts @@ -0,0 +1,11 @@ + + +export class CorePhone { + public phoneKey: string; + public isPrimaryForType: boolean; + public isInternational: boolean; + public countryCode: string; + public areaCode: string; + public phoneNumber: string; + public extension: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/individual-credentials.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/individual-credentials.ts new file mode 100644 index 0000000..a212e22 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/individual-credentials.ts @@ -0,0 +1,10 @@ + +export class IndividualCredentials { + public dateOfBirth: string; + public socialSecurityNumber: string; + + constructor(dateOfBirth: string, socialSecurityNumber: string) { + this.dateOfBirth = dateOfBirth; + this.socialSecurityNumber = socialSecurityNumber; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/login-status.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/login-status.ts new file mode 100644 index 0000000..7560baf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/login-status.ts @@ -0,0 +1,13 @@ + + +export class LoginStatus { + public statusMessage: string; + public errorInfo: ErrorInfo; +} + +export class ErrorInfo { + public errorCode: string; + public errorMessage: string; + public errorType: string; +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-credentials.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-credentials.ts new file mode 100644 index 0000000..0f40967 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-credentials.ts @@ -0,0 +1,10 @@ + +export class MemberCredentials { + public username: string; + public password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-meta.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-meta.ts new file mode 100644 index 0000000..bd7fe89 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/member/models/member-meta.ts @@ -0,0 +1,12 @@ + +export class MemberMeta { + public isTwoFactor: boolean; + public isMember: boolean; + public isIndividual: boolean; + + constructor() { + this.isTwoFactor = false; + this.isMember = false; + this.isIndividual = false; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/card.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/card.component.scss new file mode 100644 index 0000000..04dfe59 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/card.component.scss @@ -0,0 +1,102 @@ +//main-card +.main-card-text { + font-weight: bold; + font-size: 25px; +} +.main-card-footer { + margin-bottom: 10px; +} + +@media (max-width: 767px) { + .main-card-text { + float: left; + font-size: 17px; + } + .main-card-footer { + margin-bottom: 0px; + } +} + +//sub-card +.sub-card-text { + font-weight: bold; + font-size: 25px; + margin-top: 30px +} +.sub-card-footer { + margin-bottom: 30px; +} + +@media (max-width: 767px) { + .sub-card-text { + float: left; + font-size: 17px; + margin-top: 0px; + } + .sub-card-footer { + margin-bottom: 0px; + } +} + + +//shared +.card { + font-family: Arial, sans-serif; + margin-bottom: 25px; + text-decoration: none; + color:#565656; +} +.row { + text-align: center; + padding-right: 0px; + padding-left: 0px; +} +.card-body { + padding-right: 0px; + padding-left: 0px; +} +.main-icon { + margin-top: 10px; + margin-bottom: 8px; +} +.footer-text { + margin-right: 8px; +} +.desktop-view { + font-size: 20px; + margin-right: 8px; +} + +.fa-angle-right { + padding-top: 4px; +} + +/*mobile*/ +@media (max-width: 767px) { + .card { + margin-bottom: 10px; + } + .footer-text { + float: right; + } + .main-icon { + margin-top: 0px; + margin-bottom: 0px; + } + .desktop-view { + display: none; + } + .mobile-view[style] { + display: inline !important; + } + + .card-body { + padding-right: 15px; + padding-left: 15px; + } + + .fa-angle-right { + padding-top: 0px; + } + +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.html new file mode 100644 index 0000000..773f73c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.html @@ -0,0 +1,30 @@ + +
+
+
+
+ + + + +
+
+
+
{{cardName}}
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.scss new file mode 100644 index 0000000..afdf01d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.scss @@ -0,0 +1 @@ +// see card-component.scss for styling \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.spec.ts new file mode 100644 index 0000000..84b4a22 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.spec.ts @@ -0,0 +1,45 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { MainCardComponent } from './main-card.component'; + +describe('MainCardComponent', () => { + let component: MainCardComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + MainCardComponent, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MainCardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should create with input', () => { + fixture.detectChanges(); + component.desktopIcon = 'fas fa-credit-card fa-3x'; + component.cardName = 'Credit Card'; + component.cardPath = 'credit-card'; + fixture.detectChanges(); + const icon = fixture.nativeElement.querySelectorAll('.fa-credit-card'); + expect(icon).toBeDefined(); + expect(icon.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.ts new file mode 100644 index 0000000..44957f9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/main-card/main-card.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'lw-main-card', + templateUrl: './main-card.component.html', + styleUrls: ['../card.component.scss'] +}) +export class MainCardComponent { + @Input() + public cardPath: string; + @Input() + public desktopIcon: string; + @Input() + public mobileIcon: string; + @Input() + public cardName: string; + @Input() + public footerName: string; + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.html new file mode 100644 index 0000000..d295a21 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.html @@ -0,0 +1,20 @@ + +
+
+
+
{{cardName}}
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.scss new file mode 100644 index 0000000..afdf01d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.scss @@ -0,0 +1 @@ +// see card-component.scss for styling \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.spec.ts new file mode 100644 index 0000000..b647041 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { SubCardComponent } from './sub-card.component'; + +describe('SubCardComponent', () => { + let component: SubCardComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SubCardComponent, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SubCardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.ts new file mode 100644 index 0000000..f4afd04 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/cards/sub-card/sub-card.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'lw-sub-card', + templateUrl: './sub-card.component.html', + styleUrls: ['../card.component.scss'] +}) +export class SubCardComponent { + @Input() + public cardPath: string; + @Input() + public cardName: string; + @Input() + public footerName: string; + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.html new file mode 100644 index 0000000..59eee6d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.html @@ -0,0 +1,21 @@ +
LANDING.CREDIT_CARD
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.spec.ts new file mode 100644 index 0000000..cea3918 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SelectCreditCardComponent } from './select-credit-card.component'; +import { configureTestSuite } from 'ng-bullet'; +import { SubCardComponent } from '../cards/sub-card/sub-card.component'; + +describe('SelectCreditCardComponent', () => { + let component: SelectCreditCardComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SelectCreditCardComponent, + SubCardComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectCreditCardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.ts new file mode 100644 index 0000000..419309d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/credit-card/select-credit-card.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-select-credit-card', + templateUrl: './select-credit-card.component.html', + styleUrls: ['../select-loan.component.scss'] +}) + +export class SelectCreditCardComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.html new file mode 100644 index 0000000..107f16c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.html @@ -0,0 +1,25 @@ +
LANDING.HOME_EQUITY
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.spec.ts new file mode 100644 index 0000000..6d5646e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { SelectHomeEquityComponent } from './select-home-equity.component'; +import { SubCardComponent } from '../cards/sub-card/sub-card.component'; + +describe('SelectHomeEquityComponent', () => { + let component: SelectHomeEquityComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SelectHomeEquityComponent, + SubCardComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectHomeEquityComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.ts new file mode 100644 index 0000000..b34c82c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/home/select-home-equity.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-select-home-equity', + templateUrl: './select-home-equity.component.html', + styleUrls: ['../select-loan.component.scss'] +}) + +export class SelectHomeEquityComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.html new file mode 100644 index 0000000..90c6b64 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.spec.ts new file mode 100644 index 0000000..2d0fb66 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SelectLoanComponent } from '../select-loan.component'; +import { configureTestSuite } from 'ng-bullet'; +import { SelectLoanOutletComponent } from './select-loan-outlet.component'; +import { SubCardComponent } from '../cards/sub-card/sub-card.component'; + +describe('SelectLoanOutletComponent', () => { + let component: SelectLoanOutletComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + ], + declarations: [ + SelectLoanOutletComponent, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectLoanOutletComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.ts new file mode 100644 index 0000000..58630e3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/outlet/select-loan-outlet.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-select-loan-outlet', + templateUrl: './select-loan-outlet.component.html', + styleUrls: ['../select-loan.component.scss'] +}) +export class SelectLoanOutletComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.html new file mode 100644 index 0000000..c58cdbc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.html @@ -0,0 +1,24 @@ +
+
LANDING.TITLE
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.scss new file mode 100644 index 0000000..a243617 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.scss @@ -0,0 +1,35 @@ +.back-text { + font-family: Arial, sans-serif; + padding-left: 15px; + color: #3A383B; + text-decoration: underline; +} + +.loan-header { + font-family: 'Open Sans', sans-serif !important; + font-size:44px; + color:#012E50 +} + +.title-hr { + margin: 0 0 2.5em 0; + color: #B1B1B1; + height: 1px; + border: none; + background-color: #B1B1B1; +} + +/*mobile*/ +@media (max-width: 767px) { + .back-text { + padding-left:0px; + display: block; + width: 100%; + text-align: center !important; + } + .loan-header { + font-size:30px; + font-weight:400; + text-align: center; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.spec.ts new file mode 100644 index 0000000..b6c1b07 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.spec.ts @@ -0,0 +1,50 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SelectLoanComponent } from './select-loan.component'; +import { configureTestSuite } from 'ng-bullet'; +import { MainCardComponent } from './cards/main-card/main-card.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationService } from '../shared/credit-application.service'; +import { MemberMeta } from '../member/models/member-meta'; +import { of } from 'rxjs'; + +describe('SelectLoanComponent', () => { + let component: SelectLoanComponent; + let service: CreditApplicationService; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [ + SelectLoanComponent, + MainCardComponent, + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectLoanComponent); + service = TestBed.get(CreditApplicationService); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.ts new file mode 100644 index 0000000..1f891a0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/select-loan.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'lw-select-loan', + templateUrl: './select-loan.component.html', + styleUrls: ['./select-loan.component.scss'] +}) +export class SelectLoanComponent { + + constructor() {} + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.html new file mode 100644 index 0000000..229f4f1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.html @@ -0,0 +1,21 @@ +
LANDING.VEHICLE
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.spec.ts new file mode 100644 index 0000000..e82725f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { SelectVehicleComponent } from './select-vehicle.component'; +import { SubCardComponent } from '../cards/sub-card/sub-card.component'; + +describe('SelectVehicleComponent', () => { + let component: SelectVehicleComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SelectVehicleComponent, + SubCardComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectVehicleComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.ts new file mode 100644 index 0000000..c088bfd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/select-vehicle.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-select-vehicle', + templateUrl: './select-vehicle.component.html', + styleUrls: ['../select-loan.component.scss'] +}) + +export class SelectVehicleComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.html new file mode 100644 index 0000000..16bc846 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.html @@ -0,0 +1,13 @@ +
LANDING.VEHICLE
+
+
+
+
+ +
+
+ +
+
+
+ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.spec.ts new file mode 100644 index 0000000..d2a7b92 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { SelectSubVehicleComponent } from './select-sub-vehicle.component'; +import { SubCardComponent } from '../../cards/sub-card/sub-card.component'; + +describe('SelectSubVehicleComponent', () => { + let component: SelectSubVehicleComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SelectSubVehicleComponent, + SubCardComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectSubVehicleComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.ts new file mode 100644 index 0000000..8e505d2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/select-loan/vehicle/sub-vehicle/select-sub-vehicle.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-select-sub-vehicle', + templateUrl: './select-sub-vehicle.component.html', + styleUrls: ['../../select-loan.component.scss'] +}) + +export class SelectSubVehicleComponent { + constructor() { } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.spec.ts new file mode 100644 index 0000000..b860750 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.spec.ts @@ -0,0 +1,42 @@ +import { ReactiveFormsModule, FormGroup } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressFormBuilderService } from './address-form-builder.service'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; + + +describe('Address Form Builder Service', () => { + const formGroupName = 'someGroupName'; + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + it('should create addressVerifyInfoGroup and add it to the page form', () => { + const formGroup = new FormGroup({}); + AddressFormBuilderService.addAddressCompareGroupIfMissing(formGroup, formGroupName); + expect(formGroup.get(formGroupName)).toBeTruthy(); + CommonGroups.ADDRESS_VERIFY_STATUS_INFO.fields.forEach(control => { + expect(formGroup.get(formGroupName).get(control.field)).toBeTruthy(); + }); + }); + + it('should add validator function to the control', () => { + const formGroup = new FormGroup({}); + AddressFormBuilderService.addAddressCompareGroupIfMissing(formGroup, formGroupName); + formGroup.patchValue({addressVerifyStatus: false}); + formGroup.updateValueAndValidity(); + expect(formGroup.valid).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.ts new file mode 100644 index 0000000..d587fe3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-form-builder.service.ts @@ -0,0 +1,26 @@ +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups'; +import { FormGroup, FormControl } from '@angular/forms'; + +export class AddressFormBuilderService { + + public static addAddressCompareGroupIfMissing(group: FormGroup, groupName: string) { + if (group.get(groupName)) { + return; + } + const addressVerifyInfoGroup = this.createAddressVerifyInformationGroup(); + group.addControl(groupName, addressVerifyInfoGroup); + } + + public static createAddressVerifyInformationGroup(): FormGroup { + const addressVerifyInfoGroup = new FormGroup({}); + + CommonGroups.ADDRESS_VERIFY_STATUS_INFO.fields.forEach((eachField) => { + + const validators = eachField.validation.slice(); + addressVerifyInfoGroup.addControl(eachField.field, new FormControl('', validators)); + }, this); + + return addressVerifyInfoGroup; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.spec.ts new file mode 100644 index 0000000..1134747 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.spec.ts @@ -0,0 +1,93 @@ +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AddressValidationFormatConverter } from './address-validation-format-converter'; +import { AddressDiffInformation } from 'src/app/loan-wizard/field-groups/address-compare/address-compare-information'; +import { AddressInformation } from 'src/app/loan-wizard/field-groups/address/address-information'; +import { ApiAddress } from '../api-models/api-address'; + + +describe('Address Validation Format Service', () => { + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + it('should convert a UI Address to an ApiAddress', () => { + const apiAddress: ApiAddress = { + address1: '1980 OurHouseInDaMiddleOfOur St', + address2: 'Apt 39', + city: 'Madness', + state: 'UK', + postalCode: '54545' + }; + const uiAddress: AddressInformation = { + addressLine1: '1980 OurHouseInDaMiddleOfOur St', + addressLine2: 'Apt 39', + zipcode: '54545', + city: 'Madness', + state: 'UK' + }; + + expect(AddressValidationFormatConverter.makeApiAddress(uiAddress)).toEqual(apiAddress); + }); + + it('should convert ApiAddress back to UI Address', () => { + const apiAddress: ApiAddress = { + address1: '1980 OurHouseInDaMiddleOfOur St', + address2: 'Apt 39', + city: 'Madness', + state: 'UK', + postalCode: '54545' + }; + const uiAddress: AddressInformation = { + addressLine1: '1980 OurHouseInDaMiddleOfOur St', + addressLine2: 'Apt 39', + zipcode: '54545', + city: 'Madness', + state: 'UK' + }; + + expect(AddressValidationFormatConverter.makeUiAddress(apiAddress)).toEqual(uiAddress); + }); + + it('should detect differences (non case-sensitive)', () => { + const apiAddress: ApiAddress = { + address1: '1980 OURHOUSEINTHEMIDDLEOFOUR St', + address2: 'APT 39', + city: 'Madness', + state: 'UK', + postalCode: '54545', + postalCodeExt: '4444' + }; + const uiAddress: AddressInformation = { + addressLine1: '1980 OurHouseInDaMiddleOfOur Street', + addressLine2: 'Apt 39', + zipcode: '54544', + city: 'Madness', + state: 'UK' + }; + + const diff: AddressDiffInformation = { + address1: false, + address2: true, + city: true, + state: true, + postalCode: false, + allFieldsSame: false + }; + + expect(AddressValidationFormatConverter.getAddressValidationDiff(apiAddress, uiAddress)).toEqual(diff); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.ts new file mode 100644 index 0000000..63329ca --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation-format-converter.ts @@ -0,0 +1,67 @@ +import { ApiAddress } from '../api-models/api-address'; +import { AddressInformation } from 'src/app/loan-wizard/field-groups/address/address-information'; +import { AddressDiffInformation } from 'src/app/loan-wizard/field-groups/address-compare/address-compare-information'; + +export class AddressValidationFormatConverter { + + public static filterUiAddressInformation(data: any): AddressInformation { + if (!data) { + return; + } + return { + addressLine1: data.addressLine1, + addressLine2: data.addressLine2, + city: data.city, + state: data.state, + zipcode: data.zipcode, + zipcodeExt: data.zipcodeExt + }; + } + + public static getAddressValidationDiff(apiAddress: ApiAddress, userInput: AddressInformation): AddressDiffInformation { + const address1Diff = this.isFieldDifferent(apiAddress.address1, userInput.addressLine1); + const address2Diff = userInput.addressLine2 && apiAddress.address2 ? + this.isFieldDifferent(apiAddress.address2, userInput.addressLine2) : true; + const cityDiff = this.isFieldDifferent(apiAddress.city, userInput.city); + const stateDiff = this.isFieldDifferent(apiAddress.state, userInput.state); + const zipDiff = this.isFieldDifferent(apiAddress.postalCode, userInput.zipcode); + return { + address1: address1Diff, + address2: address2Diff, + city: cityDiff, + state: stateDiff, + postalCode: zipDiff, + allFieldsSame: address1Diff && address2Diff && cityDiff && stateDiff && zipDiff + }; + } + + public static isFieldDifferent(field1: string, field2: string): boolean { + if (!field1 || !field2) { + throw new Error('AddressValidationConverter got a null field'); + } + return field1.toLocaleUpperCase().trim() === field2.toLocaleUpperCase().trim(); + } + + public static makeApiAddress(addressModelValues: AddressInformation): ApiAddress { + return { + address1: addressModelValues.addressLine1, + address2: addressModelValues.addressLine2, + city: addressModelValues.city, + state: addressModelValues.state, + postalCode: addressModelValues.zipcode, + postalCodeExt: addressModelValues.zipcodeExt, + country: 'UnitedStates' + }; + } + + public static makeUiAddress(addressModelValues: ApiAddress): AddressInformation { + return { + addressLine1: addressModelValues.address1, + addressLine2: addressModelValues.address2, + city: addressModelValues.city, + state: addressModelValues.state, + zipcode: addressModelValues.postalCode, + zipcodeExt: addressModelValues.postalCodeExt + }; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation.service.ts new file mode 100644 index 0000000..0d6c7d6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/address-validation/address-validation.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; +import { Observable } from 'rxjs'; +import { ApiAddress } from '../api-models/api-address'; + +@Injectable({ + providedIn: 'root' +}) +export class AddressValidationService { + + constructor(private httpClient: HttpClient) {} + + public buildAddressValidationURL(): string { + return environment.urlBase + '/AddressValidation/'; + } + + public getValidatedAddress(address: ApiAddress): Observable { + return this.httpClient.post(this.buildAddressValidationURL(), address); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-address.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-address.ts new file mode 100644 index 0000000..0ebcd85 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-address.ts @@ -0,0 +1,11 @@ + +export class ApiAddress { + public address1: string; + public address2?: string; + public city: string; + public state: string; // [AA, AE, AK, ...] + public postalCode: string; + public postalCodeExt?: string; + public country?: string; + public id?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-employer.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-employer.ts new file mode 100644 index 0000000..d1756a6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-employer.ts @@ -0,0 +1,15 @@ +import { ApiIncome } from './api-income'; + +export class ApiEmployer { + public id?: string; + public name: string; + public currentEmployer: boolean; + public employmentType: string; // [ NoneSelected, Contract, FullTime, PartTime, SelfEmployed, Temporary ] + public title: string; + public income: ApiIncome; + public monthsEmployed?: number; + public startDate?: string; + public endDate?: string; + public workPhone?: string; + public language: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-enums.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-enums.ts new file mode 100644 index 0000000..0bd740e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-enums.ts @@ -0,0 +1,41 @@ + +export enum FrequencyTypeEnum { + 'Annually', 'SemiAnnually', 'Monthly', 'SemiMonthly', 'TwoWeeks', 'Weekly', 'Hourly' +} + +export enum ResidenceTypeEnum { + 'Own', 'Rent', 'Other' +} + +export enum OccupancyTypeEnum { + 'OwnerOccupied', 'SecondHome', 'Rental', 'VacationHome', 'Vacant' +} + +export enum PhoneTypeEnum { + 'Home', 'Mobile' +} + +export enum ValuationTypeEnum { + 'Invoice', 'Loan', 'MSRP', 'Retail', 'TradeIn', 'Wholesale', 'Manual' +} + +export enum MfgHomeTypeEnum { + 'Single', 'Double', 'Triple', 'Quad' +} + +// TODO: These enums are not used. Implement like vehicle or delete. +export enum PropertyTypeEnum { + 'SingleFamily', 'Condo', 'Townhouse', 'Family2_4', 'Other' +} + +export enum ConditionTypeEnum { + 'New', 'Used' +} + +export enum SellerTypeEnum { + 'Dealer', 'Private_Party' +} + +export enum UsageTypeEnum { + 'Personal', 'Business' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-error.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-error.ts new file mode 100644 index 0000000..44f3fb8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-error.ts @@ -0,0 +1,12 @@ + +export class ApiError { + code: ApiErrorCode; + message: string; +} + +export enum ApiErrorCode { + No_Error, + Error_Updating, + Error_Submitting, + Error_Deleting +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda-enums.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda-enums.ts new file mode 100644 index 0000000..65e043e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda-enums.ts @@ -0,0 +1,35 @@ +export const HMDA_RACES = { + americanIndianAlaskaNative: 'AmericanIndianAlaskaNative', + asian: 'Asian', + nativeHawaiian: 'NativeHawaiian', + blackOrAfricanAmerican: 'BlackOrAfricanAmerican', + white: 'White', + notProvided: 'NotProvided', + asianIndian: 'AsianIndian', + chinese: 'Chinese', + filipino: 'Filipino', + japanese: 'Japanese', + korean: 'Korean', + vietnamese: 'Vietnamese', + otherAsian: 'OtherAsian', + hawaiian: 'Hawaiian', + guamanianChamorro: 'GuamanianChamorro', + samoan: 'Samoan', + otherPacificIslander: 'OtherPacificIslander' +}; + +export const HMDA_ETHNICITIES = { + hispanicOrLatino: 'HispanicOrLatino', + notHispanicOrLatino: 'NotHispanicOrLatino', + notProvided: 'NotProvided', + mexican: 'Mexican', + puertoRican: 'PuertoRican', + cuban: 'Cuban', + otherHispanicLatino: 'OtherHispanicLatino' +}; + +export const HMDA_SEXES = { + female: 'Female', + male: 'Male', + notProvided: 'NotProvided' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda.ts new file mode 100644 index 0000000..0c39529 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-hmda.ts @@ -0,0 +1,11 @@ + +export class ApiHMDA { + public races: Array; + public ethnicities: Array; + public sexes: Array; + public americanIndianAlaskaNativeTribe?: string; + public otherAsianRace?: string; + public otherPacificIslanderRace?: string; + public otherHispanicLatinoEthnicity?: string; + public hmdaNoticeDisclosed?: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-income.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-income.ts new file mode 100644 index 0000000..d314acd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-income.ts @@ -0,0 +1,13 @@ +export class ApiIncome { + public incomeTypeCD: string; /* [Alimony, Bonuses, ChildSupport, RetirementDisability, Dividends, + Commissions, Other, Spouse, RentalProperty, Interest, SocialSecurity, + IndividualAccount, Salary] */ + public sourceName: string; + public alimonyStatus?: string; // [CourtOrdered, OralUnderstanding, Written] + public amountPerPeriod?: number; + public frequency: string; // [ Annually, SemiAnnually, Monthly, SemiMonthly, TwoWeeks, Weekly, Hourly] + public hoursPerWeek?: string; + public incomeAdjustmentPercentage?: number; + public adjustedIncome?: number; + public incomeStatus: string; // [IncomeStatus_Excluded, IncomeStatus_Included] +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-individual.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-individual.ts new file mode 100644 index 0000000..4d4b5ce --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-individual.ts @@ -0,0 +1,45 @@ +import { ApiResidenceAddress } from './api-residence-address'; +import { ApiIncome } from './api-income'; +import { ApiEmployer } from './api-employer'; +import { ApiTattleSpouse } from './api-tattle-spouse'; +import { ApiHMDA } from './api-hmda'; +import { ApiAddress } from './api-address'; +import { ApiSuffix } from './api-suffix'; + +export class ApiIndividual { + public firstName: string; + public middleInitial?: string; + public lastName: string; + public dob: string; + public suffix?: ApiSuffix; + public ssn: string; + public phone: string; + public phoneType: string; // [Mobile, Home] + public secondaryPhone?: string; + public secondaryPhoneType?: string; + public email: string; + public address: ApiResidenceAddress; + public previousAddresses?: Array; + public mailingAddress?: ApiAddress; + public employers: Array; + public otherIncomes: Array; + public races?: Array; + public americanIndianAlaskaNativeTribe?: string; + public otherAsianRace?: string; + public otherPacificIslanderRace?: string; + public ethnicities?: Array; + public otherHispanicLatinoEthnicity?: string; + public sexes?: Array; + public isMarried?: boolean; + public language: string; + public customerNumber?: string; + public id?: string; + public detailedNonApplicantSpouse?: ApiIndividual; + public nonApplicantSpouse?: ApiTattleSpouse; + public isMarriedToPrimaryBorrower?: boolean; + public hmdaValues?: ApiHMDA; + public isLandmarkEmployee?: boolean; + public hasOptedOutTCPA: boolean; + public primaryApplicantEmail?: string; + public coApplicantEmail?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lien-position.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lien-position.ts new file mode 100644 index 0000000..15a2026 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lien-position.ts @@ -0,0 +1,6 @@ +export class ApiLienPosition { + public hasFirstMortgage : boolean; + public hasSecondMortgage: boolean; + public payoffFirstMortgage: boolean; + public payoffSecondMortgage: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-application.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-application.ts new file mode 100644 index 0000000..ce203dc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-application.ts @@ -0,0 +1,30 @@ +import { ApiIndividual } from './api-individual'; +import { ApiRealEstate } from './api-real-estate'; +import { ApiVehicle } from './api-vehicle'; +import { ApiManufacturedHome } from './api-manufactured-home'; +import { LoanCode } from '../loan-type/loan-code'; +import { LoanPurpose } from '../loan-type/loan-purpose'; + +export class ApiLoanApplication { + public id: string; + public loanType: LoanCode; + public termLengthInMonths?: number; + public requestedAmount: number; + public primaryBorrower: ApiIndividual; + public coApplicant: ApiIndividual; + public purpose?: LoanPurpose; + public purposeOfLoanSignature?: string; + public date: string; + public disclosureDate?: Date; + public realEstate?: ApiRealEstate; + public vehicle?: ApiVehicle; + public manufacturedHome?: ApiManufacturedHome; + public modFeeAccount?: string; + public note?: string; + public primaryContactMethod: string; + public promoCode?: string; + public ipAddress?: string; + public loanTitle?: string; + public clientMessage?: string; + public agreementRequirement? : boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-container.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-container.ts new file mode 100644 index 0000000..dd5c095 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-loan-container.ts @@ -0,0 +1,21 @@ +import { ApiLoanApplication } from './api-loan-application'; +import { ApiPage } from './api-page'; +import { ApiStatus } from './api-status'; +import { ApiError } from './api-error'; +import { LoanCode } from '../loan-type/loan-code'; + +export class ApiLoanContainer { + public id: string; + public loanId: string; + public loanApplication: ApiLoanApplication; + public loanType: LoanCode; + public page: ApiPage; + public status: ApiStatus; + public error: ApiError; + public date: Date; + public firstName: string; + public lastName: string; + public branch: string; + public memberId?: string; + public individualId?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lot.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lot.ts new file mode 100644 index 0000000..0f1b55d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-lot.ts @@ -0,0 +1,7 @@ +import { ApiAddress } from './api-address'; + +export class ApiPropertyLot extends ApiAddress { + public isRented: boolean; + public rentalAmt?: number; + public annualPropertyTax?: number; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-manufactured-home.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-manufactured-home.ts new file mode 100644 index 0000000..afd67c9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-manufactured-home.ts @@ -0,0 +1,12 @@ +import { MfgHomeTypeEnum, OccupancyTypeEnum } from './api-enums'; +import { ApiPropertyLot } from './api-lot'; + +export class ApiManufacturedHome { + public type: MfgHomeTypeEnum; + public use: OccupancyTypeEnum; + public id?: string; + public estimatedHomeValuation?: number; + public lot?: ApiPropertyLot; + public currentlyListedForSale: boolean; + public currentlyBeingRemodeledOrImproved: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-page.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-page.ts new file mode 100644 index 0000000..45f9771 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-page.ts @@ -0,0 +1,32 @@ + + +export class ApiPage { + public hasPersonal: boolean; + public hasAddress: boolean; + public hasEmployment: boolean; + public hasMemberInfo: boolean; + public hasSpouseInfo: boolean; + public hasTattleSpouseInfo: boolean; + public hasResidenceInfo: boolean; + public hasVehicleCollateral: boolean; + public hasHMDA: boolean; + public hasDisclosure: boolean; + public hasCollateralInfo: boolean; + public hasNote: boolean; + + constructor() { + this.hasPersonal = false; + this.hasAddress = false; + this.hasEmployment = false; + this.hasMemberInfo = false; + this.hasSpouseInfo = false; + this.hasTattleSpouseInfo = false; + this.hasResidenceInfo = false; + this.hasVehicleCollateral = false; + this.hasHMDA = false; + this.hasDisclosure = false; + this.hasCollateralInfo = false; + this.hasNote = false; + } +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-real-estate.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-real-estate.ts new file mode 100644 index 0000000..4dcb397 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-real-estate.ts @@ -0,0 +1,21 @@ +import { ApiAddress } from './api-address'; +import { ApiLienPosition } from './api-lien-position'; + +export class ApiRealEstate { + public type: string; + public use: string; + public currentlyListedForSale: boolean; + public currentlyBeingRemodeledOrImproved: boolean; + public lienPosition?: ApiLienPosition; + public initialEstimatedValue?: number; + public hasSpa?: boolean; + public hasPool?: boolean; + public hasBasement?: boolean; + public hasCentralAir?: boolean; + public hasGarage?: boolean; + public inFloodZone?: boolean; + public inEquity?: boolean; + public isPropertyHeldInTrust?: boolean; + public realEstateAddress?: ApiAddress; + public id?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-residence-address.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-residence-address.ts new file mode 100644 index 0000000..b2ff674 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-residence-address.ts @@ -0,0 +1,7 @@ +import { ApiAddress } from './api-address'; + +export class ApiResidenceAddress extends ApiAddress { + public monthsResided: number; + public monthlyPayment?: number; + public residenceType: string; // [Own, Rent, Other] +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-status.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-status.ts new file mode 100644 index 0000000..c1432f4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-status.ts @@ -0,0 +1,10 @@ + + +export enum ApiStatus { + VENDOR_ERROR = 'Vendor_Error', + APPLICATION_ERROR = 'Application_Error', + IN_PROGRESS = 'In_Progress', + DELETED = 'Deleted', + SUBMITTED = 'Submitted', + DECISIONED = 'Decisioned' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-suffix.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-suffix.ts new file mode 100644 index 0000000..cb41a7d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-suffix.ts @@ -0,0 +1,10 @@ + + +export enum ApiSuffix { + II, + III, + IV, + V, + Jr, + Sr +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-tattle-spouse.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-tattle-spouse.ts new file mode 100644 index 0000000..70c1972 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-tattle-spouse.ts @@ -0,0 +1,13 @@ +import { ApiAddress } from './api-address'; + +export class ApiTattleSpouse { + public firstName: string; + public middleInitial?: string; + public lastName: string; + public ssn?: string; + public email?: string; + public mailingAddress: ApiAddress; + public language: string; + public id?: string; + public hasOptedOutTCPA: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-vehicle.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-vehicle.ts new file mode 100644 index 0000000..2d43170 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-models/api-vehicle.ts @@ -0,0 +1,48 @@ +import { VehicleType } from '../vehicle-type/vehicle-type'; +import { ApiAddress } from './api-address'; + +export class ApiVehicle { + public valuationId?: string; + public valuationType: string; + public condition: string; + public seller?: string; + public type: VehicleType; + public usage?: string; + public hasClearTitle?: boolean; + public initialEstimatedValue: number; + public isForbusinessUse?: boolean; + public isOwnerOperator?: boolean; + public numberOfCylinders?: number; + public numberOfWheels?: number; + public year: number; + public mileage: number; + public make: string; + public model: string; + public color?: string; + public body?: string; + public style?: string; + public series?: string; + public trim?: string; + public vin?: string; + public id?: string; + public transferring?: string; + public licensePlateNumber?: string; + public dealerName?: string; + public dealerPhone?: string; + public stateTitled?: string; + public countyTitled?: string; + public titleBranded?: boolean; + public lien?: boolean; + public boatLength?: number; + public boatSameAddress?: string; + public boatAddress?: ApiAddress; + public trailerYear?: number; + public trailerMake?: string; + public trailerType?: string; // [Single, Tandem, Tri-Axle] + public motorYear?: number; + public motorMake?: string; + public motorModel?: string; + public motorType?: number; // [Inboard, Outboard, Inboard/Outboard] + public municipalityType?: string; + public municipalityWhereKept?: string; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.spec.ts new file mode 100644 index 0000000..19e72b7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.spec.ts @@ -0,0 +1,164 @@ +import { CreditApplication } from '../credit-application'; +import { LoanTypeSet } from '../loan-type'; +import { ApiTranslatorService } from './api-translator.service'; +import { ApiLoanApplication } from '../api-models/api-loan-application'; +import { CreditApplicationValues } from '../credit-application-values'; +import { apiTranslatorPreTranslate } from '../../testing/shared-mocks/api-translator-pre-post'; +import { LoanPurpose } from '../loan-type/loan-purpose'; +import { ApiStatus } from '../api-models/api-status'; +import { ApiErrorCode } from '../api-models/api-error'; +import { PhoneTypeEnum } from '../api-models/api-enums'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { LoanCode } from '../loan-type/loan-code'; +import { ConfigurationService } from '../configuration/configuration.service'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { UiSettings } from '../configuration/ui-settings'; +import { DefaultSettings } from '../configuration/default-settings.mock'; +import { configureTestSuite } from 'ng-bullet'; + +describe('ApiTranslatorService', () => { + + let service: ApiTranslatorService; + let creditApplicationStub: CreditApplication; + let loanApplication: CreditApplication; + let configService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + ConfigurationService + ] + }).compileComponents(); + }); + beforeEach(() => { + service = TestBed.get(ApiTranslatorService); + configService = TestBed.get(ConfigurationService); + configService.saveConfiguration(defaultSettings); + creditApplicationStub = { + primaryContactMethod: '1234567890', + promoCode: 'abc-1235', + note: 'this is a test note', + loanId: '123334252345', + limitRequested: 5446, + loanType: LoanTypeSet[1], + hasBeenCompleted: false, + disclosureInformation: { + truthful: true, + contact: false, + date: new Date() + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + }, + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + loanApplication = apiTranslatorPreTranslate; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should translate a CreditApplication to a ApiLoanApplication', () => { + const apiApplication: ApiLoanApplication = service.translateToApiLoanApplication(creditApplicationStub); + expect(apiApplication).toBeTruthy(); + }); + + it('should translate a CreditApplication to a ApiLoanApplication with masked fields for reporting', () => { + const apiApplicationWithHiddenFields: ApiLoanApplication = service.translateToApiLoanApplicationHiddenValues(creditApplicationStub); + const apiApplication: ApiLoanApplication = service.translateToApiLoanApplication(creditApplicationStub); + expect(apiApplication).toBeTruthy(); + expect(apiApplicationWithHiddenFields).toBeTruthy(); + expect(apiApplication.primaryBorrower.firstName).toEqual(apiApplicationWithHiddenFields.primaryBorrower.firstName); + expect(apiApplication.primaryBorrower.lastName).not.toEqual(apiApplicationWithHiddenFields.primaryBorrower.lastName); + expect(apiApplication.primaryBorrower.ssn).not.toEqual(apiApplicationWithHiddenFields.primaryBorrower.ssn); + expect(apiApplication.primaryBorrower.address).not.toEqual(apiApplicationWithHiddenFields.primaryBorrower.address); + expect(apiApplication.primaryBorrower.phone).not.toEqual(apiApplicationWithHiddenFields.primaryBorrower.phone); + }); + + it('should translate a CreditApplication to an ApiLoanApplication (full)', () => { + const apiApplication = service.translateToApiLoanApplication(loanApplication); + expect(apiApplication).toBeTruthy(); + // GJF, I have to stop here for now. I will continue testing after merging. + }); + + it('should build loan container', () => { // TODO Figure out why personalInformation is populated from application-wizard spec + spyOn(configService, 'getAllConfiguration').and.returnValue(defaultSettings); + let loanContainer: ApiLoanContainer = new ApiLoanContainer(); + loanContainer = service.translateToApiLoanContainer(creditApplicationStub); + expect(loanContainer.id).toEqual('123334252345'); + expect(loanContainer.loanType).toEqual(LoanCode.CC_Rewards); + expect(loanContainer.status).toEqual(ApiStatus.IN_PROGRESS); + expect(loanContainer.firstName).toEqual('Frank'); + expect(loanContainer.lastName).toEqual('Reynolds'); + expect(loanContainer.branch).toEqual('Not Found'); + }); + + it('should build note data on summary page', () => { + spyOn(configService, 'getAllConfiguration').and.returnValue(defaultSettings); + let loanContainer: ApiLoanContainer = new ApiLoanContainer(); + loanContainer = service.translateToApiLoanContainer(creditApplicationStub); + expect(loanContainer.loanApplication.promoCode).toEqual('abc-1235'); + expect(loanContainer.loanApplication.note).toEqual('this is a test note'); + expect(loanContainer.loanApplication.primaryContactMethod).toEqual('1234567890'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.ts new file mode 100644 index 0000000..c336cb3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/api-translator.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core'; +import { ApiLoanApplication } from '../api-models/api-loan-application'; +import { CreditApplication } from '../credit-application'; +import { EquityTranslator } from './loan-type-translators/equity-translator'; +import { VehicleTranslator } from './loan-type-translators/vehicle-translator'; +import { HeaderTranslator } from './header-translator'; +import { CreditCardTranslator } from './loan-type-translators/credit-card-translator'; +import { IndividualTranslator } from './individual-translator'; +import { SignatureLoanTranslator } from './loan-type-translators/signature-loan-translator'; +import { LoanCategory } from '../loan-type/loan-category'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { BranchOptions } from '../options/branch-options'; +import { ConfigurationService } from '../configuration/configuration.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiTranslatorService { + constructor( + private configurationService: ConfigurationService + ) { } + + public translateToApiLoanContainer(creditApplication: CreditApplication): ApiLoanContainer { + const apiLoanApplication: ApiLoanApplication = this.translateToApiLoanApplication(creditApplication); + const uiSettings = this.configurationService.getAllConfiguration(); + const apiLoanContainer = new ApiLoanContainer(); + apiLoanContainer.loanApplication = apiLoanApplication; + apiLoanContainer.id = creditApplication.loanId; + apiLoanContainer.loanType = apiLoanApplication.loanType; + apiLoanContainer.status = creditApplication.status; + apiLoanContainer.page = creditApplication.page; + apiLoanContainer.error = creditApplication.error; + apiLoanContainer.firstName = apiLoanApplication.primaryBorrower.firstName; + apiLoanContainer.lastName = apiLoanApplication.primaryBorrower.lastName; + apiLoanContainer.branch = BranchOptions.getBranchNameByZip(apiLoanApplication.primaryBorrower.address.postalCode, uiSettings); + return apiLoanContainer; + } + + public translateToApiLoanApplication(creditApplication: CreditApplication): ApiLoanApplication { + + const apiLoanApplication = new ApiLoanApplication(); + const headerTranslator = this.createHeaderTranslatorForApplication(creditApplication); + Object.assign(apiLoanApplication, headerTranslator.createApiLoanHeader(creditApplication)); + Object.assign(apiLoanApplication, headerTranslator.createCollateralFields(creditApplication)); + Object.assign(apiLoanApplication, headerTranslator.createVehicleFields(creditApplication)); + + const individualTranslator = new IndividualTranslator(); + Object.assign(apiLoanApplication, individualTranslator.createPrimaryBorrower(creditApplication)); + Object.assign(apiLoanApplication, individualTranslator.createSecondaryBorrower(creditApplication)); + + return apiLoanApplication; + } + + public translateToApiLoanApplicationHiddenValues(creditApplication: CreditApplication): ApiLoanApplication { + + const apiLoanApplication = new ApiLoanApplication(); + const headerTranslator = this.createHeaderTranslatorForApplication(creditApplication); + Object.assign(apiLoanApplication, headerTranslator.createApiLoanHeader(creditApplication)); + Object.assign(apiLoanApplication, headerTranslator.createCollateralFields(creditApplication)); + Object.assign(apiLoanApplication, headerTranslator.createVehicleFields(creditApplication)); + + const individualTranslator = new IndividualTranslator(); + Object.assign(apiLoanApplication, individualTranslator.createPrimaryBorrowerWithMaskedData(creditApplication)); + Object.assign(apiLoanApplication, individualTranslator.createSecondaryBorrowerWithMaskedData(creditApplication)); + return apiLoanApplication; + } + + public createHeaderTranslatorForApplication(creditApplication: CreditApplication): HeaderTranslator { + if (creditApplication.loanType.category === LoanCategory.Equity) { + return new EquityTranslator(); + } else if (creditApplication.loanType.category === LoanCategory.Vehicle) { + return new VehicleTranslator(); + } else if (creditApplication.loanType.category === LoanCategory.Signature) { + return new SignatureLoanTranslator(); + } else { + return new CreditCardTranslator(); + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/address-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/address-translator.ts new file mode 100644 index 0000000..a6c8f96 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/address-translator.ts @@ -0,0 +1,19 @@ +import { ApiAddress } from '../../api-models/api-address'; +import { CreditApplicationValues } from '../../credit-application-values'; + +export class AddressTranslator { + public static readonly DEFAULT_COUNTRY = 'UnitedStates'; + + public static generate(addressData): ApiAddress { + return { + address1: addressData.addressLine1, + address2: addressData.addressLine2 || null, // Make test pass, GJF. + city: addressData.city, + state: addressData.state !== CreditApplicationValues.UNDEFINED ? addressData.state : '', + postalCode: addressData.zipcode, + postalCodeExt: addressData.zipcodeExt || null, + country: this.DEFAULT_COUNTRY + }; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/elements-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/elements-translator.spec.ts new file mode 100644 index 0000000..935764a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/elements-translator.spec.ts @@ -0,0 +1,258 @@ +import { CreditApplication } from '../../credit-application'; +import { LoanTypeSet } from '../../loan-type'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { EmployerTranslator } from './employer-translator'; +import { IncomeOtherTranslator } from './income-other-translator'; +import { IncomeAlimonyTranslator } from './income-alimony-translator'; +import { HmdaTranslator } from './hmda-translator'; +import { HMDA_ETHNICITIES, HMDA_RACES, HMDA_SEXES } from '../../api-models/api-hmda-enums'; +import { IncomeEmployerTranslator } from './income-employer-translator'; +import { ApiStatus } from '../../api-models/api-status'; +import { ApiError, ApiErrorCode } from '../../api-models/api-error'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; + + +describe('Elements Translators', () => { + + let creditApplicationStub: CreditApplication; + + beforeEach(() => { + creditApplicationStub = new CreditApplication(LoanTypeSet[0]); + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + loanType: LoanTypeSet[1], + hasBeenCompleted: false, + disclosureInformation: { + contact: true, + truthful: true + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + } + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + }); + + it('should generate an ApiEmployer and ApiIncome from that employer', () => { + + const apiEmployer = EmployerTranslator.generate({ + employerName: 'NVISIA', + employmentType: 'FullTime', + jobTitle: 'Developer', + employmentLengthMonths: 11, + employmentLengthYears: 2, + schedule: 'Annually', + salary: 20000 + }, false); + expect(apiEmployer).toBeTruthy(); + expect(apiEmployer.monthsEmployed).toBe(35); + expect(apiEmployer.income.frequency).toBe('Annually'); + + const employerApiJson = { + 'language': 'English', + 'name': 'NVISIA', + 'currentEmployer': false, + 'employmentType': 'FullTime', + 'title': 'Developer', + 'workPhone': null, + 'income': { + 'incomeTypeCD': 'Salary', + 'sourceName': 'NVISIA', + 'amountPerPeriod': 20000, + 'frequency': 'Annually', + 'incomeStatus': 'IncomeStatus_Included', + 'hoursPerWeek': null + }, + 'monthsEmployed': 35, + }; + + expect(apiEmployer).toEqual(employerApiJson); + }); + + it('should generate ApiIncome from other income', () => { + const apiIncome = IncomeOtherTranslator.generate({ + incomeName: 'Lottery', + amount: 80000000 + }); + expect(apiIncome).toBeTruthy(); + expect(apiIncome.sourceName).toBe('Lottery'); + expect(apiIncome.incomeTypeCD).toBe('Other'); + + const incomeApiJson = { + 'incomeTypeCD': 'Other', + 'sourceName': 'Lottery', + 'frequency': 'Annually', + 'amountPerPeriod': 80000000, + 'incomeStatus': 'IncomeStatus_Included' + }; + + expect(apiIncome).toEqual(incomeApiJson); + }); + + it('should generate ApiIncome from alimony payments', () => { + const apiIncome = IncomeAlimonyTranslator.generate({ + childSupportAmount: 5446 + }); + expect(apiIncome).toBeTruthy(); + expect(apiIncome.amountPerPeriod).toEqual(5446); + expect(apiIncome.incomeTypeCD).toBe('Alimony'); + }); + + + it('should convert HMDA "race" responses to correct API enums', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({ + americanIndianAlaskaNative: true, + americanIndianAlaskaNativeText: 'Nanuk' + }, HMDA_RACES); + expect(hmdaGroup.length).toBe(1); + expect(hmdaGroup[0]).toBe('AmericanIndianAlaskaNative'); + }); + + it('should return default for no valid "race" responses', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({}, HMDA_RACES); + expect(hmdaGroup.length).toBe(1); + expect(hmdaGroup[0]).toBe('NotProvided'); + }); + + it('should convert HMDA "sex" responses to correct API enums', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({ + male: true + }, HMDA_SEXES); + expect(hmdaGroup.length).toBe(1); + expect(hmdaGroup[0]).toBe('Male'); + }); + + it('should return default for no valid "sex" responses', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({}, HMDA_SEXES); + expect(hmdaGroup.length).toBe(1); + expect(hmdaGroup[0]).toBe('NotProvided'); + }); + + it('should convert HMDA "ethnicity" responses to correct API enums', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({ + cuban: true, + puertoRican: true + }, HMDA_ETHNICITIES); + expect(hmdaGroup.length).toBe(2); + expect(hmdaGroup[0]).toBe('Cuban'); + }); + + it('should return default for no valid "ethnicity" responses', () => { + const hmdaGroup = HmdaTranslator.generateHMDAGroup({}, HMDA_ETHNICITIES); + expect(hmdaGroup.length).toBe(1); + expect(hmdaGroup[0]).toBe('NotProvided'); + }); + + it('should return all race, ethnicity, and sex information', () => { + spyOn(HmdaTranslator, 'generateHMDAGroup').and.callThrough(); + const formattedHMDAGroup = HmdaTranslator.generateEquityHMDAFields({ + sexes: { + female: true + }, + ethnicities: {}, + races: { + vietnamese: true, + otherPacificIslander: true, + otherPacificIslanderText: 'EasterIslander' + } + }); + expect(formattedHMDAGroup.hmdaNoticeDisclosed).toEqual(true); + expect(formattedHMDAGroup.sexes[0]).toBe('Female'); + expect(formattedHMDAGroup.ethnicities[0]).toBe('NotProvided'); + expect(formattedHMDAGroup.races[1]).toBe('OtherPacificIslander'); + expect(formattedHMDAGroup.races[0]).toBe('Vietnamese'); + expect(formattedHMDAGroup['otherPacificIslanderRace']).toBe('EasterIslander'); + }); + + + it('should return an hourly employment correctly', () => { + const inputEmployer = { + employerName: 'NVISIA', + employmentType: 'FullTime', + jobTitle: 'Developer', + employmentLengthMonths: 11, + employmentLengthYears: 2, + schedule: 'Annually', + salary: 20 + }; + const outputIncome = { + 'incomeTypeCD': 'Salary', + 'sourceName': 'NVISIA', + 'amountPerPeriod': 20, + 'frequency': 'Annually', + 'hoursPerWeek': null, + 'incomeStatus': 'IncomeStatus_Included' + }; + const employmentResult = IncomeEmployerTranslator.generate(inputEmployer); + expect( employmentResult ).toEqual( outputIncome ); + }); + + it('should return a salary employment correctly', () => { + const inputEmployer = { + employerName: 'NVISIA', + employmentType: 'FullTime', + jobTitle: 'Developer', + employmentLengthMonths: 11, + employmentLengthYears: 2, + schedule: 'Annually', + salary: 20000 + }; + const outputIncome = { + 'incomeTypeCD': 'Salary', + 'sourceName': 'NVISIA', + 'amountPerPeriod': 20000, + 'frequency': 'Annually', + 'hoursPerWeek': null, + 'incomeStatus': 'IncomeStatus_Included' + }; + const employmentResult = IncomeEmployerTranslator.generate(inputEmployer); + expect( employmentResult ).toEqual( outputIncome ); + }); + + // @TODO should return part time employment, with specified hours +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/employer-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/employer-translator.ts new file mode 100644 index 0000000..0237523 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/employer-translator.ts @@ -0,0 +1,26 @@ +import { EmploymentInformation } from '../../../loan-wizard/field-groups/employment/employment-information'; +import { ApiEmployer } from '../../api-models/api-employer'; +import { IncomeEmployerTranslator } from './income-employer-translator'; +import { MaskingService } from '../../masking/masking.service'; + +export class EmployerTranslator { + public static readonly DEFAULT_LANGUAGE = 'English'; + + public static generate(employerData: EmploymentInformation, isCurrent: boolean): ApiEmployer { + return { + name: employerData.employerName, + currentEmployer: isCurrent, + employmentType: employerData.employmentType, + title: employerData.jobTitle, + income: IncomeEmployerTranslator.generate(employerData), + monthsEmployed: this.calculateMonths(employerData.employmentLengthMonths, employerData.employmentLengthYears), + language: this.DEFAULT_LANGUAGE, + workPhone: employerData.workPhoneNumber ? MaskingService.unMaskNumber(employerData.workPhoneNumber) : null + }; + } + + public static calculateMonths(months, years) { + return parseInt(months, 10) + parseInt(years, 10) * 12; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/hmda-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/hmda-translator.ts new file mode 100644 index 0000000..c3e7e01 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/hmda-translator.ts @@ -0,0 +1,64 @@ +import { Individual } from '../../credit-application'; +import { ApiHMDA } from '../../api-models/api-hmda'; +import { HMDA_ETHNICITIES, HMDA_RACES, HMDA_SEXES } from '../../api-models/api-hmda-enums'; + +export class HmdaTranslator { + + public static generate(individual: Individual): {hmdaValues: ApiHMDA} { + return { + hmdaValues: this.generateEquityHMDAFields({ + ethnicities: individual.ethnicityInformation, + sexes: individual.genderInformation, + races: individual.raceInformation + } + ) + }; + } + + public static generateEquityHMDAFields(hmdaResponses): ApiHMDA { + const formattedHMDA = { + sexes: this.generateHMDAGroup(hmdaResponses.sexes, HMDA_SEXES), + races: this.generateHMDAGroup(hmdaResponses.races, HMDA_RACES), + ethnicities: this.generateHMDAGroup(hmdaResponses.ethnicities, HMDA_ETHNICITIES), + hmdaNoticeDisclosed: true + }; + if (hmdaResponses.ethnicities.otherHispanicLatinoText) { + Object.assign(formattedHMDA, {otherHispanicLatinoEthnicity: hmdaResponses.ethnicities.otherHispanicLatinoText}); + formattedHMDA.ethnicities.push(HMDA_ETHNICITIES.otherHispanicLatino); + } + if (hmdaResponses.races.americanIndianAlaskaNativeText) { + Object.assign(formattedHMDA, {americanIndianAlaskaNativeTribe: hmdaResponses.races.americanIndianAlaskaNativeText}); + formattedHMDA.races.push(HMDA_RACES.americanIndianAlaskaNative); + } + if (hmdaResponses.races.otherAsianText) { + Object.assign(formattedHMDA, {otherAsianRace: hmdaResponses.races.otherAsianText}); + formattedHMDA.races.push(HMDA_RACES.otherAsian); + } + if (hmdaResponses.races.otherPacificIslanderText) { + Object.assign(formattedHMDA, {otherPacificIslanderRace: hmdaResponses.races.otherPacificIslanderText}); + formattedHMDA.races.push(HMDA_RACES.otherPacificIslander); + } + return formattedHMDA; + } + + public static generateHMDAGroup(hmdaResponses, enumGroup): Array { + const filteredCollection = []; + let textOverride = false; + Object.keys(hmdaResponses).forEach(label => { + if (label.includes('Text') && hmdaResponses[label] && hmdaResponses[label].length > 0) { + textOverride = true; + } + if (hmdaResponses[label] === true) { + filteredCollection.push(enumGroup[label]); + } + }); + if (!filteredCollection.length && !textOverride) { + return JSON.stringify(enumGroup) === JSON.stringify(HMDA_RACES) ? + ['NotProvided'] + : + ['NotProvided']; + } + + return filteredCollection; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-alimony-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-alimony-translator.ts new file mode 100644 index 0000000..89a8f37 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-alimony-translator.ts @@ -0,0 +1,19 @@ +import { AlimonyInformation } from '../../../loan-wizard/field-groups/employment/alimony-information'; +import { ApiIncome } from '../../api-models/api-income'; +import { FrequencyTypeEnum } from '../../api-models/api-enums'; + +export class IncomeAlimonyTranslator { + + public static generate(incomeData: AlimonyInformation): ApiIncome { + return { + incomeTypeCD: 'Alimony', + // @TODO: We do not have a protocol for naming Alimony + sourceName: 'Child Support', + // @TODO: We do not have a protocol for determining 'other income frequency' + frequency: FrequencyTypeEnum[0], + amountPerPeriod: incomeData.childSupportAmount, + incomeStatus: 'IncomeStatus_Included' + }; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-employer-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-employer-translator.ts new file mode 100644 index 0000000..de61a73 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-employer-translator.ts @@ -0,0 +1,27 @@ +import { ApiIncome } from '../../api-models/api-income'; + +export class IncomeEmployerTranslator { + + public static generate(incomeData): ApiIncome { + return { + sourceName: incomeData.employerName, + frequency: incomeData.schedule, + amountPerPeriod: incomeData.salary, + incomeTypeCD: 'Salary', + incomeStatus: 'IncomeStatus_Included', + hoursPerWeek: this.determineHoursPerWeek(incomeData) + }; + } + + // EmploymentType: FullTime, PartTime, or Contract + // HoursPerWeek = ? + public static determineHoursPerWeek(incomeData: any) { + if (incomeData.schedule === 'Hourly') { + if (incomeData.employmentType === 'FullTime') { + return '40'; + } + return incomeData.weeklyHours; + } + return null; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-other-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-other-translator.ts new file mode 100644 index 0000000..71b0fde --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/income-other-translator.ts @@ -0,0 +1,17 @@ +import { ApiIncome } from '../../api-models/api-income'; +import { FrequencyTypeEnum } from '../../api-models/api-enums'; +import { IncomeInformation } from '../../../loan-wizard/field-groups/employment/income-information'; + +export class IncomeOtherTranslator { + + public static generate(incomeData: IncomeInformation): ApiIncome { + return { + incomeTypeCD: 'Other', + sourceName: incomeData.incomeName, + // @TODO: We do not have a protocol for determining 'other income frequency' + frequency: FrequencyTypeEnum[0], + amountPerPeriod: incomeData.amount, + incomeStatus: 'IncomeStatus_Included' + }; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/personal-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/personal-translator.ts new file mode 100644 index 0000000..d7d8c45 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/personal-translator.ts @@ -0,0 +1,36 @@ +import { Individual } from '../../credit-application'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { MaskingService } from '../../masking/masking.service'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; + +export class PersonalTranslator { + public static readonly DEFAULT_COUNTRY = 'UnitedStates'; + + public static generate(individual: Individual) { + if (individual.personalInformation) { + const personalData = individual.personalInformation; + let married = personalData.maritalStatus === CreditApplicationValues.CHOICE_YES; + if (!married) { + married = personalData.maritalStatus === CreditApplicationValues.CHOICE_NO ? false : null; + } + return { + firstName: personalData.firstName, + middleInitial: personalData.middleInitial ? personalData.middleInitial : null, + lastName: personalData.lastName, + suffix: personalData.suffix ? personalData.suffix : null, + dob: personalData.dateOfBirth.toString(), + ssn: MaskingService.unMaskNumber(personalData.socialSecurityNumber), + phone: MaskingService.unMaskNumber(personalData.primaryPhoneNumber), + phoneType: PhoneTypeEnum[personalData.primaryPhoneNumberType], + secondaryPhone: personalData.secondaryPhoneNumber ? MaskingService.unMaskNumber(personalData.secondaryPhoneNumber) : null, + secondaryPhoneType: personalData.secondaryPhoneNumber ? PhoneTypeEnum[personalData.secondaryPhoneNumberType] : null, + email: personalData.email, + isMarried: married, + primaryApplicantEmail: personalData.primaryApplicantEmail, + coApplicantEmail: personalData.coApplicantEmail + }; + } + return null; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/residence-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/residence-translator.ts new file mode 100644 index 0000000..dd3ffc2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/residence-translator.ts @@ -0,0 +1,18 @@ +import { ResidenceTypeEnum } from '../../api-models/api-enums'; + +export class ResidenceTranslator { + + public static generate(residenceData) { + return { + residenceType: ResidenceTypeEnum[residenceData.residenceStatus], + monthsResided: this.calculateMonths(residenceData.residenceTimeMonths, residenceData.residenceTimeYears), + monthlyPayment: (residenceData.residenceMonthlyPayment || residenceData.residenceMonthlyPayment === 0) ? + residenceData.residenceMonthlyPayment : null + }; + } + + public static calculateMonths(months, years): number { + return parseInt(months, 10) + parseInt(years, 10) * 12; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/tattle-spouse-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/tattle-spouse-translator.ts new file mode 100644 index 0000000..7a18817 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/elements/tattle-spouse-translator.ts @@ -0,0 +1,25 @@ +import { ApiTattleSpouse } from '../../api-models/api-tattle-spouse'; +import { AddressTranslator } from './address-translator'; +import { Individual } from '../../credit-application'; +import { CreditApplicationValues } from '../../credit-application-values'; + +export class TattleSpouseTranslator { + public static readonly DEFAULT_LANGUAGE = 'English'; + + public static generate(individual: Individual): ApiTattleSpouse { + const tattleSpouse = individual.tattleSpouse; + const address = tattleSpouse.hasSameAddressAsSpouse === CreditApplicationValues.CHOICE_YES ? + individual.addressInformation + : + tattleSpouse.addressInformation; + return { + firstName: tattleSpouse.personalInformation.firstName, + middleInitial: tattleSpouse.personalInformation.middleInitial, + lastName: tattleSpouse.personalInformation.lastName, + mailingAddress: AddressTranslator.generate(address), + language: this.DEFAULT_LANGUAGE, + hasOptedOutTCPA: null + }; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/header-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/header-translator.ts new file mode 100644 index 0000000..c88bacb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/header-translator.ts @@ -0,0 +1,11 @@ +import { CreditApplication } from '../credit-application'; + +export interface HeaderTranslator { + + createApiLoanHeader(creditApplication: CreditApplication): any; + + createCollateralFields(creditApplication: CreditApplication): any; + + createVehicleFields(creditApplication: CreditApplication): any; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.spec.ts new file mode 100644 index 0000000..87c64f7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.spec.ts @@ -0,0 +1,462 @@ +import { CreditApplication, Individual, SpouseType, TattleIndividual } from '../credit-application'; +import { LoanTypeSet } from '../loan-type'; +import { CreditApplicationValues } from '../credit-application-values'; +import { IndividualTranslator } from './individual-translator'; +import { ApiIndividual } from '../api-models/api-individual'; +import { ApiAddress } from '../api-models/api-address'; +import { ApiResidenceAddress } from '../api-models/api-residence-address'; +import { ApiStatus } from '../api-models/api-status'; +import { ApiErrorCode } from '../api-models/api-error'; +import { PhoneTypeEnum } from '../api-models/api-enums'; + +describe('Individual Translator', () => { + + let service: IndividualTranslator; + let creditApplicationStub: CreditApplication; + let spouse: Individual; + beforeEach(() => { + service = new IndividualTranslator(); + + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + loanType: LoanTypeSet[1], + hasBeenCompleted: false, + disclosureInformation: { + contact: true, + truthful: true, + date: new Date() + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + }, + employmentInformation: [{ + employerName: 'Johnson', + employmentType: 'Part Time', + jobTitle: 'Engineer', + salary: 60000, + schedule: 'Annually', + employmentLengthYears: 3, + employmentLengthMonths: 6, + weeklyHours: 40, + workPhoneNumber: '(414) 888-9999' + }], + otherIncomeInformation: [{ + amount: 100, + incomeName: 'Mowing', + frequency: '' + }], + alimonyInformation: { + childSupportAmount: 256 + } + }, + { + personalInformation: { + firstName: 'Jane', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-19-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54547' + }, + residenceInformation: { + residenceTimeMonths: '1', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + }, + employmentInformation: [{ + employerName: 'Ville', + employmentType: 'Full Time', + jobTitle: 'Engineer', + salary: 50000, + schedule: 'Annually', + employmentLengthYears: 2, + employmentLengthMonths: 2, + weeklyHours: 40, + workPhoneNumber: '(414) 777-9999' + }], + otherIncomeInformation: [{ + amount: 200, + incomeName: 'Shoveling', + frequency: '' + }], + alimonyInformation: { + childSupportAmount: 512 + } + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + spouse = { + personalInformation: { + firstName: 'Spousy', + lastName: 'McSpouseface', + dateOfBirth: '4-29-1992', + socialSecurityNumber: '444-44-4444', + primaryPhoneNumber: '262-796-4500', + primaryPhoneNumberType: PhoneTypeEnum.Home, + email: 'GreatSpouse@same-address.org' + }, + addressInformation: { + addressLine1: '', + city: '', + state: '', + zipcode: '' + }, + residenceInformation: { + residenceStatus: '', + residenceMonthlyPayment: null, + residenceTimeMonths: '', + residenceTimeYears: '' + }, + hasSameAddressAsSpouse: CreditApplicationValues.CHOICE_YES + }; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create a primary borrower', () => { + const individual = service.createPrimaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.primaryBorrower).toBeTruthy(); + const primaryBorrower = individual.primaryBorrower; + + expect(primaryBorrower).toBeTruthy(); + expect(primaryBorrower.isMarried).toBeFalsy(); + expect(primaryBorrower.dob).toBe('09-17-1991'); + expect(primaryBorrower.firstName).toBe('Frank'); + }); + + it('should create a secondary borrower', () => { + const individual = service.createSecondaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.coApplicant).toBeTruthy(); + const secondaryBorrower = individual.coApplicant; + + expect(secondaryBorrower).toBeTruthy(); + expect(secondaryBorrower.isMarried).toBeFalsy(); + expect(secondaryBorrower.dob).toBe('09-19-1991'); + expect(secondaryBorrower.firstName).toBe('Jane'); + }); + + it('should NOT create a secondary borrower', () => { + creditApplicationStub.hasCoApplicant = null; + const individual = service.createSecondaryBorrower(creditApplicationStub); + expect(individual).toEqual(null); + }); + + it('should build a secondary borrower with co-applicant spouse', () => { + creditApplicationStub.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_YES; + const individual = service.buildSecondaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.coApplicant).toBeTruthy(); + expect(individual.coApplicant.isMarriedToPrimaryBorrower).toBe(true); + }); + + it('should build a secondary borrower with NO co-applicant spouse', () => { + const individual = service.buildSecondaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.coApplicant).toBeTruthy(); + expect(individual.coApplicant.isMarriedToPrimaryBorrower).toBe(false); + }); + + it('should determine if individual hasOptedOutTCPAValue', () => { + const hasOptedOutTCPAValue = service.hasOptedOutTCPAValue(creditApplicationStub.disclosureInformation); + expect(hasOptedOutTCPAValue).toBe(false); + }); + + it('should generate an ApiAddress', () => { + const addressApiJson = { + 'country': 'UnitedStates', + 'address1': '51 Gardner', + 'address2': null, + 'city': 'Alston', + 'state': 'MA', + 'postalCode': '54545', + 'postalCodeExt': null, + 'monthsResided': 38, + 'residenceType': 'Rent', + 'monthlyPayment': 5445 + }; + const individual = service.createPrimaryBorrower(creditApplicationStub); + const apiAddress = individual.primaryBorrower.address; + expect(apiAddress).toBeTruthy(); + expect(apiAddress.residenceType).toBe('Rent'); + expect(apiAddress.monthsResided).toBe(38); + expect(apiAddress.postalCode).toBe('54545'); + expect(apiAddress).toEqual(addressApiJson); + }); + + it('should generate an ApiAddress for secondary borrow', () => { + const addressApiJson: ApiResidenceAddress = { + 'country': 'UnitedStates', + 'address1': '51 Gardner', + 'address2': null, + 'city': 'Alston', + 'state': 'MA', + 'postalCode': '54547', + 'postalCodeExt': null, + 'monthsResided': 37, + 'residenceType': 'Rent', + 'monthlyPayment': 5445 + }; + const individual = service.createSecondaryBorrower(creditApplicationStub); + const apiAddress = individual.coApplicant.address; + expect(apiAddress).toBeTruthy(); + expect(apiAddress.residenceType).toBe('Rent'); + expect(apiAddress.monthsResided).toBe(37); + expect(apiAddress.postalCode).toBe('54547'); + expect(apiAddress).toEqual(addressApiJson); + }); + + it('should generate otherIncomes for primaryBorrower', () => { + const individual = service.createPrimaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.primaryBorrower).toBeTruthy(); + const primaryBorrower = individual.primaryBorrower; + + expect(primaryBorrower).toBeTruthy(); + expect(primaryBorrower.otherIncomes[0].sourceName).toBe('Mowing'); + expect(primaryBorrower.otherIncomes[0].amountPerPeriod).toBe(100); + expect(primaryBorrower.otherIncomes[1].sourceName).toBe('Child Support'); + expect(primaryBorrower.otherIncomes[1].amountPerPeriod).toBe(256); + }); + + it('should generate otherIncomes for secondaryBorrower', () => { + const individual = service.createSecondaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.coApplicant).toBeTruthy(); + const secondaryBorrower = individual.coApplicant; + + expect(secondaryBorrower).toBeTruthy(); + expect(secondaryBorrower.otherIncomes[0].sourceName).toBe('Shoveling'); + expect(secondaryBorrower.otherIncomes[0].amountPerPeriod).toBe(200); + expect(secondaryBorrower.otherIncomes[1].sourceName).toBe('Child Support'); + expect(secondaryBorrower.otherIncomes[1].amountPerPeriod).toBe(512); + + }); + + it('should generate employers for primaryBorrower', () => { + const individual = service.createPrimaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.primaryBorrower).toBeTruthy(); + const primaryBorrower = individual.primaryBorrower; + + expect(primaryBorrower).toBeTruthy(); + expect(primaryBorrower.employers[0].income.sourceName).toBe('Johnson'); + expect(primaryBorrower.employers[0].employmentType).toBe('Part Time'); + expect(primaryBorrower.employers[0].income.amountPerPeriod).toBe(60000); + }); + + it('should generate employers for secondaryBorrower', () => { + const individual = service.createSecondaryBorrower(creditApplicationStub); + expect(individual).toBeTruthy(); + expect(individual.coApplicant).toBeTruthy(); + const secondaryBorrower = individual.coApplicant; + + expect(secondaryBorrower).toBeTruthy(); + expect(secondaryBorrower.employers[0].income.sourceName).toBe('Ville'); + expect(secondaryBorrower.employers[0].employmentType).toBe('Full Time'); + expect(secondaryBorrower.employers[0].income.amountPerPeriod).toBe(50000); + }); + + it('should generate an ApiAddress for a mailing address', () => { + const mailingAddress: ApiAddress = { + country: 'UnitedStates', + address1: '12000 Alder Lake Rd', + address2: null, + city: 'Manitowish Waters', + state: 'WI', + postalCode: '54545', + postalCodeExt: null + }; + creditApplicationStub.fullInfoBorrowers[0].mailingAddressInformation = { + addressLine1: '12000 Alder Lake Rd', + city: 'Manitowish Waters', + state: 'WI', + zipcode: '54545' + }; + creditApplicationStub.fullInfoBorrowers[0].residenceInformation.isMailingAddressSame = false; + const individual: ApiIndividual = service.createPrimaryBorrower(creditApplicationStub).primaryBorrower; + expect(individual.mailingAddress).toEqual(mailingAddress); + }); + + it('should handle PRIMARY SPOUSE with the same address', () => { + creditApplicationStub.fullInfoBorrowers[0].fullInfoSpouse = spouse; + creditApplicationStub.fullInfoBorrowers[0].spouseType = SpouseType.FullInfoSpouse; + const primary = service.createPrimaryBorrower(creditApplicationStub); + expect(primary.primaryBorrower.detailedNonApplicantSpouse.address.address1).toEqual(primary.primaryBorrower.address.address1); + expect(primary.primaryBorrower.detailedNonApplicantSpouse.address.monthlyPayment).toBe(0); + }); + + it('should handle CO BORROWER SPOUSE with the same address', () => { + creditApplicationStub.hasCoApplicantSpouse = null; + creditApplicationStub.fullInfoBorrowers[1].fullInfoSpouse = spouse; + creditApplicationStub.fullInfoBorrowers[1].spouseType = SpouseType.FullInfoSpouse; + const secondary = service.createSecondaryBorrower(creditApplicationStub); + expect(secondary.coApplicant.detailedNonApplicantSpouse.address.address1).toEqual(secondary.coApplicant.address.address1); + expect(secondary.coApplicant.detailedNonApplicantSpouse.address.monthlyPayment).toBe(0); + }); + + it('should handle PRIMARY is married to CO BORROWER with the same address', () => { + creditApplicationStub.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_YES; + creditApplicationStub.fullInfoBorrowers[1].addressInformation = { + addressLine1: '', + city: '', + state: '', + zipcode: '' + }; + creditApplicationStub.fullInfoBorrowers[1].residenceInformation = { + residenceStatus: '', + residenceTimeMonths: '', + residenceTimeYears: '' + }; + creditApplicationStub.fullInfoBorrowers[1].hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_YES; + const primary = service.createPrimaryBorrower(creditApplicationStub); + const secondary = service.createSecondaryBorrower(creditApplicationStub); + creditApplicationStub.fullInfoBorrowers[1].hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_YES; + expect(secondary.coApplicant.address.address1).toEqual(primary.primaryBorrower.address.address1); + expect(secondary.coApplicant.address.monthlyPayment).toBe(0); + }); + + it('should recognize a Landmark Credit Union employee', () => { + const individual = creditApplicationStub.fullInfoBorrowers[0]; + individual.employmentInformation = [{ + employerName: 'Landmark', + employmentType: 'Full time', + jobTitle: 'Project Manager', + schedule: 'Annually', + employmentLengthMonths: 3, + employmentLengthYears: 3, + salary: 120000 + }]; + const apiIndividualLandmarkEmployee: ApiIndividual = service.generateApiIndividual(individual, 'Credit Card'); + expect(apiIndividualLandmarkEmployee.isLandmarkEmployee).toBe(true); + individual.employmentInformation[0].employerName = 'Mandlark'; + const apiIndividualNonLandmarkEmployee: ApiIndividual = service.generateApiIndividual(individual, 'Credit Card'); + expect(apiIndividualNonLandmarkEmployee.isLandmarkEmployee).toBe(false); + }); + + it('should determine whether an individual has provided the minimal amount of data to save', () => { + const validIndividual = creditApplicationStub.fullInfoBorrowers[0]; + expect(service.individualHasMinimumInformationSaved(validIndividual)).toEqual(true); + const invalidIndividual1: Individual = { + personalInformation: { + firstName: '', + lastName: '', + dateOfBirth: '', + socialSecurityNumber: '', + primaryPhoneNumber: '', + primaryPhoneNumberType: PhoneTypeEnum.Home, + email: '' + }, + addressInformation: { + addressLine1: '', + city: '', + state: '', + zipcode: '' + }, + residenceInformation: { + residenceStatus: '', + residenceTimeMonths: '', + residenceTimeYears: '' + } + }; + expect(service.individualHasMinimumInformationSaved(invalidIndividual1)).toEqual(false); + const invalidIndividual2: TattleIndividual = { + personalInformation: { + firstName: '', + lastName: '' + }, + hasSameAddressAsSpouse: CreditApplicationValues.CHOICE_YES + }; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(false); + }); + + it('should require a co-applicant to have an address, regardless of spouse condition', () => { + const invalidIndividual2: TattleIndividual = { + personalInformation: { + firstName: 'JOhn', + lastName: 'Doe' + }, + addressInformation: { + addressLine1: '555 s 1st', addressLine2: '', city: '', state: 'WI', zipcode: '' + }, + hasSameAddressAsSpouse: CreditApplicationValues.CHOICE_YES + }; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(true); + invalidIndividual2.hasSameAddressAsSpouse = null; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(true); + invalidIndividual2.addressInformation = null; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(false); + invalidIndividual2.hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_YES; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(true); + invalidIndividual2.hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_NO; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(false); + invalidIndividual2.addressInformation = { + addressLine1: '555 s. 1st', addressLine2: '', city: '', state: 'WI', zipcode: '' + }; + expect(service.individualHasMinimumInformationSaved(invalidIndividual2)).toEqual(true); + }); + + it('should set all employers to current', () => { + expect(service.isCurrentEmployer(0)).toBe(true); + expect(service.isCurrentEmployer(3)).toBe(true); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.ts new file mode 100644 index 0000000..997381b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/individual-translator.ts @@ -0,0 +1,294 @@ +import { CreditApplication, Individual, SpouseType, TattleIndividual } from '../credit-application'; +import { ApiIndividual } from '../api-models/api-individual'; +import { CreditApplicationValues } from '../credit-application-values'; +import { ApiEmployer } from '../api-models/api-employer'; +import { ApiResidenceAddress } from '../api-models/api-residence-address'; +import { AddressTranslator } from './elements/address-translator'; +import { IncomeAlimonyTranslator } from './elements/income-alimony-translator'; +import { IncomeOtherTranslator } from './elements/income-other-translator'; +import { PersonalTranslator } from './elements/personal-translator'; +import { TattleSpouseTranslator } from './elements/tattle-spouse-translator'; +import { ResidenceTranslator } from './elements/residence-translator'; +import { EmployerTranslator } from './elements/employer-translator'; +import { HmdaTranslator } from './elements/hmda-translator'; +import { DataTableService } from '../data-table.service'; +import { ApiTattleSpouse } from '../api-models/api-tattle-spouse'; +import { ApiAddress } from '../api-models/api-address'; +import { LoanCategory } from '../loan-type/loan-category'; +import { ApiHMDA } from '../api-models/api-hmda'; +import { ApiIncome } from '../api-models/api-income'; +import { DisclosureInformation } from 'src/app/loan-wizard/field-groups/disclosures/credit-card-disclosure'; + +export class IndividualTranslator { + public readonly DEFAULT_LANGUAGE = 'English'; + + public createPrimaryBorrower(creditApplication: CreditApplication): { primaryBorrower: ApiIndividual } { + const primary = { + primaryBorrower: this.generateApiIndividual(creditApplication.fullInfoBorrowers[0], creditApplication.loanType.category) + }; + + const hasOptedOutTCPAValue = this.hasOptedOutTCPAValue(creditApplication.disclosureInformation); + primary.primaryBorrower.hasOptedOutTCPA = hasOptedOutTCPAValue; + + if (primary.primaryBorrower.detailedNonApplicantSpouse) { + primary.primaryBorrower.detailedNonApplicantSpouse.isMarriedToPrimaryBorrower = true; + primary.primaryBorrower.detailedNonApplicantSpouse.hasOptedOutTCPA = hasOptedOutTCPAValue; + Object.assign(primary.primaryBorrower.detailedNonApplicantSpouse, + this.patchMaritalDependentFields( + creditApplication.fullInfoBorrowers[0], primary.primaryBorrower.detailedNonApplicantSpouse + ) + ); + } else if (primary.primaryBorrower.nonApplicantSpouse) { + primary.primaryBorrower.nonApplicantSpouse.hasOptedOutTCPA = hasOptedOutTCPAValue; + } + return primary; + } + + public createPrimaryBorrowerWithMaskedData(creditApplication: CreditApplication): { primaryBorrower: ApiIndividual } { + const primaryBorrower = this.createPrimaryBorrower(creditApplication); + return (primaryBorrower ? {primaryBorrower: this.createBorrowerWithMaskedData(primaryBorrower.primaryBorrower)} : primaryBorrower); + } + + public createSecondaryBorrowerWithMaskedData(creditApplication: CreditApplication): { coApplicant: ApiIndividual } { + const secondaryBorrower = this.createSecondaryBorrower(creditApplication); + return (secondaryBorrower ? {coApplicant: this.createBorrowerWithMaskedData(secondaryBorrower.coApplicant)} : secondaryBorrower); + } + + public createTattleSpouseWithMaskedData(tattleSpouse: ApiTattleSpouse): ApiTattleSpouse { + if (tattleSpouse) { + tattleSpouse.lastName = 'TATTLE_TEST'; + tattleSpouse.ssn = ''; + tattleSpouse.email = 'test@test.test'; + tattleSpouse.mailingAddress = null; + } + return tattleSpouse; + } + + public createBorrowerWithMaskedData(borrower: ApiIndividual): ApiIndividual { + if (borrower) { + borrower.lastName = 'TEST'; + borrower.dob = '01/01/1990'; + borrower.ssn = ''; + borrower.email = 'test@test.test'; + borrower.phone = '1112223333'; + borrower.address = null; + borrower.previousAddresses = null; + borrower.customerNumber = '0'; + borrower.employers = []; + if (borrower.detailedNonApplicantSpouse) { + borrower.detailedNonApplicantSpouse = this.createBorrowerWithMaskedData(borrower.detailedNonApplicantSpouse); + } + if (borrower.nonApplicantSpouse) { + borrower.nonApplicantSpouse = this.createTattleSpouseWithMaskedData(borrower.nonApplicantSpouse); + } + } + return borrower; + } + + public createSecondaryBorrower(creditApplication: CreditApplication): { coApplicant: ApiIndividual } { + if (creditApplication.hasCoApplicant && creditApplication.fullInfoBorrowers.length === 2 && + this.individualHasMinimumInformationSaved(creditApplication.fullInfoBorrowers[1])) { + return this.buildSecondaryBorrower(creditApplication); + } + return null; + } + + public buildSecondaryBorrower(creditApplication: CreditApplication): { coApplicant: ApiIndividual } { + const secondaryBorrower = { + coApplicant: this.generateApiIndividual(creditApplication.fullInfoBorrowers[1], creditApplication.loanType.category) + }; + + if (creditApplication.hasCoApplicantSpouse === CreditApplicationValues.CHOICE_YES) { + secondaryBorrower.coApplicant.isMarriedToPrimaryBorrower = true; + if (creditApplication.fullInfoBorrowers[1].hasSameAddressAsSpouse === CreditApplicationValues.CHOICE_YES) { + Object.assign(secondaryBorrower.coApplicant, + this.patchMaritalDependentFields( + creditApplication.fullInfoBorrowers[0], secondaryBorrower.coApplicant + ) + ); + } + } else { + secondaryBorrower.coApplicant.isMarriedToPrimaryBorrower = false; + if (secondaryBorrower.coApplicant.detailedNonApplicantSpouse && + creditApplication.fullInfoBorrowers[1].fullInfoSpouse.hasSameAddressAsSpouse === CreditApplicationValues.CHOICE_YES) { + Object.assign(secondaryBorrower.coApplicant.detailedNonApplicantSpouse, + this.patchMaritalDependentFields( + creditApplication.fullInfoBorrowers[1], secondaryBorrower.coApplicant.detailedNonApplicantSpouse + ) + ); + } + } + + const hasOptedOutTCPAValue = this.hasOptedOutTCPAValue(creditApplication.disclosureInformation); + secondaryBorrower.coApplicant.hasOptedOutTCPA = hasOptedOutTCPAValue; + + if (secondaryBorrower.coApplicant.detailedNonApplicantSpouse) { + secondaryBorrower.coApplicant.detailedNonApplicantSpouse.hasOptedOutTCPA = hasOptedOutTCPAValue; + } else if (secondaryBorrower.coApplicant.nonApplicantSpouse) { + secondaryBorrower.coApplicant.nonApplicantSpouse.hasOptedOutTCPA = hasOptedOutTCPAValue; + } + return secondaryBorrower; + } + + public patchMaritalDependentFields(individual: Individual, apiIndividual: ApiIndividual): ApiIndividual { + if (individual.fullInfoSpouse) { + if (individual.fullInfoSpouse.hasSameAddressAsSpouse === CreditApplicationValues.CHOICE_YES) { + Object.assign(apiIndividual, this.createApiAddress(individual)); + apiIndividual.address.monthlyPayment = 0; + } + } else { + Object.assign(apiIndividual, this.createApiAddress(individual)); + apiIndividual.address.monthlyPayment = 0; + } + return apiIndividual; + } + + public generateApiIndividual(individual: Individual, category: string): ApiIndividual { + const apiIndividual = new ApiIndividual(); + apiIndividual.language = this.DEFAULT_LANGUAGE; + + Object.assign(apiIndividual, PersonalTranslator.generate(individual)); + Object.assign(apiIndividual, this.createApiAddress(individual)); + Object.assign(apiIndividual, this.createApiMailingAddress(individual)); + Object.assign(apiIndividual, this.createPriorApiAddresses(individual)); + Object.assign(apiIndividual, this.createHMDAFields(individual, category)); + Object.assign(apiIndividual, this.createEmploymentFields(individual)); + Object.assign(apiIndividual, this.createOtherIncomeFields(individual)); + Object.assign(apiIndividual, this.createDetailedSpouseFields(individual, category)); + Object.assign(apiIndividual, this.createTattleSpouseFields(individual)); + Object.assign(apiIndividual, this.createIsLandmarkEmployeeField(individual)); + return apiIndividual; + } + + private createIsLandmarkEmployeeField(individual: Individual): { isLandmarkEmployee: boolean } { + if (individual.employmentInformation && individual.employmentInformation.length) { + const dataTableService = new DataTableService(); + return { + isLandmarkEmployee: dataTableService.isLandmarkEmployee(individual.employmentInformation[0].employerName) + }; + } + return null; + } + + public createHMDAFields(individual: Individual, category: string): { hmdaValues: ApiHMDA } { + if (individual.ethnicityInformation && individual.raceInformation && individual.genderInformation && category === LoanCategory.Equity) { + return HmdaTranslator.generate(individual); + } + return null; + } + + public createApiAddress(individual: Individual): { address: ApiResidenceAddress } { + if (individual.addressInformation && individual.residenceInformation) { + const apiAddress: ApiResidenceAddress = Object.assign({}, + AddressTranslator.generate(individual.addressInformation), + ResidenceTranslator.generate(individual.residenceInformation) + ); + return {address: apiAddress}; + } + return null; + } + + public createApiMailingAddress(individual: Individual): { mailingAddress: ApiAddress } { + if (individual.mailingAddressInformation) { + const apiAddress: ApiAddress = Object.assign({}, + AddressTranslator.generate(individual.mailingAddressInformation) + ); + return {mailingAddress: apiAddress}; + } + return null; + } + + private createPriorApiAddresses(individual: Individual): { previousAddresses: ApiResidenceAddress[] } { + if (individual.priorAddressInformation && + individual.priorResidenceInformation && + individual.priorAddressInformation.length === individual.priorResidenceInformation.length) { + + const priorAddresses: Array = []; + individual.priorAddressInformation.forEach((priorAddress, index) => { + priorAddresses.push( + Object.assign({}, + AddressTranslator.generate(priorAddress), + ResidenceTranslator.generate(individual.priorResidenceInformation[index]) + ) + ); + }); + return {previousAddresses: priorAddresses}; + } + return null; + } + + private createEmploymentFields(individual: Individual): { employers: ApiEmployer[] } { + if (individual.employmentInformation) { + const apiEmployerHistory: Array = []; + individual.employmentInformation.forEach((employer, i) => { + apiEmployerHistory.push(EmployerTranslator.generate(employer, this.isCurrentEmployer(i))); + }); + if (apiEmployerHistory.length) { + return { + employers: apiEmployerHistory + }; + } + } + return null; + } + + public isCurrentEmployer(employerNumber: number): boolean { + return true; + } + + private createOtherIncomeFields(individual: Individual): { otherIncomes: ApiIncome[] } { + const otherIncomes: Array = []; + if (individual.otherIncomeInformation) { + for (let i = 0; i < individual.otherIncomeInformation.length; i++) { + otherIncomes.push(IncomeOtherTranslator.generate(individual.otherIncomeInformation[i])); + } + } + if (individual.alimonyInformation) { + otherIncomes.push(IncomeAlimonyTranslator.generate(individual.alimonyInformation)); + } + + if (otherIncomes.length > 0) { + return {otherIncomes: otherIncomes}; + } + return null; + } + + private createDetailedSpouseFields(individual: Individual, category: string): { detailedNonApplicantSpouse: ApiIndividual } { + if (individual.spouseType === SpouseType.FullInfoSpouse && + individual.fullInfoSpouse && + this.individualHasMinimumInformationSaved(individual.fullInfoSpouse)) { + return {detailedNonApplicantSpouse: this.generateApiIndividual(individual.fullInfoSpouse, category)}; + } + return null; + } + + private createTattleSpouseFields(individual: Individual): { nonApplicantSpouse: ApiTattleSpouse } { + if (individual.spouseType === SpouseType.TattleSpouse && + individual.tattleSpouse && + this.individualHasMinimumInformationSaved(individual.tattleSpouse)) { + return {nonApplicantSpouse: TattleSpouseTranslator.generate(individual)}; + } + return null; + } + + public individualHasMinimumInformationSaved(individual: (Individual | TattleIndividual)): boolean { + return this.individualHasValidPersonalInformation(individual) && this.individualHasValidAddressInformation(individual); + } + + public individualHasValidAddressInformation(individual: (Individual | TattleIndividual)): boolean { + const addressInformation = individual.addressInformation; + if (addressInformation && addressInformation.addressLine1 && addressInformation.addressLine1.length > 1) { + return true; + } + return (individual.hasSameAddressAsSpouse === CreditApplicationValues.CHOICE_YES); + } + + public individualHasValidPersonalInformation(individual: (Individual | TattleIndividual)): boolean { + const personalInformation = individual.personalInformation; + return !!(personalInformation && personalInformation.firstName && personalInformation.firstName.length > 1); + } + + public hasOptedOutTCPAValue(disclosureInformation: DisclosureInformation): boolean { + return !!(disclosureInformation && !disclosureInformation.contact); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.spec.ts new file mode 100644 index 0000000..b0704a1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.spec.ts @@ -0,0 +1,99 @@ +import { CreditCardTranslator } from './credit-card-translator'; +import { CreditApplication } from '../../credit-application'; +import { LoanTypeSet } from '../../loan-type'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { ApiStatus } from '../../api-models/api-status'; +import { ApiError, ApiErrorCode } from '../../api-models/api-error'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; + +describe('Credit Card Translator', () => { + + let service: CreditCardTranslator; + let creditApplicationStub: CreditApplication; + + beforeEach(() => { + service = new CreditCardTranslator(); + creditApplicationStub = new CreditApplication(LoanTypeSet[0]); + + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + loanType: LoanTypeSet[1], + hasBeenCompleted: false, + disclosureInformation: { + contact: true, + truthful: true + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + } + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should generate top level ApiLoanApplication fields', () => { + const topLevelGroup = service.createApiLoanHeader(creditApplicationStub); + expect(topLevelGroup.requestedAmount).toBe(5446); + expect(topLevelGroup.purpose).toBe(LoanTypeSet[1].defaultPurpose); + expect(topLevelGroup.loanType).toBe(LoanTypeSet[1].apiCode); + }); + + it('should return null value with no collateral information', () => { + const result = service.createCollateralFields(creditApplicationStub); + expect(result).toBe(null); + }); + + it('should return null value with no vehicle information', () => { + const result = service.createVehicleFields(creditApplicationStub); + expect(result).toBe(null); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.ts new file mode 100644 index 0000000..2fd76cd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/credit-card-translator.ts @@ -0,0 +1,32 @@ +import { HeaderTranslator } from '../header-translator'; +import { CreditApplication } from '../../credit-application'; +import { MaskingService } from '../../masking/masking.service'; + +export class CreditCardTranslator implements HeaderTranslator { + + public createApiLoanHeader(creditApplication: CreditApplication): any { + return { + loanType: creditApplication.loanType.apiCode, + requestedAmount: MaskingService.unMaskNumber(creditApplication.limitRequested), + purpose: creditApplication.loanType.defaultPurpose, + note: this.createNote(creditApplication), + primaryContactMethod: creditApplication.primaryContactMethod, + promoCode: creditApplication.promoCode, + disclosureDate: creditApplication.disclosureInformation.date, + loanTitle: creditApplication.loanType.title, + clientMessage: creditApplication.clientMessage + }; + } + + public createCollateralFields(creditApplication: CreditApplication): any { + return null; + } + + public createVehicleFields(creditApplication: CreditApplication): any { + return null; + } + + public createNote(creditApplication: CreditApplication): string { + return creditApplication.note; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.spec.ts new file mode 100644 index 0000000..36df246 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.spec.ts @@ -0,0 +1,247 @@ +import { CreditCardTranslator } from './credit-card-translator'; +import { CreditApplication, CollateralInformationGroup, LienPosition } from '../../credit-application'; +import { LoanType, LoanTypeSet } from '../../loan-type'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { CollateralInformation } from '../../../loan-wizard/field-groups/equity/collateral-information'; +import { AddressInformation } from '../../../loan-wizard/field-groups/address/address-information'; +import { ApiRealEstate } from '../../api-models/api-real-estate'; +import { EquityTranslator } from './equity-translator'; +import { ApiStatus } from '../../api-models/api-status'; +import { ApiErrorCode } from '../../api-models/api-error'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; +import { LoanPurpose } from '../../loan-type/loan-purpose'; +import { LoanTitle } from '../../loan-type/loan-title'; +import { LoanCode } from '../../loan-type/loan-code'; + +describe('Equity Translator', () => { + + let service: EquityTranslator; + let creditApplicationStub: CreditApplication; + + beforeEach(() => { + service = new EquityTranslator(); + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + loanType: LoanTypeSet[4], + hasBeenCompleted: false, + disclosureInformation: { + contact: true, + truthful: true, + date: new Date() + }, + purposeOfLoan: LoanTypeSet[4].defaultPurpose, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + } + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should generate top level ApiLoanApplication fields', () => { + const topLevelGroup = service.createApiLoanHeader(creditApplicationStub); + expect(topLevelGroup.requestedAmount).toBe(5446); + expect(topLevelGroup.purpose).toBe(LoanTypeSet[4].defaultPurpose); + expect(topLevelGroup.loanType).toBe(LoanTypeSet[4].apiCode); + }); + + it('should generate ApiRealEstate from collateral information', () => { + const collateralData: CollateralInformation = { + estimatedValue: 100000, + propertyType: 'Townhouse', + isBeingImproved: CreditApplicationValues.CHOICE_YES, + isListedForSale: CreditApplicationValues.CHOICE_NO, + occupancyType: 'Rental' + }; + const addressData: AddressInformation = { + addressLine1: '6 Spooner St.', + city: 'Quohog', + state: 'RI', + zipcode: '53207' + }; + Object.assign(creditApplicationStub, {collateralInformation: Object.assign({}, addressData, collateralData)}); + const apiRealEstate: ApiRealEstate = service.createCollateralFields(creditApplicationStub).realEstate; + expect(apiRealEstate).toBeTruthy(); + expect(apiRealEstate.type).toBe('Townhouse'); + expect(apiRealEstate.use).toBe('Rental'); + }); + + it('should return null value with no collateral information', () => { + const result = service.createCollateralFields(creditApplicationStub); + expect(result).toBe(null); + }); + + it('should return null value with no vehicle information', () => { + const result = service.createVehicleFields(creditApplicationStub); + expect(result).toBe(null); + }); + + it('should return the correct purpose for a Mfd Home purchase', () => { + const manufacturedHomeType = (LoanTypeSet.find((loanType) => loanType.title === LoanTitle.ManufacturedHome) || + ({ id: 'mock-mh', apiCode: LoanCode.MANUFACTURED_HOME_LOAN, defaultPurpose: LoanPurpose.Home_Purchase, title: LoanTitle.ManufacturedHome } as LoanType)); + Object.assign(creditApplicationStub, { + loanType: manufacturedHomeType, + purposeOfLoan: LoanPurpose.Home_Purchase + }); + + const apiHeader = service.createApiLoanHeader(creditApplicationStub); + expect(apiHeader.purpose).toEqual(LoanPurpose.Purchase_Manufactured_Home); + }); + + it('should create correct JSON for Manufactured Housing', () => { + const manufacturedHomeType = (LoanTypeSet.find((loanType) => loanType.title === LoanTitle.ManufacturedHome) || + ({ id: 'mock-mh', apiCode: LoanCode.MANUFACTURED_HOME_LOAN, defaultPurpose: LoanPurpose.Home_Purchase, title: LoanTitle.ManufacturedHome } as LoanType)); + Object.assign(creditApplicationStub, { + loanType: manufacturedHomeType, + purposeOfLoan: manufacturedHomeType.defaultPurpose, + }); + const collateralData: CollateralInformation = { + estimatedValue: 250000, + propertyType: 'Townhouse', + isBeingImproved: CreditApplicationValues.CHOICE_YES, + isListedForSale: CreditApplicationValues.CHOICE_NO, + occupancyType: 'Rental', + onRentedLot: CreditApplicationValues.CHOICE_YES, + rentPaid: '123.45', + annualPropertyTax: '5000.00' + }; + const addressData: AddressInformation = { + addressLine1: '123 W Main St', + city: 'Milwaukee', + state: 'WI', + zipcode: '53202' + }; + + Object.assign(creditApplicationStub, {collateralInformation: Object.assign({}, addressData, collateralData)}); + const apiRealEstate: ApiRealEstate = service.createCollateralFields(creditApplicationStub).manufacturedHome; + + const manufacturedHomeApiJson = { + 'type': 'Townhouse', + 'use': 'Rental', + 'currentlyBeingRemodeledOrImproved': true, + 'currentlyListedForSale': false, + 'estimatedHomeValuation': 250000, + 'lot': { + 'country': 'UnitedStates', + 'address1': '123 W Main St', + 'address2': null, + 'city': 'Milwaukee', + 'state': 'WI', + 'postalCode': '53202', + 'rentalAmt': '123.45', + 'isRented': true, + 'postalCodeExt': null, + 'annualPropertyTax': '5000.00' + }, + }; + expect(apiRealEstate).toEqual(manufacturedHomeApiJson); + + }); + + + it('should default isListedForSale to false if it IS a heloc rate mod loan', () => { + + const collateralData = { + estimatedValue: 100000, + propertyType: 'Townhouse', + isBeingImproved: 'no', + isListedForSale: CreditApplicationValues.CHOICE_YES, + occupancyType: 'Rental', + hasFirstMortgage: CreditApplicationValues.CHOICE_NO, + hasSecondMortgage: CreditApplicationValues.CHOICE_NO, + payoffFirstMortgage: CreditApplicationValues.CHOICE_NO, + payoffSecondMortgage: CreditApplicationValues.CHOICE_NO, + }; + + const result = service.generateRealEstateCollateralFields(collateralData, true); + + expect(result.currentlyListedForSale).toBe(false); + }); + + it('should default isListedForSale to true if it IS NOT a heloc rate mod loan', () => { + + const collateralData = { + estimatedValue: 100000, + propertyType: 'Townhouse', + isBeingImproved: 'no', + isListedForSale: CreditApplicationValues.CHOICE_YES, + occupancyType: 'Rental', + hasFirstMortgage: CreditApplicationValues.CHOICE_NO, + hasSecondMortgage: CreditApplicationValues.CHOICE_NO, + payoffFirstMortgage: CreditApplicationValues.CHOICE_NO, + payoffSecondMortgage: CreditApplicationValues.CHOICE_NO, + }; + + const result = service.generateRealEstateCollateralFields(collateralData, false); + + expect(result.currentlyListedForSale).toBe(true); + }); + + it('should default isListedForSale to false if it IS a mfg home loan', () => { + + const collateralData = { + estimatedValue: 100000, + propertyType: 'Townhouse', + isBeingImproved: 'no', + isListedForSale: CreditApplicationValues.CHOICE_YES, + occupancyType: 'Rental', + hasFirstMortgage: CreditApplicationValues.CHOICE_NO, + hasSecondMortgage: CreditApplicationValues.CHOICE_NO, + payoffFirstMortgage: CreditApplicationValues.CHOICE_NO, + payoffSecondMortgage: CreditApplicationValues.CHOICE_NO, + }; + + const result = service.generateMfgHome(collateralData); + + expect(result.currentlyListedForSale).toBe(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.ts new file mode 100644 index 0000000..c3a3f67 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/equity-translator.ts @@ -0,0 +1,100 @@ +import { HeaderTranslator } from '../header-translator'; +import { CreditApplication } from '../../credit-application'; +import { ApiManufacturedHome } from '../../api-models/api-manufactured-home'; +import { ApiPropertyLot } from '../../api-models/api-lot'; +import { ApiAddress } from '../../api-models/api-address'; +import { ApiRealEstate } from '../../api-models/api-real-estate'; +import { AddressTranslator } from '../elements/address-translator'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { LoanCode } from '../../loan-type/loan-code'; +import { LoanPurpose } from '../../loan-type/loan-purpose'; + +export class EquityTranslator implements HeaderTranslator { + + public createApiLoanHeader(creditApplication: CreditApplication): any { + if (!creditApplication.purposeOfLoan) { + throw new Error('Purpose of loan is required in Equity applications'); + } + + if (creditApplication.loanType.apiCode === LoanCode.MANUFACTURED_HOME_LOAN && + creditApplication.purposeOfLoan === LoanPurpose.Home_Purchase) { + // Manufactured Home Loan has its own special purchase purpose + creditApplication.purposeOfLoan = LoanPurpose.Purchase_Manufactured_Home; + } + + return { + loanType: creditApplication.loanType.apiCode, + requestedAmount: creditApplication.limitRequested, + purpose: creditApplication.purposeOfLoan, + termLengthInMonths: creditApplication.termOfLoan, + note: this.createNote(creditApplication), + primaryContactMethod: creditApplication.primaryContactMethod, + promoCode: creditApplication.promoCode, + disclosureDate: creditApplication.disclosureInformation.date, + loanTitle: creditApplication.loanType.title, + clientMessage: creditApplication.clientMessage, + modFeeAccount: creditApplication.modFeeAccount + }; + } + + public createCollateralFields(creditApplication: CreditApplication): any { + if (creditApplication.collateralInformation) { + if (creditApplication.collateralInformation.isAddressSame === CreditApplicationValues.CHOICE_YES) { + Object.assign(creditApplication.collateralInformation, creditApplication.fullInfoBorrowers[0].addressInformation); + } + + if (creditApplication.loanType.apiCode === LoanCode.MANUFACTURED_HOME_LOAN) { + return {manufacturedHome: this.generateMfgHome(creditApplication.collateralInformation)}; + } else { + return {realEstate: this.generateRealEstateCollateralFields(creditApplication.collateralInformation, creditApplication.loanType.apiCode === LoanCode.EQ_HELOC_Rate_Mod)}; + } + } + return null; + } + + public createVehicleFields(creditApplication: CreditApplication): any { + return null; + } + + public generateMfgHome(collateralData): ApiManufacturedHome { + return { + type: collateralData.propertyType, + use: collateralData.occupancyType, + currentlyListedForSale: false, + currentlyBeingRemodeledOrImproved: collateralData.isBeingImproved === CreditApplicationValues.CHOICE_YES, + estimatedHomeValuation: collateralData.estimatedValue, + lot: this.generateLotForMfgHome(collateralData) + }; + } + + public generateLotForMfgHome(lotInfo): ApiPropertyLot { + const address: ApiAddress = AddressTranslator.generate(lotInfo); + const extendedFields = { + isRented: !!lotInfo.rentPaid, + rentalAmt: lotInfo.rentPaid, + annualPropertyTax: lotInfo.annualPropertyTax + }; + return {...address, ...extendedFields}; + } + + public generateRealEstateCollateralFields(collateralData, isHelocRateModLoan): ApiRealEstate { + return { + initialEstimatedValue: collateralData.estimatedValue, + type: collateralData.propertyType, + use: collateralData.occupancyType, + realEstateAddress: AddressTranslator.generate(collateralData), + currentlyListedForSale: !isHelocRateModLoan && collateralData.isListedForSale === CreditApplicationValues.CHOICE_YES, + currentlyBeingRemodeledOrImproved: collateralData.isBeingImproved === CreditApplicationValues.CHOICE_YES, + lienPosition : { + hasFirstMortgage : collateralData.hasFirstMortgage === CreditApplicationValues.CHOICE_YES, + hasSecondMortgage: collateralData.hasSecondMortgage === CreditApplicationValues.CHOICE_YES, + payoffFirstMortgage: collateralData.payoffFirstMortgage ? true : false, + payoffSecondMortgage: collateralData.payoffSecondMortgage ? true : false, + } + }; + } + + public createNote(creditApplication: CreditApplication): string { + return creditApplication.note; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.spec.ts new file mode 100644 index 0000000..896f069 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.spec.ts @@ -0,0 +1,107 @@ +import { CreditApplication } from '../../credit-application'; +import { LoanType, LoanTypeSet } from '../../loan-type'; +import { LoanTitle } from '../../loan-type/loan-title'; +import { LoanCode } from '../../loan-type/loan-code'; +import { LoanPurpose } from '../../loan-type/loan-purpose'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { SignatureLoanTranslator } from './signature-loan-translator'; +import { ApiStatus } from '../../api-models/api-status'; +import { ApiError, ApiErrorCode } from '../../api-models/api-error'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; + +describe('Signature Loan Translator', () => { + + let service: SignatureLoanTranslator; + let creditApplicationStub: CreditApplication; + + beforeEach(() => { + service = new SignatureLoanTranslator(); + const signatureType = LoanTypeSet.find((loanType) => loanType.title === LoanTitle.Signature) as LoanType; + creditApplicationStub = new CreditApplication(signatureType || LoanTypeSet[0]); + + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + termOfLoan: '12', + loanType: signatureType || LoanTypeSet[0], + hasBeenCompleted: false, + disclosureInformation: { + contact: true, + truthful: true + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + } + }], + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should generate top level ApiLoanApplication fields', () => { + const signatureType = LoanTypeSet.find((loanType) => loanType.title === LoanTitle.Signature) as LoanType; + const workingCreditApplication = signatureType ? creditApplicationStub : { ...creditApplicationStub, loanType: { ...creditApplicationStub.loanType, apiCode: LoanCode.SIGNATURE, defaultPurpose: LoanPurpose.Other } }; + const topLevelGroup = service.createApiLoanHeader(workingCreditApplication); + expect(topLevelGroup.requestedAmount).toBe(5446); + expect(topLevelGroup.purpose).toBe((workingCreditApplication.loanType.defaultPurpose)); + expect(topLevelGroup.loanType).toBe(workingCreditApplication.loanType.apiCode); + expect(topLevelGroup.termLengthInMonths).toBe(12); + }); + + it('should return null value with no collateral information', () => { + const result = service.createCollateralFields(creditApplicationStub); + expect(result).toBe(null); + }); + + it('should return null value with no vehicle information', () => { + const result = service.createVehicleFields(creditApplicationStub); + expect(result).toBe(null); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.ts new file mode 100644 index 0000000..99d117a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/signature-loan-translator.ts @@ -0,0 +1,34 @@ +import { HeaderTranslator } from '../header-translator'; +import { CreditApplication } from '../../credit-application'; +import { MaskingService } from '../../masking/masking.service'; + +export class SignatureLoanTranslator implements HeaderTranslator { + + public createApiLoanHeader(creditApplication: CreditApplication): any { + return { + loanType: creditApplication.loanType.apiCode, + requestedAmount: MaskingService.unMaskNumber(creditApplication.limitRequested), + purpose: creditApplication.loanType.defaultPurpose, + purposeOfLoanSignature: creditApplication.purposeOfLoanSignature, + note: this.createNote(creditApplication), + primaryContactMethod: creditApplication.primaryContactMethod, + promoCode: creditApplication.promoCode, + disclosureDate: creditApplication.disclosureInformation.date, + termLengthInMonths: parseInt(creditApplication.termOfLoan, 10), + loanTitle: creditApplication.loanType.title, + clientMessage: creditApplication.clientMessage + }; + } + + public createCollateralFields(creditApplication: CreditApplication): any { + return null; + } + + public createVehicleFields(creditApplication: CreditApplication): any { + return null; + } + + public createNote(creditApplication: CreditApplication): string { + return creditApplication.note; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.spec.ts new file mode 100644 index 0000000..9e3cf05 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.spec.ts @@ -0,0 +1,180 @@ +import { CreditApplication } from '../../credit-application'; +import { LoanTypeSet } from '../../loan-type'; +import { CreditApplicationValues } from '../../credit-application-values'; +import { VehicleTranslator } from './vehicle-translator'; +import { ApiVehicle } from '../../api-models/api-vehicle'; +import { ApiStatus } from '../../api-models/api-status'; +import { ApiErrorCode } from '../../api-models/api-error'; +import { PhoneTypeEnum } from '../../api-models/api-enums'; +import { VehicleType } from '../../vehicle-type/vehicle-type'; +import { LoanPurpose } from '../../loan-type/loan-purpose'; + +describe('Vehicle Translator', () => { + + let service: VehicleTranslator; + let creditApplicationStub: CreditApplication; + + beforeEach(() => { + service = new VehicleTranslator(); + creditApplicationStub = new CreditApplication(LoanTypeSet[0]); + + creditApplicationStub = { + primaryContactMethod: '1234567890', + limitRequested: 5446, + loanType: LoanTypeSet[1], + hasBeenCompleted: false, + purposeOfLoan: LoanPurpose.Purchase, + disclosureInformation: { + contact: true, + truthful: true + }, + hasCoApplicant: CreditApplicationValues.CHOICE_YES, + hasCoApplicantSpouse: CreditApplicationValues.CHOICE_NO, + fullInfoBorrowers: [{ + personalInformation: { + firstName: 'Frank', + lastName: 'Reynolds', + middleInitial: 'M', + dateOfBirth: '09-17-1991', + socialSecurityNumber: '999-88-7777', + primaryPhoneNumber: '(414) 439-0116', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + email: 'developer@git.com', + secondaryPhoneNumber: '(414) 867-5309', + secondaryPhoneNumberType: PhoneTypeEnum.Home + }, + addressInformation: { + addressLine1: '51 Gardner', + city: 'Alston', + state: 'MA', + zipcode: '54545' + }, + residenceInformation: { + residenceTimeMonths: '2', + residenceTimeYears: '3', + residenceStatus: '1', + residenceMonthlyPayment: 5445, + isMailingAddressSame: true + } + }], + vehicleInformation: { + value: 12000, + type: VehicleType.AUTO, + condition: 'Used', + year: 2015, + make: 'Honda', + model: 'Fit', + mileage: 69000, + term: '36', + loanPurpose: 'Other', + seller: 'Dealer', + vin: '123456789', + licensePlateNumber: 'Plate#1234', + dealerName: 'Dealer Test', + dealerPhone: '9999999999', + stateTitled: 'WI', + countyTitled: 'Washington', + titleBranded: true, + lien: false + }, + status: ApiStatus.IN_PROGRESS, + error: { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + page: { + hasAddress: true, + hasEmployment: true, + hasPersonal: true, + hasMemberInfo: true, + hasSpouseInfo: true, + hasTattleSpouseInfo: true, + hasResidenceInfo: true, + hasVehicleCollateral: true, + hasHMDA: true, + hasDisclosure: true, + hasCollateralInfo: true, + hasNote: true + }, + }; + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should generate top level ApiLoanApplication fields', () => { + const topLevelGroup = service.createApiLoanHeader(creditApplicationStub); + expect(topLevelGroup.requestedAmount).toBe(5446); + }); + + it('should generate top level fields for vehicle information', () => { + + const vehicleTopLevelGroup = service.createApiLoanHeader(creditApplicationStub); + const vehicleGroup = Object.assign({}, service.generateVehicleInformationFields(creditApplicationStub)); + expect(vehicleTopLevelGroup.purpose).toBe('Other'); + expect(vehicleGroup.initialEstimatedValue).toEqual(12000); + expect(vehicleGroup.make).toBe('Honda'); + + const vehicleApiJson = { + 'valuationType': 'Manual', + 'condition': 'Used', + 'seller': 'Dealer', + 'type': 'Automobile', + 'initialEstimatedValue': 12000, + 'year': 2015, + 'mileage': 69000, + 'make': 'Honda', + 'model': 'Fit', + 'vin': '123456789', + 'licensePlateNumber': 'Plate#1234', + 'dealerName': 'Dealer Test', + 'dealerPhone': '9999999999', + 'stateTitled': 'WI', + 'countyTitled': 'Washington', + 'titleBranded': true, + 'lien': false, + 'boatLength': undefined, + 'boatSameAddress': undefined, + 'trailerYear': undefined, + 'trailerMake': undefined, + 'trailerType': undefined, + 'motorYear': undefined, + 'motorMake': undefined, + 'motorModel': undefined, + 'motorType': undefined + }; + + expect(vehicleGroup).toEqual(vehicleApiJson); + }); + + it('should generate correct header purpose (boat loan)', () => { + spyOn(service, 'createApiLoanHeader').and.callThrough(); + const customCreditAppStub = creditApplicationStub; + customCreditAppStub.loanType = LoanTypeSet[11]; + customCreditAppStub.vehicleInformation.loanPurpose = null; + const vehicleTopLevelGroup = service.createApiLoanHeader(customCreditAppStub); + expect(service.createApiLoanHeader).toHaveBeenCalled(); + expect(vehicleTopLevelGroup.purpose).toBe('Purchase'); + }); + + it('should throw error for loan with no purpose header (boat loan)', () => { + spyOn(service, 'createApiLoanHeader').and.callThrough(); + const customCreditAppStub = creditApplicationStub; + customCreditAppStub.loanType.defaultPurpose = null; + service.createApiLoanHeader(customCreditAppStub); + expect(service.createApiLoanHeader).toHaveBeenCalled(); + expect(service.createApiLoanHeader).toThrowError(); + }); + + it('should return null value with no collateral information', () => { + const result = service.createCollateralFields(creditApplicationStub); + expect(result).toBe(null); + }); + + it('should return null value with no vehicle information', () => { + creditApplicationStub.vehicleInformation = null; + const result = service.createCollateralFields(creditApplicationStub); + expect(result).toBe(null); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.ts new file mode 100644 index 0000000..89b3938 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/api-translator/loan-type-translators/vehicle-translator.ts @@ -0,0 +1,108 @@ +import { HeaderTranslator } from '../header-translator'; +import { CreditApplication } from '../../credit-application';import { ApiVehicle } from '../../api-models/api-vehicle'; +import { ValuationTypeEnum } from '../../api-models/api-enums'; +import { ApiAddress } from '../../api-models/api-address'; +import { AddressInformation } from 'src/app/loan-wizard/field-groups/address/address-information'; +import { MaskingService } from '../../masking/masking.service'; + +export class VehicleTranslator implements HeaderTranslator { + + // Currently, sending manual all of the time, is fine. This may change. + public static DEFAULT_VALUATION_TYPE = ValuationTypeEnum[6]; + + public createApiLoanHeader(creditApplication: CreditApplication): any { + if (!creditApplication.purposeOfLoan && !creditApplication.vehicleInformation.loanPurpose) { + throw new Error('Purpose of loan is required in Vehicle applications'); + } + + return { + loanType: creditApplication.loanType.apiCode, + termLengthInMonths: creditApplication.vehicleInformation ? creditApplication.vehicleInformation.term : creditApplication.termOfLoan, + purpose: this.setPurpose(creditApplication), + requestedAmount: creditApplication.limitRequested, + note: this.createNote(creditApplication), + primaryContactMethod: creditApplication.primaryContactMethod, + disclosureDate: creditApplication.disclosureInformation.date, + promoCode: creditApplication.promoCode, + loanTitle: creditApplication.loanType.title, + clientMessage: creditApplication.clientMessage + }; + } + + public createCollateralFields(creditApplication: CreditApplication): any { + return null; + } + + public createVehicleFields(creditApplication: CreditApplication): any { + if (creditApplication.vehicleInformation) { + return {vehicle: this.generateVehicleInformationFields(creditApplication)}; + } + return null; + } + + public generateVehicleInformationFields(creditApplication: CreditApplication): ApiVehicle { + + const vehicleInfo = creditApplication.vehicleInformation; + const apiVehicle = new ApiVehicle(); + + apiVehicle.valuationType = VehicleTranslator.DEFAULT_VALUATION_TYPE; + apiVehicle.condition = vehicleInfo.condition; + apiVehicle.seller = vehicleInfo.seller; + apiVehicle.type = vehicleInfo.type; + apiVehicle.year = vehicleInfo.year; + apiVehicle.mileage = vehicleInfo.mileage; + apiVehicle.make = vehicleInfo.make; + apiVehicle.model = vehicleInfo.model; + apiVehicle.initialEstimatedValue = vehicleInfo.value; + apiVehicle.vin = vehicleInfo.vin; + apiVehicle.licensePlateNumber = vehicleInfo.licensePlateNumber; + apiVehicle.dealerName = vehicleInfo.dealerName; + apiVehicle.dealerPhone = vehicleInfo.dealerPhone; + apiVehicle.stateTitled = vehicleInfo.stateTitled; + apiVehicle.countyTitled = vehicleInfo.countyTitled; + apiVehicle.titleBranded = vehicleInfo.titleBranded; + apiVehicle.lien = vehicleInfo.lien; + apiVehicle.boatLength = MaskingService.unMaskNumber(vehicleInfo.boatLength), + apiVehicle.boatSameAddress = vehicleInfo.boatSameAddress; + apiVehicle.trailerYear = vehicleInfo.trailerYear; + apiVehicle.trailerMake = vehicleInfo.trailerMake; + apiVehicle.trailerType = vehicleInfo.trailerType; + apiVehicle.motorYear = vehicleInfo.motorYear; + apiVehicle.motorMake = vehicleInfo.motorMake; + apiVehicle.motorModel = vehicleInfo.motorModel; + apiVehicle.motorType = vehicleInfo.motorType; + apiVehicle.municipalityType = vehicleInfo.municipalityType; + apiVehicle.municipalityWhereKept = vehicleInfo.municipalityWhereKept; + if (vehicleInfo.boatAddress) apiVehicle.boatAddress = this.createApiAddress(vehicleInfo.boatAddress); + + return apiVehicle; + } + + public createNote(creditApplication: CreditApplication): string { + return creditApplication.note; + } + + public setPurpose(creditApplication: CreditApplication): string { + if (creditApplication.vehicleInformation && creditApplication.vehicleInformation.loanPurpose) { + return creditApplication.vehicleInformation.loanPurpose; + } else { + return creditApplication.purposeOfLoan; + } + } + + public createApiAddress(boatAddress: AddressInformation): ApiAddress { + if (boatAddress) { + const apiBoatAddress = new ApiAddress(); + + apiBoatAddress.address1 = boatAddress.addressLine1; + apiBoatAddress.address2 = boatAddress.addressLine2; + apiBoatAddress.city = boatAddress.city; + apiBoatAddress.state = boatAddress.state; + apiBoatAddress.postalCode = boatAddress.zipcode; + apiBoatAddress.country = 'UnitedStates'; + + return apiBoatAddress; + } + return; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.html new file mode 100644 index 0000000..60c9816 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.html @@ -0,0 +1,12 @@ +
+
DOWN.HEADER
+
+
+
+

DOWN.BODY_TOP

+ +
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.scss new file mode 100644 index 0000000..691fef9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.scss @@ -0,0 +1,18 @@ + +.loan-header { + font-family: 'Open Sans', sans-serif !important; + font-size:44px; + color:#012E50 +} + +.btn-home { + margin-top: 20px; +} + +.title-hr { + margin: 0 0 2.5em 0; + color: #B1B1B1; + height: 1px; + border: none; + background-color: #B1B1B1; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.spec.ts new file mode 100644 index 0000000..98a7183 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.spec.ts @@ -0,0 +1,32 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { AppDownComponent } from './app-down-page.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('AppDownComponent', () => { + let component: AppDownComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [AppDownComponent] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppDownComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.ts new file mode 100644 index 0000000..0dd4497 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-down/app-down-page.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-app-down-page', + templateUrl: './app-down-page.component.html', + styleUrls: ['./app-down-page.component.scss'] +}) + + +export class AppDownComponent { + + constructor() {} + + public commandHome() { + location.href = 'https://www.landmarkcu.com'; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.spec.ts new file mode 100644 index 0000000..8c68c96 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.spec.ts @@ -0,0 +1,95 @@ +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AppLoaderTranslatorService } from './app-loader-translator.service'; +import { FormatService } from '../pipes/format.service'; +import { apiTranslatorPostTranslate, apiTranslatorPreTranslate } from '../../testing/shared-mocks/api-translator-pre-post'; +import { configureTestSuite } from 'ng-bullet'; +import { TestApiLoanContainer, TestTranslateCreditApplication } from 'src/app/testing/shared-mocks/test-api-loan-container'; +import { CreditApplicationValues } from '../credit-application-values'; + +describe('AppLoaderTranslatorService', () => { + + let service: AppLoaderTranslatorService; + let formatService: FormatService; + const mockApiApp = apiTranslatorPostTranslate; + const mockCredApp = apiTranslatorPreTranslate; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot() + ], + declarations: [], + providers: [ + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + service = TestBed.get( AppLoaderTranslatorService ); + formatService = TestBed.get( FormatService ); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should translate residence', () => { + spyOn(service, 'translateResidence').and.callThrough(); + const residence = service.translateResidence(mockApiApp.primaryBorrower); + expect(service.translateResidence).toHaveBeenCalled(); + expect(residence).toBeTruthy(); + }); + + it('should translate address', () => { + spyOn(service, 'translateAddress').and.callThrough(); + const address = service.translateAddress(mockApiApp.primaryBorrower.address); + expect(service.translateAddress).toHaveBeenCalled(); + expect(address).toEqual(mockCredApp.fullInfoBorrowers[0].addressInformation); + }); + + it('should translate employment', () => { + spyOn(service, 'translateEmployment').and.callThrough(); + const employment = service.translateEmployment(mockApiApp.primaryBorrower.employers[0]); + expect(service.translateEmployment).toHaveBeenCalled(); + expect(employment).toEqual(mockCredApp.fullInfoBorrowers[0].employmentInformation[0]); + }); + + it('should translate personal', () => { + spyOn(service, 'translatePersonal').and.callThrough(); + const personal = service.translatePersonal(mockApiApp.primaryBorrower); + expect(service.translatePersonal).toHaveBeenCalled(); + expect(personal).toBeTruthy(); + expect(personal.maritalStatus).toEqual(CreditApplicationValues.CHOICE_YES); + }); + + it('should translate borrower', () => { + spyOn(service, 'translateBorrower').and.callThrough(); + const borrower = service.translateBorrower(mockApiApp.primaryBorrower); + expect(service.translateBorrower).toHaveBeenCalled(); + expect(borrower).toBeTruthy(); + }); + + it('should translate the api application', () => { + spyOn(service, 'translateData').and.callThrough(); + const translatedApplication = service.translateData(TestApiLoanContainer); + expect(service.translateData).toHaveBeenCalled(); + expect(translatedApplication).toBeTruthy(); + // GJF having trouble with this line due to types. + // doesn't find the problem anyway. + // expect(translatedApplication.fullInfoBorrowers[0]).toEqual(TestTranslateCreditApplication.fullInfoBorrowers[0]); + expect(translatedApplication.fullInfoBorrowers[0].personalInformation.maritalStatus).toEqual(CreditApplicationValues.CHOICE_NO); + expect(translatedApplication.loanId).toEqual('12341234'); + }); + + it('should get membership information from application', () => { + spyOn(service, 'getMembershipInformation').and.callThrough(); + const membershipinfo = service.getMembershipInformation(mockApiApp); + expect(service.getMembershipInformation).toHaveBeenCalled(); + expect(membershipinfo).toBeTruthy(); + }); + + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.ts new file mode 100644 index 0000000..f04ed48 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader-translator.service.ts @@ -0,0 +1,385 @@ +import { ResidenceInformation } from '../../loan-wizard/field-groups/address/residence-information'; +import { AddressInformation } from '../../loan-wizard/field-groups/address/address-information'; +import { EmploymentInformation } from '../../loan-wizard/field-groups/employment/employment-information'; +import { PersonalInformation } from '../../loan-wizard/field-groups/personal/personal-information'; +import { AlimonyInformation } from '../../loan-wizard/field-groups/employment/alimony-information'; +import { IncomeInformation } from '../../loan-wizard/field-groups/employment/income-information'; +import { MembershipInformation } from '../../loan-wizard/field-groups/member/membership-information'; +import { CreditApplicationValues } from '../credit-application-values'; +import { CreditApplication, Individual, SpouseType, TattleIndividual, LienPosition } from '../credit-application'; +import { throwError } from 'rxjs'; +import { LoanType, LoanTypeSet } from '../loan-type'; +import { Injectable } from '@angular/core'; +import { FormatService } from '../pipes/format.service'; +import { ApiLoanApplication } from '../api-models/api-loan-application'; +import { ApiIndividual } from '../api-models/api-individual'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { AddressValidationFormatConverter } from '../address-validation/address-validation-format-converter'; +import { ApiIncome } from '../api-models/api-income'; +import { ApiTattleSpouse } from '../api-models/api-tattle-spouse'; +import { TattleSpousePersonalInformation } from 'src/app/loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { ResidenceTypeEnum, PhoneTypeEnum } from '../api-models/api-enums'; +import { LoanPurpose } from '../loan-type/loan-purpose'; +import { CollateralInformation } from 'src/app/loan-wizard/field-groups/equity/collateral-information'; +import { ApiLienPosition } from '../api-models/api-lien-position'; +import { ApiHMDA } from '../api-models/api-hmda'; +import { ApiAddress } from '../api-models/api-address'; +import { IsAddressSameInformation } from 'src/app/loan-wizard/field-groups/address/is-address-same-information'; +import { HMDA_RACES, HMDA_ETHNICITIES, HMDA_SEXES } from '../api-models/api-hmda-enums'; +import { VehicleCollateralInformation } from 'src/app/loan-wizard/field-groups/vehicle/vehicle-collateral-information'; +import { LoanCode } from '../loan-type/loan-code'; + +@Injectable({ + providedIn: 'root' +}) +export class AppLoaderTranslatorService { + constructor(private formatService: FormatService) {} + + public translateResidence(address): ResidenceInformation { + return { + residenceMonthlyPayment: parseInt(address.monthlyPayment, 10), + residenceStatus: ResidenceTypeEnum[address.residenceType], + residenceTimeMonths: (address.monthsResided % 12).toString(), + residenceTimeYears: (Math.floor(address.monthsResided / 12)).toString() + }; + } + + public translateAddress(address): AddressInformation { + return { + addressLine1: address.address1, + addressLine2: address.address2, + city: address.city, + state: address.state, + zipcode: address.postalCode + }; + } + + public translateEmployment(employer): EmploymentInformation { + return { + employerName: employer.name, + employmentType: employer.employmentType, + jobTitle: employer.title, + salary: employer.income.amountPerPeriod, + schedule: employer.income.frequency, + employmentLengthYears: Math.floor(employer.monthsEmployed / 12), + employmentLengthMonths: (employer.monthsEmployed % 12), + weeklyHours: employer.income.hoursPerWeek ? employer.income.hoursPerWeek : null, + workPhoneNumber: this.formatService.formatPhoneNumber(employer.workPhone) + }; + } + + public translatePersonal(borrower: ApiIndividual): PersonalInformation { + const personalInformation = { + dateOfBirth: this.formatService.formatDob(borrower.dob), + email: borrower.email, + firstName: borrower.firstName, + lastName: borrower.lastName, + maritalStatus: borrower.isMarried ? CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + middleInitial: borrower.middleInitial, + primaryPhoneNumber: this.formatService.formatPhoneNumber(borrower.phone), + primaryPhoneNumberType: PhoneTypeEnum[borrower.phoneType], + secondaryPhoneNumber: borrower.secondaryPhone ? this.formatService.formatPhoneNumber(borrower.secondaryPhone) : null, + secondaryPhoneNumberType: borrower.secondaryPhoneType ? PhoneTypeEnum[borrower.secondaryPhoneType] : '', + socialSecurityNumber: this.formatService.formatSsnNoStars(borrower.ssn), + primaryApplicantEmail: borrower.primaryApplicantEmail, + coApplicantEmail: borrower.coApplicantEmail + }; + return personalInformation; + } + + public translateTattlePersonal(tattleSpouse: ApiTattleSpouse): TattleSpousePersonalInformation { + return { + firstName: tattleSpouse.firstName, + middleInitial: tattleSpouse.middleInitial, + lastName: tattleSpouse.lastName + }; + } + + public translateAlimony(alimonyIncome: ApiIncome): AlimonyInformation { + return { + childSupportAmount: alimonyIncome.amountPerPeriod + }; + } + + public translateOtherIncome(income: ApiIncome): IncomeInformation { + return { + incomeName: income.sourceName, + frequency: income.frequency, + amount: income.amountPerPeriod + }; + } + + public translateTattleSpouse(tattleSpouse: ApiTattleSpouse): TattleIndividual { + return { + personalInformation: this.translateTattlePersonal(tattleSpouse), + addressInformation: AddressValidationFormatConverter.makeUiAddress(tattleSpouse.mailingAddress) + }; + } + + public translateBorrower(borrower: ApiIndividual): Individual { + const individual: Individual = new Individual(); + const personalInfo: PersonalInformation = this.translatePersonal(borrower); + const addressInfo: AddressInformation = this.translateAddress(borrower.address); + const residenceInfo: ResidenceInformation = this.translateResidence(borrower.address); + const sameMailingAddress = borrower.mailingAddress ? false : true; + residenceInfo.isMailingAddressSame = sameMailingAddress; + if (!sameMailingAddress) { + individual.mailingAddressInformation = this.translateAddress(borrower.mailingAddress); + } + + if (borrower.previousAddresses) { + const addressArray = []; + const residenceArray = []; + borrower.previousAddresses.forEach(prevAddresses => { + addressArray.push(this.translateAddress(prevAddresses)); + residenceArray.push(this.translateResidence(prevAddresses)); + }); + individual.priorAddressInformation = addressArray; + individual.priorResidenceInformation = residenceArray; + } + if (borrower.employers) { + const employersArray = []; + borrower.employers.forEach(employer => { + employersArray.push(this.translateEmployment(employer)); + }); + individual.employmentInformation = employersArray; + } + if (borrower.otherIncomes) { + individual.otherIncomeInformation = []; + borrower.otherIncomes.forEach((income: ApiIncome) => { + if (income.incomeTypeCD === 'Alimony') { + individual.alimonyInformation = this.translateAlimony(income); + } else { + individual.otherIncomeInformation.push(this.translateOtherIncome(income)); + } + }); + if (!individual.otherIncomeInformation.length) { + // @TODO: Just being safe here + individual.otherIncomeInformation = null; + } + } + individual.addressInformation = addressInfo; + individual.residenceInformation = residenceInfo; + individual.personalInformation = personalInfo; + + Object.assign(individual, this.getHMDAInformation(borrower.hmdaValues)); + + if (borrower.detailedNonApplicantSpouse) { + individual.fullInfoSpouse = this.translateBorrower(borrower.detailedNonApplicantSpouse); + individual.spouseType = SpouseType.FullInfoSpouse; + } else if (borrower.nonApplicantSpouse) { + const spouse = this.translateTattleSpouse(borrower.nonApplicantSpouse); + const diff = AddressValidationFormatConverter.getAddressValidationDiff( + borrower.address, spouse.addressInformation + ); + if (diff.allFieldsSame) { + spouse.hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_YES; + } else { + spouse.hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_NO; + } + individual.spouseType = SpouseType.TattleSpouse; + individual.tattleSpouse = spouse; + } + return individual; + } + + public translateData(container: ApiLoanContainer): CreditApplication { + const loanApp: ApiLoanApplication = container.loanApplication; + const loanType = this.getLoanTypeForAPIValue(loanApp.loanType); + const parsedCreditApp: CreditApplication = new CreditApplication(loanType); + // GJF, read the container id as the loan ID, same as creation. + parsedCreditApp.loanId = container.id; + + if (loanApp.primaryBorrower) { + parsedCreditApp.fullInfoBorrowers = []; + parsedCreditApp.fullInfoBorrowers.push(this.translateBorrower(loanApp.primaryBorrower)); + parsedCreditApp.hasCoApplicant = loanApp.coApplicant ? CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO; + if (loanApp.coApplicant) { + parsedCreditApp.fullInfoBorrowers.push(this.translateBorrower(loanApp.coApplicant)); + parsedCreditApp.hasCoApplicantSpouse = loanApp.coApplicant.isMarriedToPrimaryBorrower ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO; + parsedCreditApp.confirmJointCredit = CreditApplicationValues.CHOICE_YES; + + const diff = AddressValidationFormatConverter.getAddressValidationDiff( + loanApp.coApplicant.address, parsedCreditApp.fullInfoBorrowers[0].addressInformation + ); + if (diff.allFieldsSame) { + parsedCreditApp.fullInfoBorrowers[1].hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_YES; + } else { + parsedCreditApp.fullInfoBorrowers[1].hasSameAddressAsSpouse = CreditApplicationValues.CHOICE_NO; + } + } + } + + Object.assign(parsedCreditApp, this.getMembershipInformation(loanApp)); + Object.assign(parsedCreditApp, { + collateralInformation: this.getCollateralInformation(loanApp) + }); + Object.assign(parsedCreditApp, { + vehicleInformation: this.getVehicleCollateralInformation(loanApp) + }); + parsedCreditApp.limitRequested = loanApp.requestedAmount; + parsedCreditApp.purposeOfLoan = loanApp.purpose; + parsedCreditApp.purposeOfLoanSignature = loanApp.purposeOfLoanSignature; + if (!parsedCreditApp) { + throwError('unable to parse payload data'); + } + + return parsedCreditApp; + } + + public getLoanTypeForAPIValue(loanTypeApiCode: string): LoanType { + let foundLoanType = null; + LoanTypeSet.forEach((loanType, index) => { + if (loanType.apiCode === loanTypeApiCode) { + foundLoanType = loanType; + } + }, this); + return foundLoanType; + } + + public getMembershipInformation(application: ApiLoanApplication): MembershipInformation { + if (application && application.primaryBorrower) { + const memberInfo: MembershipInformation = { + applicantType: application.coApplicant ? CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + limitRequested: application.requestedAmount, + livesInMaritalPropertyState: application.primaryBorrower.address.state === CreditApplicationValues.STATE_REQUIRED_COAPP ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + coApplicantLivesInMaritalPropertyState: application.coApplicant && application.coApplicant.address.state + === CreditApplicationValues.STATE_REQUIRED_COAPP ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + purposeOfLoan: LoanPurpose[application.purpose], + purposeOfLoanSignature: application.purposeOfLoanSignature, + termOfLoan: application.termLengthInMonths + }; + return memberInfo; + } else { + return null; + } + } + + public getHMDAInformation(hmdaInfo: ApiHMDA) { + if (!hmdaInfo) { + return; + } + return { + ethnicityInformation: Object.assign( + this.parseHMDAInformation(hmdaInfo.ethnicities, HMDA_ETHNICITIES), {otherHispanicLatinoText: hmdaInfo.otherHispanicLatinoEthnicity} + ), + genderInformation: this.parseHMDAInformation(hmdaInfo.sexes, HMDA_SEXES), + raceInformation: Object.assign( + this.parseHMDAInformation(hmdaInfo.races, HMDA_RACES), + { + americanIndianAlaskaNativeText: hmdaInfo.americanIndianAlaskaNativeTribe, + otherAsianText: hmdaInfo.otherAsianRace, + otherPacificIslanderText: hmdaInfo.otherPacificIslanderRace + } + ), + }; + } + + + public parseHMDAInformation(hmdaValues: Array, enumGroup) { + const parsedValues = {}; + Object.keys(enumGroup).forEach(value => { + if (hmdaValues.indexOf(enumGroup[value]) > -1) { + Object.assign(parsedValues, {[value]: true}); + } + }); + + return parsedValues; + } + + public getVehicleCollateralInformation(application: ApiLoanApplication): VehicleCollateralInformation { + if (!application.vehicle) { + return; + } + return { + value: application.vehicle.initialEstimatedValue, + condition: application.vehicle.condition, + type: application.vehicle.type, + year: application.vehicle.year, + make: application.vehicle.make, + mileage: application.vehicle.mileage, + model: application.vehicle.model, + term: application.termLengthInMonths.toString(), + loanPurpose: application.purpose, + seller: application.vehicle.seller, + vin: application.vehicle.vin, + transferring: application.vehicle.transferring, + licensePlateNumber: application.vehicle.licensePlateNumber, + dealerName: application.vehicle.dealerName, + dealerPhone: application.vehicle.dealerPhone, + stateTitled: application.vehicle.stateTitled, + countyTitled: application.vehicle.countyTitled, + titleBranded: application.vehicle.titleBranded, + lien: application.vehicle.lien, + boatLength: application.vehicle.boatLength, + boatSameAddress: application.vehicle.boatSameAddress, + trailerYear: application.vehicle.trailerYear, + trailerMake: application.vehicle.trailerMake, + trailerType: application.vehicle.trailerType, + motorYear: application.vehicle.motorYear, + motorMake: application.vehicle.motorMake, + motorModel: application.vehicle.motorModel, + motorType: application.vehicle.motorType, + municipalityType: application.vehicle.municipalityType, + municipalityWhereKept: application.vehicle.municipalityWhereKept + }; + } + + public getCollateralInformation(application: ApiLoanApplication) { + if (!application.realEstate) { + return; + } + + let isListedForSale = application.realEstate.currentlyListedForSale ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO; + + if(application.loanType === LoanCode.MANUFACTURED_HOME_LOAN || application.loanType === LoanCode.EQ_HELOC_Rate_Mod) { + isListedForSale = CreditApplicationValues.CHOICE_NO; + } + + const collateralInformation: CollateralInformation = { + estimatedValue: application.realEstate.initialEstimatedValue, + propertyType: application.realEstate.type, + isBeingImproved: application.realEstate.currentlyBeingRemodeledOrImproved ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + isListedForSale, + occupancyType: application.realEstate.use + }; + + const realEstateAddress: AddressInformation = AddressValidationFormatConverter.makeUiAddress(application.realEstate.realEstateAddress); + const isAddressSame: IsAddressSameInformation = { + isAddressSame: AddressValidationFormatConverter.getAddressValidationDiff( + application.realEstate.realEstateAddress, + AddressValidationFormatConverter.makeUiAddress(application.primaryBorrower.address) + ).allFieldsSame ? CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO + }; + const lienPosition: LienPosition = this.getLienPosition(application.realEstate.lienPosition); + + return Object.assign( + collateralInformation, + realEstateAddress, + isAddressSame, + lienPosition); + } + + public getRealEstateAddress(address: ApiAddress): AddressInformation { + return AddressValidationFormatConverter.makeUiAddress(address); + } + + public getLienPosition(lienPosition: ApiLienPosition): LienPosition { + if (!lienPosition) { + return; + } + return { + hasFirstMortgage: lienPosition.hasFirstMortgage ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + hasSecondMortgage: lienPosition.hasSecondMortgage ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO, + payoffFirstMortgage: lienPosition.payoffFirstMortgage, + payoffSecondMortgage: lienPosition.payoffSecondMortgage + }; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.html new file mode 100644 index 0000000..418e184 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.html @@ -0,0 +1,19 @@ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.scss new file mode 100644 index 0000000..8f8f08a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.scss @@ -0,0 +1,41 @@ +.list-header { + font-weight: bold; + margin-right: 5px; + color: #565656; +} +.list { + text-decoration: underline; + margin-right: 5px; +} + +ul#menu li { + display:inline; +} + +ul#menu { + padding-left: 0px; + margin-bottom: 0px; +} + +.btn-continue-loan { + text-decoration: underline; + border: 0px; + font-size: 1em; + padding-top: 0px; + padding-bottom: 0px; +} + +.btn-link { + color: #565656; +} + +.btn-link:hover { + color: #005B9A; +} + +/*mobile*/ +@media(max-width: 768px) { + ul#menu li { + display: block; + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.spec.ts new file mode 100644 index 0000000..c00a033 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.spec.ts @@ -0,0 +1,81 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { configureTestSuite } from 'ng-bullet'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { MemberService } from 'src/app/member/member.service'; +import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe'; +import { TestApiLoanContainer } from 'src/app/testing/shared-mocks/test-api-loan-container'; +import { AppLoaderService } from './app-loader.service'; +import { AppLoaderComponent } from './app-loader.component'; +import { MemberMeta } from 'src/app/member/models/member-meta'; +import { LoanIconPipe } from '../pipes/loan-icon.pipe'; + +describe('SelectLoanComponent', () => { + let component: AppLoaderComponent; + let fixture: ComponentFixture; + let memberService: MemberService; + let appLoaderService: AppLoaderService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot(), + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [ + AppLoaderComponent, + LoanCodePipe, + LoanIconPipe + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppLoaderComponent); + component = fixture.componentInstance; + memberService = TestBed.get(MemberService); + appLoaderService = TestBed.get(AppLoaderService); + router = TestBed.get(Router); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should intialize with default display state', () => { + expect(component.localState.display).toBe('empty'); + }); + + it('should create application and route user to loan app', () => { + spyOn(appLoaderService, 'createApplication'); + spyOn(router, 'navigateByUrl').and.returnValue(true); + component.commandContinueLoan(TestApiLoanContainer); + expect(component.localState.display).toBe('loading'); + expect(appLoaderService.createApplication).toHaveBeenCalled(); + expect(router.navigateByUrl).toHaveBeenCalledWith('/loan-wizard/cc/cb'); + }); + + it('should set display state to empty when user has no loans', () => { + component.updateLocalState([]); + expect(component.localState.display).toBe('empty'); + }); + + it('should set display state to loans when user has loans', () => { + component.updateLocalState([TestApiLoanContainer]); + expect(component.localState.display).toBe('loans'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.ts new file mode 100644 index 0000000..dcb5386 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { RouteOptions } from 'src/app/shared/options/route-options'; +import { AppLoaderService } from 'src/app/shared/app-loader/app-loader.service'; +import { CookieUtil } from '../cookie/cookie-util'; + +export enum AppLoaderState { + LOADER_EMPTY = 'empty', + LOADER_LOANS = 'loans', + LOADER_LOADING = 'loading' +} + +@Component({ + selector: 'lw-app-loader-selector', + templateUrl: './app-loader.component.html', + styleUrls: ['./app-loader.component.scss'] +}) + +export class AppLoaderComponent implements OnInit { + public containers: ApiLoanContainer[]; + public localState = { + display: AppLoaderState.LOADER_EMPTY, + showLoanList: false + }; + + constructor(private router: Router, private appLoaderService: AppLoaderService) {} + + public ngOnInit() { + + if (CookieUtil.allowLoanResume()) { + this.localState.display = AppLoaderState.LOADER_LOADING; + this.localState.showLoanList = true; + this.appLoaderService.requestIndividualApplications().subscribe((loans) => { + this.updateLocalState(loans || []); + }, (error) => { + this.updateLocalState([]); + this.localState.display = AppLoaderState.LOADER_EMPTY; + }); + } + } + + public updateLocalState(loanContainers: Array): void { + this.containers = loanContainers; + if (this.containers.length < 1) { + this.localState.display = AppLoaderState.LOADER_EMPTY; + } else { + this.localState.display = AppLoaderState.LOADER_LOANS; + } + } + + public commandContinueLoan(loanContainer: ApiLoanContainer): void { + this.localState.display = AppLoaderState.LOADER_LOADING; + this.appLoaderService.createApplication(loanContainer); + this.router.navigateByUrl(RouteOptions.getRouteFromLoanCode(loanContainer.loanType)); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.spec.ts new file mode 100644 index 0000000..502fe4f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.spec.ts @@ -0,0 +1,55 @@ +import { AppLoaderService } from './app-loader.service'; +import { TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { CreditApplicationService } from '../credit-application.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { TestApiLoanContainer } from '../../testing/shared-mocks/test-api-loan-container'; +import { CreditApplicationValues } from '../credit-application-values'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; + +describe('AppLoaderService', () => { + + let service: AppLoaderService; + let appWizardService: ApplicationWizardService; + let creditAppService: CreditApplicationService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + service = TestBed.get(AppLoaderService); + appWizardService = TestBed.get(ApplicationWizardService); + creditAppService = TestBed.get(CreditApplicationService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create application', () => { + spyOn(appWizardService, 'setLoanType').and.callThrough(); + service.createApplication(TestApiLoanContainer); + expect(appWizardService.setLoanType).toHaveBeenCalled(); + const application = appWizardService.getFullCreditApplication(); + expect(application).toBeTruthy(); + expect(application.fullInfoBorrowers[0].personalInformation.maritalStatus).toEqual(CreditApplicationValues.CHOICE_NO); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.ts new file mode 100644 index 0000000..2fa1461 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-loader/app-loader.service.ts @@ -0,0 +1,49 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { Observable, of, BehaviorSubject } from 'rxjs'; +import { catchError, map, delay } from 'rxjs/operators'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { environment } from '../../../environments/environment'; +import { CreditApplication, Individual } from '../credit-application'; +import { AppLoaderTranslatorService } from './app-loader-translator.service'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { TestApiLoanContainer } from 'src/app/testing/shared-mocks/test-api-loan-container'; +import { MemberMeta } from 'src/app/member/models/member-meta'; +import { CreditApplicationService } from '../credit-application.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AppLoaderService { + + constructor( + private httpClient: HttpClient, + private applicationWizardService: ApplicationWizardService, + private translatorService: AppLoaderTranslatorService + ) { + + } + + public buildGetLoansURL(): string { + return environment.urlBase + '/Storage/GetMemberApplications'; + } + + public createApplication(container: ApiLoanContainer): void { + const parsedData: CreditApplication = this.translatorService.translateData(container); + this.applicationWizardService.setLoanType(parsedData.loanType.id, parsedData); + } + + public requestIndividualApplications(): Observable { + return this.httpClient.get(this.buildGetLoansURL(), {withCredentials: true}) + .pipe( + map((data: any) => { + return data; + }), + catchError((err, caught) => { + console.log(err); + return of([]); + }) + ); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.html new file mode 100644 index 0000000..f065b39 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.html @@ -0,0 +1,5 @@ +
+

UNAVAILABLE.HEADER

+
UNAVAILABLE.BODY_TOP
+
UNAVAILABLE.BODY_BOTTOM
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.spec.ts new file mode 100644 index 0000000..7d4c220 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.spec.ts @@ -0,0 +1,32 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { AppUnavailableComponent } from './app-unavailable-page.component'; +import { configureTestSuite } from 'ng-bullet'; + +describe('AppUnavailableComponent', () => { + let component: AppUnavailableComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [AppUnavailableComponent] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppUnavailableComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.ts new file mode 100644 index 0000000..617c802 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/app-unavailable/app-unavailable-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lw-app-unavailable-page', + templateUrl: './app-unavailable-page.component.html' +}) +export class AppUnavailableComponent { + + constructor() {} + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.html new file mode 100644 index 0000000..cb0d676 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.html @@ -0,0 +1,11 @@ +
+ +
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.spec.ts new file mode 100644 index 0000000..cee287d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.spec.ts @@ -0,0 +1,80 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { By } from "@angular/platform-browser"; +import { configureTestSuite } from 'ng-bullet'; +import { BeforeYouBeginComponent } from './before-you-begin.component'; + +describe('BeforeYouBeginComponent', () => { + let component: BeforeYouBeginComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [ + BeforeYouBeginComponent, + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BeforeYouBeginComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should intialize with default message', () => { + expect(component.message).toBe(null); + }); + + it('should display intro message with list', () => { + component.message = { + "value": { + "INTRO": "Intro message", + "HEADINGS": [ + { + "HEADING": "Main header", + "VALUES": ["First value"] + } + ] + } + } + + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(By.css('span')).length).toBe(1); + expect(fixture.debugElement.queryAll(By.css('b')).length).toBe(1); + expect(fixture.debugElement.queryAll(By.css('ol')).length).toBe(1); + expect(fixture.debugElement.queryAll(By.css('li')).length).toBe(1); + + }); + + it('should display intro message with list', () => { + component.message = { + "value": { + "INTRO": "Intro message", + "HEADINGS": [ + { + "HEADING": "First header", + "VALUES": ["1", "2"] + }, + { + "HEADING": "Second header", + "VALUES": ["1", "2", "3"] + } + ] + } + } + + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(By.css('span')).length).toBe(1); + expect(fixture.debugElement.queryAll(By.css('b')).length).toBe(2); + expect(fixture.debugElement.queryAll(By.css('ol')).length).toBe(2); + expect(fixture.debugElement.queryAll(By.css('li')).length).toBe(5); + + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.ts new file mode 100644 index 0000000..324fdb3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/before-you-begin/before-you-begin.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; + +export enum AppLoaderState { + LOADER_EMPTY = 'empty', + LOADER_LOANS = 'loans', + LOADER_LOADING = 'loading' +} + +@Component({ + selector: 'lw-before-you-begin', + templateUrl: './before-you-begin.component.html' +}) + +export class BeforeYouBeginComponent { + + @Input() + public message: object = null; + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/borrower-type.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/borrower-type.ts new file mode 100644 index 0000000..60bad19 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/borrower-type.ts @@ -0,0 +1,7 @@ + +export enum BorrowerType { + PRIMARY_BORROWER = 'PRIMARY_BORROWER', + CO_BORROWER = 'CO_BORROWER', + PRIMARY_BORROWER_SPOUSE = 'PRIMARY_BORROWER_SPOUSE', + CO_BORROWER_SPOUSE = 'CO_BORROWER_SPOUSE' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.resolver.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.resolver.ts new file mode 100644 index 0000000..b615898 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.resolver.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Resolve, Router } from '@angular/router'; +import { ConfigurationService } from './configuration.service'; +import { UiSettings } from './ui-settings'; +import { Observable, Subject } from 'rxjs'; + +@Injectable() +export class ConfigurationResolver implements Resolve { + + public ERROR_ROUTE = '/error'; + public DOWN_ROUTE = '/down'; + + constructor(private configurationService: ConfigurationService, private router: Router) {} + + public resolve(): Observable | boolean { + if (this.configurationService.hasValidConfiguration()) { + return true; + } + const isDoneResolvingSubject = new Subject(); + this.configurationService.reloadConfiguration() + .subscribe( + (success: UiSettings) => { + this.configurationService.saveConfiguration(success); + if (this.configurationService.isApplicationDisabled()) { + this.router.navigate([this.DOWN_ROUTE]); + isDoneResolvingSubject.next(false); + isDoneResolvingSubject.complete(); + } + isDoneResolvingSubject.next(true); + isDoneResolvingSubject.complete(); + }, + (failure) => { + this.router.navigate([this.ERROR_ROUTE]); + isDoneResolvingSubject.next(false); + isDoneResolvingSubject.complete(); + } + ); + + return isDoneResolvingSubject.asObservable(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.spec.ts new file mode 100644 index 0000000..33e83b0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.spec.ts @@ -0,0 +1,92 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ConfigurationService } from './configuration.service'; +import { UiSettings } from './ui-settings'; +import { DefaultSettings } from './default-settings.mock'; +import { HttpClient } from '@angular/common/http'; +import { configureTestSuite } from 'ng-bullet'; +import { environment } from 'src/environments/environment'; + + +describe('Configuration Service', () => { + + let service: ConfigurationService; + + const defaultSettings: UiSettings = DefaultSettings; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + ConfigurationService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(ConfigurationService); + httpClient = TestBed.get(HttpClient); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should save and return configurations', () => { + service.saveConfiguration(defaultSettings); + const links = service.getConfigurationByField('links'); + expect(links.HomePage).toBe('https://www.landmarkcu.com/'); + const allConfig: UiSettings = service.getAllConfiguration(); + expect(allConfig.mortgageRefiExpress[0]).toEqual(96); + }); + + it('should load configuration', () => { + const originalUseLocalSettings = environment.useLocalSettings; + environment.useLocalSettings = false; + + service.reloadConfiguration().subscribe((data) => { + expect(data.test).toEqual('x'); + service.saveConfiguration(data); + } + ); + const req = httpTestingController.expectOne(service.buildPingURL()); + + // Assert that the request is a GET. + expect(req.request.method).toEqual('GET'); + + // Respond with mock data, causing Observable to resolve. + // Subscribe callback asserts that correct data was returned. + req.flush({'test': 'x'}); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + environment.useLocalSettings = originalUseLocalSettings; + }); + + it('should throw an error if there is no configuration', () => { + let allConfig; + try { + allConfig = service.getAllConfiguration(); + } catch (exception) { + allConfig = exception; + } + expect(allConfig.toString()).toBe('Error: UI Settings not loaded'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.ts new file mode 100644 index 0000000..756eeaa --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; +import { UiSettings, SiteLink } from './ui-settings'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ConfigurationService { + + public static readonly LOCAL_SETTINGS_URL = `${environment.baseHref}/assets/settings/default_settings.json`; + private uiSettings: UiSettings; + + constructor(private httpClient: HttpClient) {} + + private buildHeaders(): HttpHeaders { + return new HttpHeaders().set('withCredentials', 'true'); + } + + public buildPingURL(): string { + return environment.urlBase + '/UiSettings/'; + } + + public reloadConfiguration(): Observable { + if (this.shouldUseLocalSettings()) { + console.log('using local settings...'); + return this.httpClient.get(ConfigurationService.LOCAL_SETTINGS_URL); + } + return this.httpClient.get(this.buildPingURL(), {headers: this.buildHeaders()}); + } + + public hasValidConfiguration(): boolean { + return !!(this.uiSettings); + } + + public shouldUseLocalSettings() { + return environment.useLocalSettings; + } + + public saveConfiguration(config: UiSettings): void { + this.uiSettings = config; + } + + public getConfigurationByField(field: string) { + if (!this.uiSettings) { + throw new Error('UI Settings not loaded'); + } + return this.uiSettings[field]; + } + + public getSiteLink(site: SiteLink): string { + return this.getConfigurationByField('links')[site]; + } + + public getSystemConfiguration(configuration: string): string { + return this.getConfigurationByField('configuration')[configuration]; + } + + public isApplicationDisabled(): boolean { + if (this.getSystemConfiguration('applicationDisabled')) { + return true; + } + return false; + } + + public getAllConfiguration(): UiSettings { + if (this.uiSettings) { + return this.uiSettings; + } + throw new Error('UI Settings not loaded'); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.ts new file mode 100644 index 0000000..63788d1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/configuration.ts @@ -0,0 +1,6 @@ +export class Configuration { + public id: string; + public lastUpdated: Date; + public lastUpdatedBy: string; + public applicationDisabled: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/default-settings.mock.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/default-settings.mock.ts new file mode 100644 index 0000000..bf9e994 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/default-settings.mock.ts @@ -0,0 +1,1102 @@ +import { BranchConfig, UiSettings, BranchNameConfig } from './ui-settings'; + +export const DefaultSettings: UiSettings = { + 'configuration': { + 'lastUpdated': new Date(), + 'lastUpdatedBy': 'gfrick', + 'applicationDisabled': false, + 'id': '758f64a9-0e3e-41a1-8b09-e3c37b27437b' + }, + 'mfdHomeAndRvByAmount': { + '10000': [ + 84 + ], + '20000': [ + 120, + 180 + ] + }, + 'homeEquity': [ + 60, + 84, + 120, + 180, + 240 + ], + 'mortgageRefiExpress': [ + 96, + 120, + 144 + ], + 'vehiclePreApproval': [ + 36, + 48, + 63, + 75 + ], + 'termsByYear': { + '2010': [ + 36, + 48 + ], + '2011': [ + 36, + 48 + ], + '2012': [ + 36, + 48 + ], + '2013': [ + 36, + 48, + 63 + ], + '2014': [ + 36, + 48, + 63, + 75 + ], + '2015': [ + 36, + 48, + 63, + 75 + ], + '2016': [ + 36, + 48, + 63, + 75 + ], + '2017': [ + 36, + 48, + 63, + 75, + ], + '2018': [ + 36, + 48, + 63, + 75, + ], + '2019': [ + 36, + 48, + 63, + 75, + 84 + ] + }, + 'boatLoansByAmount': { + '0': [ + 63 + ], + '10000': [ + 63, + 84 + ], + '20000': [ + 120, + 180 + ] + }, + 'powersportByAmount': { + '0': [ + 63 + ], + '10000': [ + 72 + ] + }, + 'signatureByAmount': { + '0': [ + 12, + 24, + 36 + ], + '5000': [ + 48 + ] + }, + 'typePurposes': { + 'CC_CashBack': [ + 'Other', + 'Consolidation', + 'Reestablish_Credit' + ], + 'CC_Rewards': [ + 'Other', + 'Consolidation', + 'Reestablish_Credit' + ], + 'CC_Student': [ + 'Other', + 'Consolidation', + 'Reestablish_Credit' + ], + 'CC_Secure': [ + 'Other', + 'Consolidation', + 'Reestablish_Credit' + ], + 'EQ_HELOC_Flex': [ + 'Home_Improvement', + 'Home_Purchase', + 'Home_Refinance', + 'Other', + 'Cash_Out_Refinance' + ], + 'EQ_Mtg_Refi_Express': [ + 'Home_Improvement', + 'Home_Purchase', + 'Home_Refinance', + 'Other', + 'Cash_Out_Refinance' + ], + 'EQ_Home_Equity_Fixed': [ + 'Home_Improvement', + 'Home_Purchase', + 'Home_Refinance', + 'Other', + 'Cash_Out_Refinance' + ], + 'EQ_Mfg_Home_With_or_Without_Land': [ + 'Home_Improvement', + 'Purchase_Manufactured_Home', + 'Home_Refinance', + 'Other', + 'Cash_Out_Refinance' + ], + 'V_Vehicle': [ + 'Purchase', + 'Cash_Out_Refinance', + 'Refinance' + ], + 'V_VehiclePreapproval': [ + 'Purchase', + 'Cash_Out_Refinance', + 'Refinance' + ], + 'M_Boat': [ + 'Purchase', + 'Refinance' + ], + 'P_Powersport': [ + 'Purchase', + 'Refinance' + ], + 'T_TrailerMotorhome': [ + 'Purchase', + 'Refinance' + ], + 'S_Signature': [ + 'Other', + 'Consolidation' + ] + }, + 'limits': { + 'CC_CashBack': { + 'min': 250, + 'max': 25000 + }, + 'CC_Rewards': { + 'min': 250, + 'max': 25000 + }, + 'CC_Student': { + 'min': 250, + 'max': 25000 + }, + 'CC_Secure': { + 'min': 250, + 'max': 25000 + }, + 'EQ_HELOC_Flex': { + 'min': 5000, + 'max': 250000 + }, + 'EQ_Mtg_Refi_Express': { + 'min': 5000, + 'max': 250000 + }, + 'EQ_Home_Equity_Fixed': { + 'min': 5000, + 'max': 250000 + }, + 'EQ_Mfg_Home_With_or_Without_Land': { + 'min': 10000, + 'max': 250000 + }, + 'V_Vehicle': { + 'min': 250, + 'max': 250000 + }, + 'V_VehiclePreapproval': { + 'min': 250, + 'max': 250000 + }, + 'M_Boat': { + 'min': 250, + 'max': 250000 + }, + 'P_Powersport': { + 'min': 250, + 'max': 250000 + }, + 'T_TrailerMotorhome': { + 'min': 10000, + 'max': 250000 + }, + 'S_Signature': { + 'min': 250, + 'max': 25000 + } + }, + 'statesRequiringSpouseInformation': [ + 'WI' + ], + 'stateOptions': [ + 'AA', + 'AE', + 'AK', + 'AL', + 'AP', + 'AR', + 'AS', + 'AZ', + 'CA', + 'CO', + 'CT', + 'DC', + 'DE', + 'FL', + 'GA', + 'GU', + 'HI', + 'IA', + 'ID', + 'IL', + 'IN', + 'KS', + 'KY', + 'LA', + 'MA', + 'MD', + 'ME', + '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' + ], + 'abaRouting': '275079714', + 'contact': { + 'HelpPhone': '262-796-4500' + }, + 'links': { + 'PrivacyPolicy': 'https://www.example.com/', + 'MemberSite': 'https://www.example.net', + 'HomePage': 'https://www.landmarkcu.com/' + }, + 'incomeLimits': { + 'Hourly': { + 'min': 0, + 'max': 999 + }, + 'Annually': { + 'min': 999, + 'max': 1000000000 + } + }, + 'branchMapping': { + 'branchCodesByZip': [ + {'zipcode': '53001', 'channelId': 1561599}, + {'zipcode': '53002', 'channelId': 1561599}, + {'zipcode': '53003', 'channelId': 1561245}, + {'zipcode': '53004', 'channelId': 1561591}, + {'zipcode': '53005', 'channelId': 1561255}, + {'zipcode': '53006', 'channelId': 1561657}, + {'zipcode': '53007', 'channelId': 1561255}, + {'zipcode': '53008', 'channelId': 1561255}, + {'zipcode': '53010', 'channelId': 1561599}, + {'zipcode': '53011', 'channelId': 1561599}, + {'zipcode': '53012', 'channelId': 1561591}, + {'zipcode': '53013', 'channelId': 1561591}, + {'zipcode': '53014', 'channelId': 1561657}, + {'zipcode': '53015', 'channelId': 1561241}, + {'zipcode': '53016', 'channelId': 1561665}, + {'zipcode': '53017', 'channelId': 1561664}, + {'zipcode': '53018', 'channelId': 1561267}, + {'zipcode': '53019', 'channelId': 1561599}, + {'zipcode': '53020', 'channelId': 1561599}, + {'zipcode': '53021', 'channelId': 1561604}, + {'zipcode': '53022', 'channelId': 1561288}, + {'zipcode': '53023', 'channelId': 1561288}, + {'zipcode': '53024', 'channelId': 1561591}, + {'zipcode': '53026', 'channelId': 1561591}, + {'zipcode': '53027', 'channelId': 1561662}, + {'zipcode': '53029', 'channelId': 1561267}, + {'zipcode': '53031', 'channelId': 1561599}, + {'zipcode': '53032', 'channelId': 1561657}, + {'zipcode': '53033', 'channelId': 1561664}, + {'zipcode': '53034', 'channelId': 1561665}, + {'zipcode': '53035', 'channelId': 1561657}, + {'zipcode': '53036', 'channelId': 1561284}, + {'zipcode': '53037', 'channelId': 1561664}, + {'zipcode': '53038', 'channelId': 1561284}, + {'zipcode': '53039', 'channelId': 1561665}, + {'zipcode': '53040', 'channelId': 1561599}, + {'zipcode': '53042', 'channelId': 1561599}, + {'zipcode': '53044', 'channelId': 1561599}, + {'zipcode': '53045', 'channelId': 1561588}, + {'zipcode': '53046', 'channelId': 1561265}, + {'zipcode': '53047', 'channelId': 1561284}, + {'zipcode': '53048', 'channelId': 1561662}, + {'zipcode': '53049', 'channelId': 1561657}, + {'zipcode': '53050', 'channelId': 1561657}, + {'zipcode': '53051', 'channelId': 1561288}, + {'zipcode': '53052', 'channelId': 1561288}, + {'zipcode': '53056', 'channelId': 1561267}, + {'zipcode': '53057', 'channelId': 1561599}, + {'zipcode': '53058', 'channelId': 1561267}, + {'zipcode': '53059', 'channelId': 1561662}, + {'zipcode': '53060', 'channelId': 1561599}, + {'zipcode': '53061', 'channelId': 1561599}, + {'zipcode': '53062', 'channelId': 1561599}, + {'zipcode': '53063', 'channelId': 1561599}, + {'zipcode': '53064', 'channelId': 1561267}, + {'zipcode': '53065', 'channelId': 1561657}, + {'zipcode': '53066', 'channelId': 1561245}, + {'zipcode': '53069', 'channelId': 1561267}, + {'zipcode': '53070', 'channelId': 1561591}, + {'zipcode': '53072', 'channelId': 1561262}, + {'zipcode': '53073', 'channelId': 1561599}, + {'zipcode': '53074', 'channelId': 1561591}, + {'zipcode': '53075', 'channelId': 1561599}, + {'zipcode': '53076', 'channelId': 1561664}, + {'zipcode': '53078', 'channelId': 1561662}, + {'zipcode': '53079', 'channelId': 1561599}, + {'zipcode': '53080', 'channelId': 1561591}, + {'zipcode': '53081', 'channelId': 1561591}, + {'zipcode': '53082', 'channelId': 1561604}, + {'zipcode': '53083', 'channelId': 1561599}, + {'zipcode': '53085', 'channelId': 1561599}, + {'zipcode': '53086', 'channelId': 1561662}, + {'zipcode': '53088', 'channelId': 1561657}, + {'zipcode': '53089', 'channelId': 1561265}, + {'zipcode': '53090', 'channelId': 1561599}, + {'zipcode': '53091', 'channelId': 1561657}, + {'zipcode': '53092', 'channelId': 1561591}, + {'zipcode': '53093', 'channelId': 1561599}, + {'zipcode': '53094', 'channelId': 1561284}, + {'zipcode': '53095', 'channelId': 1561604}, + {'zipcode': '53096', 'channelId': 1561599}, + {'zipcode': '53097', 'channelId': 1561591}, + {'zipcode': '53098', 'channelId': 1561284}, + {'zipcode': '53099', 'channelId': 1561662}, + {'zipcode': '53101', 'channelId': 1561631}, + {'zipcode': '53102', 'channelId': 1561631}, + {'zipcode': '53103', 'channelId': 2081492}, + {'zipcode': '53104', 'channelId': 1561624}, + {'zipcode': '53105', 'channelId': 1561631}, + {'zipcode': '53108', 'channelId': 1561598}, + {'zipcode': '53109', 'channelId': 1561631}, + {'zipcode': '53110', 'channelId': 1561616}, + {'zipcode': '53114', 'channelId': 1561631}, + {'zipcode': '53115', 'channelId': 1561631}, + {'zipcode': '53118', 'channelId': 1561245}, + {'zipcode': '53119', 'channelId': 1561245}, + {'zipcode': '53120', 'channelId': 1561631}, + {'zipcode': '53121', 'channelId': 1561631}, + {'zipcode': '53122', 'channelId': 1561588}, + {'zipcode': '53125', 'channelId': 1561631}, + {'zipcode': '53126', 'channelId': 1561624}, + {'zipcode': '53127', 'channelId': 1561262}, + {'zipcode': '53128', 'channelId': 1561631}, + {'zipcode': '53129', 'channelId': 1561289}, + {'zipcode': '53130', 'channelId': 1561289}, + {'zipcode': '53132', 'channelId': 1561289}, + {'zipcode': '53137', 'channelId': 1561284}, + {'zipcode': '53138', 'channelId': 1561661}, + {'zipcode': '53139', 'channelId': 1561631}, + {'zipcode': '53140', 'channelId': 1561639}, + {'zipcode': '53141', 'channelId': 1561639}, + {'zipcode': '53142', 'channelId': 1561639}, + {'zipcode': '53143', 'channelId': 1561639}, + {'zipcode': '53144', 'channelId': 1561639}, + {'zipcode': '53146', 'channelId': 1561243}, + {'zipcode': '53147', 'channelId': 1561631}, + {'zipcode': '53148', 'channelId': 1561631}, + {'zipcode': '53149', 'channelId': 2081492}, + {'zipcode': '53150', 'channelId': 2081492}, + {'zipcode': '53151', 'channelId': 1561243}, + {'zipcode': '53152', 'channelId': 1561631}, + {'zipcode': '53153', 'channelId': 1561262}, + {'zipcode': '53154', 'channelId': 1561598}, + {'zipcode': '53156', 'channelId': 1561245}, + {'zipcode': '53157', 'channelId': 1561631}, + {'zipcode': '53158', 'channelId': 1561639}, + {'zipcode': '53159', 'channelId': 1561631}, + {'zipcode': '53167', 'channelId': 1561631}, + {'zipcode': '53168', 'channelId': 1561631}, + {'zipcode': '53170', 'channelId': 1561631}, + {'zipcode': '53171', 'channelId': 1561639}, + {'zipcode': '53172', 'channelId': 1561598}, + {'zipcode': '53176', 'channelId': 1561631}, + {'zipcode': '53177', 'channelId': 1561624}, + {'zipcode': '53178', 'channelId': 1561245}, + {'zipcode': '53179', 'channelId': 1561631}, + {'zipcode': '53181', 'channelId': 1561631}, + {'zipcode': '53182', 'channelId': 1561624}, + {'zipcode': '53183', 'channelId': 1561267}, + {'zipcode': '53184', 'channelId': 1561631}, + {'zipcode': '53185', 'channelId': 1561631}, + {'zipcode': '53186', 'channelId': 1561262}, + {'zipcode': '53187', 'channelId': 1561262}, + {'zipcode': '53188', 'channelId': 1561262}, + {'zipcode': '53189', 'channelId': 1561262}, + {'zipcode': '53190', 'channelId': 1561245}, + {'zipcode': '53191', 'channelId': 1561631}, + {'zipcode': '53192', 'channelId': 1561631}, + {'zipcode': '53194', 'channelId': 1561624}, + {'zipcode': '53195', 'channelId': 1561631}, + {'zipcode': '53199', 'channelId': 1561631}, + {'zipcode': '53201', 'channelId': 1561241}, + {'zipcode': '53202', 'channelId': 1561615}, + {'zipcode': '53203', 'channelId': 1561241}, + {'zipcode': '53204', 'channelId': 1561241}, + {'zipcode': '53205', 'channelId': 1561241}, + {'zipcode': '53206', 'channelId': 1561241}, + {'zipcode': '53207', 'channelId': 1561615}, + {'zipcode': '53208', 'channelId': 1561241}, + {'zipcode': '53209', 'channelId': 1561255}, + {'zipcode': '53210', 'channelId': 1561241}, + {'zipcode': '53211', 'channelId': 1561615}, + {'zipcode': '53212', 'channelId': 1561241}, + {'zipcode': '53213', 'channelId': 1561241}, + {'zipcode': '53214', 'channelId': 1561587}, + {'zipcode': '53215', 'channelId': 1561283}, + {'zipcode': '53216', 'channelId': 1561241}, + {'zipcode': '53217', 'channelId': 1561255}, + {'zipcode': '53218', 'channelId': 1561255}, + {'zipcode': '53219', 'channelId': 1561241}, + {'zipcode': '53220', 'channelId': 1561283}, + {'zipcode': '53221', 'channelId': 1561283}, + {'zipcode': '53222', 'channelId': 1561255}, + {'zipcode': '53223', 'channelId': 1561255}, + {'zipcode': '53224', 'channelId': 1561255}, + {'zipcode': '53225', 'channelId': 1561255}, + {'zipcode': '53226', 'channelId': 1561255}, + {'zipcode': '53227', 'channelId': 1561587}, + {'zipcode': '53228', 'channelId': 1561587}, + {'zipcode': '53233', 'channelId': 1561241}, + {'zipcode': '53234', 'channelId': 1561241}, + {'zipcode': '53235', 'channelId': 1561616}, + {'zipcode': '53237', 'channelId': 1561283}, + {'zipcode': '53259', 'channelId': 1561615}, + {'zipcode': '53263', 'channelId': 1561255}, + {'zipcode': '53267', 'channelId': 1561241}, + {'zipcode': '53268', 'channelId': 1561615}, + {'zipcode': '53274', 'channelId': 1561283}, + {'zipcode': '53278', 'channelId': 1561241}, + {'zipcode': '53288', 'channelId': 1561615}, + {'zipcode': '53290', 'channelId': 1561241}, + {'zipcode': '53293', 'channelId': 1561241}, + {'zipcode': '53295', 'channelId': 1561241}, + {'zipcode': '53401', 'channelId': 1561639}, + {'zipcode': '53402', 'channelId': 1561285}, + {'zipcode': '53403', 'channelId': 1561639}, + {'zipcode': '53404', 'channelId': 1561639}, + {'zipcode': '53405', 'channelId': 1561285}, + {'zipcode': '53406', 'channelId': 1561624}, + {'zipcode': '53407', 'channelId': 1561639}, + {'zipcode': '53408', 'channelId': 1561624}, + {'zipcode': '53490', 'channelId': 1561285}, + {'zipcode': '53501', 'channelId': 1561661}, + {'zipcode': '53502', 'channelId': 1561661}, + {'zipcode': '53503', 'channelId': 1561661}, + {'zipcode': '53505', 'channelId': 1561631}, + {'zipcode': '53506', 'channelId': 1561661}, + {'zipcode': '53507', 'channelId': 1561661}, + {'zipcode': '53508', 'channelId': 1561661}, + {'zipcode': '53511', 'channelId': 1561661}, + {'zipcode': '53512', 'channelId': 1561661}, + {'zipcode': '53515', 'channelId': 1561661}, + {'zipcode': '53517', 'channelId': 1561661}, + {'zipcode': '53520', 'channelId': 1561661}, + {'zipcode': '53521', 'channelId': 1561661}, + {'zipcode': '53522', 'channelId': 1561661}, + {'zipcode': '53523', 'channelId': 1561284}, + {'zipcode': '53525', 'channelId': 1561661}, + {'zipcode': '53526', 'channelId': 1561661}, + {'zipcode': '53527', 'channelId': 1561661}, + {'zipcode': '53528', 'channelId': 1561661}, + {'zipcode': '53529', 'channelId': 1561661}, + {'zipcode': '53531', 'channelId': 1561661}, + {'zipcode': '53532', 'channelId': 1561661}, + {'zipcode': '53533', 'channelId': 1561661}, + {'zipcode': '53534', 'channelId': 1561661}, + {'zipcode': '53535', 'channelId': 1561661}, + {'zipcode': '53536', 'channelId': 1561661}, + {'zipcode': '53537', 'channelId': 1561661}, + {'zipcode': '53538', 'channelId': 1561284}, + {'zipcode': '53542', 'channelId': 1561661}, + {'zipcode': '53543', 'channelId': 1561661}, + {'zipcode': '53544', 'channelId': 1561661}, + {'zipcode': '53545', 'channelId': 1561661}, + {'zipcode': '53546', 'channelId': 1561631}, + {'zipcode': '53547', 'channelId': 1561661}, + {'zipcode': '53548', 'channelId': 1561661}, + {'zipcode': '53549', 'channelId': 1561284}, + {'zipcode': '53550', 'channelId': 1561661}, + {'zipcode': '53551', 'channelId': 1561284}, + {'zipcode': '53553', 'channelId': 1561661}, + {'zipcode': '53555', 'channelId': 1561661}, + {'zipcode': '53557', 'channelId': 1561665}, + {'zipcode': '53558', 'channelId': 1561661}, + {'zipcode': '53559', 'channelId': 1561660}, + {'zipcode': '53560', 'channelId': 1561661}, + {'zipcode': '53562', 'channelId': 1561661}, + {'zipcode': '53563', 'channelId': 1561284}, + {'zipcode': '53565', 'channelId': 1561661}, + {'zipcode': '53566', 'channelId': 1561661}, + {'zipcode': '53570', 'channelId': 1561661}, + {'zipcode': '53571', 'channelId': 1561661}, + {'zipcode': '53572', 'channelId': 1561661}, + {'zipcode': '53574', 'channelId': 1561661}, + {'zipcode': '53575', 'channelId': 1561661}, + {'zipcode': '53576', 'channelId': 1561661}, + {'zipcode': '53579', 'channelId': 1561660}, + {'zipcode': '53580', 'channelId': 1561661}, + {'zipcode': '53582', 'channelId': 1561661}, + {'zipcode': '53585', 'channelId': 1561631}, + {'zipcode': '53589', 'channelId': 1561661}, + {'zipcode': '53590', 'channelId': 1561661}, + {'zipcode': '53593', 'channelId': 1561661}, + {'zipcode': '53594', 'channelId': 1561284}, + {'zipcode': '53595', 'channelId': 1561661}, + {'zipcode': '53596', 'channelId': 1561661}, + {'zipcode': '53597', 'channelId': 1561661}, + {'zipcode': '53598', 'channelId': 1561661}, + {'zipcode': '53701', 'channelId': 1561661}, + {'zipcode': '53702', 'channelId': 1561661}, + {'zipcode': '53703', 'channelId': 1561661}, + {'zipcode': '53704', 'channelId': 1561661}, + {'zipcode': '53705', 'channelId': 1561661}, + {'zipcode': '53706', 'channelId': 1561661}, + {'zipcode': '53707', 'channelId': 1561661}, + {'zipcode': '53708', 'channelId': 1561661}, + {'zipcode': '53711', 'channelId': 1561661}, + {'zipcode': '53713', 'channelId': 1561661}, + {'zipcode': '53714', 'channelId': 1561661}, + {'zipcode': '53715', 'channelId': 1561661}, + {'zipcode': '53716', 'channelId': 1561661}, + {'zipcode': '53717', 'channelId': 1561661}, + {'zipcode': '53718', 'channelId': 1561661}, + {'zipcode': '53719', 'channelId': 1561661}, + {'zipcode': '53725', 'channelId': 1561661}, + {'zipcode': '53726', 'channelId': 1561661}, + {'zipcode': '53744', 'channelId': 1561661}, + {'zipcode': '53774', 'channelId': 1561661}, + {'zipcode': '53777', 'channelId': 1561661}, + {'zipcode': '53778', 'channelId': 1561661}, + {'zipcode': '53779', 'channelId': 1561661}, + {'zipcode': '53782', 'channelId': 1561661}, + {'zipcode': '53783', 'channelId': 1561661}, + {'zipcode': '53784', 'channelId': 1561661}, + {'zipcode': '53785', 'channelId': 1561661}, + {'zipcode': '53786', 'channelId': 1561661}, + {'zipcode': '53788', 'channelId': 1561661}, + {'zipcode': '53789', 'channelId': 1561661}, + {'zipcode': '53790', 'channelId': 1561661}, + {'zipcode': '53791', 'channelId': 1561661}, + {'zipcode': '53792', 'channelId': 1561661}, + {'zipcode': '53793', 'channelId': 1561661}, + {'zipcode': '53794', 'channelId': 1561661}, + {'zipcode': '53901', 'channelId': 1561660}, + {'zipcode': '53911', 'channelId': 1561661}, + {'zipcode': '53916', 'channelId': 1561649}, + {'zipcode': '53919', 'channelId': 1561649}, + {'zipcode': '53920', 'channelId': 1561660}, + {'zipcode': '53922', 'channelId': 1561657}, + {'zipcode': '53923', 'channelId': 1561660}, + {'zipcode': '53925', 'channelId': 1561660}, + {'zipcode': '53926', 'channelId': 1561660}, + {'zipcode': '53928', 'channelId': 1561660}, + {'zipcode': '53930', 'channelId': 1561660}, + {'zipcode': '53931', 'channelId': 1561649}, + {'zipcode': '53932', 'channelId': 1561660}, + {'zipcode': '53933', 'channelId': 1561649}, + {'zipcode': '53935', 'channelId': 1561660}, + {'zipcode': '53939', 'channelId': 1561649}, + {'zipcode': '53946', 'channelId': 1561649}, + {'zipcode': '53947', 'channelId': 1561649}, + {'zipcode': '53949', 'channelId': 1561660}, + {'zipcode': '53952', 'channelId': 1561660}, + {'zipcode': '53953', 'channelId': 1561660}, + {'zipcode': '53954', 'channelId': 1561660}, + {'zipcode': '53955', 'channelId': 1561660}, + {'zipcode': '53956', 'channelId': 1561660}, + {'zipcode': '53957', 'channelId': 1561649}, + {'zipcode': '53960', 'channelId': 1561660}, + {'zipcode': '53963', 'channelId': 1561657}, + {'zipcode': '53964', 'channelId': 1561660}, + {'zipcode': '53965', 'channelId': 1561660}, + {'zipcode': '53969', 'channelId': 1561661}, + {'zipcode': '54106', 'channelId': 1561657}, + {'zipcode': '54110', 'channelId': 1561599}, + {'zipcode': '54113', 'channelId': 1561657}, + {'zipcode': '54115', 'channelId': 1561657}, + {'zipcode': '54123', 'channelId': 1561657}, + {'zipcode': '54126', 'channelId': 1561599}, + {'zipcode': '54129', 'channelId': 1561657}, + {'zipcode': '54130', 'channelId': 1561657}, + {'zipcode': '54131', 'channelId': 1561657}, + {'zipcode': '54136', 'channelId': 1561657}, + {'zipcode': '54140', 'channelId': 1561657}, + {'zipcode': '54152', 'channelId': 1561657}, + {'zipcode': '54155', 'channelId': 1561657}, + {'zipcode': '54160', 'channelId': 1561599}, + {'zipcode': '54162', 'channelId': 1561657}, + {'zipcode': '54165', 'channelId': 1561657}, + {'zipcode': '54169', 'channelId': 1561657}, + {'zipcode': '54170', 'channelId': 1561657}, + {'zipcode': '54173', 'channelId': 1561657}, + {'zipcode': '54180', 'channelId': 1561657}, + {'zipcode': '54207', 'channelId': 1561599}, + {'zipcode': '54208', 'channelId': 1561599}, + {'zipcode': '54214', 'channelId': 1561599}, + {'zipcode': '54215', 'channelId': 1561599}, + {'zipcode': '54220', 'channelId': 1561599}, + {'zipcode': '54221', 'channelId': 1561599}, + {'zipcode': '54227', 'channelId': 1561599}, + {'zipcode': '54228', 'channelId': 1561599}, + {'zipcode': '54229', 'channelId': 1561599}, + {'zipcode': '54230', 'channelId': 1561599}, + {'zipcode': '54232', 'channelId': 1561599}, + {'zipcode': '54240', 'channelId': 1561599}, + {'zipcode': '54241', 'channelId': 1561599}, + {'zipcode': '54245', 'channelId': 1561599}, + {'zipcode': '54247', 'channelId': 1561599}, + {'zipcode': '54301', 'channelId': 1561657}, + {'zipcode': '54302', 'channelId': 1561599}, + {'zipcode': '54303', 'channelId': 1561657}, + {'zipcode': '54304', 'channelId': 1561657}, + {'zipcode': '54305', 'channelId': 1561657}, + {'zipcode': '54306', 'channelId': 1561657}, + {'zipcode': '54307', 'channelId': 1561657}, + {'zipcode': '54308', 'channelId': 1561657}, + {'zipcode': '54311', 'channelId': 1561599}, + {'zipcode': '54313', 'channelId': 1561657}, + {'zipcode': '54324', 'channelId': 1561657}, + {'zipcode': '54344', 'channelId': 1561657}, + {'zipcode': '54901', 'channelId': 1561657}, + {'zipcode': '54902', 'channelId': 1561657}, + {'zipcode': '54903', 'channelId': 1561657}, + {'zipcode': '54904', 'channelId': 1561657}, + {'zipcode': '54906', 'channelId': 1561657}, + {'zipcode': '54911', 'channelId': 1561657}, + {'zipcode': '54912', 'channelId': 1561657}, + {'zipcode': '54913', 'channelId': 1561657}, + {'zipcode': '54914', 'channelId': 1561657}, + {'zipcode': '54915', 'channelId': 1561657}, + {'zipcode': '54919', 'channelId': 1561657}, + {'zipcode': '54922', 'channelId': 1561657}, + {'zipcode': '54923', 'channelId': 1561615}, + {'zipcode': '54927', 'channelId': 1561657}, + {'zipcode': '54931', 'channelId': 1561657}, + {'zipcode': '54932', 'channelId': 1561657}, + {'zipcode': '54934', 'channelId': 1561649}, + {'zipcode': '54935', 'channelId': 1561657}, + {'zipcode': '54936', 'channelId': 1561657}, + {'zipcode': '54937', 'channelId': 1561657}, + {'zipcode': '54941', 'channelId': 1561649}, + {'zipcode': '54942', 'channelId': 1561657}, + {'zipcode': '54944', 'channelId': 1561657}, + {'zipcode': '54947', 'channelId': 1561657}, + {'zipcode': '54952', 'channelId': 1561657}, + {'zipcode': '54956', 'channelId': 1561657}, + {'zipcode': '54957', 'channelId': 1561657}, + {'zipcode': '54960', 'channelId': 1561649}, + {'zipcode': '54963', 'channelId': 1561657}, + {'zipcode': '54964', 'channelId': 1561657}, + {'zipcode': '54968', 'channelId': 1561649}, + {'zipcode': '54971', 'channelId': 1561649}, + {'zipcode': '54974', 'channelId': 1561657}, + {'zipcode': '54979', 'channelId': 1561657}, + {'zipcode': '54980', 'channelId': 1561649}, + {'zipcode': '54985', 'channelId': 1561657}, + {'zipcode': '54986', 'channelId': 1561657}, + {'zipcode': '60001', 'channelId': 1561631}, + {'zipcode': '60002', 'channelId': 1561624}, + {'zipcode': '60010', 'channelId': 1561631}, + {'zipcode': '60011', 'channelId': 1561631}, + {'zipcode': '60012', 'channelId': 1561631}, + {'zipcode': '60013', 'channelId': 1561631}, + {'zipcode': '60014', 'channelId': 1561631}, + {'zipcode': '60015', 'channelId': 1561639}, + {'zipcode': '60020', 'channelId': 1561631}, + {'zipcode': '60021', 'channelId': 1561631}, + {'zipcode': '60030', 'channelId': 1561631}, + {'zipcode': '60031', 'channelId': 1561639}, + {'zipcode': '60033', 'channelId': 1561631}, + {'zipcode': '60034', 'channelId': 1561631}, + {'zipcode': '60035', 'channelId': 1561639}, + {'zipcode': '60037', 'channelId': 1561639}, + {'zipcode': '60039', 'channelId': 1561631}, + {'zipcode': '60040', 'channelId': 1561639}, + {'zipcode': '60041', 'channelId': 1561631}, + {'zipcode': '60042', 'channelId': 1561631}, + {'zipcode': '60044', 'channelId': 1561639}, + {'zipcode': '60045', 'channelId': 1561639}, + {'zipcode': '60046', 'channelId': 1561631}, + {'zipcode': '60047', 'channelId': 1561631}, + {'zipcode': '60048', 'channelId': 1561639}, + {'zipcode': '60049', 'channelId': 1561631}, + {'zipcode': '60050', 'channelId': 1561631}, + {'zipcode': '60051', 'channelId': 1561631}, + {'zipcode': '60060', 'channelId': 1561639}, + {'zipcode': '60061', 'channelId': 1561639}, + {'zipcode': '60064', 'channelId': 1561639}, + {'zipcode': '60069', 'channelId': 1561639}, + {'zipcode': '60071', 'channelId': 1561631}, + {'zipcode': '60072', 'channelId': 1561631}, + {'zipcode': '60073', 'channelId': 1561631}, + {'zipcode': '60075', 'channelId': 1561639}, + {'zipcode': '60079', 'channelId': 1561639}, + {'zipcode': '60081', 'channelId': 1561631}, + {'zipcode': '60083', 'channelId': 1561639}, + {'zipcode': '60084', 'channelId': 1561631}, + {'zipcode': '60085', 'channelId': 1561639}, + {'zipcode': '60086', 'channelId': 1561631}, + {'zipcode': '60087', 'channelId': 1561639}, + {'zipcode': '60088', 'channelId': 1561639}, + {'zipcode': '60089', 'channelId': 1561639}, + {'zipcode': '60092', 'channelId': 1561631}, + {'zipcode': '60096', 'channelId': 1561639}, + {'zipcode': '60097', 'channelId': 1561631}, + {'zipcode': '60098', 'channelId': 1561631}, + {'zipcode': '60099', 'channelId': 1561639}, + {'zipcode': '60102', 'channelId': 1561631}, + {'zipcode': '60142', 'channelId': 1561631}, + {'zipcode': '60152', 'channelId': 1561631}, + {'zipcode': '60156', 'channelId': 1561631}, + {'zipcode': '60180', 'channelId': 1561631} + ] + }, + 'branchNameMapping': { + 'branchNamesByChannelId': [ + { + "channelId": 0, + "branchName": "Not Found" + }, + { + "channelId": 1, + "branchName": "ALL" + }, + { + 'channelId': 12801, + 'branchName': 'INDIRECT LENDING DEPT' + }, + { + 'channelId': 1561241, + 'branchName': 'West Milwaukee' + }, + { + 'channelId': 1561243, + 'branchName': 'New Berlin' + }, + { + 'channelId': 1561245, + 'branchName': 'Oconomowoc' + }, + { + 'channelId': 1561255, + 'branchName': 'Wauwatosa' + }, + { + 'channelId': 1561262, + 'branchName': 'Waukesha' + }, + { + 'channelId': 1561265, + 'branchName': 'Sussex' + }, + { + 'channelId': 1561267, + 'branchName': 'Hartland' + }, + { + 'channelId': 1561283, + 'branchName': 'Milwaukee South' + }, + { + 'channelId': 1561284, + 'branchName': 'Watertown' + }, + { + 'channelId': 1561285, + 'branchName': 'Racine Rapids Drive' + }, + { + 'channelId': 1561288, + 'branchName': 'Germantown' + }, + { + 'channelId': 1561289, + 'branchName': 'Franklin' + }, + { + 'channelId': 1561587, + 'branchName': 'West Allis' + }, + { + 'channelId': 1561588, + 'branchName': 'Brookfield East' + }, + { + 'channelId': 1561591, + 'branchName': 'Grafton' + }, + { + 'channelId': 1561598, + 'branchName': 'Oak Creek' + }, + { + 'channelId': 1561599, + 'branchName': 'West Bend-Washington St' + }, + { + 'channelId': 1561604, + 'branchName': 'West Bend-Main St' + }, + { + 'channelId': 1561615, + 'branchName': 'Bay View' + }, + { + 'channelId': 1561616, + 'branchName': 'Cudahy' + }, + { + 'channelId': 1561624, + 'branchName': 'Racine Washington Ave' + }, + { + 'channelId': 1561631, + 'branchName': 'Burlington' + }, + { + 'channelId': 1561639, + 'branchName': 'Racine Grove Ave' + }, + { + 'channelId': 1561649, + 'branchName': 'Beaver Dam' + }, + { + 'channelId': 1561656, + 'branchName': 'Juneau Western Ave' + }, + { + 'channelId': 1561657, + 'branchName': 'Mayville' + }, + { + 'channelId': 1561660, + 'branchName': 'Fall River' + }, + { + 'channelId': 1561661, + 'branchName': 'Madison Spring Street' + }, + { + 'channelId': 1561662, + 'branchName': 'Hartford' + }, + { + 'channelId': 1561664, + 'branchName': 'Hubertus' + }, + { + 'channelId': 1561665, + 'branchName': 'Juneau Main St' + }, + { + 'channelId': 1627725, + 'branchName': 'E Channel' + }, + { + 'channelId': 1698432, + 'branchName': 'Member Service Center' + }, + { + 'channelId': 2081486, + 'branchName': 'ACTion Online' + }, + { + 'channelId': 2081492, + 'branchName': 'Muskego' + }, + { + 'channelId': 2399724, + 'branchName': 'Online Applications' + }, + { + 'channelId': 2399734, + 'branchName': 'Online Credit Card Applications' + }, + { + 'channelId': 2863435, + 'branchName': 'Brookfield West' + }, + { + 'channelId': 2863440, + 'branchName': 'Greenfield' + }, + { + 'channelId': 2863439, + 'branchName': 'Glendale' + } + ] + }, + 'termOptions': [ + { + 'value': 12, + 'desc': '12 months / 1 year' + }, + { + 'value': 24, + 'desc': '24 months / 2 years' + }, + { + 'value': 36, + 'desc': '36 months / 3 years' + }, + { + 'value': 48, + 'desc': '48 months / 4 years' + }, + { + 'value': 60, + 'desc': '60 months / 5 years' + }, + { + 'value': 63, + 'desc': '63 months' + }, + { + 'value': 72, + 'desc': '72 months / 6 years' + }, + { + 'value': 75, + 'desc': '75 months' + }, + { + 'value': 84, + 'desc': '84 months / 7 years' + }, + { + 'value': 120, + 'desc': '120 months / 10 years' + }, + { + 'value': 144, + 'desc': '144 months / 12 years' + }, + { + 'value': 180, + 'desc': '180 months / 15 years' + }, + { + 'value': 240, + 'desc': '240 months / 20 years' + } + ], + 'termOptionsNoYear': [ + { + 'value': 12, + 'desc': '12 months' + }, + { + 'value': 24, + 'desc': '24 months' + }, + { + 'value': 36, + 'desc': '36 months' + }, + { + 'value': 48, + 'desc': '48 months' + }, + { + 'value': 60, + 'desc': '60 months' + }, + { + 'value': 63, + 'desc': '63 months' + }, + { + 'value': 72, + 'desc': '72 months' + }, + { + 'value': 75, + 'desc': '75 months' + }, + { + 'value': 84, + 'desc': '84 months' + }, + { + 'value': 120, + 'desc': '120 months' + }, + { + 'value': 144, + 'desc': '144 months' + }, + { + 'value': 180, + 'desc': '180 months' + }, + { + 'value': 240, + 'desc': '240 months' + } + ] +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/ui-settings.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/ui-settings.ts new file mode 100644 index 0000000..14844d2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/configuration/ui-settings.ts @@ -0,0 +1,117 @@ +import { MinMaxLimit } from '../loan-limit/loan-limit'; +import { Configuration } from './configuration'; + +export class UiSettings { + public homeEquity: Array; + public mortgageRefiExpress: Array; + public mfdHomeAndRvByAmount: any; + public vehiclePreApproval: Array; + public termsByYear: TermsByYear; + public boatLoansByAmount: any; + public powersportByAmount: any; + public signatureByAmount: any; + public statesRequiringSpouseInformation: Array; + public typePurposes: TypePurposes; + public abaRouting: string; + public contact: Contacts; + public links: Links; + public stateOptions: Array; + public limits: LoanLimits; + public incomeLimits: IncomeLimits; + public branchMapping: BranchConfig; + public branchNameMapping: BranchNameConfig; + public termOptions: Array; + public termOptionsNoYear: Array; + public configuration: Configuration; +} + +export interface BranchData { + zipcode: string; + channelId: number; +} + +export interface BranchConfig { + branchCodesByZip: Array; +} + +export interface BranchNameData { + channelId: number; + branchName: string; +} + +export interface BranchNameConfig { + branchNamesByChannelId: Array; +} + +export class TermOption { + public value: any; + public desc: string; +} + +export class IncomeLimits { + public Hourly: MinMaxLimit; + public Annually: MinMaxLimit; +} + +export class LoanLimits { + public CC_CashBack: MinMaxLimit; + public CC_Rewards: MinMaxLimit; + public CC_Student: MinMaxLimit; + public CC_Secure: MinMaxLimit; + public EQ_HELOC_Flex: MinMaxLimit; + public EQ_Mtg_Refi_Express: MinMaxLimit; + public EQ_Home_Equity_Fixed: MinMaxLimit; + public EQ_Mfg_Home_With_or_Without_Land: MinMaxLimit; + public V_Vehicle: MinMaxLimit; + public V_VehiclePreapproval: MinMaxLimit; + public M_Boat: MinMaxLimit; + public P_Powersport: MinMaxLimit; + public T_TrailerMotorhome: MinMaxLimit; + public S_Signature: MinMaxLimit; +} + +class TermsByYear { + public 2010: Array; + public 2011: Array; + public 2012: Array; + public 2013: Array; + public 2014: Array; + public 2015: Array; + public 2016: Array; + public 2017: Array; + public 2018: Array; + public 2019: Array; +} + +class TypePurposes { + public CC_CashBack: Array; + public CC_Rewards: Array; + public CC_Student: Array; + public CC_Secure: Array; + public EQ_HELOC_Flex: Array; + public EQ_Mtg_Refi_Express: Array; + public EQ_Home_Equity_Fixed: Array; + public EQ_Mfg_Home_With_or_Without_Land: Array; + public V_Vehicle: Array; + public V_VehiclePreapproval: Array; + public M_Boat: Array; + public P_Powersport: Array; + public T_TrailerMotorhome: Array; + public S_Signature: Array; +} + +class Contacts { + public HelpPhone: string; +} + +class Links { + public PrivacyPolicy: string; + public MemberSite: string; + public HomePage: string; +} + +export enum SiteLink { + HOME_PAGE = 'HomePage', + MEMBER_SITE = 'MemberSite', + PRIVACY_POLICY = 'PrivacyPolicy' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.spec.ts new file mode 100644 index 0000000..4c9e251 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.spec.ts @@ -0,0 +1,33 @@ +import { CookieUtil } from './cookie-util'; + +describe('CookieUtil', () => { + + afterEach(() => { + document.cookie = 'TokenAuthorization=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + }); + + it('should not find an auth cookie by default', () => { + const hasCookie = CookieUtil.hasAuthCookie(); + expect(hasCookie).toBe(false); + }); + + it('should find an auth cookie when set', () => { + document.cookie = 'TokenAuthorization=112341231234'; + const hasCookie = CookieUtil.hasAuthCookie(); + expect(hasCookie).toBe(true); + }); + + it('should read value of cookie', () => { + document.cookie = 'TokenAuthorization=112341231234'; + const cookieValue = CookieUtil.getCookieValue('TokenAuthorization'); + expect(cookieValue).toBe('112341231234'); + }); + + it('should not read value of cookie when cookie doesnt exist', () => { + document.cookie = 'TokenAuthorization=112341231234'; + const cookieValue = CookieUtil.getCookieValue('RandomCookie'); + expect(cookieValue).toBe(''); + }); +}); + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.ts new file mode 100644 index 0000000..01fb2f7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/cookie/cookie-util.ts @@ -0,0 +1,61 @@ +import { MemberMeta } from '../../member/models/member-meta'; + +export class CookieUtil { + + public static readonly PUBLIC_COOKIE_KEY = 'TokenAuthorization'; + + public static hasAuthCookie(): boolean { + return document.cookie.indexOf(this.PUBLIC_COOKIE_KEY) > -1; + } + + public static getCookieValue(cookieName: string): string { + const name = cookieName + '='; + const decodedCookie = decodeURIComponent(document.cookie); + const cookieArray = decodedCookie.split(';'); + for (let i = 0; i < cookieArray.length; i++) { + let cookie = cookieArray[i]; + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1); + } + if (cookie.indexOf(name) === 0) { + return cookie.substring(name.length, cookie.length); + } + } + return ''; + } + + /** + * This method checks the public cookie for member and individual. + * Reminder: This data is not trusted by the server, so can't be manipulated to get auth. + */ + public static requiresVerification(): boolean { + const parsed = CookieUtil.getUserAuth(); + return !!(parsed.isMember === true && parsed.isIndividual === false); + } + + public static allowLoanResume(): boolean { + const parsed = CookieUtil.getUserAuth(); + return !!(parsed.isMember && parsed.isIndividual && parsed.isTwoFactor); + } + + public static allowPrefill(): boolean { + const parsed = CookieUtil.getUserAuth(); + return !!(parsed.isMember && parsed.isIndividual); + } + + public static getUserAuth(): MemberMeta { + const rawCookie = this.getCookieValue(this.PUBLIC_COOKIE_KEY); + + if (rawCookie) { + const unencoded = atob(rawCookie); + const parsed = JSON.parse(unencoded); + const memberMeta = { + isTwoFactor: parsed.IsTwoFactor, + isMember: parsed.IsMember, + isIndividual: parsed.IsIndividual + }; + return memberMeta; + } + return new MemberMeta(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.spec.ts new file mode 100644 index 0000000..b65da90 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.spec.ts @@ -0,0 +1,104 @@ +import {ReactiveFormsModule} from '@angular/forms'; +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {configureTestSuite} from 'ng-bullet'; +import {CoreTranslatorService} from './core-translator.service'; +import {convertedPersonalInformation, convertedPrimaryAddress, testCoreIndividual} from '../../testing/shared-mocks/test-individual.mock'; +import {AddressInformation} from '../../loan-wizard/field-groups/address/address-information'; + + +describe('Core Translator Service', () => { + + let service: CoreTranslatorService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + CoreTranslatorService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(CoreTranslatorService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should correctly convert the mock data', () => { + const personalInfo = service.translateCoreToIndividual(testCoreIndividual); + expect(personalInfo).toEqual(convertedPersonalInformation); + }); + + it('should correctly convert the mock primary address data', () => { + const addressInfo = service.translateCoreToPrimaryAddress(testCoreIndividual); + expect(addressInfo).toEqual(convertedPrimaryAddress); + }); + + it('should correctly process a core-zipcode', () => { + const result1 = service.splitZipcode('53027-1981'); + const result2 = service.splitZipcode('53027'); + const result3 = service.splitZipcode(null); + expect(result1).toEqual({ + zipcode: '53027', + ext: '1981' + }); + expect(result2).toEqual({ + zipcode: '53027', + ext: null + }); + expect(result3).toEqual({ + zipcode: null, + ext: null + }); + }); + + it('should correctly convert an address with extended zipcode', () => { + // A sample address as it would arrive from APEX/CORE + const individualAddress = { + 'addressId': 3212176, + 'line1': '1400A SCHAUER DR', + 'line2': '', + 'line3': '', + 'city': 'HARTFORD', + 'state': 'WI', + 'zipCode': '53027-1981', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': true, + 'isInternational': false, + 'addressVerification': 1400, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': true + }; + const expectedResult: AddressInformation = { + addressLine1: individualAddress.line1, + addressLine2: individualAddress.line2, + city: individualAddress.city, + state: individualAddress.state, + zipcode: '53027', + zipcodeExt: '1981' + }; + const addressInfo = service.translateCoreToAddress(individualAddress); + expect(addressInfo).toEqual(expectedResult); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.ts new file mode 100644 index 0000000..95f9a34 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/core-translator/core-translator.service.ts @@ -0,0 +1,117 @@ +import {Injectable} from '@angular/core'; +import {CoreIndividual} from 'src/app/member/models/core-individual'; +import {CreditApplication} from '../credit-application'; +import {PhoneTypeEnum} from '../api-models/api-enums'; +import {PersonalInformation} from 'src/app/loan-wizard/field-groups/personal/personal-information'; +import {FormatService} from '../pipes/format.service'; +import {CorePhone} from '../../member/models/core-phone'; +import {AddressInformation} from '../../loan-wizard/field-groups/address/address-information'; +import {CoreAddress} from '../../member/models/core-address'; + +@Injectable({ + providedIn: 'root' +}) +export class CoreTranslatorService { + + + public constructor(private formatService: FormatService) { + } + + public setCoreData(creditApplication: CreditApplication, individual: CoreIndividual): CreditApplication { + Object.assign(creditApplication.fullInfoBorrowers[0], {personalInformation: this.translateCoreToIndividual(individual)}); + const address = this.translateCoreToPrimaryAddress(individual); + if (address) { + Object.assign(creditApplication.fullInfoBorrowers[0], {addressInformation: address}); + } + return creditApplication; + } + + // If it turns out that start and end dates work, we could put them as historical addresses, in order... + public translateCoreToPrimaryAddress(individual: CoreIndividual): AddressInformation { + if (individual && individual.primaryAddressId && individual.address.length > 0) { + const primaryAddressInfo = this.findPrimaryCoreAddress(individual); + if (primaryAddressInfo) { + return this.translateCoreToAddress(primaryAddressInfo); + } + } + return null; + } + + public findPrimaryCoreAddress(individual: CoreIndividual): CoreAddress { + let primaryAddressInfo: CoreAddress = null; + individual.address.forEach((eachAddress: CoreAddress) => { + if (eachAddress.addressId === individual.primaryAddressId) { + primaryAddressInfo = eachAddress; + } + }, this); + return primaryAddressInfo; + } + + public translateCoreToAddress(addressIn: CoreAddress): AddressInformation { + const splitCode = this.splitZipcode(addressIn.zipCode); + const address = { + addressLine1: addressIn.line1, + addressLine2: addressIn.line2, + city: addressIn.city, + state: addressIn.state, + zipcode: addressIn.zipCode + }; + if (splitCode != null && splitCode.zipcode) { + address.zipcode = splitCode.zipcode; + } + if (splitCode != null && splitCode.ext) { + address.zipcodeExt = splitCode.ext; + } + return address; + } + + /** + * Core tends to send zipcode like this: 53027-1981 + * @param zipInput + */ + public splitZipcode(zipInput: string): { zipcode: string, ext: string } { + const result = { + zipcode: zipInput, + ext: null + }; + + if (zipInput && zipInput.indexOf('-') > 0) { + const split = zipInput.split('-'); + result.zipcode = split[0].trim(); + result.ext = split[1].trim(); + } + return result; + } + + /** + * GJF: Will have trouble with these fields: Suffix, Phone Type + * @param individual + */ + public translateCoreToIndividual(individual: CoreIndividual): PersonalInformation { + const basicInfo = { + firstName: individual.firstName ? individual.firstName : '', + middleInitial: individual.middleName ? individual.middleName.charAt(0) : '', + lastName: individual.lastName ? individual.lastName : '', + dateOfBirth: individual.birthDate ? this.formatService.formatDob(individual.birthDate) : '', + socialSecurityNumber: individual.taxId ? individual.taxId : undefined, + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + primaryPhoneNumber: null, + secondaryPhoneNumberType: null, + secondaryPhoneNumber: null, + email: individual.email1 ? individual.email1 : '' + }; + if (individual.phone) { + individual.phone.forEach((eachPhone: CorePhone) => { + if (eachPhone.isPrimaryForType) { + basicInfo.primaryPhoneNumber = eachPhone.areaCode + '-' + eachPhone.phoneNumber; + } else { + basicInfo.secondaryPhoneNumber = eachPhone.areaCode + '-' + eachPhone.phoneNumber; + basicInfo.secondaryPhoneNumberType = PhoneTypeEnum.Mobile; + } + }, this); + } + return basicInfo; + } + + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.spec.ts new file mode 100644 index 0000000..2199f9f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.spec.ts @@ -0,0 +1,126 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from './credit-application-dao.service'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { apiTranslatorPreTranslate } from '../testing/shared-mocks/api-translator-pre-post'; +import { ApplicationWizardService } from '../loan-wizard/application-wizard.service'; +import { CreditApplication } from './credit-application'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { ConfigurationService } from './configuration/configuration.service'; +import { UiSettings } from './configuration/ui-settings'; +import { DefaultSettings } from './configuration/default-settings.mock'; + +describe('CreditApplicationDaoService', () => { + + let service: CreditApplicationDaoService; + let httpMock: HttpTestingController; + let applicationWizardService: ApplicationWizardService; + const creditApplication: CreditApplication = apiTranslatorPreTranslate; + let configService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + CreditApplicationDaoService, ApplicationWizardService, + ConfigurationService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(CreditApplicationDaoService); + configService = TestBed.get(ConfigurationService); + configService.saveConfiguration(defaultSettings); + httpMock = TestBed.get(HttpTestingController); + applicationWizardService = TestBed.get(ApplicationWizardService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should build check decision url', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(service, 'buildCheckDecisionUrl').and.callThrough(); + const creditApp = applicationWizardService.getFullCreditApplication(); + const testLoanID = '1234567890'; + creditApp.loanId = testLoanID; + const decisionUrl = service.buildCheckDecisionUrl(creditApp); + expect(service.buildCheckDecisionUrl).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(decisionUrl.includes(testLoanID)).toBe(true); + expect(decisionUrl.includes('DecisioningDetails')).toBe(true); + }); + + it('should build submit decision url', () => { + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(service, 'buildSubmitDecisionUrl').and.callThrough(); + const creditApp = applicationWizardService.getFullCreditApplication(); + const testLoanID = '1234567890'; + creditApp.loanId = testLoanID; + const submitDecisionUrl = service.buildSubmitDecisionUrl(creditApp); + expect(service.buildSubmitDecisionUrl).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(submitDecisionUrl.includes(testLoanID)).toBe(true); + expect(submitDecisionUrl.includes('DecisioningDetails')).toBe(true); + }); + + it('should build submit application url', () => { + spyOn(service, 'buildSubmitApplicationUrl').and.callThrough(); + service.buildSubmitApplicationUrl(); + expect(service.buildSubmitApplicationUrl).toHaveBeenCalled(); + }); + + it('should save an application', () => { + spyOn(service, 'saveApplication').and.callThrough(); + spyOn(configService, 'getAllConfiguration').and.returnValue(defaultSettings); + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const observable = service.saveApplication(applicationWizardService.getFullCreditApplication()); + expect(service.saveApplication).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(observable).toBeTruthy(); + }); + + it('should submit an application', () => { + spyOn(service, 'submitApplication').and.callThrough(); + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const observable = service.submitApplication(applicationWizardService.getFullCreditApplication()); + expect(service.submitApplication).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(observable).toBeTruthy(); + }); + + it('should decide application', () => { + spyOn(service, 'decideApplication').and.callThrough(); + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const observable = service.decideApplication(applicationWizardService.getFullCreditApplication()); + expect(service.decideApplication).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(observable).toBeTruthy(); + }); + + it('should get application decision', () => { + spyOn(service, 'getApplicationDecision').and.callThrough(); + spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const observable = service.getApplicationDecision(applicationWizardService.getFullCreditApplication()); + expect(service.getApplicationDecision).toHaveBeenCalled(); + expect(applicationWizardService.getFullCreditApplication).toHaveBeenCalled(); + expect(observable).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.ts new file mode 100644 index 0000000..610a764 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-dao.service.ts @@ -0,0 +1,197 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CreditApplication } from './credit-application'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { ApiLoanApplication } from './api-models/api-loan-application'; +import { environment } from '../../environments/environment'; +import { catchError, map } from 'rxjs/operators'; +import { ApiTranslatorService } from './api-translator/api-translator.service'; +import { ApiLoanContainer } from './api-models/api-loan-container'; +import { DecisionStatus } from './decision/decision-status'; +import { DecisionService } from './decision/decision.service'; + +/** + * This class has an important MOCK, so this interface helps ensure the mock matches. + */ +export interface ICreditApplicationDaoService { + saveApplication(creditApplication: CreditApplication): Observable; + + createApplication(creditApplication: CreditApplication): Observable; + + updateApplication(creditApplication: CreditApplication): Observable; + + submitApplication(creditApplication: CreditApplication): Observable; + + decideApplication(creditApplication: CreditApplication): Observable; + + getApplicationDecision(creditApplication: CreditApplication): Observable; +} + +@Injectable({ + providedIn: 'root' +}) +export class CreditApplicationDaoService implements ICreditApplicationDaoService { + + constructor(private httpClient: HttpClient, + private apiTranslatorService: ApiTranslatorService, + private decisionService: DecisionService, + private eHandler: ErrorHandler) { } + + public buildCheckDecisionUrl(creditApplication: CreditApplication): string { + return environment.urlBase + '/LoanApplication/' + creditApplication.loanId + '/DecisioningDetails'; + } + + public buildSubmitDecisionUrl(creditApplication: CreditApplication): string { + return environment.urlBase + '/LoanApplication/' + creditApplication.loanId + '/DecisioningDetails'; + } + + public buildSubmitApplicationUrl(): string { + return environment.urlBase + '/LoanApplication/Submit'; + } + + public buildStorageUrl(): string { + return environment.urlBase + '/Storage'; + } + + public saveApplication(creditApplication: CreditApplication): Observable { + if (creditApplication.loanId) { + return this.updateApplication(creditApplication); + } else { + return this.createApplication(creditApplication); + } + } + + public createEmptyApplication(creditApplication: CreditApplication): Observable { + const loanContainer = this.commandBuildApiLoanContainer(creditApplication); + const hiddenPersonalInfoLoanApplication = this.commandBuildApiLoanApplicationObstructedFields(creditApplication); + let observable: Observable; + observable = this.httpClient.post(this.buildStorageUrl() + "/New", loanContainer, {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + }) + ); + return observable; + } + + public createApplication(creditApplication: CreditApplication): Observable { + const loanContainer = this.commandBuildApiLoanContainer(creditApplication); + const hiddenPersonalInfoLoanApplication = this.commandBuildApiLoanApplicationObstructedFields(creditApplication); + let observable: Observable; + observable = this.httpClient.post(this.buildStorageUrl(), loanContainer, {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + }) + ); + return observable; + } + + public updateApplication(creditApplication: CreditApplication): Observable { + const loanContainer = this.commandBuildApiLoanContainer(creditApplication); + const hiddenPersonalInfoLoanApplication = this.commandBuildApiLoanApplicationObstructedFields(creditApplication); + return this.httpClient.put(this.buildStorageUrl(), loanContainer, {withCredentials: true}) + .pipe( + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + })); + } + + public submitApplication(creditApplication: CreditApplication): Observable { + const hiddenPersonalInfoLoanApplication = this.commandBuildApiLoanApplicationObstructedFields(creditApplication); + let observable: Observable; + observable = this.httpClient.post(this.buildSubmitApplicationUrl() + '/' + creditApplication.loanId, '', {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else if (err.status === 520) { + throw new Error('520: Server could not communicate with CRIF'); + } else { + throw new Error('Bad request'); + } + }) + ); + return observable; + } + + public decideApplication(creditApplication: CreditApplication): Observable { + return this.httpClient.put(this.buildSubmitDecisionUrl(creditApplication), + { + 'decisioningState': 'Requested' + }) + .pipe(map(result => { + return result; + }), + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else if (err.status === 520) { + throw new Error('520: Server could not communicate with CRIF'); + } else { + throw new Error('Bad request'); + } + }) + ); + } + + public getApplicationDecision(creditApplication: CreditApplication): Observable { + return this.httpClient.get(this.buildCheckDecisionUrl(creditApplication)) + .pipe(map(result => { + const dStatus = { + state: result['decisioningState'], + result: result['decisioningResult'], + stipulations: result['stipulations'] + }; + + return this.decisionService.createDecisionResult(dStatus, creditApplication.loanType.shouldDecision); + }), + catchError(err => { + // @TODO ERROR_NOTIFY + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else if (err.status === 520) { + throw new Error('520: Server could not communicate with CRIF'); + } else { + throw new Error('Bad request'); + } + }) + ); + } + + public commandBuildApiLoanContainer(application: CreditApplication): ApiLoanContainer { + return this.apiTranslatorService.translateToApiLoanContainer(application); + } + + public commandBuildApiLoanApplication(application: CreditApplication): ApiLoanApplication { + return this.apiTranslatorService.translateToApiLoanApplication(application); + } + + public commandBuildApiLoanApplicationObstructedFields(application: CreditApplication): ApiLoanApplication { + return this.apiTranslatorService.translateToApiLoanApplicationHiddenValues(application); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-values.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-values.ts new file mode 100644 index 0000000..b617b58 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application-values.ts @@ -0,0 +1,9 @@ +export class CreditApplicationValues { + public static readonly STATE_REQUIRED_COAPP = 'WI'; + public static readonly CHOICE_YES = 'yes'; + public static readonly CHOICE_NO = 'no'; + public static readonly VEHICLE_CUT_OFF_YEAR_LOWER = 1900; + public static readonly VEHICLE_CUT_OFF_YEAR_UPPER = 2030; + public static readonly VEHICLE_CURRENT_YEAR = 2023; + public static readonly UNDEFINED = 'undefined'; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.spec.ts new file mode 100644 index 0000000..07787b2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.spec.ts @@ -0,0 +1,422 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CreditApplicationDaoService } from './credit-application-dao.service'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { CreditApplicationService } from './credit-application.service'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanType, LoanTypeSet } from './loan-type'; +import { LoanTitle } from './loan-type/loan-title'; +import { CreditApplicationValues } from './credit-application-values'; +import { of } from 'rxjs'; +import { apiTranslatorPreTranslate } from '../testing/shared-mocks/api-translator-pre-post'; +import { EthnicityInformation } from '../loan-wizard/field-groups/hmda/ethnicity-information'; +import { GenderInformation } from '../loan-wizard/field-groups/hmda/gender-information'; +import { RaceInformation } from '../loan-wizard/field-groups/hmda/race-information'; +import { BorrowerType } from './borrower-type'; +import { EmploymentInformation } from '../loan-wizard/field-groups/employment/employment-information'; +import { IncomeInformation } from '../loan-wizard/field-groups/employment/income-information'; +import { AlimonyInformation } from '../loan-wizard/field-groups/employment/alimony-information'; +import { CreditApplication, SpouseType, TattleIndividual } from './credit-application'; +import { AddressInformation } from '../loan-wizard/field-groups/address/address-information'; +import { ResidenceInformation } from '../loan-wizard/field-groups/address/residence-information'; +import { PersonalInformation } from '../loan-wizard/field-groups/personal/personal-information'; +import { IsFullInfoSpouse } from '../loan-wizard/field-groups/spouse-type/is-full-info-spouse-information'; +import { PhoneTypeEnum, PropertyTypeEnum } from './api-models/api-enums'; +import { MembershipInformation } from '../loan-wizard/field-groups/member/membership-information'; +import { primaryAndCoApplicationAddressSameApplication } from '../testing/shared-mocks/broken-applications'; +import { ConfigurationService } from './configuration/configuration.service'; +import { UiSettings } from './configuration/ui-settings'; +import { DefaultSettings } from './configuration/default-settings.mock'; + +describe('CreditApplicationService', () => { + + let creditApplicationService: CreditApplicationService; + let creditApplicationDaoService: CreditApplicationDaoService; + let httpMock: HttpTestingController; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + CreditApplicationDaoService + ] + }); + }); + + beforeEach(() => { + creditApplicationService = TestBed.get(CreditApplicationService); + creditApplicationDaoService = TestBed.get(CreditApplicationDaoService); + httpMock = TestBed.get(HttpTestingController); + creditApplicationService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(creditApplicationService).toBeTruthy(); + }); + + it('should save term of loan for equity member', () => { + creditApplicationService.setLoanType(LoanTypeSet[4].id); + spyOn(creditApplicationService, 'getFullCreditApplication').and.callThrough(); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + const testStub = { + 'applicantType': CreditApplicationValues.CHOICE_NO, + 'limitRequested': '1000', + 'termOfLoan': 120, + 'maritalStatus': CreditApplicationValues.CHOICE_NO, + 'clientMessage': 'test message' + }; + creditApplicationService.saveEquityMemberInformation(testStub); + const creditApplication = creditApplicationService.getFullCreditApplication(); + expect(creditApplicationService.getFullCreditApplication).toHaveBeenCalled(); + expect(creditApplication.termOfLoan).toBeGreaterThanOrEqual(120); + expect(creditApplication.clientMessage).toEqual(testStub.clientMessage); + }); + + it('should save HMDA information', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + const ethnicity: EthnicityInformation = { + cuban: true, + otherHispanicLatinoText: 'San Lorenzoan' + }; + const sex: GenderInformation = { + male: true, + female: true + }; + const race: RaceInformation = { + filipino: true, + americanIndianAlaskaNativeText: 'False Karass' + }; + const hmda = { + ethnicityInformation: ethnicity, + genderInformation: sex, + raceInformation: race + }; + creditApplicationService.saveHMDAInformation(hmda, BorrowerType.CO_BORROWER); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[1].ethnicityInformation).toEqual(ethnicity); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[1].genderInformation).toEqual(sex); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[1].raceInformation).toEqual(race); + }); + + it('should set loantype if existing credit application has different loantype', () => { + const result = creditApplicationService.shouldSetupCreditApplication(LoanTypeSet[1]); + expect(result).toBe(true); + }); + + it('should NOT set loantype if existing credit application has same loantype', () => { + const result = creditApplicationService.shouldSetupCreditApplication(LoanTypeSet[0]); + expect(result).toBe(false); + }); + + it('should save and process employment/income information, only once', () => { + const testEmploymentInformation: Array = [{ + employerName: 'emp1', + employmentType: '3', + jobTitle: 'senior qa', + salary: 5000, + schedule: 'Hourly', + employmentLengthYears: 5, + employmentLengthMonths: 0, + workPhoneNumber: '' + }]; + const testIncomeInformation: Array = [{ + incomeName: 'Pension', + amount: 4000 + }, + { + incomeName: 'Gambling', + amount: 10000 + }, + { + incomeName: 'Check from parents', + amount: 20000 + }]; + const testAlimonyInformaiton: AlimonyInformation = { + childSupportAmount: 3000 + }; + spyOn(creditApplicationService, 'saveEmploymentInformation').and.callThrough(); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + + creditApplicationService.saveAllEmploymentInformation( + testEmploymentInformation, testIncomeInformation, testAlimonyInformaiton, BorrowerType.PRIMARY_BORROWER); + // Bug #840, Make sure calling twice doesn't ADD + creditApplicationService.saveAllEmploymentInformation( + testEmploymentInformation, testIncomeInformation, testAlimonyInformaiton, BorrowerType.PRIMARY_BORROWER); + const employmentInfo = creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].employmentInformation; + const otherIncome = creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].otherIncomeInformation; + const alimony = creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].alimonyInformation; + expect(creditApplicationService.getFullCreditApplication().page.hasEmployment).toBe(true); + expect(employmentInfo).toBeTruthy(); + expect(employmentInfo.length).toEqual(1); // Called twice, should only be one. + expect(employmentInfo[0].jobTitle).toEqual('senior qa'); + expect(employmentInfo[0].employerName).toEqual('emp1'); + expect(employmentInfo[0].salary).toEqual(5000); + expect(otherIncome[0].amount).toEqual(4000); + expect(otherIncome[0].incomeName).toBe('Pension'); + expect(otherIncome[1].amount).toEqual(10000); + expect(otherIncome[1].incomeName).toBe('Gambling'); + expect(otherIncome[2].amount).toEqual(20000); + expect(otherIncome[2].incomeName).toBe('Check from parents'); + expect(alimony.childSupportAmount).toEqual(3000); + }); + + // tslint:disable-next-line:max-line-length + it('should remove spouseType from a fullInfoBorrower who has changed state of residence such that spouse information is not required', () => { + const marriedPrimaryApplication: CreditApplication = apiTranslatorPreTranslate; + marriedPrimaryApplication.fullInfoBorrowers[0].fullInfoSpouse = apiTranslatorPreTranslate.fullInfoBorrowers[1]; + marriedPrimaryApplication.fullInfoBorrowers[0].spouseType = SpouseType.FullInfoSpouse; + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(marriedPrimaryApplication); + const addressInformation: AddressInformation = marriedPrimaryApplication.fullInfoBorrowers[0].addressInformation; + addressInformation.state = 'IL'; + creditApplicationService.saveAllAddressInformation(BorrowerType.PRIMARY_BORROWER, addressInformation); + expect(creditApplicationService.getFullCreditApplication().page.hasAddress).toBe(true); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].spouseType).toBeFalsy(); + // This is an 'after' hook + addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP; + creditApplicationService.saveAllAddressInformation(BorrowerType.PRIMARY_BORROWER, addressInformation); + }); + + it('should save address and residence history', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + + const addressInformation: AddressInformation = apiTranslatorPreTranslate.fullInfoBorrowers[0].addressInformation; + const residenceInformation: ResidenceInformation = apiTranslatorPreTranslate.fullInfoBorrowers[0].residenceInformation; + const priorAddress: Array = [apiTranslatorPreTranslate.fullInfoBorrowers[1].addressInformation]; + const priorResidence: Array = [apiTranslatorPreTranslate.fullInfoBorrowers[1].residenceInformation]; + creditApplicationService.saveAddressAndResidenceInformation({ + addressInfo: addressInformation, + residenceInfo: residenceInformation, + priorAddressInfo: priorAddress, + priorResidenceInfo: priorResidence + }, BorrowerType.CO_BORROWER); + const result = creditApplicationService.getFullCreditApplication(); + expect(result.fullInfoBorrowers[1].addressInformation).toEqual(addressInformation); + expect(result.fullInfoBorrowers[1].residenceInformation).toEqual(residenceInformation); + expect(result.fullInfoBorrowers[1].priorAddressInformation).toEqual(priorAddress); + expect(result.fullInfoBorrowers[1].priorResidenceInformation).toEqual(priorResidence); + }); + + it('should save mailing address when it differs from current address', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + + const PRIMARY = 0; + const mailingAddress = { + addressLine1: '200 S Wacker', + addressLine2: 'Dr # 3600', + zipcode: '60606', + city: 'Chicago', + state: 'IL' + }; + apiTranslatorPreTranslate.fullInfoBorrowers[PRIMARY].residenceInformation.isMailingAddressSame = false; + apiTranslatorPreTranslate.fullInfoBorrowers[PRIMARY].mailingAddressInformation = mailingAddress; + const addressInformation: AddressInformation = apiTranslatorPreTranslate.fullInfoBorrowers[PRIMARY].addressInformation; + const residenceInformation: ResidenceInformation = apiTranslatorPreTranslate.fullInfoBorrowers[PRIMARY].residenceInformation; + const mailingInformation: AddressInformation = apiTranslatorPreTranslate.fullInfoBorrowers[PRIMARY].mailingAddressInformation; + creditApplicationService.saveAddressAndResidenceInformation({ + addressInfo: addressInformation, + residenceInfo: residenceInformation, + mailingInfo: mailingInformation + }, BorrowerType.PRIMARY_BORROWER); + const result = creditApplicationService.getFullCreditApplication(); + expect(result.fullInfoBorrowers[PRIMARY].addressInformation).toEqual(addressInformation); + expect(result.fullInfoBorrowers[PRIMARY].mailingAddressInformation).toEqual(mailingInformation); + expect(result.fullInfoBorrowers[PRIMARY].residenceInformation).toEqual(residenceInformation); + }); + + it('should determine if there is enough data to save ', () => { + const application = creditApplicationService.getFullCreditApplication(); + application.fullInfoBorrowers[0].personalInformation.firstName = 'Bob'; + application.fullInfoBorrowers[0].addressInformation.addressLine1 = '1235 Maple Ave'; + const result = creditApplicationService.hasMinimumSaveInformation(); + expect(result).toBe(true); + }); + + it('should nullify previously entered spouse information when an applicant changes their marital status from YES to NO', () => { + const marriedPrimaryApplication: CreditApplication = apiTranslatorPreTranslate; + marriedPrimaryApplication.fullInfoBorrowers[0].fullInfoSpouse = apiTranslatorPreTranslate.fullInfoBorrowers[1]; + marriedPrimaryApplication.fullInfoBorrowers[0].spouseType = SpouseType.FullInfoSpouse; + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(marriedPrimaryApplication); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].spouseType).toBe(SpouseType.FullInfoSpouse); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].fullInfoSpouse).toBeTruthy(); + const personalInfo: PersonalInformation = { + firstName: 'Someone', + lastName: 'who changed their mind', + dateOfBirth: 'about being married', + primaryPhoneNumber: 'after already', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + socialSecurityNumber: 'submitting a bunch of information about an imaginary spouse', + email: 'notMarried@forever_alone.com', + maritalStatus: CreditApplicationValues.CHOICE_NO + }; + creditApplicationService.savePersonalInformation(personalInfo, BorrowerType.PRIMARY_BORROWER); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].spouseType).toBeFalsy(); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].fullInfoSpouse).toBeFalsy(); + }); + + it('should nullify previously entered spouse information when an applicant changes their marital status from NO to co-applicant', () => { + const marriedPrimaryApplication: CreditApplication = apiTranslatorPreTranslate; + marriedPrimaryApplication.fullInfoBorrowers[0].fullInfoSpouse = apiTranslatorPreTranslate.fullInfoBorrowers[1]; + marriedPrimaryApplication.fullInfoBorrowers[0].spouseType = SpouseType.FullInfoSpouse; + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(marriedPrimaryApplication); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].spouseType).toBe(SpouseType.FullInfoSpouse); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].fullInfoSpouse).toBeTruthy(); + const personalInfo: PersonalInformation = { + firstName: 'Someone', + lastName: 'who changed their mind', + dateOfBirth: 'about being married', + primaryPhoneNumber: 'after already', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + socialSecurityNumber: 'submitting a bunch of information about an imaginary spouse', + email: 'notMarried@forever_alone.com', + maritalStatus: CreditApplicationValues.CHOICE_YES, + coApplicantSpouse: CreditApplicationValues.CHOICE_YES + }; + creditApplicationService.savePersonalInformation(personalInfo, BorrowerType.PRIMARY_BORROWER); + expect(creditApplicationService.getFullCreditApplication().page.hasPersonal).toBe(true); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].spouseType).toBeFalsy(); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].fullInfoSpouse).toBeFalsy(); + }); + + it('should save tattle spouse information', () => { + const marriedPrimaryApplication: CreditApplication = apiTranslatorPreTranslate; + const tattleSpouse: TattleIndividual = { + personalInformation: { + firstName: 'Tattly', + lastName: 'McSpouseface' + } + }; + + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(marriedPrimaryApplication); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + + creditApplicationService.saveTattleSpouseInformation(tattleSpouse, BorrowerType.PRIMARY_BORROWER_SPOUSE); + + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].tattleSpouse).toBeTruthy(); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].tattleSpouse).toEqual(tattleSpouse); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[0].fullInfoSpouse).toBeFalsy(); + }); + + it('should set spouseType information', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + + const spouseType: IsFullInfoSpouse = { + isFullInfoSpouse: CreditApplicationValues.CHOICE_YES + }; + creditApplicationService.saveSpouseTypeInformation(spouseType, BorrowerType.PRIMARY_BORROWER); + expect(creditApplicationService.getSpouseType(BorrowerType.PRIMARY_BORROWER)).toBe(SpouseType.FullInfoSpouse); + expect(creditApplicationService.getIndividualGroupValues('addressInformation', BorrowerType.PRIMARY_BORROWER_SPOUSE)).toBeTruthy(); + }); + + it('should save an application ', () => { + const application = creditApplicationService.getFullCreditApplication(); + application.fullInfoBorrowers[0].personalInformation.firstName = 'Bob'; + application.fullInfoBorrowers[0].addressInformation.addressLine1 = '1235 Maple Ave'; + spyOn(creditApplicationDaoService, 'saveApplication').and.returnValue(of(true)); + creditApplicationService.saveCreditApplication(); + expect(creditApplicationDaoService.saveApplication).toHaveBeenCalled(); + }); + + it('should gather member information from the application', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + const expectedReturn: MembershipInformation = { + limitRequested: apiTranslatorPreTranslate.limitRequested, + applicantType: apiTranslatorPreTranslate.hasCoApplicant, + livesInMaritalPropertyState: CreditApplicationValues.CHOICE_YES, + confirmJointCredit: apiTranslatorPreTranslate.confirmJointCredit, + coApplicantLivesInMaritalPropertyState: CreditApplicationValues.CHOICE_YES, + purposeOfLoanSignature: apiTranslatorPreTranslate.purposeOfLoanSignature, + agreementRequirement: undefined + }; + expect(creditApplicationService.getMemberInformation()).toEqual(expectedReturn); + }); + + it('should determine that an applicant (or coapplicant) is married', () => { + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate); + expect(creditApplicationService.isMarriedApplicant()).toBe(true); + spyOn(creditApplicationService, 'getIndividualByBorrowerType').and.returnValue({ + personalInformation: { + maritalStatus: CreditApplicationValues.CHOICE_NO + } + }); + expect(creditApplicationService.isMarriedApplicant()).toBe(false); + }); + + it('should handle situations where applications change their mind about co-applicant spouse', () => { + const marriedPrimaryApplication: any = primaryAndCoApplicationAddressSameApplication; + + spyOn(creditApplicationService, 'getFullCreditApplication').and.returnValue(marriedPrimaryApplication); + spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true)); + expect(creditApplicationService.getFullCreditApplication() + .fullInfoBorrowers[1].hasSameAddressAsSpouse).toEqual(CreditApplicationValues.CHOICE_YES); + + const personalInfo: PersonalInformation = Object.assign({}, + primaryAndCoApplicationAddressSameApplication.fullInfoBorrowers[0].personalInformation); + personalInfo.coApplicantSpouse = CreditApplicationValues.CHOICE_NO; + creditApplicationService.savePersonalInformation(personalInfo, BorrowerType.PRIMARY_BORROWER); + expect(creditApplicationService.getFullCreditApplication().fullInfoBorrowers[1].hasSameAddressAsSpouse).toBeNull(); + + }); + + it('should set Estimated Value & Property Type defaults on HELOC Rate Mod loan type', () => { + const loanConfigurationService = TestBed.get(ConfigurationService); + loanConfigurationService.saveConfiguration(DefaultSettings as UiSettings); + const helocRateMod = LoanTypeSet.find((loanType) => loanType.title === LoanTitle.HELOC_RateMod); + if (helocRateMod) { + creditApplicationService.setLoanType(helocRateMod.id); + } else { + creditApplicationService.setLoanType(LoanTypeSet[0].id); + const application = creditApplicationService.getFullCreditApplication(); + application.loanType = { + ...application.loanType, + title: LoanTitle.HELOC_RateMod + }; + } + creditApplicationService.saveCollateralInformation( + { + "collateralInformation": { + "isAddressSame": "yes", + "estimatedValue": 100000, + "propertyType": "Duplex", + "isBeingImproved": "false", + "isListedForSale": "yes", + "occupancyType": "OwnerOccupied", + "hasFirstMortgage": "true", + "hasSecondMortgage": "false", + "payoffFirstMortgage": "false", + "addressLine1": "Fake Street 123", + "addressLine2": "", + "zipcode": "66666", + "city": "Test Town", + "state": "WI", + "residenceStatus": "0", + "residenceTimeYears": "2", + "residenceTimeMonths": "0", + "residenceMonthlyPayment": "2000", + "isMailingAddressSame": true + } + } + ); + const creditApplication = creditApplicationService.getFullCreditApplication(); + expect(creditApplication.loanType.title).toEqual(LoanTitle.HELOC_RateMod); + expect(creditApplication.collateralInformation.estimatedValue).toEqual(250000); + expect(creditApplication.collateralInformation.propertyType).toEqual(PropertyTypeEnum[PropertyTypeEnum.SingleFamily]); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.ts new file mode 100644 index 0000000..8d76cb3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.service.ts @@ -0,0 +1,871 @@ +import {Injectable} from '@angular/core'; +import {LoanType, LoanTypeSet} from './loan-type'; +import {CreditApplication, Individual, SpouseType, TattleIndividual} from './credit-application'; +import {DisclosureInformation} from '../loan-wizard/field-groups/disclosures/credit-card-disclosure'; +import {AddressInformation} from '../loan-wizard/field-groups/address/address-information'; +import {ResidenceInformation} from '../loan-wizard/field-groups/address/residence-information'; +import {PersonalInformation} from '../loan-wizard/field-groups/personal/personal-information'; +import {GenderInformation} from '../loan-wizard/field-groups/hmda/gender-information'; +import {EthnicityInformation} from '../loan-wizard/field-groups/hmda/ethnicity-information'; +import {RaceInformation} from '../loan-wizard/field-groups/hmda/race-information'; +import {AdditionalInformation} from '../loan-wizard/field-groups/summary/additional-info'; +import {TattleSpousePersonalInformation} from '../loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information'; +import {ApiPage} from './api-models/api-page'; +import {ApiStatus} from './api-models/api-status'; +import {ApiError, ApiErrorCode} from './api-models/api-error'; +import {FrequencyTypeEnum, PhoneTypeEnum, PropertyTypeEnum} from './api-models/api-enums'; +import {Observable, of, Subject} from 'rxjs'; +import {MaskingService} from './masking/masking.service'; +import {CreditApplicationValues} from './credit-application-values'; +import {CreditApplicationDaoService} from './credit-application-dao.service'; +import {catchError, map} from 'rxjs/operators'; +import {MembershipInformation} from '../loan-wizard/field-groups/member/membership-information'; +import {BorrowerType} from './borrower-type'; +import {EmploymentInformation} from '../loan-wizard/field-groups/employment/employment-information'; +import {IncomeInformation} from '../loan-wizard/field-groups/employment/income-information'; +import {AlimonyInformation} from '../loan-wizard/field-groups/employment/alimony-information'; +import {VehicleCollateralInformation} from '../loan-wizard/field-groups/vehicle/vehicle-collateral-information'; +import {IsFullInfoSpouse} from '../loan-wizard/field-groups/spouse-type/is-full-info-spouse-information'; +import {EquityMemberInformation} from '../loan-wizard/field-groups/equity/equity-member-information'; +import {LoanPurpose} from './loan-type/loan-purpose'; +import {LoanTitle} from './loan-type/loan-title'; +import { MemberInfoFormService } from '../loan-wizard/field-groups/member/member-info-form.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CreditApplicationService { + + protected creditApplication: CreditApplication; + public memberService: MemberInfoFormService; + private loanTypeSubject: Subject = new Subject(); + private loanType$ = this.loanTypeSubject.asObservable(); + + // DEFAULT HELOC RATE MOD VALUES + private readonly defaultHELOCestimatedValue = 250000; + private readonly defaultHELOCpropertyType = PropertyTypeEnum[PropertyTypeEnum.SingleFamily]; + + constructor(private creditApplicationDaoService: CreditApplicationDaoService, + ) {} + + public setLoanType(loanTypeValue: string, parsedData?: any): void { + const loanType = this.getLoanTypeForValue(loanTypeValue); + if (!loanType) { + throw new Error('Could not lookup loan type for ' + loanTypeValue); + } + if (this.shouldSetupCreditApplication(loanType)) { + this.creditApplication = this.constructCreditApplicationByType(loanType, parsedData); + this.loanTypeSubject.next(this.creditApplication.loanType); + } + } + + public shouldSetupCreditApplication(loanType: LoanType): boolean { + if (this.getFullCreditApplication()) { + if (this.getFullCreditApplication().loanType !== loanType) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + public resetLoanType(): void { + this.loanTypeSubject.next(null); + } + + public getLoanTypeObservable(): Observable { + return this.loanType$; + } + + public getFullCreditApplication(): CreditApplication { + return this.creditApplication; + } + + public getIndividualByBorrowerType(individual: BorrowerType) { + const creditApplication = this.getFullCreditApplication(); // DO NOT REMOVE THIS, tests will fail. + switch (individual) { + case BorrowerType.PRIMARY_BORROWER: + return creditApplication.fullInfoBorrowers[0]; + case BorrowerType.CO_BORROWER: + this.addDefaultCoApplicantIfNecessary(); + return creditApplication.fullInfoBorrowers[1]; + case BorrowerType.PRIMARY_BORROWER_SPOUSE: + return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[0]); + case BorrowerType.CO_BORROWER_SPOUSE: + return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[1]); + } + throw new Error('Invalid individual type in getIndividualByBorrowerType: ' + individual); + } + + public getIndividualSpouse(individual: Individual): any { + if (individual.spouseType === SpouseType.FullInfoSpouse) { + return individual.fullInfoSpouse; + } else { + return individual.tattleSpouse; + } + } + + public isCoApplicantSpouse(): boolean { + return this.getFullCreditApplication().hasCoApplicantSpouse === CreditApplicationValues.CHOICE_YES; + } + + public addDefaultCoApplicantIfNecessary() { + const creditApplication = this.getFullCreditApplication(); + if (creditApplication.hasCoApplicant === CreditApplicationValues.CHOICE_YES + && creditApplication.fullInfoBorrowers.length === 1) { + creditApplication.fullInfoBorrowers.push(this.generateDefaultFullInfoBorrower()); + if (creditApplication.coApplicantLivesInMaritalPropertyState) { + creditApplication.fullInfoBorrowers[1].addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP; + } + if (this.isCoApplicantSpouse()) { + creditApplication.fullInfoBorrowers[1].personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES; + } + } + } + + public getSpouseType(borrowerType: BorrowerType): SpouseType { + const individual = this.getIndividualByBorrowerType(borrowerType); + if (borrowerType === BorrowerType.PRIMARY_BORROWER || (borrowerType === BorrowerType.CO_BORROWER && individual)) { + return individual.spouseType; + } + return null; + } + + public getIndividualGroupValues(group: string, individual: BorrowerType) { + const selectedIndividual = this.getIndividualByBorrowerType(individual); + if (selectedIndividual && selectedIndividual[group]) { + return selectedIndividual[group]; + } + return {}; + } + + public getEquityMemberInformation(): EquityMemberInformation { + const creditApplication: CreditApplication = this.getFullCreditApplication(); + return { + purposeOfLoan: creditApplication.purposeOfLoan || LoanPurpose.Unselected, + termOfLoan: creditApplication.termOfLoan + }; + } + + public getMemberInformation(): MembershipInformation { + const creditApplication: CreditApplication = this.getFullCreditApplication(); + let livesInMaritalPropertyState = null; + const stateOfResidence = creditApplication.fullInfoBorrowers[0].addressInformation.state; + if (stateOfResidence) { + livesInMaritalPropertyState = stateOfResidence === CreditApplicationValues.STATE_REQUIRED_COAPP ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO; + } + const memberInfo: MembershipInformation = { + limitRequested: creditApplication.limitRequested ? creditApplication.limitRequested : 0, + applicantType: creditApplication.hasCoApplicant, + livesInMaritalPropertyState: livesInMaritalPropertyState, + confirmJointCredit: creditApplication.confirmJointCredit, + purposeOfLoanSignature: creditApplication.purposeOfLoanSignature, + agreementRequirement: creditApplication.agreementRequirement + }; + if (memberInfo.applicantType === CreditApplicationValues.CHOICE_YES) { + memberInfo.coApplicantLivesInMaritalPropertyState = creditApplication.coApplicantLivesInMaritalPropertyState ? + CreditApplicationValues.CHOICE_YES : CreditApplicationValues.CHOICE_NO; + } + if (this.isHelocRateMod(creditApplication.loanType.title)) { + memberInfo.applicantType = CreditApplicationValues.CHOICE_NO; + memberInfo.livesInMaritalPropertyState = CreditApplicationValues.CHOICE_YES; + } + return memberInfo; + } + + public getPrimaryIndividualIfSpouseBorrowerType(borrowerType: BorrowerType): Individual { + const creditApplication = this.getFullCreditApplication(); // DO NOT REMOVE THIS, tests will fail. + switch (borrowerType) { + case BorrowerType.PRIMARY_BORROWER_SPOUSE: + return creditApplication.fullInfoBorrowers[0]; + case BorrowerType.CO_BORROWER_SPOUSE: + return creditApplication.fullInfoBorrowers[1]; + case BorrowerType.PRIMARY_BORROWER: + return null; + case BorrowerType.CO_BORROWER: + return null; + } + throw new Error('Invalid individual type in getPrimaryIndividualIfSpouseBorrowerType: ' + borrowerType); + } + + public saveSpouseTypeInformation(spouseType: IsFullInfoSpouse, individual: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + const setIndividual = this.getIndividualByBorrowerType(individual); + if (spouseType.isFullInfoSpouse === CreditApplicationValues.CHOICE_YES && setIndividual.spouseType !== SpouseType.FullInfoSpouse) { + setIndividual.spouseType = SpouseType.FullInfoSpouse; + setIndividual.fullInfoSpouse = this.generateDefaultFullInfoBorrower(); + setIndividual.tattleSpouse = null; + } else if (spouseType.isFullInfoSpouse === CreditApplicationValues.CHOICE_NO && setIndividual.spouseType !== SpouseType.TattleSpouse) { + setIndividual.spouseType = SpouseType.TattleSpouse; + setIndividual.tattleSpouse = this.generateDefaultTattleSpouse(); + setIndividual.fullInfoSpouse = null; + } + creditApplication.page.hasSpouseInfo = true; + return this.saveCreditApplication(); + } + + public saveTattleSpouseInformation(tattleSpouseInfo: TattleIndividual, borrowerType: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + const parentIndividual: Individual = this.getPrimaryIndividualIfSpouseBorrowerType(borrowerType); + parentIndividual.tattleSpouse = tattleSpouseInfo; + parentIndividual.fullInfoSpouse = null; + creditApplication.page.hasTattleSpouseInfo = true; + return this.saveCreditApplication(); + } + + public savePersonalInformation(personalInfo: PersonalInformation, borrowerType: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + const currentIndividual = this.getIndividualByBorrowerType(borrowerType); + if (personalInfo.coApplicantSpouse === CreditApplicationValues.CHOICE_YES) { + creditApplication.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_YES; + const primaryBorrower = this.getIndividualByBorrowerType(BorrowerType.PRIMARY_BORROWER); + primaryBorrower.spouseType = null; + primaryBorrower.fullInfoSpouse = null; + primaryBorrower.tattleSpouse = null; + } else if (personalInfo.coApplicantSpouse === CreditApplicationValues.CHOICE_NO) { + creditApplication.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_NO; + // GJF - if the person returns and changes their mind about their spouse, we have to reset this flag. #767 + if (borrowerType === BorrowerType.PRIMARY_BORROWER && creditApplication.fullInfoBorrowers.length > 1) { + creditApplication.fullInfoBorrowers[1].hasSameAddressAsSpouse = null; + } + // how do I get this person's spouse? + // reset the hasSameAddressAsSpouse flag. + } + if (personalInfo.maritalStatus === CreditApplicationValues.CHOICE_NO) { + currentIndividual.spouseType = null; + currentIndividual.tattleSpouse = null; + currentIndividual.fullInfoSpouse = null; + } + currentIndividual.personalInformation = personalInfo; + // GJF - This is determining if the maritalStatus should be forced to yes for a co-applicant. + if (this.getPrimaryIndividualIfSpouseBorrowerType(borrowerType)) { + currentIndividual.personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES; + } + creditApplication.page.hasPersonal = true; + return this.saveCreditApplication(); + } + + public saveAllEmploymentInformation( + employmentHistory: Array = [], + otherIncomes: Array = [], + alimonyInfo: AlimonyInformation = null, + borrowerType: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + const setIndividual: Individual = this.getIndividualByBorrowerType(borrowerType); + + this.saveEmploymentInformation(employmentHistory, borrowerType); + this.saveOtherIncomeInformation(otherIncomes, borrowerType); + + if (alimonyInfo) { + setIndividual.alimonyInformation = alimonyInfo; + setIndividual.alimonyInformation.childSupportAmount = MaskingService.unMaskDecimalNumber(alimonyInfo.childSupportAmount); + } + creditApplication.page.hasEmployment = true; + return this.saveCreditApplication(); + } + + public saveOtherIncomeInformation(otherIncomes: Array, borrowerType: BorrowerType): void { + if (otherIncomes) { + const setIndividual = this.getIndividualByBorrowerType(borrowerType); + otherIncomes.forEach((eachIncome: IncomeInformation) => { + if (eachIncome) { + eachIncome.amount = MaskingService.unMaskDecimalNumber(eachIncome.amount); + } + }, this); + setIndividual.otherIncomeInformation = otherIncomes; + return; + } + } + + public saveEmploymentInformation(employmentHistory: Array, borrowerType: BorrowerType): void { + if (employmentHistory) { + const setIndividual = this.getIndividualByBorrowerType(borrowerType); + employmentHistory.forEach((eachEmployer: EmploymentInformation) => { + if (eachEmployer) { + eachEmployer.salary = MaskingService.unMaskDecimalNumber(eachEmployer.salary); + } + }, this); + setIndividual.employmentInformation = employmentHistory; + return; + } + } + + public saveHMDAInformation(formPage, borrowerType: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + const setIndividual: Individual = this.getIndividualByBorrowerType(borrowerType); + setIndividual.genderInformation = formPage.genderInformation; + setIndividual.ethnicityInformation = formPage.ethnicityInformation; + setIndividual.raceInformation = formPage.raceInformation; + creditApplication.page.hasHMDA = true; + return this.saveCreditApplication(); + } + + public saveNoteInformation(noteData: any, creditApplication: CreditApplication) { + if (creditApplication.note) { + creditApplication.note += noteData.note; + } else { + creditApplication.note = noteData.note; + } + creditApplication.primaryContactMethod = noteData.primaryContact; + creditApplication.promoCode = noteData.promoCode; + creditApplication.page.hasNote = true; + return creditApplication; + } + + public saveAdditionalInformation(additionalInformation: AdditionalInformation, creditApplication: CreditApplication) { + creditApplication.modFeeAccount = additionalInformation.modFeeAccount; + return creditApplication; + } + + public saveSummaryInformation(additionalInformation: AdditionalInformation, noteData: any): Observable { + let creditApplication = this.getFullCreditApplication(); + creditApplication = this.saveNoteInformation(noteData, creditApplication); + creditApplication = this.saveAdditionalInformation(additionalInformation, creditApplication); + return this.saveCreditApplication(); + } + + public saveDisclosureInformation(disclosureInformation: DisclosureInformation): Observable { + const creditApplication = this.getFullCreditApplication(); + creditApplication.disclosureInformation = disclosureInformation; + creditApplication.disclosureInformation.date = new Date(); + creditApplication.page.hasDisclosure = true; + return this.saveCreditApplication(); + } + + public saveEquityMemberInformation(equityMemberInfo: any): Observable { + const creditApplication = this.getFullCreditApplication(); + + equityMemberInfo.limitRequested = MaskingService.unMaskNumber(equityMemberInfo.limitRequested); + creditApplication.purposeOfLoan = equityMemberInfo.purposeOfLoan ? + equityMemberInfo.purposeOfLoan : creditApplication.loanType.defaultPurpose; + if (equityMemberInfo.termOfLoan) { + creditApplication.termOfLoan = equityMemberInfo.termOfLoan; + } + creditApplication.clientMessage = equityMemberInfo.clientMessage; + creditApplication.agreementRequirement = equityMemberInfo.agreementRequirement; + return this.saveMemberInformation(equityMemberInfo); + } + + public saveMemberInformation(memberInfo: MembershipInformation): Observable { + const creditApplication = this.getFullCreditApplication(); + + memberInfo.limitRequested = MaskingService.unMaskNumber(memberInfo.limitRequested); + creditApplication.hasCoApplicant = memberInfo.applicantType; + creditApplication.limitRequested = memberInfo.limitRequested ? memberInfo.limitRequested : 0; + creditApplication.purposeOfLoanSignature = memberInfo.purposeOfLoanSignature; + creditApplication.confirmJointCredit = memberInfo.confirmJointCredit; + creditApplication.agreementRequirement = memberInfo.agreementRequirement; + if (memberInfo.livesInMaritalPropertyState === CreditApplicationValues.CHOICE_YES) { + creditApplication.fullInfoBorrowers[0].addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP; + } else if (memberInfo.livesInMaritalPropertyState === CreditApplicationValues.CHOICE_NO && + !creditApplication.fullInfoBorrowers[0].addressInformation.state) { + creditApplication.fullInfoBorrowers[0].addressInformation.state = CreditApplicationValues.UNDEFINED; + } + + if (creditApplication.hasCoApplicant === CreditApplicationValues.CHOICE_NO && + creditApplication.fullInfoBorrowers.length > 1) { + creditApplication.fullInfoBorrowers.pop(); + } + if (memberInfo.applicantType === CreditApplicationValues.CHOICE_YES && + memberInfo.coApplicantLivesInMaritalPropertyState === CreditApplicationValues.CHOICE_YES) { + creditApplication.coApplicantLivesInMaritalPropertyState = true; + if (creditApplication.fullInfoBorrowers.length === 2) { + creditApplication.fullInfoBorrowers[1].addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP; + } + } else if (memberInfo.applicantType === CreditApplicationValues.CHOICE_YES && + memberInfo.coApplicantLivesInMaritalPropertyState === CreditApplicationValues.CHOICE_NO) { + creditApplication.coApplicantLivesInMaritalPropertyState = false; + if (creditApplication.fullInfoBorrowers.length === 2) { + creditApplication.fullInfoBorrowers[1].addressInformation.state = CreditApplicationValues.UNDEFINED; + } + } + creditApplication.page.hasMemberInfo = true; + return this.saveCreditApplication(); + } + + public saveCollateralInformation(values: any): Observable { + const creditApplication = this.getFullCreditApplication(); + values.collateralInformation.estimatedValue = MaskingService.unMaskNumber(values.collateralInformation.estimatedValue); + if (values.collateralInformation.rentPaid && values.collateralInformation.onRentedLot === CreditApplicationValues.CHOICE_YES) { + values.collateralInformation.rentPaid = MaskingService.unMaskDecimalNumber(values.collateralInformation.rentPaid); + } else if (values.collateralInformation.rentPaid) { + delete values.collateralInformation.rentPaid; + } + + if (values.collateralInformation.annualPropertyTax && values.collateralInformation.onRentedLot === CreditApplicationValues.CHOICE_NO) { + values.collateralInformation.annualPropertyTax = MaskingService.unMaskDecimalNumber(values.collateralInformation.annualPropertyTax); + } else if (values.collateralInformation.annualPropertyTax) { + delete values.collateralInformation.annualPropertyTax; + } + + creditApplication.collateralInformation = values.collateralInformation; + if (this.isHelocRateMod(creditApplication.loanType.title)) { + creditApplication.collateralInformation.estimatedValue = this.defaultHELOCestimatedValue; + creditApplication.collateralInformation.propertyType = this.defaultHELOCpropertyType; + } + Object.assign(creditApplication.collateralInformation, values.addressInformation); + creditApplication.page.hasCollateralInfo = true; + return this.saveCreditApplication(); + } + + public saveVehicleCollateralInformation(fields: { + vehicleInformation: VehicleCollateralInformation, + boatAddressInformation?: AddressInformation}): Observable { + + const vehicleInfo = this.deleteUnusedVehicleFields(fields.vehicleInformation); + const addressInfo = fields.boatAddressInformation; + const creditApplication = this.getFullCreditApplication(); + + vehicleInfo.mileage = MaskingService.unMaskNumber(vehicleInfo.mileage); + vehicleInfo.value = MaskingService.unMaskNumber(vehicleInfo.value); + + if (!vehicleInfo.condition) { + vehicleInfo.condition = 'Used'; + } + + if (vehicleInfo.term) { + creditApplication.termOfLoan = vehicleInfo.term; + } + + if (creditApplication.loanType.title === LoanTitle.Boat) { + if (addressInfo) { + vehicleInfo.boatAddress = addressInfo; + } else { + vehicleInfo.boatAddress = creditApplication.fullInfoBorrowers[0].addressInformation; + } + } + + creditApplication.vehicleInformation = vehicleInfo; + creditApplication.page.hasVehicleCollateral = true; + + return this.saveCreditApplication(); + } + + public saveAddressAndResidenceInformation(fields: { + addressInfo?: AddressInformation, + priorAddressInfo?: Array, + mailingInfo?: AddressInformation, + residenceInfo?: ResidenceInformation, + priorResidenceInfo?: Array, + hasSameAddressAsSpouse?: string + }, borrowerType: BorrowerType): Observable { + const creditApplication = this.getFullCreditApplication(); + this.saveAllResidenceInformation(borrowerType, fields.residenceInfo, fields.priorResidenceInfo); + this.saveAllAddressInformation(borrowerType, fields.addressInfo, fields.mailingInfo, fields.priorAddressInfo); + + const setIndividual: Individual = this.getIndividualByBorrowerType(borrowerType); + if (fields.hasSameAddressAsSpouse && setIndividual) { + setIndividual.hasSameAddressAsSpouse = fields.hasSameAddressAsSpouse; + } + creditApplication.page.hasResidenceInfo = true; + return this.saveCreditApplication(); + } + + public saveAllAddressInformation( + borrowerType: BorrowerType, + addressInfo: AddressInformation, + mailingInfo?: AddressInformation, + priorAddressInfo?: Array): Observable { + const creditApplication = this.getFullCreditApplication(); + const setIndividual: Individual = this.getIndividualByBorrowerType(borrowerType); + if (!addressInfo || !setIndividual) { + return; + } + + setIndividual.addressInformation = addressInfo; + if (setIndividual.addressInformation.state !== CreditApplicationValues.STATE_REQUIRED_COAPP) { + setIndividual.spouseType = null; + } + + if (priorAddressInfo) { + setIndividual.priorAddressInformation = priorAddressInfo; + } + if (mailingInfo) { + setIndividual.mailingAddressInformation = mailingInfo; + } + creditApplication.page.hasAddress = true; + } + + public saveAllResidenceInformation( + borrowerType: BorrowerType, + residenceInfo: ResidenceInformation, + priorResidenceInfo?: Array): Observable { + + const setIndividual: Individual = this.getIndividualByBorrowerType(borrowerType); + if (!residenceInfo || !setIndividual) { + return; + } + + if (residenceInfo.residenceMonthlyPayment) { + residenceInfo.residenceMonthlyPayment = MaskingService.unMaskDecimalNumber(residenceInfo.residenceMonthlyPayment); + } + + setIndividual.residenceInformation = residenceInfo; + + if (priorResidenceInfo) { + priorResidenceInfo.forEach((eachResidenceInfo) => { + if (eachResidenceInfo.residenceMonthlyPayment) { + eachResidenceInfo.residenceMonthlyPayment = MaskingService.unMaskDecimalNumber(eachResidenceInfo.residenceMonthlyPayment); + } + }, this); + + setIndividual.priorResidenceInformation = priorResidenceInfo; + } + } + + public saveCreditApplication(): Observable { + const creditApplication = this.getFullCreditApplication(); + if (this.hasMinimumSaveInformation()) { + return this.creditApplicationDaoService.saveApplication(creditApplication) + .pipe( + map(result => { + if (result) { + creditApplication.loanId = result['id']; + creditApplication.appDetailId = result['appDetailId']; + creditApplication.error = result['error']; + return true; + } + return false; + }), + catchError((err, caught) => { + return of(false); + }) + ); + } else if(creditApplication.loanId == null) { + return this.creditApplicationDaoService.createEmptyApplication(creditApplication) + .pipe( + map(result => { + if (result) { + creditApplication.loanId = result['id']; + creditApplication.appDetailId = result['appDetailId']; + creditApplication.error = result['error']; + return true; + } + return false; + }), + catchError((err, caught) => { + return of(false); + }) + ); + } else { + return of(true); + } + } + + public hasMinimumSaveInformation(): boolean { + const creditApplication = this.getFullCreditApplication(); + const personalInformation = creditApplication.fullInfoBorrowers[0].personalInformation; + const addressInformation = creditApplication.fullInfoBorrowers[0].addressInformation; + if (personalInformation.firstName.length > 1 && addressInformation.addressLine1.length > 1) { + return true; + } else { + return false; + } + } + + public constructCreditApplicationByType(loanType: LoanType, parsedData?: any): CreditApplication { + const application = new CreditApplication(loanType); + if (parsedData) { + Object.assign(application, parsedData); + } + this.setDefaults(application); // Refactor here when different types have different defaults. + return application; + } + + public getLoanTypeForValue(loanTypeValue: string): LoanType { + let foundLoanType = null; + LoanTypeSet.forEach((loanType, index) => { + if (loanType.id === loanTypeValue) { + foundLoanType = loanType; + } + }, this); + + return foundLoanType; + } + + public isMarriedApplicant(): boolean { + const primary: Individual = this.getIndividualByBorrowerType(BorrowerType.PRIMARY_BORROWER); + const coborrower: Individual = this.getIndividualByBorrowerType(BorrowerType.CO_BORROWER); + if (primary + && primary.personalInformation.maritalStatus === CreditApplicationValues.CHOICE_YES + && primary.addressInformation.state === CreditApplicationValues.STATE_REQUIRED_COAPP) { + return true; + } else if (coborrower + && coborrower.personalInformation.maritalStatus === CreditApplicationValues.CHOICE_YES + && coborrower.addressInformation.state === CreditApplicationValues.STATE_REQUIRED_COAPP) { + return true; + } + return false; + } + + public canAcceptIndividualPrefill(): boolean { + const application = this.getFullCreditApplication(); + if (application && application.fullInfoBorrowers && application.fullInfoBorrowers.length > 0) { + const personal = application.fullInfoBorrowers[0].personalInformation; + if (personal && personal.lastName && personal.firstName && personal.email) { + return false; + } + } + return true; + } + + public generateDefaultDisclosure(): DisclosureInformation { + return { + contact: true, + truthful: false + }; + } + + public generateDefaultTattleSpousePersonalInformation(): TattleSpousePersonalInformation { + return { + firstName: '', + middleInitial: '', + lastName: '' + }; + } + + public static generateDefaultResidenceInformation(isHelocRateMod = false): ResidenceInformation { + return { + isMailingAddressSame: true, + residenceMonthlyPayment: null, + residenceTimeYears: isHelocRateMod ? '2' : '', + residenceTimeMonths: '0', + residenceStatus: '0' + }; + } + + public generateDefaultFullInfoBorrower(isHelocRateMod = false): Individual { + if (isHelocRateMod) { + return { + addressInformation: this.generateDefaultAddressInformation(), + residenceInformation: CreditApplicationService.generateDefaultResidenceInformation(isHelocRateMod), + personalInformation: this.generateDefaultPersonalInformation(), + ethnicityInformation: this.generateDefaultEthnicityInformation(isHelocRateMod), + genderInformation: this.generateDefaultGenderInformation(isHelocRateMod), + raceInformation: this.generateDefaultRaceInformation(isHelocRateMod), + employmentInformation: this.generateDefaultEmploymentForHelocRateModification() + }; + } + return { + addressInformation: this.generateDefaultAddressInformation(), + residenceInformation: CreditApplicationService.generateDefaultResidenceInformation(), + personalInformation: this.generateDefaultPersonalInformation() + }; + } + + public generateDefaultEmploymentForHelocRateModification(): Array { + return [{ + employerName: 'Other', + employmentType: '2', + jobTitle: 'Other', + salary: 10000, + schedule: FrequencyTypeEnum.Monthly.toString(), + employmentLengthYears: 2, + employmentLengthMonths: 0, + workPhoneNumber: null + }]; + } + + public generateDefaultTattleSpouse(): TattleIndividual { + return { + personalInformation: this.generateDefaultTattleSpousePersonalInformation(), + addressInformation: this.generateDefaultAddressInformation(), + hasSameAddressAsSpouse: 'yes' + }; + } + + public generateDefaultAddressInformation(): AddressInformation { + return { + addressLine1: '', + addressLine2: '', + city: '', + state: '', + zipcode: '', + }; + } + + public generateDefaultPersonalInformation(): PersonalInformation { + return { + firstName: '', + middleInitial: '', + lastName: '', + dateOfBirth: '', + socialSecurityNumber: null, + primaryPhoneNumberType: PhoneTypeEnum.Home, + primaryPhoneNumber: null, + secondaryPhoneNumberType: null, + secondaryPhoneNumber: null, + email: '' + }; + } + + public generateDefaultEthnicityInformation(notProvidedDefault = false): EthnicityInformation { + return { + hispanicOrLatino: false, + mexican: false, + puertoRican: false, + cuban: false, + otherHispanicLatino: false, + otherHispanicLatinoText: '', + notHispanicOrLatino: false, + notProvided: notProvidedDefault + }; + } + + public generateDefaultGenderInformation(notProvidedDefault = false): GenderInformation { + return { + female: false, + male: false, + notProvided: notProvidedDefault + }; + } + + public generateDefaultRaceInformation(notProvidedDefault = false): RaceInformation { + return { + americanIndianAlaskaNative: false, + americanIndianAlaskaNativeText: '', + blackOrAfricanAmerican: false, + asian: false, + asianIndian: false, + chinese: false, + filipino: false, + japanese: false, + korean: false, + vietnamese: false, + otherAsian: false, + otherAsianText: '', + hawaiian: false, + nativeHawaiian: false, + guamanianChamorro: false, + samoan: false, + otherPacificIslander: false, + otherPacificIslanderText: '', + white: false, + notProvided: notProvidedDefault, + }; + } + + public generateDefaultPage(helocRateMod = false): ApiPage { + return { + hasAddress: false, + hasEmployment: helocRateMod, + hasPersonal: false, + hasMemberInfo: false, + hasSpouseInfo: false, + hasTattleSpouseInfo: false, + hasResidenceInfo: false, + hasVehicleCollateral: false, + hasHMDA: helocRateMod, + hasDisclosure: false, + hasCollateralInfo: false, + hasNote: helocRateMod + }; + } + + public generateDefaultError(): ApiError { + return { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }; + } + + public generateSpecialNote(title: string): string { + return `Custom Product-${title} `; + } + + public deleteUnusedVehicleFields(vehicleInfo: VehicleCollateralInformation): VehicleCollateralInformation { + + if (vehicleInfo.loanPurpose !== 'Purchase') { + delete vehicleInfo.transferring; + delete vehicleInfo.licensePlateNumber; + delete vehicleInfo.dealerName; + delete vehicleInfo.dealerPhone; + delete vehicleInfo.seller; + delete vehicleInfo.titleBranded; + delete vehicleInfo.lien; + vehicleInfo.boatTitleFiles = []; + vehicleInfo.trailerTitleFiles = []; + vehicleInfo.purchaseContractFiles = []; + } + + if (vehicleInfo.transferring === 'no') { + delete vehicleInfo.licensePlateNumber; + } + + if (vehicleInfo.seller === 'Private_Party') { + delete vehicleInfo.dealerName; + delete vehicleInfo.dealerPhone; + } + + return vehicleInfo; + } + + public isHelocRateMod(title: string): boolean { + return title === LoanTitle.HELOC_RateMod; + } + + private setDefaults(creditApplication: CreditApplication) { + // for now, set these defaults, when they are missing + // this may need to be refactored later on, but since there's only 3 defaults set we can check each one + if (!creditApplication.disclosureInformation) { + Object.assign(creditApplication, {disclosureInformation: this.generateDefaultDisclosure()}); + } + + const isHelocRateModApp = this.isHelocRateMod(creditApplication.loanType.title); + + if (creditApplication && isHelocRateModApp) { + Object.assign(creditApplication, {limitRequested: 5000}); + Object.assign(creditApplication, {purposeOfLoan: LoanPurpose.Other}); + Object.assign(creditApplication, {termOfLoan: 60}); + } + + if (creditApplication.fullInfoBorrowers) { + // we need to check each part of the full info borrower now.... + if (!creditApplication.fullInfoBorrowers[0].addressInformation) { + Object.assign(creditApplication.fullInfoBorrowers[0], + {addressInformation: this.generateDefaultAddressInformation()}); + } + if (!creditApplication.fullInfoBorrowers[0].residenceInformation) { + Object.assign(creditApplication.fullInfoBorrowers[0], + {residenceInformation: CreditApplicationService.generateDefaultResidenceInformation()}); + } + if (!creditApplication.fullInfoBorrowers[0].personalInformation) { + Object.assign(creditApplication.fullInfoBorrowers[0], + {personalInformation: this.generateDefaultPersonalInformation()}); + } + } + if (!creditApplication.fullInfoBorrowers) { + Object.assign(creditApplication, {fullInfoBorrowers: [this.generateDefaultFullInfoBorrower(isHelocRateModApp)]}); + } + if (!creditApplication.note && isHelocRateModApp) { + Object.assign(creditApplication, {note: this.generateSpecialNote(LoanTitle.HELOC_RateMod)}); + } + if (!creditApplication.status) { + Object.assign(creditApplication, {status: ApiStatus.IN_PROGRESS}); + } + if (!creditApplication.error) { + Object.assign(creditApplication, {error: this.generateDefaultError()}); + } + if (!creditApplication.page) { + Object.assign(creditApplication, {page: this.generateDefaultPage(isHelocRateModApp)}); + } + return creditApplication; + } + + /** + * Clear an application after sending. This may mature; for now we keep only the + * loan type, loan id, and decision. + */ + public clearApplication(): CreditApplication { + const creditApplication = this.getFullCreditApplication(); + if (creditApplication) { + const application = new CreditApplication(creditApplication.loanType); + Object.assign(application, creditApplication.decision, creditApplication.loanId, creditApplication.hasBeenCompleted); + return application; + } + return creditApplication; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.ts new file mode 100644 index 0000000..a8b6d73 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/credit-application.ts @@ -0,0 +1,95 @@ +import { VehicleCollateralInformation } from '../loan-wizard/field-groups/vehicle/vehicle-collateral-information'; +import { AddressInformation } from '../loan-wizard/field-groups/address/address-information'; +import { PersonalInformation } from '../loan-wizard/field-groups/personal/personal-information'; +import { EmploymentInformation } from '../loan-wizard/field-groups/employment/employment-information'; +import { DisclosureInformation } from '../loan-wizard/field-groups/disclosures/credit-card-disclosure'; +import { LoanType } from './loan-type'; +import { ResidenceInformation } from '../loan-wizard/field-groups/address/residence-information'; +import { EthnicityInformation } from '../loan-wizard/field-groups/hmda/ethnicity-information'; +import { GenderInformation } from '../loan-wizard/field-groups/hmda/gender-information'; +import { RaceInformation } from '../loan-wizard/field-groups/hmda/race-information'; +import { CollateralInformation } from '../loan-wizard/field-groups/equity/collateral-information'; +import { IsAddressSameInformation } from '../loan-wizard/field-groups/address/is-address-same-information'; +import { IncomeInformation } from '../loan-wizard/field-groups/employment/income-information'; +import { AlimonyInformation } from '../loan-wizard/field-groups/employment/alimony-information'; +import { TattleSpousePersonalInformation } from '../loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-information'; +import { LoanPurpose } from './loan-type/loan-purpose'; +import { ApiPage } from './api-models/api-page'; +import { ApiStatus } from './api-models/api-status'; +import { ApiError } from './api-models/api-error'; +import { DecisionStatus } from './decision/decision-status'; + +export interface CollateralInformationGroup extends CollateralInformation, AddressInformation, IsAddressSameInformation, LienPosition {} + +export enum SpouseType { + 'FullInfoSpouse' = 'fullInfoSpouse', + 'TattleSpouse' = 'tattleSpouse' +} + +export class Individual { + public personalInformation: PersonalInformation; + public mailingAddressInformation?: AddressInformation; + public addressInformation: AddressInformation; + public priorAddressInformation?: Array; + public residenceInformation: ResidenceInformation; + public priorResidenceInformation?: Array; + public employmentInformation?: Array; + public otherIncomeInformation?: Array; + public alimonyInformation?: AlimonyInformation; + public ethnicityInformation?: EthnicityInformation; + public genderInformation?: GenderInformation; + public raceInformation?: RaceInformation; + public spouseType?: SpouseType; + public fullInfoSpouse?: Individual; + public tattleSpouse?: TattleIndividual; + public hasSameAddressAsSpouse?: string; +} + +export class TattleIndividual { + public personalInformation: TattleSpousePersonalInformation; + public addressInformation?: AddressInformation; + public hasSameAddressAsSpouse?: string; +} + +export class CreditApplication { + public loanId?: string; + public limitRequested: number; + public appDetailId?: string; + public loanType: LoanType; + public decision?: DecisionStatus; + public vehicleInformation?: VehicleCollateralInformation; + public disclosureInformation?: DisclosureInformation; + public collateralInformation?: CollateralInformationGroup; + public lienPosition?: LienPosition; + public hasCoApplicant: string; + public confirmJointCredit?: string; + public hasCoApplicantSpouse: string; + public coApplicantLivesInMaritalPropertyState?: boolean; + public hasBeenCompleted: boolean; + public fullInfoBorrowers: Array; + public purposeOfLoan?: LoanPurpose; + public purposeOfLoanSignature?: string; + public termOfLoan?: string; + public note?: string; + public primaryContactMethod: string; + public promoCode?: string; + public page: ApiPage; + public status: ApiStatus; + public error: ApiError; + public continueLoan?: boolean; + public clientMessage?: string; + public modFeeAccount?: string; + public agreementRequirement?: boolean; + + constructor(loanType: LoanType) { + this.loanType = loanType; + this.hasBeenCompleted = false; + } +} + +export class LienPosition { + public hasFirstMortgage: string; + public hasSecondMortgage: string; + public payoffFirstMortgage: boolean; + public payoffSecondMortgage: boolean; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.spec.ts new file mode 100644 index 0000000..743ced4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.spec.ts @@ -0,0 +1,53 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DataTableService } from './data-table.service'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Data Table Service', () => { + + let service: DataTableService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot() + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [ + + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(DataTableService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should determine that an applicant is a Landmark Credit Union employee', () => { + expect(service.isLandmarkEmployee('landmark')).toBe(true); + expect(service.isLandmarkEmployee('LMCU')).toBe(true); + expect(service.isLandmarkEmployee('LCU')).toBe(true); + expect(service.isLandmarkEmployee('Landmark CU')).toBe(true); + expect(service.isLandmarkEmployee('Landmark Credit')).toBe(true); + expect(service.isLandmarkEmployee('Landmark Credit Union')).toBe(true); + expect(service.isLandmarkEmployee('LMCU')).toBe(true); + expect(service.isLandmarkEmployee('Landmark ')).toBe(true); + expect(service.isLandmarkEmployee('Landmark Banking Services')).toBe(true); + expect(service.isLandmarkEmployee('LandmarkCreditUnion')).toBe(true); + expect(service.isLandmarkEmployee('LandMark')).toBe(true); + + expect(service.isLandmarkEmployee('Blandmark')).toBe(false); + expect(service.isLandmarkEmployee('GLMCU')).toBe(false); + expect(service.isLandmarkEmployee('Landmarc')).toBe(false); + expect(service.isLandmarkEmployee('CLCU')).toBe(false); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.ts new file mode 100644 index 0000000..e929f3c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/data-table.service.ts @@ -0,0 +1,97 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DataTableService { + constructor() { } + + private stateList: {[abbrev: string]: string} = { + 'AA' : 'Armed Forces Americas', + 'AE' : 'Armed Forces Europe', + 'AL' : 'Alabama', + 'AK' : 'Alaska', + 'AP' : 'Armed Forces Pacific', + 'AS' : 'American Samoa', + 'AZ' : 'Arizona', + 'AR' : 'Arkansas', + 'CA' : 'California', + 'CO' : 'Colorado', + 'CT' : 'Connecticut', + 'DE' : 'Delaware', + 'DC' : 'District Of Columbia', + 'FM' : 'Federated States Of Micronesia', + 'FL' : 'Florida', + 'GA' : 'Georgia', + 'GU' : 'Guam', + 'HI' : 'Hawaii', + 'ID' : 'Idaho', + 'IL' : 'Illinois', + 'IN' : 'Indiana', + 'IA' : 'Iowa', + 'KS' : 'Kansas', + 'KY' : 'Kentucky', + 'LA' : 'Louisiana', + 'ME' : 'Maine', + 'MH' : 'Marshall Islands', + 'MD' : 'Maryland', + 'MA' : 'Massachusetts', + 'MI' : 'Michigan', + 'MN' : 'Minnesota', + 'MS' : 'Mississippi', + 'MO' : 'Missouri', + 'MT' : 'Montana', + 'NE' : 'Nebraska', + 'NV' : 'Nevada', + 'NH' : 'New Hampshire', + 'NJ' : 'New Jersey', + 'NM' : 'New Mexico', + 'NY' : 'New York', + 'NC' : 'North Carolina', + 'ND' : 'North Dakota', + 'MP' : 'Northern Mariana Islands', + 'OH' : 'Ohio', + 'OK' : 'Oklahoma', + 'OR' : 'Oregon', + 'PW' : 'Palau', + 'PA' : 'Pennsylvania', + 'PR' : 'Puerto Rico', + 'RI' : 'Rhode Island', + 'SC' : 'South Carolina', + 'SD' : 'South Dakota', + 'TN' : 'Tennessee', + 'TX' : 'Texas', + 'UT' : 'Utah', + 'VT' : 'Vermont', + 'VI' : 'Virgin Islands', + 'VA' : 'Virginia', + 'WA' : 'Washington', + 'WV' : 'West Virginia', + 'WI' : 'Wisconsin', + 'WY' : 'Wyoming' + }; + + private landmarkEmployerNameList = [ + 'LCU', + 'LANDMARK', + 'LANDMARK CREDIT', + 'LANDMARK CREDIT UNION', + 'LANDMARK CU', + 'LMCU' + ]; + + public getStateList(): {[abbrev: string]: string} { + return this.stateList; + } + + public isLandmarkEmployee(employerName: string): boolean { + employerName = employerName.toUpperCase(); + employerName.trim(); + for (let i = 0; i < this.landmarkEmployerNameList.length; i++) { + if (employerName.indexOf(this.landmarkEmployerNameList[i]) === 0) { + return true; + } + } + return false; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/date/date-builder.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/date/date-builder.ts new file mode 100644 index 0000000..1175464 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/date/date-builder.ts @@ -0,0 +1,44 @@ + + +export class DateBuilder { + public static buildPastDate(days: number): Date { + const date = new Date(); + date.setDate(date.getDate() - days); + return date; + } + + public static buildFutureDate(days: number): Date { + const date = new Date(); + date.setDate(date.getDate() + days); + return date; + } + + public static formatDateAsString(date: Date): string { + const month = date.getMonth() + 1; + const day = date.getDate(); + const year = date.getFullYear(); + + const formattedMonth = (month < 10) ? ('0' + month) : month; + const formattedDay = (day < 10) ? ('0' + day) : day; + + return formattedMonth + '/' + formattedDay + '/' + year; + } + + public static formatStringAsDate(date: string): Date { + const dateObj: Date = new Date(); + dateObj.setDate(parseInt(date.substring(3, 5), 10)); + dateObj.setMonth(parseInt(date.substring(0, 2), 10) - 1); + dateObj.setFullYear(parseInt(date.substring(6, 10), 10)); + return dateObj; + } + + public static setBeginOfDay (date: Date): Date { + date.setHours(0, 0, 0, 0); + return date; + } + + public static setEndOfDay (date: Date): Date { + date.setHours(23, 59, 59, 999); + return date; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.html new file mode 100644 index 0000000..83536ac --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.html @@ -0,0 +1,186 @@ +
+

Dealer Portal

+ For password resets, please contact Indirect Lending at Landmark Credit Union: 262.796.4511 +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+ +
+
+
+
+ + + + +
+
+
+
+ +
+
+
+
+ + +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+

+ No matching application record found. Please verify the information entered and try again. Click here to reset the form. +

+
+
+
+ +
+
+
+
+
+ + Transmitting Documents: {{ localState.uploadedFileCount }} of {{ localState.totalFileCount }} +
+
+
+
+
+ +
+
+
+

+ Documents successfully uploaded to Application ID {{ localState.applicationId }}. +

+
+
+
+ +
+
+
+

+ We are unable to complete your request at this time, please try again later. +

+

+ If the problem continues, please contact Indirect Lending at Landmark Credit Union: 262.796.4511 +

+
+
+
+ +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.ts new file mode 100644 index 0000000..d72d8b5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.component.ts @@ -0,0 +1,207 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; + +import { DealershipPortalPageService } from './dealership-portal-page.service'; +import { CreditApplication } from 'src/app/shared/credit-application'; +import { LoanTypeSet } from '../loan-type'; +import { LoanTitle } from '../loan-type/loan-title'; + +@Component({ + selector: 'lw-dealership-portal-page', + templateUrl: './dealership-portal-page.component.html' +}) +export class DealershipPortalComponent implements OnInit, OnDestroy { + protected creditApplication: CreditApplication; + public pageForm: FormGroup; + private statusSubscription: Subscription; + + private readonly FORM_STATE_INVALID = 'INVALID'; + + public localState = { + nextDisabled: true, + previousDisabled: false, + applicationId: null, + cosmosId: null, + enableUploadDocumentsButton: false, + showUploadDocumentsButtonSpinner: false, + showUploadForm: false, + files: [], + isUploading: false, + canTransmit: false, + isTransmitting: false, + transmitSuccess: null, + uploadedFileCount: 0, + totalFileCount: 0 + }; + + constructor( + private dealershipPortalPageService: DealershipPortalPageService, + private route: ActivatedRoute) {} + + public ngOnInit() { + this.initializeFormGroups(); + this.updateLocalState(this.pageForm.value); + + (document.querySelector('lw-header b') as HTMLElement).innerText = 'Call 262.796.4511'; + (document.querySelector('lw-header nav div a') as HTMLElement).setAttribute('href', 'tel:+1-262-796-4511') + + this.creditApplication = new CreditApplication(LoanTypeSet[7]); + this.creditApplication.loanType.title = LoanTitle.Vehicle; + } + + public initializeFormGroups() { + this.pageForm = this.dealershipPortalPageService.createPageGroup(); + + this.statusSubscription = this.pageForm.statusChanges.subscribe((newStatus) => { + this.localState.cosmosId = null; + if(this.pageForm.get('dealershipCode').valid && + this.pageForm.get('dealershipPassword').valid && + this.pageForm.get('applicationId').valid && + this.pageForm.get('applicantLastName').valid) { + this.localState.enableUploadDocumentsButton = true; + } else { + this.localState.enableUploadDocumentsButton = false; + } + }); + } + + ngOnDestroy(): void { + if(this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + public authenticateDealership() { + this.localState.showUploadForm = null; + this.localState.showUploadDocumentsButtonSpinner = true; + + this.disableInputs(); + + this.localState.applicationId = this.pageForm.get('applicationId').value; + this.dealershipPortalPageService.authenticate(this.pageForm.value).toPromise().then(result => { + this.localState.showUploadDocumentsButtonSpinner = false; + if (result['cosmosId'] != null) { + this.localState.cosmosId = result['cosmosId']; + this.localState.showUploadForm = true; + } else { + this.enableInputs(); + this.localState.showUploadForm = false; + this.localState.cosmosId = 'error'; + } + }).catch(error => { + this.localState.showUploadDocumentsButtonSpinner = false; + this.localState.cosmosId = 'error'; + this.localState.showUploadForm = false; + this.enableInputs(); + }); + } + + public enableInputs() { + let elements = document.querySelectorAll("input"); + for(let i = 0; i < elements.length; i++) { + elements[i].disabled = false; + } + } + + public disableInputs() { + let elements = document.querySelectorAll("input"); + for(let i = 0; i < elements.length; i++) { + elements[i].disabled = true; + } + } + + public resetForm() { + this.enableInputs(); + + this.pageForm.get('dealershipPassword').setValue(""); + this.pageForm.get('dealershipPassword').markAsPristine(); + this.pageForm.get('dealershipPassword').markAsUntouched(); + + this.pageForm.get('applicationId').setValue(""); + this.pageForm.get('applicationId').markAsPristine(); + this.pageForm.get('applicationId').markAsUntouched(); + + + this.pageForm.get('applicantLastName').setValue("") + this.pageForm.get('applicantLastName').markAsPristine(); + this.pageForm.get('applicantLastName').markAsUntouched(); + + if(this.pageForm.get('dealershipCode').invalid) { + (document.querySelector('#dealershipCode') as HTMLElement).focus(); + this.pageForm.get('dealershipCode').markAsPristine(); + this.pageForm.get('dealershipCode').setValue(""); + this.pageForm.get('dealershipCode').markAsUntouched(); + } else { + (document.querySelector('#dealershipPassword') as HTMLElement).focus(); + } + + this.localState.showUploadForm = false; + this.localState.canTransmit = false; + this.localState.transmitSuccess = null; + this.localState.isTransmitting = false; + this.localState.files = []; + this.localState.applicationId = null; + this.localState.cosmosId = null; + this.localState.enableUploadDocumentsButton = false; + this.localState.showUploadDocumentsButtonSpinner = false; + } + + public updateLocalState(nextValues: any) { + this.localState = { + nextDisabled: this.localState.nextDisabled, + previousDisabled: false, + applicationId: null, + cosmosId: null, + enableUploadDocumentsButton: false, + showUploadDocumentsButtonSpinner: false, + showUploadForm: false, + files: [], + isUploading: false, + isTransmitting: false, + canTransmit: false, + transmitSuccess: null, + uploadedFileCount: 0, + totalFileCount: 0 + }; + } + + public handleFiles(files) { + // If the file lengths are different, then make transmitSuccess null so that the error message is hidden if files are changed. + if(this.localState.files.length != files.length) { + this.localState.transmitSuccess = null; + } + this.localState.files = files; + this.localState.canTransmit = this.localState.files.filter(file => file.valid == false).length == 0; + } + + public handleUploadingFiles(isUploading) { + this.localState.isUploading = isUploading; + } + + public async transmitFiles() { + let transmissionError = false; + this.localState.uploadedFileCount = 1 + this.localState.totalFileCount = this.localState.files.length + while(this.localState.files.length > 0) { + this.localState.isTransmitting = true; + await this.dealershipPortalPageService.submitFiles(this.localState.cosmosId, this.pageForm.value).toPromise().then(result => { + this.localState.isTransmitting = false; + this.localState.files = result.files; + this.localState.uploadedFileCount += 1 + }).catch(error => { + this.localState.isTransmitting = false; + this.localState.transmitSuccess = false; + transmissionError = true; + }); + } + + if(transmissionError == false) { + this.localState.isTransmitting = false; + this.localState.transmitSuccess = true; + } + + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.service.ts new file mode 100644 index 0000000..d415855 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/dealership-portal/dealership-portal-page.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { CommonGroups } from 'src/app/loan-wizard/field-groups/common-groups' +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; +import { catchError, map } from 'rxjs/operators'; +import { CreditApplication } from '../credit-application'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { ApiTranslatorService } from '../api-translator/api-translator.service'; +// import { PageService } from 'src/app/loan-wizard/'; + +@Injectable({ + providedIn: 'root' +}) +export class DealershipPortalPageService { + + public static readonly FORM_NAME = 'dealershipPortal'; + + constructor( + private router: Router, + private formBuilder: FormBuilder, + private httpClient: HttpClient, + private apiTranslatorService: ApiTranslatorService) { } + + public createPageGroup(): FormGroup { + const group = this.formBuilder.group({}); + + CommonGroups.DEALERSHIP_PORTAL.fields.forEach((eachField) => { + group.addControl(eachField.field, this.formBuilder.control('', eachField.validation.slice())); + }, this); + + return group; + } + + public buildStorageUrl(): string { + return environment.urlBase + '/Storage'; + } + + public buildLoanApplicationUrl(): string { + return environment.urlBase + '/LoanApplication'; + } + + public getApplication(applicationId: string) { + let observable: Observable; + observable = this.httpClient.post(this.buildStorageUrl() + "/DealershipPortal/" + applicationId, null, {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + }) + ); + + return observable; + } + + public authenticate(form: any) { + let observable: Observable; + observable = this.httpClient.post(this.buildLoanApplicationUrl() + "/AuthenticateDealership/", form, {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + }) + ); + + return observable; + } + + public submitFiles(cosmosId: string, form: any) { + let observable: Observable; + observable = this.httpClient.post(this.buildLoanApplicationUrl() + "/SubmitDealershipFiles/" + cosmosId, form, {withCredentials: true}) + .pipe(map(result => { + return result; + }), + catchError(err => { + if (err.status === 500) { + throw new Error('500: Server could not process API response'); + } else { + throw new Error('Bad request'); + } + }) + ); + + return observable; + } + + + public commandBuildApiLoanContainer(application: CreditApplication): ApiLoanContainer { + return this.apiTranslatorService.translateToApiLoanContainer(application); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision-status.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision-status.ts new file mode 100644 index 0000000..675ba8b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision-status.ts @@ -0,0 +1,26 @@ +export interface Stipulation { + id: number; + description: string; +} + +export enum DecisionState { + 'NotStarted' = 'NotStarted', + 'Requested' = 'Requested', + 'Ready' = 'Ready', + 'ErrorSubmit' = 'ErrorSubmit', + 'ErrorDecide' = 'ErrorDecide' +} + +export enum DecisionResult { + 'DecisionStatus_None' = 'DecisionStatus_None', + 'DecisionStatus_Approved' = 'DecisionStatus_Approved', + 'DecisionStatus_Counteroffer' = 'DecisionStatus_Counteroffer', + 'DecisionStatus_Review' = 'DecisionStatus_Review', + 'DecisionStatus_Declined' = 'DecisionStatus_Declined' +} + +export interface DecisionStatus { + state: DecisionState; + result: DecisionResult; + stipulations: Array; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.spec.ts new file mode 100644 index 0000000..c339003 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.spec.ts @@ -0,0 +1,68 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { DecisionService } from './decision.service'; +import { DecisionState } from './decision-status'; + +describe('DecisionService', () => { + + let service: DecisionService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot() + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [] + }); + }); + + beforeEach(() => { + service = TestBed.get(DecisionService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should create empty decision result', () => { + spyOn(service, 'createEmptyDecisionResult').and.callThrough(); + spyOn(service, 'createDecisionResult').and.callThrough(); + const emptyDecisionResult = service.createEmptyDecisionResult(true); + expect(service.createEmptyDecisionResult).toHaveBeenCalled(); + expect(service.createDecisionResult).toHaveBeenCalled(); + expect(emptyDecisionResult.state).toBe(DecisionState.NotStarted); + }); + + it('should create error decision result', () => { + spyOn(service, 'createErrorDecisionResult').and.callThrough(); + spyOn(service, 'createDecisionResult').and.callThrough(); + const errorDecision = service.createErrorDecisionResult(true, DecisionState.ErrorSubmit); + expect(service.createErrorDecisionResult).toHaveBeenCalled(); + expect(errorDecision.state).toBe(DecisionState.ErrorSubmit); + }); + + it('should lookup stipulation', () => { + spyOn(service, 'lookupStipulation').and.callThrough(); + let stipulation = service.lookupStipulation(20); + expect(service.lookupStipulation).toHaveBeenCalled(); + expect(stipulation).toEqual(service.stipulationMap[20]); + stipulation = service.lookupStipulation(58); + expect(service.lookupStipulation).toHaveBeenCalled(); + expect(stipulation).toEqual(service.stipulationMap[58]); + service.lookupStipulation(-2); + expect(service.lookupStipulation).toHaveBeenCalled(); + expect(service.stipulationMap[-2]).toBeFalsy(); + expect(service.lookupStipulation).toThrowError(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.ts new file mode 100644 index 0000000..dafeded --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/decision/decision.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { DecisionResult, DecisionState, DecisionStatus, Stipulation } from './decision-status'; + +@Injectable({ + providedIn: 'root' +}) +export class DecisionService { + + constructor() { } + + public readonly DEFAULT_STIPULATION = 'Additional stipulations apply and will be provided by your lender.'; + + public readonly stipulationMap = { + 20: 'A Personal Guaranty form is required for all business loans', + 58: 'Final Approval is dependent on Appraisal, Title, and Flood', + 59: 'Two years of income verification is required', + 82: 'Landmark Business savings account must be established', + 85: 'Must show proof borrower is a current student', + 92: 'Cards can only be issued to business owner and owner\'s spouse' + }; + public readonly alwaysStipulations = [ + 'All applications are subject to final review and verification by our underwriters' + ]; + + public readonly alwaysNoDecisionStipulations = [ + 'You\'ll be contacted by a loan officer' + ]; + + public createEmptyDecisionResult(shouldDecision: boolean): DecisionStatus { + const emptyDecision = {result: DecisionResult.DecisionStatus_Review, state: DecisionState.NotStarted, stipulations: []}; + return this.createDecisionResult(emptyDecision, shouldDecision); + } + + public createErrorDecisionResult(shouldDecision: boolean, + decisionState: DecisionState): DecisionStatus { + const emptyDecision = {result: '', state: decisionState, stipulations: []}; + return this.createDecisionResult(emptyDecision, shouldDecision); + } + + public createDecisionResult(response: any, shouldDecision: boolean = true): DecisionStatus { + const result = response; + const stipulations: Array = []; + if (!shouldDecision) { + this.alwaysNoDecisionStipulations.forEach((stipulation: string) => { + stipulations.push({ + id: -1, + description: stipulation + }); + }); + } + this.alwaysStipulations.forEach((stipulation: string) => { + stipulations.push({ + id: -1, + description: stipulation + }); + }); + response.stipulations.forEach((stipulation: number) => { + stipulations.push({ + id: stipulation, + description: this.lookupStipulation(stipulation) + }); + }, this); + result.stipulations = stipulations; + return result; + } + + public lookupStipulation(stipulationId: number): string { + return this.stipulationMap[stipulationId] || this.DEFAULT_STIPULATION; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.spec.ts new file mode 100644 index 0000000..83960fb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.spec.ts @@ -0,0 +1,97 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { TestApiLoanContainer } from 'src/app/testing/shared-mocks/test-api-loan-container'; +import { ExportService } from './export.service'; +import { BranchData } from '../configuration/ui-settings'; +import { BranchConfig } from '../../admin/branch-config/branch-config'; + + +describe('Export Service', () => { + + let service: ExportService; + const loanContainer: ApiLoanContainer = TestApiLoanContainer; + const loanContainers: ApiLoanContainer[] = [loanContainer]; + const formattedDataSource = [{ + 'Date': loanContainers[0].date, + 'Branch': 'milwaukee', + 'Status': 'In_Progress', + 'Member #': '1231', + 'Individual #': '1231', + 'Loan Id #': '12341234', + 'Loan Type': 'CC_CashBack', + 'First Name': 'john', + 'Last Name': 'sanders', + 'Email Address': 'f@f.com', + 'Phone Number': '2622221215', + 'Secondary Number': undefined, + 'Work Number': '' + }]; + const branchConfig: BranchConfig = { + id: '5', + date: new Date(), + branchCodesByZip: [ + {zipcode: '53027', channelId: 123456789}, + {zipcode: '53111', channelId: 987654321} + ] + }; + const formattedBranchCodes = + [ + {'ZIP': '53027', 'ChannelId': 123456789}, + {'ZIP': '53111', 'ChannelId': 987654321} + ] + ; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [ + ExportService + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(ExportService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should format a loan data source', () => { + const formattedData = service.formatLoanContainer(loanContainers); + expect(formattedData).toEqual(formattedDataSource); + }); + + it('should format a branch data source', () => { + const formattedData = service.formatBranchContainer(branchConfig.branchCodesByZip); + expect(formattedData).toEqual(formattedBranchCodes); + }); + + it('should construct a CSV from branch data', () => { + const formattedData = service.formatBranchContainer(branchConfig.branchCodesByZip); + const csvData = service.constructCSVData(formattedData, true); + expect(csvData).toEqual('ZIP,ChannelId\r\n53027,123456789\r\n53111,987654321'); + }); + + it('should construct a CSV from branch data, with stringify', () => { + const formattedData = service.formatBranchContainer(branchConfig.branchCodesByZip); + const csvData = service.constructCSVData(formattedData, false); + expect(csvData).toEqual('ZIP,ChannelId\r\n"53027",123456789\r\n"53111",987654321'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.ts new file mode 100644 index 0000000..e7a84fe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/export/export.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import { ApiLoanContainer } from '../api-models/api-loan-container'; +import { saveAs } from 'file-saver'; +import { BranchConfig } from '../../admin/branch-config/branch-config'; + +@Injectable({ + providedIn: 'root' +}) +export class ExportService { + + public buildCSV(data: any, fileName: string, skipStringify?: boolean) { + const csvArray = this.constructCSVData(data, skipStringify); + const blob = new Blob([csvArray], {type: 'text/csv'}); + saveAs(blob, fileName + '.csv'); + } + + public constructCSVData(data: any, skipStringify: boolean): string { + const replacer = (key, value) => value === null ? '' : value; + const header = Object.keys(data[0]); + let csv = []; + if (skipStringify === true) { + csv = data.map(row => header.map(fieldName => row[fieldName]).join(',')); + } else { + csv = data.map(row => header.map(fieldName => JSON.stringify(row[fieldName], replacer)).join(',')); + } + csv.unshift(header.join(',')); + const csvArray = csv.join('\r\n'); + return csvArray; + } + + public formatLoanContainer(dataSource: ApiLoanContainer[]): any { + return dataSource.map(item => { + const container = {}; + container['Date'] = item.date; + container['Branch'] = item.branch; + container['Status'] = item.status; + container['Member #'] = item.memberId; + container['Individual #'] = item.individualId; + container['Loan Id #'] = item.loanId; + container['Loan Type'] = item.loanType; + container['First Name'] = item.firstName; + container['Last Name'] = item.lastName; + container['Email Address'] = item.loanApplication === null ? '' : item.loanApplication.primaryBorrower.email; + container['Phone Number'] = item.loanApplication === null ? '' : item.loanApplication.primaryBorrower.phone; + container['Secondary Number'] = item.loanApplication === null ? '' : item.loanApplication.primaryBorrower.secondaryPhone; + container['Work Number'] = item.loanApplication === null ? '' + : item.loanApplication.primaryBorrower.employers === null ? '' : item.loanApplication.primaryBorrower.employers[0].workPhone; + return container; + }); + } + + public exportLoanContainerAsCSV(dataSource: ApiLoanContainer[]) { + if (dataSource.length > 0) { + this.buildCSV(this.formatLoanContainer(dataSource), 'loan'); + } + } + + public exportBranchConfigAsCSV(dataSource: Array) { + if (dataSource.length > 0) { + this.buildCSV(this.formatBranchContainer(dataSource), 'branch_mapping', true); + } + } + + public formatBranchContainer(dataSource: any[]): any { + return dataSource.map(item => { + const container = {}; + container['ZIP'] = item.zipcode; + container['ChannelId'] = item.channelId; + return container; + }); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.html new file mode 100644 index 0000000..772fbd3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.html @@ -0,0 +1,48 @@ +
+
+
+ +
+
+
+
+
+ + Upload {{ label || description }} + + FORMS.GENERAL.OPTIONAL +
+
+ + + + {{ files.length == 0 ? "0 files added" : files.length + " files added" }} (Max {{ fileLimit }} files) + +
+
+ +
+
+ +
+ +
+ +
+ {{file.error}} +
+
+ +
+
+
\ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.scss new file mode 100644 index 0000000..e252f62 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.scss @@ -0,0 +1,95 @@ +input[type="file"] { + display: none; +} +.upload-disabled { + color: #ccc !important; + cursor: not-allowed !important; +} + +.custom-file-upload { + display: inline-block; + padding: 3px 6px; + color: black; + background-color: #f8f9fa; + cursor: pointer; +} + +.file-upload-header { + background-color: #005674; + color: white; + border: 1px #005674; +} + +.file-upload-footer p { + font-size: small; + font-style: italic; +} + +.file-form-item { + background-color: white; +} + +.unrounded-top { + border-top-left-radius: 0.0rem !important; + border-top-right-radius: 0.0rem !important; +} + +.rounded-last { + input { + border-bottom-left-radius: 0rem !important; + } + button { + border-bottom-right-radius: 0rem !important; + } +} + +.rounded-last:last-child { + input { + border-bottom-left-radius: 0.25rem !important; + } + button { + border-bottom-right-radius: 0.25rem !important; + } +} + +button { + cursor: pointer; +} + +.red-border { + border: 2px red solid; + height: 3.5rem; + padding-bottom: 1rem; +} + +.green-border { + border: 2px green solid; +} + +.file-error { + font-size: small; + color: red; + font-style: italic; + position: absolute; + bottom: 2px; + margin-left: .75rem; +} + +.icon-with-error { + padding-bottom: 10px; +} + +.mute-all { + opacity: 0.4; +} + +.loading-icon { + position: absolute; + left: calc(50% - 24px); + top: calc(50% - 24px); +} + +.optional-text { + margin-left: auto; + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.spec.ts new file mode 100644 index 0000000..5747f31 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.spec.ts @@ -0,0 +1,104 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { configureTestSuite } from 'ng-bullet'; +import { FileUploadComponent } from './file-upload.component'; +import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { InputIconComponent } from '../../loan-wizard/controls/input-icon/input-icon.component'; +import { FileUploadService } from './file-upload.service'; +import { MockFileUploadService } from './file-upload.mock.service'; + +const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => { + const group = formBuilder.group({file: new FormControl()}); + return group; +}; + +describe('FileUploadComponent', () => { + let component: FileUploadComponent; + let fixture: ComponentFixture; + let formBuilder: FormBuilder; + let formGroup: FormGroup; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [FormsModule, ReactiveFormsModule], + declarations: [ + FileUploadComponent, + InputIconComponent + ], + providers: [ + { + provide: FileUploadService, useValue: MockFileUploadService + } + ] + }); + }); + + beforeEach(() => { + formBuilder = TestBed.get(FormBuilder); + formGroup = createTestFormGroup(formBuilder); + fixture = TestBed.createComponent(FileUploadComponent); + component = fixture.componentInstance; + component.fileUploadService = TestBed.get(MockFileUploadService); + component.group = formGroup; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should disable file upload at 5 files', () => { + component.group.get('file').enable() + expect(component.group.get('file').disabled).toBeFalsy(); + component.setLoading(true); + component.addFile('','',''); + component.addFile('','',''); + component.addFile('','',''); + component.addFile('','',''); + component.addFile('','',''); + component.setLoading(false); + expect(component.group.get('file').disabled).toBeTruthy(); + }); + + it('should show an error when a file is too large', () => { + let event = { + target: { + files: [{name: 'test.jpg', size: component.fileSizeLimit*1024*1024 + 1}] + } + } + component.uploadFile(event); + + expect(component.files[0].error).toBeTruthy(); + expect(component.files[0].error).toBe(`File size exceeds ${component.fileSizeLimit} MB`); + }); + + it('should show an error when a file type is invalid', () => { + let event = { + target: { + files: [{name: 'test.exe', size: 10}] + } + } + + component.uploadFile(event); + + expect(component.files[0].error).toBeTruthy(); + expect(component.files[0].error).toBe("Invalid file type"); + }); + + it('should disable the next button when an upload is in progress', () => { + component.setLoading(true); + expect(component.disableNext).toBeTruthy(); + }); + + it('should disable the next button when a selected file is invalid', () => { + let event = { + target: { + files: [{name: 'test.exe', size: 10}] + } + } + + component.uploadFile(event); + + expect(component.disableNext).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.ts new file mode 100644 index 0000000..1090cb9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.component.ts @@ -0,0 +1,268 @@ +import { Component, EventEmitter, Injectable, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { FileUploadService } from './file-upload.service'; + + +export enum AppLoaderState { + LOADER_EMPTY = 'empty', + LOADER_LOANS = 'loans', + LOADER_LOADING = 'loading' +} + +@Component({ + selector: 'lw-file-upload', + templateUrl: './file-upload.component.html', + styleUrls: ['./file-upload.component.scss'] +}) + +@Injectable({ + providedIn: 'root' +}) +export class FileUploadComponent implements OnInit, OnDestroy { + private settingsSubscription: Subscription; + private getFilesSubscription: Subscription; + private uploadFilesSubscription: Subscription; + private deleteFilesSubscription: Subscription; + private deleteAllFilesSubscription: Subscription; + private removeAllFilesSubscription: Subscription; + + public fileUploadService: FileUploadService + + public readonly uploadLimit = 5; + public readonly filesizeLimit = 10; + + @Input() + public loanId: string; + + @Input() + public section: string; + + @Input() + public description: string; + + @Input() + public label: string; + + @Input() + public sectionId = 0; + + @Input() + public group: FormGroup; + + @Input() + public optional = true; + + public file: any; + + @Output() + public sendFiles: EventEmitter; + + @Output() + public uploadingFiles: EventEmitter; + + @Input() + removeAllFilesSubject: Subject>; + + @Output() + public removedAllFiles: EventEmitter; + + public files = []; + + public loading: boolean = false; + public disableNext: boolean = false; + + public fileLimit = 5; + public allowedFileTypes: Array = ["gif", "jpg", "jpeg", "png", "pdf"]; + public fileSizeLimit = 10.0; + + constructor(fileUploadService: FileUploadService) { + this.fileUploadService = fileUploadService; + this.sendFiles = new EventEmitter(); + this.uploadingFiles = new EventEmitter(); + this.removedAllFiles = new EventEmitter(); + } + + ngOnInit() { + this.group = new FormGroup({ + file: new FormControl() + }); + this.getFiles(); + this.getSettings(); + if (this.removeAllFilesSubject) { + this.removeAllFilesSubscription = this.removeAllFilesSubject.subscribe(async resolve => { + this.removeAllFiles(); + }) + } + } + + + public ngOnDestroy() { + if (this.settingsSubscription) { + this.settingsSubscription.unsubscribe(); + } + if (this.getFilesSubscription) { + this.getFilesSubscription.unsubscribe(); + } + if (this.uploadFilesSubscription) { + this.uploadFilesSubscription.unsubscribe(); + } + if (this.deleteFilesSubscription) { + this.deleteFilesSubscription.unsubscribe(); + } + if (this.removeAllFilesSubscription) { + this.removeAllFilesSubscription.unsubscribe(); + } + if (this.deleteAllFilesSubscription) { + this.deleteAllFilesSubscription.unsubscribe(); + } + } + + public getSettings() { + this.settingsSubscription = this.fileUploadService.getSettings().subscribe( + response => { + this.fileLimit = response.fileLimit; + this.allowedFileTypes = response.allowedFileTypes; + this.fileSizeLimit = response.fileSizeLimit; + } + ); + } + + public getFiles() { + this.setLoading(true); + + this.getFilesSubscription = this.fileUploadService.getFiles(this.loanId).subscribe( + response => { + this.files = []; + for (let file of response.files.filter(file => file.description == this.description && (file.section == this.section || file.section == null) && file.sectionId == this.sectionId)) { + this.addFile(file.fileId, file.name, file.description); + } + this.setLoading(false); + } + ); + } + + public checkFileLimit() { + if (this.files.length >= 5) { + this.group.get('file').disable(); + } else { + this.group.get('file').enable(); + } + } + + public uploadFile(e) { + this.setLoading(true); + + if (this.files.length >= this.fileLimit || e.target.files.length == 0) { + this.setLoading(false); + return; + } + + let file = e.target.files[0]; + + // Clear file input + e.target.value = ''; + + let fileErrors = [] + + if (!this.fileSizeIsValid(file)) { + fileErrors.push(`File size exceeds ${this.fileSizeLimit} MB`); + } + + if (!this.fileExtensionIsValid(file)) { + fileErrors.push(`Invalid file type`); + } + + if (fileErrors.length > 0) { + this.addFile(null, file.name, this.description, fileErrors.join("; ")); + this.setLoading(false); + return; + } + + + this.uploadFilesSubscription = this.fileUploadService.uploadFile(this.loanId, this.section, this.sectionId, this.description, file).subscribe( + response => { + this.addFile(response.fileId, file.name, this.description); + this.setLoading(false); + }, (error) => { + this.addFile(null, file.name, this.description, error.message); + this.setLoading(false); + } + ); + } + + public deleteFile(file) { + this.setLoading(true); + + if (file.invalid) { + this.removeFile(file); + this.setLoading(false); + return; + } + + this.deleteFilesSubscription = this.fileUploadService.deleteFile(this.loanId, file.fileId).subscribe( + response => { + this.removeFile(file); + this.setLoading(false); + return true; + } + ); + } + + public addFile(fileId, name, description, error = null) { + this.files.push({ fileId, name, description, valid: error == null, invalid: error != null, dirty: true, error }); + this.sendFiles.emit(this.files); + } + + public removeFile(file) { + this.files = this.files.filter(f => f.name != file.name || f.fileId != file.fileId); + this.sendFiles.emit(this.files); + } + + public setLoading(loading: boolean) { + this.loading = loading; + this.uploadingFiles.emit(loading); + this.disableNext = true; + if (loading) { + this.group.get('file').disable() + } else { + if (this.files.length < this.fileLimit) { + this.group.get('file').enable() + } else { + this.group.get('file').disable() + } + this.disableNext = this.files.filter(file => file.invalid == true).length > 0; + } + } + + public fileExtensionIsValid(file: File): boolean { + let validExtension = false; + this.allowedFileTypes.forEach(extension => { + if (file.name.toLowerCase().endsWith(extension.toLowerCase())) { + validExtension = true; + } + }) + return validExtension; + } + + public fileSizeIsValid(file: File): boolean { + return file.size <= this.fileSizeLimit * 1024 * 1024; + } + + public removeAllFiles() { + if(this.deleteAllFilesSubscription) { + return; + } + + this.deleteAllFilesSubscription = this.fileUploadService.deleteFiles(this.loanId, this.files).subscribe((result) => { + this.files = [] + this.setLoading(false); + this.sendFiles.emit(this.files); + this.removedAllFiles.emit(true); + this.deleteAllFilesSubscription.unsubscribe(); + }); + + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.mock.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.mock.service.ts new file mode 100644 index 0000000..7100a37 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.mock.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { IFileUploadService } from './file-upload.service'; + +@Injectable({ + providedIn: 'root' +}) +export class MockFileUploadService implements IFileUploadService { + + // This is a mock so we don't use HttpClient. + constructor() { } + + getSettings(): Observable { + return of(null); + } + + getFiles(loanId: string): Observable { + return of(null); + } + + uploadFile(loanId: string, section: string, idSuffix: number, description: string, file: File): Observable { + return of(null); + } + + deleteFile(loanId: string, fileId: string): Observable { + return of(null); + } + + deleteFiles(loanId: string, files: any[]): Observable { + return of(null); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.service.ts new file mode 100644 index 0000000..f4cb968 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/file-upload/file-upload.service.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; +import { catchError } from 'rxjs/operators'; + +export interface IFileUploadService { + getSettings(): Observable; + + getFiles(loanId: string): Observable; + + uploadFile(loanId: string, section: string, idSuffix: number, description: string, file: File): Observable; + + deleteFile(loanId: string, fileId: string): Observable; + + deleteFiles(loanId: string, files: any[]): Observable; +} + +@Injectable({ + providedIn: 'root' +}) +export class FileUploadService implements IFileUploadService { + + constructor(private httpClient: HttpClient) { } + + public buildGetSettingsUrl(): string { + return environment.urlBase + `/Storage/Settings` + } + + public buildGetUrl(loanId: string): string { + return environment.urlBase + `/Storage/Documents/${loanId}` + } + + public buildUploadUrl(loanId: string): string { + return environment.urlBase + `/Storage/Documents/${loanId}` + } + + public buildDeleteUrl(loanId: string, fileId: string): string { + return environment.urlBase + `/Storage/Documents/${loanId}/${fileId}` + } + + public buildDeleteFilesUrl(loanId: string): string { + return environment.urlBase + `/Storage/Documents/DeleteAll/${loanId}` + } + + public getSettings(): Observable { + let observable: Observable; + + observable = this.httpClient.get(this.buildGetSettingsUrl(), {withCredentials: true}) + .pipe(catchError(err => { + if (err.status === 500) { + throw new Error('Error when getting settings'); + } else { + if(err.error.error) { + throw new Error(err.error.error); + } else { + throw new Error('Error when getting settings') + } + } + })); + + return observable; + } + + public getFiles(loanId: string): Observable { + let observable: Observable; + + observable = this.httpClient.get(this.buildGetUrl(loanId), {withCredentials: true}) + .pipe(catchError(err => { + if (err.status === 500) { + throw new Error('Error when getting files') + } else { + if(err.error.error) { + throw new Error(err.error.error); + } else { + throw new Error('Error when getting files') + } + } + } + )); + + return observable; + } + + public uploadFile(loanId: string, section: string, idSuffix: number, description: string, file: File): Observable { + let observable: Observable; + + var formdata = new FormData(); + formdata.append("File", file); + formdata.append("Section", section); + formdata.append("SectionId", idSuffix.toString()); + formdata.append("Description", description); + + observable = this.httpClient.post(this.buildUploadUrl(loanId), formdata, {withCredentials: true}) + .pipe(catchError(err => { + if (err.status === 500) { + throw new Error('Error when uploading file'); + } else { + if(err.error.error) { + throw new Error(err.error.error); + } else { + throw new Error('Error when uploading file') + } + } + })); + + return observable; + } + + public deleteFile(loanId: string, fileId: string): Observable { + let observable: Observable; + + observable = this.httpClient.delete(this.buildDeleteUrl(loanId, fileId), {withCredentials: true}) + .pipe(catchError(err => { + throw new Error('Error when deleting file') + })); + + return observable; + } + + public deleteFiles(loanId: string, files: any[]) { + let observable: Observable; + + let fileIds = []; + files.forEach((file) => fileIds.push(file.fileId)) + + var formdata = new FormData(); + formdata.append("files", JSON.stringify(fileIds)); + + observable = this.httpClient.post(this.buildDeleteFilesUrl(loanId), formdata, { withCredentials: true, responseType: "json" }) + return observable; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.spec.ts new file mode 100644 index 0000000..d32ec18 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.spec.ts @@ -0,0 +1,65 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { AdminRoleGuard } from './admin-role.guard'; +import { MsAdalAngular6Module } from 'microsoft-adal-angular6'; +import { SecurityService } from '../security.service'; + +describe('Admin Role Guard', () => { + + let guard: AdminRoleGuard; + let service: SecurityService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + MsAdalAngular6Module.forRoot({ + tenant: '1234567', + clientId: '7654321', + redirectUri: 'http://localhost', + endpoints: {}, + navigateToLoginRequestUrl: false, + cacheLocation: 'localStorage' + }) + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [] + }); + }); + + beforeEach(() => { + guard = TestBed.get(AdminRoleGuard); + service = TestBed.get(SecurityService); + }); + + it('should create', () => { + expect(guard).toBeTruthy(); + }); + + it('should call the role check if user is authenticated', () => { + spyOn(service, 'isAuthenticated').and.returnValue(true); + spyOn(service, 'checkAdminRole').and.returnValue(true); + spyOn(service, 'login').and.returnValue(true); + guard.canActivate(null, null ); + expect( service.checkAdminRole).toHaveBeenCalled(); + expect( service.login).not.toHaveBeenCalled(); + }); + + it('should call the login if user is not authenticated', () => { + spyOn(service, 'isAuthenticated').and.returnValue(false); + spyOn(service, 'checkAdminRole').and.returnValue(true); + spyOn(service, 'login').and.returnValue(true); + guard.canActivate(null, null ); + expect( service.checkAdminRole).not.toHaveBeenCalled(); + expect( service.login).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.ts new file mode 100644 index 0000000..d2c8a8c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/admin-role.guard.ts @@ -0,0 +1,27 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { MsAdalAngular6Service } from 'microsoft-adal-angular6'; +import { SecurityService } from '../security.service'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AdminRoleGuard implements CanActivate, CanActivateChild { + + constructor(private securityService: SecurityService) { } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + if (this.securityService.isAuthenticated()) { + return this.securityService.checkAdminRole(); + } else { + this.securityService.login(); + } + return false; + } + + public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + return this.canActivate(childRoute, state); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.spec.ts new file mode 100644 index 0000000..00e2067 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.spec.ts @@ -0,0 +1,36 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AppTimeoutGuard } from './app-timeout.guard'; +import { configureTestSuite } from 'ng-bullet'; + +describe('App Timeout Guard', () => { + + let service: AppTimeoutGuard; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [] + }); + }); + + beforeEach(() => { + service = TestBed.get(AppTimeoutGuard); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.ts new file mode 100644 index 0000000..a1e371f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/app-timeout.guard.ts @@ -0,0 +1,23 @@ +import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LoanWizardComponent } from '../../loan-wizard/loan-wizard.component'; + +@Injectable({ + providedIn: 'root' +}) +export class AppTimeoutGuard implements CanDeactivate { + + private acceptableRoutes = ['/select-loan', '/admin']; + constructor() { + } + + public canDeactivate( + component: LoanWizardComponent, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + nextState: RouterStateSnapshot + ): Observable | Promise | boolean { + return (this.acceptableRoutes.indexOf(nextState.url) > -1); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.spec.ts new file mode 100644 index 0000000..a23efc0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.spec.ts @@ -0,0 +1,48 @@ +import { GuardManager } from './guard-manager.guard'; +import { AdminRoleGuard } from './admin-role.guard'; +import { AuthenticationGuard } from 'microsoft-adal-angular6'; +import { environment } from 'src/environments/environment'; +import { MemberLoginGuard } from './member-login.guard'; +import { TestRoleGuard } from './test-role.guard'; + + +describe('GuardManager', () => { + let originalTestGuard: boolean; + let originalMemberGuard: boolean; + + beforeEach(() => { + originalTestGuard = environment.testGuard; + originalMemberGuard = environment.memberGuard; + }); + + afterEach(() => { + environment.testGuard = originalTestGuard; + environment.memberGuard = originalMemberGuard; + }); + + it('should determine correct guards for admin', () => { + expect(GuardManager.determineAdminGuards()).toEqual([AuthenticationGuard, AdminRoleGuard]); + }); + + it('should determine correct guards for loanwizard', () => { + environment.memberGuard = true; + environment.testGuard = false; + expect(GuardManager.determineLoanWizardGuards()).toEqual([MemberLoginGuard]); + environment.testGuard = true; + expect(GuardManager.determineLoanWizardGuards()).toEqual([AuthenticationGuard, TestRoleGuard, MemberLoginGuard]); + }); + + it('should determine correct guards for member', () => { + environment.testGuard = false; + expect(GuardManager.determineMemberGuards()).toEqual([]); + environment.testGuard = true; + expect(GuardManager.determineMemberGuards()).toEqual([AuthenticationGuard]); + }); + + it('should determine correct guards select loan', () => { + environment.testGuard = false; + expect(GuardManager.determineSelectLoanGuards()).toEqual([]); + environment.testGuard = true; + expect(GuardManager.determineSelectLoanGuards()).toEqual([AuthenticationGuard, TestRoleGuard]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.ts new file mode 100644 index 0000000..1e0d48d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/guard-manager.guard.ts @@ -0,0 +1,44 @@ +import { environment } from 'src/environments/environment'; +import { AuthenticationGuard } from 'microsoft-adal-angular6'; +import { TestRoleGuard } from './test-role.guard'; +import { MemberLoginGuard } from './member-login.guard'; +import { AdminRoleGuard } from './admin-role.guard'; + +export class GuardManager { + + public static determineLoanWizardGuards(): any[] { + const guards = []; + if (environment.testGuard) { + guards.push(AuthenticationGuard); + guards.push(TestRoleGuard); + } + if (environment.memberGuard) { + guards.push(MemberLoginGuard); + } + return guards; + } + + public static determineAdminGuards(): any[] { + const guards = []; + guards.push(AuthenticationGuard); + guards.push(AdminRoleGuard); + return guards; + } + + public static determineMemberGuards(): any[] { + const guards = []; + if (environment.testGuard) { + guards.push(AuthenticationGuard); + } + return guards; + } + + public static determineSelectLoanGuards(): any[] { + const guards = []; + if (environment.testGuard) { + guards.push(AuthenticationGuard); + guards.push(TestRoleGuard); + } + return guards; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.spec.ts new file mode 100644 index 0000000..d12bbc3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.spec.ts @@ -0,0 +1,43 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { LoanRouteGuard } from './loan-route.guard'; +import { configureTestSuite } from 'ng-bullet'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('Loan Route Guard', () => { + + let service: LoanRouteGuard; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot(), + ReactiveFormsModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [ + ], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(LoanRouteGuard); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.ts new file mode 100644 index 0000000..27dfd9a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/loan-route.guard.ts @@ -0,0 +1,23 @@ +import { ActivatedRouteSnapshot, CanActivateChild, RouterStateSnapshot } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; + +@Injectable({ + providedIn: 'root' +}) +export class LoanRouteGuard implements CanActivateChild { + + constructor(private applicationWizardService: ApplicationWizardService) { + } + + public canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean { + const requestedPage = route.routeConfig.path; + const canVisit = this.applicationWizardService.canVisitPage(requestedPage); + if (canVisit) { this.applicationWizardService.displayWaypointsForPage(requestedPage); } + return canVisit; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.spec.ts new file mode 100644 index 0000000..9779b01 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.spec.ts @@ -0,0 +1,61 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { TranslateModule } from '@ngx-translate/core'; +import { MemberLoginGuard } from './member-login.guard'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterStateSnapshot, Router } from '@angular/router'; +import { CookieUtil } from '../cookie/cookie-util'; +import { environment } from '../../../environments/environment'; + +describe('Member Login Guard', () => { + + let guard: MemberLoginGuard; + + class RouterStub { + url = ''; + navigate(commands: any[], extras?: any) { } + } + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [{ provide: Router, useClass: RouterStub }] + }); + }); + + beforeEach(() => { + guard = TestBed.get(MemberLoginGuard); + }); + + it('should create', () => { + expect(guard).toBeTruthy(); + }); + + it('should resolve route when auth cookie is true', () => { + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(true); + expect(guard.canActivate(null, {url: 'testUrl', root: {queryParams: {}}})).toBe(true); + }); + + it('should reroute when auth cookie is false and nocookie is false', () => { + spyOn(CookieUtil, 'hasAuthCookie').and.returnValue(false); + const originalGuardFlag = environment.memberGuard; + environment.memberGuard = true; + const result = guard.canActivate(null, {url: 'testUrl', root: {queryParams: {}}}); + environment.memberGuard = originalGuardFlag; + expect(result).toBe(false); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.ts new file mode 100644 index 0000000..48691a9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-login.guard.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; +import { Observable } from 'rxjs'; +import { CookieUtil } from '../cookie/cookie-util'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberLoginGuard implements CanActivate, CanActivateChild { + constructor(private router: Router) {} + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + if (environment.memberGuard === false || CookieUtil.hasAuthCookie() || state.root.queryParams['noCookie']) { + return true; + } else { + this.router.navigate(['/member'], { + queryParams: { + redirect: state.url + } + }); + return false; + } + } + + public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + return this.canActivate(childRoute, state); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification-guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification-guard.ts new file mode 100644 index 0000000..7218a36 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification-guard.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { MemberService } from 'src/app/member/member.service'; +import { environment } from '../../../environments/environment'; +import { CookieUtil } from '../cookie/cookie-util'; + +@Injectable({ + providedIn: 'root' +}) +export class MemberVerificationGuard implements CanActivate, CanActivateChild { + private VERIFICATION_ROUTE = '/member/verification'; + + constructor(private memberService: MemberService, private router: Router) {} + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + if (environment.memberGuard) { + return this.routeToVerification(state); + } + return true; + } + + public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + return this.canActivate(childRoute, state); + } + + public routeToVerification(state: RouterStateSnapshot): boolean { + if (this.memberService.shouldShowVerificationPage()) { + this.router.navigate([this.VERIFICATION_ROUTE], { + queryParams: { + redirect: state.url + } + }); + return false; + } + return true; + } +} + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification.guard.spec.ts new file mode 100644 index 0000000..9c9fdb3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/member-verification.guard.spec.ts @@ -0,0 +1,59 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { TranslateModule } from '@ngx-translate/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterStateSnapshot, Router } from '@angular/router'; +import { MemberService } from 'src/app/member/member.service'; +import { MemberVerificationGuard } from './member-verification-guard'; +import { MemberMeta } from 'src/app/member/models/member-meta'; +import { of } from 'rxjs'; + +describe('Member Verification Guard', () => { + + let guard: MemberVerificationGuard; + let service: MemberService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + }); + }); + + beforeEach(() => { + guard = TestBed.get(MemberVerificationGuard); + service = TestBed.get(MemberService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should resolve route when we already have meta data', () => { + spyOn(service, 'shouldShowVerificationPage').and.returnValue(false); + expect(guard.canActivate(null, {url: 'testUrl', root: {queryParams: {}}})).toBe(true); + }); + + it('should route to verification', () => { + spyOn(service, 'shouldShowVerificationPage').and.returnValue(true); + spyOn(router, 'navigate'); + guard.routeToVerification({url: 'testUrl', root: {queryParams: {}}}); + expect(service.shouldShowVerificationPage).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith([ '/member/verification' ], Object({ queryParams: Object({ redirect: 'testUrl' }) })); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.spec.ts new file mode 100644 index 0000000..777f894 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.spec.ts @@ -0,0 +1,67 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { TranslateModule } from '@ngx-translate/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { StepperControllerGuard } from './stepper-controller-guard'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; + +describe('Stepper Controller Guard', () => { + + let guard: StepperControllerGuard; + let service: ApplicationWizardService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + TranslateModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + }); + }); + + beforeEach(() => { + guard = TestBed.get(StepperControllerGuard); + service = TestBed.get(ApplicationWizardService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(guard).toBeTruthy(); + }); + + it('should activate route with loan type', () => { + spyOn(guard, 'hasLoanType').and.returnValue(true); + spyOn(guard, 'setLoanType'); + const result = guard.canActivate(null, null); + expect(result).toBe(true); + expect(guard.hasLoanType).toHaveBeenCalled(); + expect(guard.setLoanType).toHaveBeenCalled(); + }); + + it('should route home without loan type', () => { + spyOn(guard, 'hasLoanType').and.returnValue(false); + spyOn(guard, 'routeHome'); + expect(guard.canActivate(null, null)).toBe(false); + expect(guard.hasLoanType).toHaveBeenCalled(); + expect(guard.routeHome).toHaveBeenCalled(); + }); + + it('should route home', () => { + spyOn(router, 'navigate'); + guard.routeHome(); + expect(router.navigate).toHaveBeenCalledWith([ '/' ]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.ts new file mode 100644 index 0000000..d5953a2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/stepper-controller-guard.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { ApplicationWizardService } from 'src/app/loan-wizard/application-wizard.service'; + +@Injectable({ + providedIn: 'root' +}) +export class StepperControllerGuard implements CanActivate { + constructor(private router: Router, private applicationWizardService: ApplicationWizardService) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + if (this.hasLoanType(route)) { + this.setLoanType(route); + return true; + } + this.routeHome(); + return false; + } + + public hasLoanType(route: ActivatedRouteSnapshot): boolean { + if (route.paramMap.get('type')) { + return true; + } else { + return false; + } + } + + public setLoanType(route: ActivatedRouteSnapshot): void { + this.applicationWizardService.setLoanType(route.paramMap.get('type')); + } + + public routeHome(): void { + this.router.navigate(['/']); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/test-role.guard.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/test-role.guard.ts new file mode 100644 index 0000000..a339bb5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/test-role.guard.ts @@ -0,0 +1,27 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { MsAdalAngular6Service } from 'microsoft-adal-angular6'; +import { SecurityService } from '../security.service'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class TestRoleGuard implements CanActivate, CanActivateChild { + + constructor(private securityService: SecurityService) { } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + if (this.securityService.isAuthenticated()) { + return this.securityService.checkTesterRole(); + } else { + this.securityService.login(); + } + return false; + } + + public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable { + return this.canActivate(childRoute, state); + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/tester-role.guard.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/tester-role.guard.spec.ts new file mode 100644 index 0000000..04b59cc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/guards/tester-role.guard.spec.ts @@ -0,0 +1,65 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { MsAdalAngular6Module } from 'microsoft-adal-angular6'; +import { SecurityService } from '../security.service'; +import { TestRoleGuard } from './test-role.guard'; + +describe('Test Role Guard', () => { + + let guard: TestRoleGuard; + let service: SecurityService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + MsAdalAngular6Module.forRoot({ + tenant: '1234567', + clientId: '7654321', + redirectUri: 'http://localhost', + endpoints: {}, + navigateToLoginRequestUrl: false, + cacheLocation: 'localStorage' + }) + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [], + providers: [] + }); + }); + + beforeEach(() => { + guard = TestBed.get(TestRoleGuard); + service = TestBed.get(SecurityService); + }); + + it('should create', () => { + expect(guard).toBeTruthy(); + }); + + it('should call the role check if user is authenticated', () => { + spyOn(service, 'isAuthenticated').and.returnValue(true); + spyOn(service, 'checkTesterRole').and.returnValue(true); + spyOn(service, 'login').and.returnValue(true); + guard.canActivate(null, null ); + expect( service.checkTesterRole).toHaveBeenCalled(); + expect( service.login).not.toHaveBeenCalled(); + }); + + it('should call the login if user is not authenticated', () => { + spyOn(service, 'isAuthenticated').and.returnValue(false); + spyOn(service, 'checkTesterRole').and.returnValue(true); + spyOn(service, 'login').and.returnValue(true); + guard.canActivate(null, null ); + expect( service.checkTesterRole).not.toHaveBeenCalled(); + expect( service.login).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/http-error-reporter.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/http-error-reporter.service.ts new file mode 100644 index 0000000..81cbeac --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/http-error-reporter.service.ts @@ -0,0 +1,33 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class HttpErrorReporter implements ErrorHandler { + + constructor(private httpClient: HttpClient) {} + + public buildErrorURL(): string { + return environment.urlBase + '/Logging/Error'; + } + + public handleError(error: Error) { + if (error['promise'] && error['rejection']) { + // Promise rejection wrapped by zone.js + error = error['rejection']; + } + + if(environment.name === 'local') { + console.log(error) + } else { + this.httpClient.post(this.buildErrorURL(), { + name: 'LoanApp Client Error', + message: error.message, + stacktrace: error.stack + }).subscribe(() => {}); + } + + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits-information.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits-information.ts new file mode 100644 index 0000000..5591c08 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits-information.ts @@ -0,0 +1,4 @@ +export enum IncomeSchedule { + HOURLY = 'Hourly', + ANNUALLY = 'Annually' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.spec.ts new file mode 100644 index 0000000..fc560ef --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; +import { IncomeLimitsService } from './income-limits.service'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { LoanTypeSet } from '../loan-type'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { UiSettings } from '../configuration/ui-settings'; +import { DefaultSettings } from '../configuration/default-settings.mock'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('Income Limits Service', () => { + + let applicationWizardService: ApplicationWizardService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + }); + + it('should create', () => { + expect(IncomeLimitsService).toBeTruthy(); + }); + + it('should create a minimum value validator', () => { + spyOn(IncomeLimitsService, 'minValueValidator'); + + const validator = IncomeLimitsService.minValueValidator(defaultSettings.incomeLimits); + expect(IncomeLimitsService.minValueValidator).toHaveBeenCalled(); + }); + + it('should create a maximum value validator', () => { + spyOn(IncomeLimitsService, 'maxValueValidator'); + + const validator = IncomeLimitsService.maxValueValidator(defaultSettings.incomeLimits); + expect(IncomeLimitsService.maxValueValidator).toHaveBeenCalled(); + }); + + it('should not allow a value below the minimum, and allow above the minimum', () => { + + const loanMin = 0; + const oneBelowMin = loanMin - 1; + const oneAboveMin = loanMin + 1; + const validator = IncomeLimitsService.minValueValidator(defaultSettings.incomeLimits); + expect(validator(new FormControl(oneBelowMin))).toEqual({incomeValueMinimum: {min: loanMin, actual: oneBelowMin}}); + expect(validator(new FormControl(loanMin))).toEqual(null); + expect(validator(new FormControl(oneAboveMin))).toEqual(null); + }); + + it('should not allow a value above the maximum, and allow below the maximum', () => { + const loanMax = 99999999999; + const oneBelowMax = loanMax - 1; + const oneAboveMax = loanMax + 1; + const validator = IncomeLimitsService.maxValueValidator(defaultSettings.incomeLimits); + + expect(validator(new FormControl(oneAboveMax))).toEqual({incomeValueMaximum: {max: loanMax, actual: oneAboveMax}}); + expect(validator(new FormControl(loanMax))).toEqual(null); + expect(validator(new FormControl(oneBelowMax))).toEqual(null); + }); + + it('should not allow negative values', () => { + const loanMin = 0; + const minValidator = IncomeLimitsService.minValueValidator(defaultSettings.incomeLimits); + + expect(minValidator(new FormControl(loanMin))).toEqual(null); + expect(minValidator(new FormControl(-1))).toEqual({incomeValueMinimum: {min: loanMin, actual: -1}}); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.ts new file mode 100644 index 0000000..c78bed6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/income-limit/income-limits.service.ts @@ -0,0 +1,64 @@ +import { AbstractControl, ValidatorFn, FormControl, FormGroup } from '@angular/forms'; +import { MaskingService } from '../masking/masking.service'; +import { IncomeSchedule } from './income-limits-information'; +import { IncomeLimits } from '../configuration/ui-settings'; + + +export class IncomeLimitsService { + + private static getSchedule(control: AbstractControl): IncomeSchedule { + const parent = control.parent; + if (parent) { + if (parent.value) { + return parent.get('schedule').value; + } + } + + return null; + } + + private static getDefaultLimit() { + return {min: 0, max: 99999999999}; + } + + public static minValueValidator(incomeLimts: IncomeLimits): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + const value = MaskingService.unMaskDecimalNumber(control.value); + const schedule = IncomeLimitsService.getSchedule(control); + let minValue; + if (schedule) { + minValue = incomeLimts[schedule].min; + } else { + minValue = IncomeLimitsService.getDefaultLimit().min; + } + if (value >= minValue) { + return null; + } else { + return {'incomeValueMinimum': {min: minValue, actual: value}}; + } + }; + } + + public static maxValueValidator(incomeLimts: IncomeLimits): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + const value = MaskingService.unMaskDecimalNumber(control.value); + const schedule = IncomeLimitsService.getSchedule(control); + + let maxValue; + if (schedule) { + maxValue = incomeLimts[schedule].max; + } else { + maxValue = IncomeLimitsService.getDefaultLimit().max; + } + if (value <= maxValue) { + return null; + } else { + return {'incomeValueMaximum': {max: maxValue, actual: value}}; + } + }; + } + +} + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.html new file mode 100644 index 0000000..b31f658 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.html @@ -0,0 +1,45 @@ +
+ + + +

+ + +
+ +

+
+
+
+
+ + + +

DISCLOSURE.CREDIT_CARD.TITLES.ACCEPTANCE

+ + +
+ +

DISCLOSURE.CREDIT_CARD.ACCEPTANCE

+
+
+
+
+ + + +

DISCLOSURE.HELOC.HEADER.REVIEW

+ + +
+ +
+ +
+
+ +
+
+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.spec.ts new file mode 100644 index 0000000..451a9c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { configureTestSuite } from 'ng-bullet'; + +import { DisclosureBoxesComponent } from './disclosure-boxes.component'; +import { CreditCardDisclosuresPageService } from 'src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service'; + +class CreditCardDisclosuresPageServiceStub { + requestWhatYouShouldKnowPdf = jasmine.createSpy('requestWhatYouShouldKnowPdf').and.returnValue(of(new Blob())); + requestImportantTermsPdf = jasmine.createSpy('requestImportantTermsPdf').and.returnValue(of(new Blob())); +} + +describe('DisclosureBoxesComponent', () => { + let component: DisclosureBoxesComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [DisclosureBoxesComponent], + imports: [ + NgbModule.forRoot(), + RouterTestingModule, + TranslateModule.forRoot() + ], + providers: [{ provide: CreditCardDisclosuresPageService, useClass: CreditCardDisclosuresPageServiceStub }] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DisclosureBoxesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.ts new file mode 100644 index 0000000..5abc9d5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/disclosure-boxes.component.ts @@ -0,0 +1,58 @@ +import { Component, Input, OnInit} from '@angular/core'; +import { NgbPanelChangeEvent } from '@ng-bootstrap/ng-bootstrap'; +import { CreditCardDisclosuresPageService } from 'src/app/loan-wizard/wizards/credit-card/credit-card-disclosures-page.service'; + +export interface AccordionValues { + id: string; + show: boolean; +} + +@Component({ + selector: 'lw-disclosure-boxes', + templateUrl: './disclosure-boxes.component.html' +}) +export class DisclosureBoxesComponent { + + @Input() + public showMarriedCoapplicantDisclosure: boolean; + @Input() + public isHeloc: boolean; + + public accordionArray: Array = [ + {id: 'spouse-panel', show: true}, + {id: 'accept-panel', show: true}, + {id: 'review-panel', show: true} + ]; + + constructor( + private service: CreditCardDisclosuresPageService + ) { + this.showMarriedCoapplicantDisclosure = false; + this.isHeloc = false; + } + + public updateAccordion($event: NgbPanelChangeEvent) { + this.accordionArray.forEach( (accordion) => { + if (accordion.id === $event.panelId) { + accordion.show = $event.nextState; + } + }); + } + + public openFileKnow(){ + var fileURL: string; + this.service.requestWhatYouShouldKnowPdf().subscribe((response)=>{ + let file = new Blob([response], { type: 'application/pdf' }); + fileURL = URL.createObjectURL(file); + window.open(fileURL); + }); + } + + public openFileCredit(){ + var fileURL: string; + this.service.requestImportantTermsPdf().subscribe((response)=>{ + let file = new Blob([response], { type: 'application/pdf' }); + fileURL = URL.createObjectURL(file); + window.open(fileURL); + }); } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.html new file mode 100644 index 0000000..ab4ba0e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.html @@ -0,0 +1 @@ + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.scss new file mode 100644 index 0000000..a7f8069 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.scss @@ -0,0 +1,3 @@ +.legal-text { + margin: 1.0em; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.spec.ts new file mode 100644 index 0000000..6856585 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { Component } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; +import { TcpaComponent } from './tcpa.component'; + +@Component({ + selector: `lw-test-parent-component`, + template: `` +}) +class TestParentComponent { +} + +describe('Legal Message Component', () => { + let component: TestParentComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ NgbModule.forRoot(), TranslateModule.forRoot()], + declarations: [ TcpaComponent, TestParentComponent ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestParentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.ts new file mode 100644 index 0000000..d94eff9 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/legal-message/tcpa.component.ts @@ -0,0 +1,12 @@ +import { Component, } from '@angular/core'; + +@Component({ + selector: 'lw-tcpa', + templateUrl: './tcpa.component.html', + styleUrls: ['./tcpa.component.scss'] +}) +export class TcpaComponent { + + constructor() { } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limit.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limit.ts new file mode 100644 index 0000000..9ec617c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limit.ts @@ -0,0 +1,4 @@ +export class MinMaxLimit { + min: number; + max: number; + } \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.spec.ts new file mode 100644 index 0000000..7764f3d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.spec.ts @@ -0,0 +1,105 @@ +import { TestBed } from '@angular/core/testing'; +import { LoanLimitsService } from './loan-limits.service'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { LoanTypeSet } from '../loan-type'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { ConfigurationService } from '../configuration/configuration.service'; +import { UiSettings } from '../configuration/ui-settings'; +import { DefaultSettings } from '../configuration/default-settings.mock'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('Loan Limits Service', () => { + + let applicationWizardService: ApplicationWizardService; + let configurationService: ConfigurationService; + const defaultSettings: UiSettings = DefaultSettings; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + TranslateModule.forRoot() + ], + declarations: [ + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ] + }); + }); + + beforeEach(() => { + applicationWizardService = TestBed.get(ApplicationWizardService); + applicationWizardService.setLoanType(LoanTypeSet[0].id); + configurationService = TestBed.get(ConfigurationService); + configurationService.saveConfiguration(defaultSettings); + }); + + it('should create', () => { + expect(LoanLimitsService).toBeTruthy(); + }); + + it('should create a minimum value validator', () => { + spyOn(LoanLimitsService, 'minValueValidator'); + const loanType = applicationWizardService.getGroupValues('loanType'); + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const validator = LoanLimitsService.minValueValidator(loanCategory, loanType, configurationService); + expect(LoanLimitsService.minValueValidator).toHaveBeenCalled(); + }); + + it('should create a maximum value validator', () => { + spyOn(LoanLimitsService, 'maxValueValidator'); + const loanType = applicationWizardService.getGroupValues('loanType'); + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const validator = LoanLimitsService.maxValueValidator(loanCategory, loanType, configurationService); + expect(LoanLimitsService.maxValueValidator).toHaveBeenCalled(); + }); + + it('should not allow a value below the minimum, and allow above the minimum', () => { + applicationWizardService.setLoanType(LoanTypeSet[4].id); + const loanType = applicationWizardService.getGroupValues('loanType'); + const loanMin = configurationService.getConfigurationByField('limits')[loanType.apiCode].min; + const oneBelowMin = loanMin - 1; + const oneAboveMin = loanMin + 1; + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const validator = LoanLimitsService.minValueValidator(loanCategory, loanType, configurationService); + + expect(validator(new FormControl(oneBelowMin))).toEqual({loanValueMinimum: {min: loanMin, actual: oneBelowMin}}); + expect(validator(new FormControl(loanMin))).toEqual(null); + expect(validator(new FormControl(oneAboveMin))).toEqual(null); + }); + + it('should not allow a value above the maximum, and allow below the maximum', () => { + applicationWizardService.setLoanType(LoanTypeSet[4].id); + const loanType = applicationWizardService.getGroupValues('loanType'); + const loanMax = configurationService.getConfigurationByField('limits')[loanType.apiCode].max; + const oneBelowMax = loanMax - 1; + const oneAboveMax = loanMax + 1; + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const validator = LoanLimitsService.maxValueValidator(loanCategory, loanType, configurationService); + expect(validator(new FormControl(oneAboveMax))).toEqual({loanValueMaximum: {max: loanMax, actual: oneAboveMax}}); + expect(validator(new FormControl(loanMax))).toEqual(null); + expect(validator(new FormControl(oneBelowMax))).toEqual(null); + }); + + it('should not allow negative values', () => { + applicationWizardService.setLoanType(LoanTypeSet[4].id); + const loanType = applicationWizardService.getGroupValues('loanType'); + const loanMin = configurationService.getConfigurationByField('limits')[loanType.apiCode].min; + const loanCategory = applicationWizardService.getFieldValue('loanType').category; + const minValidator = LoanLimitsService.minValueValidator(loanCategory, loanType, configurationService); + expect(minValidator(new FormControl(loanMin))).toEqual(null); + expect(minValidator(new FormControl(-1))).toEqual({loanValueMinimum: {min: loanMin, actual: -1}}); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.ts new file mode 100644 index 0000000..d32fa29 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-limit/loan-limits.service.ts @@ -0,0 +1,55 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { MaskingService } from '../masking/masking.service'; +import { Injectable } from '@angular/core'; +import { LoanType } from '../loan-type'; +import { LoanCategory } from '../loan-type/loan-category'; +import { ConfigurationService } from '../configuration/configuration.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class LoanLimitsService { + + public static minValueValidator(loanCategory: LoanCategory, loanType: LoanType, configurationService: ConfigurationService): ValidatorFn { + if (loanCategory !== LoanCategory.CreditCard) { + return (control: AbstractControl): { [key: string]: any } | null => { + const value = MaskingService.unMaskNumber(control.value); + const minValue = configurationService.getConfigurationByField('limits')[loanType.apiCode].min; + if (value >= minValue) { + return null; + } else { + return {'loanValueMinimum': {min: minValue, actual: value}}; + } + }; + } else { + return (control: AbstractControl): { [key: string]: any } | null => { + const value = MaskingService.unMaskNumber(control.value); + if (value === null) { + return null; + } + }; + } + } + + public static maxValueValidator(loanCategory: LoanCategory, loanType: LoanType, configurationService: ConfigurationService): ValidatorFn { + if (loanCategory !== LoanCategory.CreditCard) { + return (control: AbstractControl): { [key: string]: any } | null => { + const value = MaskingService.unMaskNumber(control.value); + const maxValue = configurationService.getConfigurationByField('limits')[loanType.apiCode].max; + if (value <= maxValue) { + return null; + } else { + return {'loanValueMaximum': {max: maxValue, actual: value}}; + } + }; + } else { + return (control: AbstractControl): { [key: string]: any } | null => { + const value = MaskingService.unMaskNumber(control.value); + if (value === null) { + return null; + } + }; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type.ts new file mode 100644 index 0000000..3579375 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type.ts @@ -0,0 +1,314 @@ +import { CreditCardPages } from '../loan-wizard/wizards/credit-card/credit-card-outline'; +import { EquityPages } from '../loan-wizard/wizards/equity/equity-outline'; +import { ApplicationPage } from '../loan-wizard/application-page'; +import { VehiclePages } from '../loan-wizard/wizards/vehicle/vehicle-outline'; +import { SignatureLoanPages } from '../loan-wizard/wizards/signature/signature-loan-outline'; +import { LoanTitle } from './loan-type/loan-title'; +import { LoanCode } from './loan-type/loan-code'; +import { LoanPurpose } from './loan-type/loan-purpose'; +import { LoanCategory } from './loan-type/loan-category'; + +export class LoanType { + id: string; + category: string; + title: LoanTitle; + icon: ApplicationTypeIcon; + iconClass: ApplicationIconClass; + imagePath?: string; + apiCode?: LoanCode; + message: string; + headerMessageKey: string; + shouldDecision: boolean; + pages: Array; + defaultPurpose: LoanPurpose; + requiresCollateral: boolean; + collateralType?: CollateralType; + decisionReviewKey: string; + beforeYouBeginKey?: string; + showProofOfIncomeUploadForm?: boolean; +} + +export enum CollateralType { + Boat = 'boat', + Vehicle = 'vehicle', + Home = 'home' +} + +export enum ApplicationTypeIcon { + CREDIT_CARD = 'credit-card', + HOUSE = 'home', + VEHICLE = 'car', + BOAT = 'boat', + PWC = 'pwc', + TRAIL_MH = 'tmh', + SIGN = 'signature', + SPECIAL = 'heart' +} + +export enum ApplicationIconClass { + CREDIT_CARD = 'fa fa-credit-card', + HOUSE = 'fa fa-home', + VEHICLE = 'fa fa-car', + BOAT = 'fa fa-ship', + PWC = 'fa fa-motorcycle', + TRAIL_MH = 'fa fa-truck', + SIGN = 'fa fa-pencil-alt', + SPECIAL = 'fa fa-heart-alt' +} + +export const LoanTypeSet: Array = [ + { + id: 'cb', + category: LoanCategory.CreditCard, + title: LoanTitle.CashBack, + apiCode: LoanCode.CC_CashBack, + icon: ApplicationTypeIcon.CREDIT_CARD, + iconClass: ApplicationIconClass.CREDIT_CARD, + imagePath: 'assets/cc-images/LCU_cashback.png', + message: 'COPY.TYPE_MESSAGE.CREDITCARD_CASHBACK', + headerMessageKey: 'HEADER.LOAN_MESSAGE.CREDITCARD_CASHBACK', + shouldDecision: true, + pages: CreditCardPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: false, + decisionReviewKey: 'TEXT_CC' + }, + { + id: 're', + category: LoanCategory.CreditCard, + title: LoanTitle.Rewards, + apiCode: LoanCode.CC_Rewards, + icon: ApplicationTypeIcon.CREDIT_CARD, + iconClass: ApplicationIconClass.CREDIT_CARD, + imagePath: 'assets/cc-images/LCU_rewards.png', + message: 'COPY.TYPE_MESSAGE.CREDITCARD_REWARDS', + headerMessageKey: 'HEADER.LOAN_MESSAGE.CREDITCARD_REWARDS', + shouldDecision: true, + pages: CreditCardPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: false, + decisionReviewKey: 'TEXT_CC' + }, + { + id: 'st', + category: LoanCategory.CreditCard, + title: LoanTitle.Student, + apiCode: LoanCode.CC_Student, + icon: ApplicationTypeIcon.CREDIT_CARD, + iconClass: ApplicationIconClass.CREDIT_CARD, + imagePath: 'assets/cc-images/LCU_student.png', + message: 'COPY.TYPE_MESSAGE.CREDITCARD_STUDENT', + headerMessageKey: 'HEADER.LOAN_MESSAGE.CREDITCARD_STUDENT', + shouldDecision: true, + pages: CreditCardPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: false, + decisionReviewKey: 'TEXT_CC', + beforeYouBeginKey: 'BEFOREYOUBEGIN.CREDITCARD_STUDENT', + showProofOfIncomeUploadForm: true + }, + { + id: 'se', + category: LoanCategory.CreditCard, + title: LoanTitle.Secure, + apiCode: LoanCode.CC_Secure, + icon: ApplicationTypeIcon.CREDIT_CARD, + iconClass: ApplicationIconClass.CREDIT_CARD, + imagePath: 'assets/cc-images/LCU_secured.png', + message: 'COPY.TYPE_MESSAGE.CREDITCARD_SECURE', + headerMessageKey: 'HEADER.LOAN_MESSAGE.CREDITCARD_SECURE', + shouldDecision: true, + pages: CreditCardPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: false, + decisionReviewKey: 'TEXT_CC', + showProofOfIncomeUploadForm: false + }, + { + id: 'he', + category: LoanCategory.Equity, + title: LoanTitle.HELOC, + apiCode: LoanCode.EQ_HELOC_Flex, + icon: ApplicationTypeIcon.HOUSE, + iconClass: ApplicationIconClass.HOUSE, + message: 'COPY.TYPE_MESSAGE.EQUITY_HELOC_FLEX', + headerMessageKey: 'HEADER.LOAN_MESSAGE.EQUITY_HELOC_FLEX', + shouldDecision: true, + pages: EquityPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: true, + collateralType: CollateralType.Home, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.EQUITY_HELOC_FLEX', + showProofOfIncomeUploadForm: true + }, + { + id: 'hef', + category: LoanCategory.Equity, + title: LoanTitle.HomeEquityFixed, + apiCode: LoanCode.EQ_HELOC_Fixed, + icon: ApplicationTypeIcon.HOUSE, + iconClass: ApplicationIconClass.HOUSE, + message: 'COPY.TYPE_MESSAGE.EQUITY_HELOC_FIXED', + headerMessageKey: 'HEADER.LOAN_MESSAGE.EQUITY_HELOC_FIXED', + shouldDecision: true, + pages: EquityPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: true, + collateralType: CollateralType.Home, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.EQUITY_HELOC_FIXED', + showProofOfIncomeUploadForm: true + }, + { + id: 'mtg', + category: LoanCategory.Equity, + title: LoanTitle.MortgageRefinanceExpress, + apiCode: LoanCode.MTG_REFI_Loan, + icon: ApplicationTypeIcon.HOUSE, + iconClass: ApplicationIconClass.HOUSE, + message: 'COPY.TYPE_MESSAGE.EQUITY_MORTGAGE_REF_EXPRESS', + headerMessageKey: 'HEADER.LOAN_MESSAGE.EQUITY_MORTGAGE_REF_EXPRESS', + shouldDecision: true, + pages: EquityPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: true, + collateralType: CollateralType.Home, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.EQUITY_MORTGAGE_REF_EXPRESS', + showProofOfIncomeUploadForm: true + }, + { + id: 'auto', + category: LoanCategory.Vehicle, + title: LoanTitle.Vehicle, + apiCode: LoanCode.VEHICLE_DEFAULT, + icon: ApplicationTypeIcon.VEHICLE, + iconClass: ApplicationIconClass.VEHICLE, + message: 'COPY.TYPE_MESSAGE.VEHICLE_AUTO', + headerMessageKey: 'HEADER.LOAN_MESSAGE.VEHICLE_AUTO', + shouldDecision: true, + pages: VehiclePages, + defaultPurpose: LoanPurpose.Purchase, + requiresCollateral: true, + collateralType: CollateralType.Vehicle, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.VEHICLE_AUTO' + }, + // { + // id: 'mhl', + // category: LoanCategory.Equity, + // title: LoanTitle.ManufacturedHome, + // apiCode: LoanCode.MANUFACTURED_HOME_LOAN, + // icon: ApplicationTypeIcon.HOUSE, + // iconClass: ApplicationIconClass.HOUSE, + // message: 'COPY.TYPE_MESSAGE.EQUITY_MANUF_HOME', + // headerMessageKey: 'HEADER.LOAN_MESSAGE.EQUITY_MANUF_HOME', + // shouldDecision: true, + // pages: EquityPages, + // defaultPurpose: LoanPurpose.Other, + // requiresCollateral: true, + // collateralType: CollateralType.Home, + // decisionReviewKey: 'TEXT', + // beforeYouBeginKey: 'BEFOREYOUBEGIN.EQUITY_MANUFACTURED' + // }, + { + id: 'pauto', + category: LoanCategory.Vehicle, + title: LoanTitle.VehiclePreApproval, + apiCode: LoanCode.VEHICLE_PRE_APPROVAL, + icon: ApplicationTypeIcon.VEHICLE, + iconClass: ApplicationIconClass.VEHICLE, + message: 'COPY.TYPE_MESSAGE.VEHICLE_PRE_APPROVAL', + headerMessageKey: 'HEADER.LOAN_MESSAGE.VEHICLE_PRE_APPROVAL', + shouldDecision: true, + pages: VehiclePages, + defaultPurpose: LoanPurpose.Purchase, + requiresCollateral: false, + collateralType: CollateralType.Vehicle, + decisionReviewKey: 'TEXT' + }, + { + id: 'boat', + category: LoanCategory.Vehicle, + title: LoanTitle.Boat, + apiCode: LoanCode.BOAT, + icon: ApplicationTypeIcon.BOAT, + iconClass: ApplicationIconClass.BOAT, + message: 'COPY.TYPE_MESSAGE.VEHICLE_BOAT', + headerMessageKey: 'HEADER.LOAN_MESSAGE.VEHICLE_BOAT', + shouldDecision: true, + pages: VehiclePages, + defaultPurpose: LoanPurpose.Purchase, + requiresCollateral: true, + collateralType: CollateralType.Boat, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.VEHICLE_BOAT' + }, + { + id: 'pwc', + category: LoanCategory.Vehicle, + title: LoanTitle.PowerSport, + apiCode: LoanCode.POWERSPORT, + icon: ApplicationTypeIcon.PWC, + iconClass: ApplicationIconClass.PWC, + message: 'COPY.TYPE_MESSAGE.VEHICLE_POWERSPORT', + headerMessageKey: 'HEADER.LOAN_MESSAGE.VEHICLE_POWERSPORT', + shouldDecision: true, + pages: VehiclePages, + defaultPurpose: LoanPurpose.Purchase, + requiresCollateral: true, + collateralType: CollateralType.Vehicle, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.VEHICLE_POWERSPORT' + }, + { + id: 'tmh', + category: LoanCategory.Vehicle, + title: LoanTitle.TrailerMotorhome, + apiCode: LoanCode.TRAILER_MOTORHOME, + icon: ApplicationTypeIcon.TRAIL_MH, + iconClass: ApplicationIconClass.TRAIL_MH, + message: 'COPY.TYPE_MESSAGE.VEHICLE_MOTORHOME', + headerMessageKey: 'HEADER.LOAN_MESSAGE.VEHICLE_MOTORHOME', + shouldDecision: true, + pages: VehiclePages, + defaultPurpose: LoanPurpose.Purchase, + requiresCollateral: true, + collateralType: CollateralType.Vehicle, + decisionReviewKey: 'TEXT', + beforeYouBeginKey: 'BEFOREYOUBEGIN.VEHICLE_MOTORHOME' + }, + { + id: 'pe', + category: LoanCategory.Signature, + title: LoanTitle.Signature, + apiCode: LoanCode.SIGNATURE, + icon: ApplicationTypeIcon.SIGN, + iconClass: ApplicationIconClass.SIGN, + message: 'COPY.TYPE_MESSAGE.SIGNATURE_LOAN', + headerMessageKey: 'HEADER.LOAN_MESSAGE.SIGNATURE_LOAN', + shouldDecision: true, + pages: SignatureLoanPages, + defaultPurpose: LoanPurpose.Other, + requiresCollateral: false, + decisionReviewKey: 'TEXT' + }, + // { + // id: 'herm', + // category: LoanCategory.Equity, + // title: LoanTitle.HELOC_RateMod, + // apiCode: LoanCode.EQ_HELOC_Flex, + // icon: ApplicationTypeIcon.HOUSE, + // iconClass: ApplicationIconClass.HOUSE, + // message: 'COPY.TYPE_MESSAGE.EQUITY_HELOC_RATE_MOD', + // headerMessageKey: 'HEADER.LOAN_MESSAGE.EQUITY_HELOC_RATE_MOD', + // shouldDecision: true, + // pages: EquityPages, + // defaultPurpose: LoanPurpose.Other, + // requiresCollateral: true, + // collateralType: CollateralType.Home, + // decisionReviewKey: 'TEXT', + // beforeYouBeginKey: 'BEFOREYOUBEGIN.EQUITY_HELOC_RATE_MOD' + // } +]; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-category.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-category.ts new file mode 100644 index 0000000..bcf0f94 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-category.ts @@ -0,0 +1,6 @@ +export enum LoanCategory { + CreditCard = 'Credit Card', + Equity = 'Equity', + Vehicle = 'Vehicle', + Signature = 'Signature' + } \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-code.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-code.ts new file mode 100644 index 0000000..084aff3 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-code.ts @@ -0,0 +1,19 @@ +// String assignment is because otherwise it auto-convers to a number. +export enum LoanCode { + CC_CashBack = 'CC_CashBack', + CC_Rewards = 'CC_Rewards', + CC_Student = 'CC_Student', + CC_Secure = 'CC_Secure', + EQ_HELOC_Flex = 'EQ_HELOC_Flex', + EQ_HELOC_Fixed = 'EQ_Home_Equity_Fixed', + MTG_REFI_Loan = 'EQ_Mtg_Refi_Express', + MANUFACTURED_HOME_LOAN = 'EQ_Mfg_Home_With_or_Without_Land', + VEHICLE_DEFAULT = 'V_Vehicle', + VEHICLE_PRE_APPROVAL = 'V_VehiclePreapproval', + BOAT = 'M_Boat', + POWERSPORT = 'P_Powersport', + TRAILER_MOTORHOME = 'T_TrailerMotorhome', + SIGNATURE = 'S_Signature', + SPECIAL = 'S_Special', + EQ_HELOC_Rate_Mod = 'EQ_HELOC_Rate_Mod' + } diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-purpose.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-purpose.ts new file mode 100644 index 0000000..0b211b1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-purpose.ts @@ -0,0 +1,19 @@ +// From Swagger: Home_Improvement, Home_Purchase, Home_Refinance, Purchase_Boat, Purchase, Other, Refinance, Dental_Expense, +// Medical_Expense, Cash_Out_Refinance, Refinance_Vehicle, Consolidation, Reestablish_Credit, Purchase_Manufactured_Home +export enum LoanPurpose { + Unselected = '', + Home_Improvement = 'Home_Improvement', + Home_Purchase = 'Home_Purchase', + Home_Refinance = 'Home_Refinance', + Purchase_Boat = 'Purchase_Boat', + Purchase = 'Purchase', + Other = 'Other', + Refinance = 'Refinance', + Dental_Expense = 'Dental_Expense', + Medical_Expense = 'Medical_Expense', + Cash_Out_Refinance = 'Cash_Out_Refinance', + Refinance_Vehicle = 'Refinance_Vehicle', + Consolidation = 'Consolidation', + Reestablish_Credit = 'Reestablish_Credit', + Purchase_Manufactured_Home = 'Purchase_Manufactured_Home' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-title.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-title.ts new file mode 100644 index 0000000..d72926b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/loan-type/loan-title.ts @@ -0,0 +1,18 @@ +export enum LoanTitle { + CashBack = 'Cash Back', + Rewards = 'Rewards', + Student = 'Student', + Secure = 'Secure', + HELOC = 'HELOC', + HomeEquityFixed = 'Home Equity Fixed', + MortgageRefinanceExpress = 'Mortgage Refinance Express', + Vehicle = 'Vehicle Auto/Motorcycle', + ManufacturedHome = 'Manufactured Home', + VehiclePreApproval = 'Vehicle Pre-Approval', + Boat = 'Boat', + PowerSport = 'Personal Water Craft', + TrailerMotorhome = 'Trailer/Motorhome', + Signature = 'Signature', + SpecialLoan = 'COVID-19', + HELOC_RateMod = 'Heloc Rate Modification' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.spec.ts new file mode 100644 index 0000000..16c531b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.spec.ts @@ -0,0 +1,85 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MaskingService } from './masking.service'; +import { MemberInfoFormComponent } from '../../loan-wizard/field-groups/member/member-info-form.component'; +import { TextMaskModule } from 'angular2-text-mask'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + +describe('MaskingWizardService', () => { + + let service: MaskingService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule, + TextMaskModule, + TranslateModule.forRoot() + ], + declarations: [ + MemberInfoFormComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + service = TestBed.get(MaskingService); + }); + + it('should format dates correctly', () => { + const formatted = MaskingService.makeDOB('09171991'); + expect(formatted).toBe('09/17/1991'); + }); + + it('should always return a correctly formatted date of the proper length', () => { + const formatted = MaskingService.makeDOB('091719912018'); + expect(formatted).toBe('09/17/1991'); + }); + + it('should convert numbers to be more human readable', () => { + const formatted = MaskingService.makeNumber('12000500250000125'); + expect(formatted).toBe('12,000,500,250,000,125'); + }); + + it('should handle already formatted numbers', () => { + const formatted = MaskingService.makeNumber('12,250,500'); + expect(formatted).toBe('12,250,500'); + }); + + it('should format phone #\'s correctly', () => { + const formatted = MaskingService.makePhone('4148675309'); + expect(formatted).toBe('(414) 867-5309'); + }); + + it('should format SSN\'s correctly', () => { + const formatted = MaskingService.makeSSN('111223333'); + expect(formatted).toBe('111-22-3333'); + }); + + it('should undo masks', () => { + const unformattedPhone = MaskingService.unMaskNumber('414-867-5309'); + expect(unformattedPhone).toBe('4148675309'); + + const unformattedSSN = MaskingService.unMaskNumber('111-22-3333'); + expect(unformattedSSN).toBe('111223333'); + + const unformattedDate = MaskingService.unMaskNumber('04/26/1992'); + expect(unformattedDate).toBe('04261992'); + + const unformattedCurrency = MaskingService.unMaskDecimalNumber('12,250,500.65'); + expect(unformattedCurrency).toBe('12250500.65'); + }); + + it('should create a mask of proper length for no-special-characters', () => { + const masked = MaskingService.noSpecial('13 or so char'); + expect(masked.length).toBe(13); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.ts new file mode 100644 index 0000000..4d30fae --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/masking/masking.service.ts @@ -0,0 +1,68 @@ +import createNumberMask from 'text-mask-addons/dist/createNumberMask'; +import emailMask from 'text-mask-addons/dist/emailMask'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) + +export class MaskingService { + + public static readonly emptyPhone = '(___) ___-____'; + public static readonly phoneMask = ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]; + public static readonly ssnMask = [/[0-9]/, /\d/, /\d/, '-', /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]; + public static readonly numberMask = createNumberMask({prefix: '', suffix: ''}); + public static readonly numberMaskWithDecimal = createNumberMask({prefix: '', suffix: '', allowDecimal: true}); + public static readonly emailMask = emailMask; + public static readonly dobMask = [/[0-1]/, /[0-9]/, '/', /[0-3]/, /[0-9]/, '/', /[1-2]/, /[0-9]/, /[0-9]/, /[0-9]/]; + public static readonly zipMask = [/[0-9]/, /\d/, /\d/, /\d/, /\d/]; + public static readonly middleInitialMask = [/[a-zA-Z]/]; + public static readonly yearMask = [/[0-9]/, /\d/, /\d/, /\d/]; + + public static unMaskNumber(val) { + + if (val && val.replace) { + return val.replace(/\D+/g, ''); + } + return val; + } + + public static unMaskDecimalNumber(val) { + if (val && val.replace) { + return val.replace(/,/g, ''); + } + return val; + } + + public static makeSSN(val): string { + return val[0] + val[1] + val[2] + '-' + val[3] + val[4] + '-' + val[5] + val[6] + val[7] + val[8]; + } + + public static makePhone(val = ''): string { + if (val === undefined || val === '' || val.length < 9) { return this.emptyPhone; } + return '(' + val[0] + val[1] + val[2] + ') ' + val[3] + val[4] + val[5] + '-' + val[6] + val[7] + val[8] + val[9]; + } + + public static makeDOB(val): string { + return val[0] + val[1] + '/' + val[2] + val[3] + '/' + val[4] + val[5] + val[6] + val[7]; + } + + public static makeNumber(val): string { + return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + public static noSpecial(val) { + if (!val) { + return [/[a-z0-9]+$/i]; + } + let iterable = val.split(''); + const mask = []; + iterable = iterable.filter((char) => { + return !!char.match(/[a-z0-9 ]+$/i); + }); + iterable.forEach(element => { + mask.push(/[a-z0-9 ]+$/i); + }); + return mask; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.spec.ts new file mode 100644 index 0000000..0f1bdec --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.spec.ts @@ -0,0 +1,36 @@ +import { BranchOptions } from './branch-options'; +import { DefaultSettings } from '../configuration/default-settings.mock'; + + + +describe('Branch Options', () => { + + it('should get branch name by zip code', () => { + spyOn(BranchOptions, 'getChannelIdByZip').and.returnValue(1561591); + const result = BranchOptions.getBranchNameByZip('53033', DefaultSettings); + expect(result).toEqual('Grafton'); + }); + + it('should NOT get branch name with invalid zip code', () => { + spyOn(BranchOptions, 'getChannelIdByZip').and.returnValue(1234555); + const result = BranchOptions.getBranchNameByZip('53033', DefaultSettings); + expect(result).toEqual('Not Found'); + }); + + it('should get list of branch names', () => { + const result = BranchOptions.getBranchNames(DefaultSettings); + expect(result[0]).toEqual('ACTion Online'); + expect(result.length).toEqual(43); + }); + + it('should get channel id by zip code', () => { + const result = BranchOptions.getChannelIdByZip(DefaultSettings, '53033'); + expect(result).toEqual(1561664); + }); + + it('should NOT get channel id with invalid zip code', () => { + const result = BranchOptions.getChannelIdByZip(DefaultSettings, '66666'); + expect(result).toEqual(0); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.ts new file mode 100644 index 0000000..68c58df --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/branch-options.ts @@ -0,0 +1,35 @@ +import { UiSettings } from '../configuration/ui-settings'; + +export class BranchOptions { + + public static getBranchNameByZip(postalCode: string, settings: UiSettings): string { + const zipCode = parseInt(postalCode, 10); + const channelId = this.getChannelIdByZip(settings, zipCode + ''); + let branchName = 'Not Found'; + if (settings.branchNameMapping && settings.branchNameMapping.branchNamesByChannelId && settings.branchNameMapping.branchNamesByChannelId.length) { + settings.branchNameMapping.branchNamesByChannelId.forEach((eachMapping) => { + if (eachMapping.channelId === channelId) { + branchName = eachMapping.branchName; + } + }); + } + return branchName; + } + + public static getBranchNames(settings: UiSettings): string[] { + const branchList = settings.branchNameMapping.branchNamesByChannelId.map(value => value.branchName); + return branchList.sort(); + } + + public static getChannelIdByZip(settings: UiSettings, zipCode: string): number { + let channelId = 0; + if (settings.branchMapping && settings.branchMapping.branchCodesByZip && settings.branchMapping.branchCodesByZip.length) { + settings.branchMapping.branchCodesByZip.forEach((eachMapping) => { + if (eachMapping.zipcode === zipCode) { + channelId = eachMapping.channelId; + } + }); + } + return channelId; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.spec.ts new file mode 100644 index 0000000..572361f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.spec.ts @@ -0,0 +1,13 @@ +import { RouteOptions } from './route-options'; +import { LoanCode } from '../loan-type/loan-code'; + + + +describe('Route Options', () => { + + it('should get route by loan code', () => { + const result = RouteOptions.getRouteFromLoanCode(LoanCode.CC_CashBack); + expect(result).toEqual('/loan-wizard/cc/cb'); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.ts new file mode 100644 index 0000000..4ee25ed --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/route-options.ts @@ -0,0 +1,25 @@ +import { LoanCode } from '../loan-type/loan-code'; + +export class RouteOptions { + public static readonly loanCodeToRoute = { + [LoanCode.CC_CashBack]: '/loan-wizard/cc/cb', + [LoanCode.CC_Rewards]: '/loan-wizard/cc/re', + [LoanCode.CC_Student]: '/loan-wizard/cc/st', + [LoanCode.CC_Secure]: '/loan-wizard/cc/se', + [LoanCode.EQ_HELOC_Flex]: '/loan-wizard/eq/he', + [LoanCode.EQ_HELOC_Fixed]: '/loan-wizard/eq/hef', + [LoanCode.MTG_REFI_Loan]: '/loan-wizard/eq/mtg', + // [LoanCode.MANUFACTURED_HOME_LOAN]: '/loan-wizard/eq/mhl', + [LoanCode.VEHICLE_DEFAULT]: '/loan-wizard/ve/auto', + [LoanCode.VEHICLE_PRE_APPROVAL]: '/loan-wizard/ve/pauto', + [LoanCode.BOAT]: '/loan-wizard/ve/boat', + [LoanCode.POWERSPORT]: '/loan-wizard/ve/pwc', + [LoanCode.TRAILER_MOTORHOME]: '/loan-wizard/ve/tmh', + [LoanCode.SIGNATURE]: '/loan-wizard/sl/pe', + // [LoanCode.EQ_HELOC_Rate_Mod]: '/loan-wizard/eq/herm' + }; + + public static getRouteFromLoanCode(loanCode: LoanCode) { + return this.loanCodeToRoute[loanCode]; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/select-options.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/select-options.ts new file mode 100644 index 0000000..fd236f6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/select-options.ts @@ -0,0 +1,138 @@ +import { PhoneTypeEnum } from '../api-models/api-enums'; +import { LoanCode } from '../loan-type/loan-code'; + + +export class SelectOptions { + public static readonly occupancyTypes: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: 'OwnerOccupied', desc: 'Owner Occupied'}, + {value: 'SecondHome', desc: 'Second Home'}, + {value: 'Rental', desc: 'Rental'}, + {value: 'VacationHome', desc: 'Vacation Home'}, + {value: 'Vacant', desc: 'Vacant'} + ]; + + public static readonly employmentTypeOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: 'FullTime', desc: 'Full time'}, + {value: 'PartTime', desc: 'Part time'}, + {value: 'Contract', desc: 'Contract'} + ]; + + public static readonly employmentScheduleOptions: Array<{value: any, desc: string}> = [ + {value: 'Hourly', desc: 'Hourly'}, + {value: 'Annually', desc: 'Salary'} + ]; + + public static readonly yearOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: '0', desc: '0 years'}, + {value: '1', desc: '1 year'}, + {value: '2', desc: '2 years'}, + {value: '3', desc: '3 years'}, + {value: '4', desc: '4 years'}, + {value: '5', desc: '5 years'}, + {value: '6', desc: '6 years'}, + {value: '7', desc: '7 years'}, + {value: '8', desc: '8 years'}, + {value: '9', desc: '9+ years'} + ]; + + public static readonly monthOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: '0', desc: '0 months'}, + {value: '1', desc: '1 month'}, + {value: '2', desc: '2 months'}, + {value: '3', desc: '3 months'}, + {value: '4', desc: '4 months'}, + {value: '5', desc: '5 months'}, + {value: '6', desc: '6 months'}, + {value: '7', desc: '7 months'}, + {value: '8', desc: '8 months'}, + {value: '9', desc: '9 months'}, + {value: '10', desc: '10 months'}, + {value: '11', desc: '11 months'} + ]; + + public static readonly residenceStatusPast: Array<{value: any, desc: string}> = [ + {value: '0', desc: 'I owned this home'}, + {value: '1', desc: 'I rented this home'}, + {value: '2', desc: 'Other'} + ]; + + public static readonly residenceStatusPresent: Array<{value: any, desc: string}> = [ + {value: '0', desc: 'I own this home'}, + {value: '1', desc: 'I rent this home'}, + {value: '2', desc: 'Other'} + ]; + + public static readonly purposeOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 'Home_Refinance', desc: 'Refinance'}, + {value: 'Home_Improvement', desc: 'Home Improvement'}, + {value: 'Home_Purchase', desc: 'Home Purchase'}, + {value: 'Other', desc: 'Other'}, + ]; + + public static readonly purposeOptionsAdditionalRefinance: Array<{value: any, desc: string}> = [ + ...SelectOptions.purposeOptions, + {value: 'Cash_Out_Refinance', desc: 'Refinance Cash-Out'} + ]; + + public static readonly phoneTypeOptions: Array<{value: any, desc: string}> = [ + {value: PhoneTypeEnum.Home, desc: 'Home'}, + {value: PhoneTypeEnum.Mobile, desc: 'Mobile'} + ]; + + public static readonly suffixOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Choose One'}, + {value: 'II', desc: 'II'}, + {value: 'III', desc: 'III'}, + {value: 'IV', desc: 'IV'}, + {value: 'V', desc: 'V'}, + {value: 'Jr', desc: 'Jr'}, + {value: 'Sr', desc: 'Sr'} + ]; + + public static readonly realEstateOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: 'SingleFamily', desc: 'Single Family Home'}, + {value: 'Condo', desc: 'Condo'}, + {value: 'Townhouse', desc: 'Duplex'}, + {value: 'Family2_4', desc: 'Multi Family 2-4'}, + {value: 'Other', desc: 'Other'} + ]; + + public static readonly manufacturedHomeRealEstateOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'Please Choose'}, + {value: 'Single', desc: 'Single'}, + {value: 'Double', desc: 'Double'}, + {value: 'Triple', desc: 'Triple'}, + {value: 'Quad', desc: 'Quad'} + ]; + + public static readonly loanOptions: Array<{value: any, desc: string}> = [ + {value: 'ALL', desc: 'ALL'}, + {value: LoanCode.CC_CashBack, desc: 'CC CashBack'}, + {value: LoanCode.CC_Rewards, desc: 'CC Rewards'}, + {value: LoanCode.CC_Student, desc: 'CC Student'}, + {value: LoanCode.CC_Secure, desc: 'CC Secure'}, + {value: LoanCode.EQ_HELOC_Flex, desc: 'HELOC Flex'}, + {value: LoanCode.EQ_HELOC_Fixed, desc: 'Home Equity Fixed'}, + {value: LoanCode.MTG_REFI_Loan, desc: 'Mtg Refi Express'}, + {value: LoanCode.MANUFACTURED_HOME_LOAN, desc: 'Manufactured Home'}, + {value: LoanCode.VEHICLE_DEFAULT, desc: 'Vehicle'}, + {value: LoanCode.VEHICLE_PRE_APPROVAL, desc: 'Vehicle Preapproval'}, + {value: LoanCode.BOAT, desc: 'Boat'}, + {value: LoanCode.POWERSPORT, desc: 'Powersport'}, + {value: LoanCode.TRAILER_MOTORHOME, desc: 'Trailer Motorhome'}, + {value: LoanCode.SIGNATURE, desc: 'Signature'}, + {value: LoanCode.SPECIAL, desc:'Special'} + ]; + + public static readonly modFeeOptions: Array<{value: any, desc: string}> = [ + {value: 'Landmark Checking Account', desc: 'Landmark Checking Account'}, + {value: 'Landmark Savings Account', desc: 'Landmark Savings Account'}, + {value: 'Other', desc: 'Other'}, + ]; +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.spec.ts new file mode 100644 index 0000000..c98b56c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.spec.ts @@ -0,0 +1,58 @@ +import { TermOption } from '../configuration/ui-settings'; +import { TermOptions } from './term-options'; +import { LoanCode } from '../loan-type/loan-code'; + + +let mockTerms: Array; + +describe('Term Options', () => { + + beforeEach(() => { + mockTerms = [ + { + 'value': 12, + 'desc': '12 months' + }, + { + 'value': 24, + 'desc': '24 months' + }, + { + 'value': 36, + 'desc': '36 months' + }, + { + 'value': 48, + 'desc': '48 months' + } + ]; + }); + + it('should insert text decoration on first value of list', () => { + const result = TermOptions.insertTextDecorations(mockTerms); + expect(result[0].desc).toBe('Up to 12 months'); + expect(result[1].desc).toBe('24 months'); + }); + + it('should insert text decoration for a vehicle loan', () => { + const result = TermOptions.insertTextDecorationsByLoan(mockTerms, LoanCode.VEHICLE_DEFAULT); + expect(result[0].desc).toBe('Up to 12 months'); + expect(result[1].desc).toBe('24 months'); + }); + + it('should NOT insert text decoration for a signature loan', () => { + const result = TermOptions.insertTextDecorationsByLoan(mockTerms, LoanCode.SIGNATURE); + expect(result[0].desc).toBe('12 months'); + expect(result[1].desc).toBe('24 months'); + }); + + it('should return term options with year for a vehicle loan', () => { + const result = TermOptions.determineTermOptionsUiSettingsField(LoanCode.VEHICLE_DEFAULT); + expect(result).toBe('termOptions'); + }); + + it('should return term options WITHOUT a year for a signature loan loan', () => { + const result = TermOptions.determineTermOptionsUiSettingsField(LoanCode.SIGNATURE); + expect(result).toBe('termOptionsNoYear'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.ts new file mode 100644 index 0000000..a284b49 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/term-options.ts @@ -0,0 +1,45 @@ +import { LoanCode } from '../loan-type/loan-code'; +import { TermOption } from '../configuration/ui-settings'; + +export class TermOptions { + + public static readonly defaultTermOption = {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}; + + public static readonly uiSettingsKeyWithYear = 'termOptions'; + public static readonly uiSettingsKeyWithoutYear = 'termOptionsNoYear'; + + public static readonly loanCodeToUiSettingKey = { + [LoanCode.VEHICLE_DEFAULT]: 'termsByYear', + [LoanCode.BOAT]: 'boatLoansByAmount', + [LoanCode.POWERSPORT]: 'powersportByAmount', + [LoanCode.TRAILER_MOTORHOME]: 'mfdHomeAndRvByAmount', + [LoanCode.MTG_REFI_Loan]: 'mortgageRefiExpress', + [LoanCode.EQ_HELOC_Flex]: 'homeEquity', + [LoanCode.EQ_HELOC_Fixed]: 'homeEquity', + [LoanCode.MANUFACTURED_HOME_LOAN]: 'mfdHomeAndRvByAmount', + [LoanCode.VEHICLE_PRE_APPROVAL]: 'vehiclePreApproval', + [LoanCode.SIGNATURE]: 'signatureByAmount', + [LoanCode.SPECIAL]: 'special', + [LoanCode.EQ_HELOC_Rate_Mod]: 'helocRateMod' + }; + + public static insertTextDecorations(list: Array): Array<{ value: string, desc: string }> { + const desc = 'Up to ' + list[0].desc; + list.splice(0, 1, {value: list[0].value, desc: desc}); + return list; + } + + public static insertTextDecorationsByLoan(list: Array, loanCode: LoanCode): Array<{ value: string, desc: string }> { + if (loanCode === LoanCode.SIGNATURE || loanCode === LoanCode.EQ_HELOC_Flex || loanCode === LoanCode.MTG_REFI_Loan) { + return list; + } + return this.insertTextDecorations(list); + } + + public static determineTermOptionsUiSettingsField(loanCode: LoanCode): string { + if (loanCode === LoanCode.SIGNATURE || loanCode === LoanCode.VEHICLE_PRE_APPROVAL) { + return this.uiSettingsKeyWithoutYear; + } + return this.uiSettingsKeyWithYear; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.spec.ts new file mode 100644 index 0000000..58ea108 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.spec.ts @@ -0,0 +1,29 @@ +import { VehicleOptions } from './vehicle-options'; +import { LoanCode } from '../loan-type/loan-code'; + + + +describe('Vehicle Options', () => { + + it('should get vehicle types for vehicle', () => { + const result = VehicleOptions.getVehicleTypesByLoanCode(LoanCode.VEHICLE_DEFAULT); + expect(result).toBe(VehicleOptions.vehicleTypeOptions); + }); + + it('should get vehicle types for powersport', () => { + const result = VehicleOptions.getVehicleTypesByLoanCode(LoanCode.POWERSPORT); + expect(result).toBe(VehicleOptions.pwcTypeOptions); + }); + + it('should try to get vehicle types for boat and return null', () => { + const result = VehicleOptions.getVehicleTypesByLoanCode(LoanCode.BOAT); + expect(result).toBe(null); + }); + + it('should get all vehicle types', () => { + const result = VehicleOptions.getAllVehicleTypes(); + expect(result).toEqual([...VehicleOptions.vehicleTypeOptions, ...VehicleOptions.pwcTypeOptions, + ...VehicleOptions.trailerTypeOptions, ...VehicleOptions.boatTypeOptions]); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.ts new file mode 100644 index 0000000..427bfbe --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/options/vehicle-options.ts @@ -0,0 +1,75 @@ +import { LoanPurpose } from '../loan-type/loan-purpose'; +import { LoanCode } from '../loan-type/loan-code'; +import { VehicleType } from '../vehicle-type/vehicle-type'; +import { TrailerType } from '../vehicle-type/trailer-type'; +import { MotorType } from '../vehicle-type/motor-type'; + +export class VehicleOptions { + public static readonly vehicleConditionOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 'New', desc: 'FORMS.VEHICLE.CONDITIONS.NEW'}, + {value: 'Used', desc: 'FORMS.VEHICLE.CONDITIONS.USED'} + ]; + + public static readonly vehicleLoanPurposeOptions: Array<{value: any, desc: string}> = [ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: LoanPurpose.Refinance_Vehicle, desc: 'FORMS.VEHICLE.REFINANCE'}, + {value: LoanPurpose.Purchase, desc: 'FORMS.VEHICLE.PURCHASE'} + ]; + + public static readonly vehicleLoanPurposeOptionsVehicle: Array<{value: any, desc: string}> = [ + ...VehicleOptions.vehicleLoanPurposeOptions, + {value: LoanPurpose.Cash_Out_Refinance, desc: 'FORMS.VEHICLE.CASHOUT_REFINANCE'} + ]; + + public static readonly vehicleSellerOptions: Array<{value: any, desc: string}> = [ + {value: 'Private_Party', desc: 'FORMS.VEHICLE.PRIVATE_PARTY'}, + {value: 'Dealer', desc: 'FORMS.VEHICLE.DEALERSHIP'} + ]; + + public static readonly vehicleTypeOptions: Array<{value: VehicleType, desc: string}> = [ + {value: VehicleType.AUTO, desc: 'FORMS.VEHICLE.TYPES.AUTOMOBILE'}, + {value: VehicleType.CYCLE, desc: 'FORMS.VEHICLE.TYPES.MOTORCYCLE'}, + ]; + + public static readonly boatTypeOptions: Array<{value: VehicleType, desc: string}> = [ + {value: VehicleType.BOAT, desc: 'FORMS.VEHICLE.TYPES.BOAT'} + ]; + + public static readonly trailerTypeOptions: Array<{value: VehicleType, desc: string}> = [ + {value: VehicleType.TRAILER, desc: 'FORMS.VEHICLE.TYPES.TRAILER'} + ]; + + public static readonly pwcTypeOptions: Array<{value: VehicleType, desc: string}> = [ + {value: VehicleType.ATV, desc: 'FORMS.VEHICLE.TYPES.ATV'}, + {value: VehicleType.SNOW, desc: 'FORMS.VEHICLE.TYPES.SNOWMOBILE'}, + {value: VehicleType.PWC, desc: 'FORMS.VEHICLE.TYPES.PWC'}, + {value: VehicleType.RV, desc: 'FORMS.VEHICLE.TYPES.RV'} + ]; + + public static readonly boatTrailerTypeOptions: Array<{value: any, desc: string}> = [ + {value: TrailerType.SINGLE_AXEL, desc: 'FORMS.VEHICLE.BOAT_TRAILER_TYPE.SINGLE'}, + {value: TrailerType.TANDEM_AXEL, desc: 'FORMS.VEHICLE.BOAT_TRAILER_TYPE.TANDEM'}, + {value: TrailerType.TRI_AXLE, desc: 'FORMS.VEHICLE.BOAT_TRAILER_TYPE.TRI_AXLE'} + ]; + + public static readonly boatMotorTypeOptions: Array<{value: any, desc: string}> = [ + {value: MotorType.INBOARD, desc: 'FORMS.VEHICLE.BOAT_MOTOR_TYPE.INBOARD'}, + {value: MotorType.OUTBOARD, desc: 'FORMS.VEHICLE.BOAT_MOTOR_TYPE.OUTBOARD'}, + {value: MotorType.INBOARD_OUTBOARD, desc: 'FORMS.VEHICLE.BOAT_MOTOR_TYPE.INBOARD_OUTBOARD'} + ]; + + public static getVehicleTypesByLoanCode(loanCode: LoanCode): Array<{ value: VehicleType, desc: string }> { + if (loanCode === LoanCode.VEHICLE_DEFAULT) { + return VehicleOptions.vehicleTypeOptions; + } else if (loanCode === LoanCode.POWERSPORT) { + return VehicleOptions.pwcTypeOptions; + } + return null; + } + + public static getAllVehicleTypes(): Array<{ value: VehicleType, desc: string }> { + return [...VehicleOptions.vehicleTypeOptions, ...VehicleOptions.pwcTypeOptions, + ...VehicleOptions.trailerTypeOptions, ...VehicleOptions.boatTypeOptions]; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/page-focus/auto-focus.directive.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/page-focus/auto-focus.directive.ts new file mode 100644 index 0000000..da8ab55 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/page-focus/auto-focus.directive.ts @@ -0,0 +1,26 @@ +import { Directive, ElementRef, Input, OnInit } from '@angular/core'; + +@Directive({ + selector: '[lwAutoFocus]' +}) +export class AutoFocusDirective implements OnInit { + private focus = true; + + @Input() + public shouldRequestFocus = true; + + constructor(private el: ElementRef) {} + + ngOnInit() { + if (this.focus && this.shouldRequestFocus) { + // Otherwise Angular throws error: Expression has changed after it was checked. + window.setTimeout(() => { + this.el.nativeElement.focus(); + }); + } + } + + @Input() set autoFocus(condition: boolean) { + this.focus = condition !== false; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.html new file mode 100644 index 0000000..cf4ce87 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.html @@ -0,0 +1,14 @@ + + + +

PATRIOT_ACT.HEADER

+ + +
+ +

PATRIOT_ACT.BODY.PART1

+

PATRIOT_ACT.BODY.PART2

+

PATRIOT_ACT.BODY.PART3

+
+
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.spec.ts new file mode 100644 index 0000000..71671d6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PatriotActComponent } from './patriot-act.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { configureTestSuite } from 'ng-bullet'; + +describe('Patriot Act Component', () => { + let component: PatriotActComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ NgbModule.forRoot()], + declarations: [ PatriotActComponent ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PatriotActComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.ts new file mode 100644 index 0000000..f5c755b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/patriot-act/patriot-act.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { NgbPanelChangeEvent } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'lw-patriot-act', + templateUrl: './patriot-act.component.html', + styleUrls: ['./patriot-act.component.scss'] +}) +export class PatriotActComponent { + public panelOpen = true; + constructor() { } + public beforeChange($event: NgbPanelChangeEvent) { + if ($event.panelId === 'patriot-act-panel') { + this.panelOpen = $event.nextState; + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/address-status.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/address-status.pipe.ts new file mode 100644 index 0000000..9ca7884 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/address-status.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'addressStatus'}) +export class AddressStatusPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(addressStatus): string { + return this.formatService.formatAddressStatus(addressStatus); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/boolean-string.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/boolean-string.pipe.ts new file mode 100644 index 0000000..4554c0b --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/boolean-string.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'booleanString'}) +export class BooleanStringPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(booleanString: string): string { + return this.formatService.formatBooleanString(booleanString); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/employment-type.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/employment-type.pipe.ts new file mode 100644 index 0000000..151e4d2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/employment-type.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'employmentType'}) +export class EmploymentTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(employmentType: string): string { + return this.formatService.formatEmploymentType(employmentType); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.spec.ts new file mode 100644 index 0000000..30d35ec --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.spec.ts @@ -0,0 +1,239 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormatService } from './format.service'; +import { SsnPipe } from './ssn.pipe'; +import { PhoneNumberPipe } from './phone-number.pipe'; +import { PhoneTypePipe } from './phone-type.pipe'; +import { AddressStatusPipe } from './address-status.pipe'; +import { EmploymentTypePipe } from './employment-type.pipe'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanCode } from '../loan-type/loan-code'; +import { VehicleOptions } from '../options/vehicle-options'; + + +describe('PipeFormatService', () => { + + let service: FormatService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + SsnPipe, + PhoneNumberPipe, + PhoneTypePipe, + AddressStatusPipe, + EmploymentTypePipe + ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + service = TestBed.get(FormatService); + }); + + it('should format ssn number properly', () => { + const formatted = service.formatSsn('111224444'); + expect(formatted).toBe('4444'); + }); + + it('should not fail when formatting invalid ssn number', () => { + let formatted = service.formatSsn('1132'); + expect(formatted).toBe(''); + formatted = service.formatSsn(null); + expect(formatted).toBe(''); + }); + + it('should format phone number properly', () => { + const formatted = service.formatPhoneNumber('1113219876'); + expect(formatted).toBe('(111) 321-9876'); + }); + + it('should not fail when formatting invalid phone number', () => { + const formatted = service.formatPhoneNumber(null); + expect(formatted).toBe(''); + }); + + it('should format phone type properly', () => { + let formatted = service.formatPhoneType('0'); + expect(formatted).toBe('Home'); + formatted = service.formatPhoneType('1'); + expect(formatted).toBe('Mobile'); + formatted = service.formatPhoneType(null); + expect(formatted).not.toBe('Home'); + formatted = service.formatPhoneType(0); + expect(formatted).toBe('Home'); + }); + + it('should format address status', () => { + let formatted = service.formatAddressStatus('0'); + expect(formatted).toBe('Own'); + formatted = service.formatAddressStatus('1'); + expect(formatted).toBe('Rent'); + formatted = service.formatAddressStatus('2'); + expect(formatted).toBe('Other'); + formatted = service.formatAddressStatus(null); + expect(formatted).not.toBe('Own'); + formatted = service.formatAddressStatus(0); + expect(formatted).toBe('Own'); + }); + + it('should format employment type', () => { + let formatted = service.formatEmploymentType('FullTime'); + expect(formatted).toBe('Full time'); + formatted = service.formatEmploymentType('PartTime'); + expect(formatted).toBe('Part time'); + formatted = service.formatEmploymentType('Contract'); + expect(formatted).toBe('Contract'); + formatted = service.formatEmploymentType(null); + expect(formatted).not.toBe('Part time'); + }); + + it('should format occupancy type', () => { + let formatted = service.formatOccupancyType('OwnerOccupied'); + expect(formatted).toBe('Owner Occupied'); + formatted = service.formatOccupancyType('VacationHome'); + expect(formatted).toBe('Vacation Home'); + formatted = service.formatOccupancyType('Rental'); + expect(formatted).toBe('Rental'); + formatted = service.formatOccupancyType(null); + expect(formatted).not.toBe('Owner Occupied'); + }); + + it('should format a boolean string', () => { + let formatted = service.formatBooleanString('true'); + expect(formatted).toBe('Yes'); + formatted = service.formatBooleanString('false'); + expect(formatted).toBe('No'); + formatted = service.formatBooleanString(null); + expect(formatted).not.toBe('No'); + }); + + it('should format property type', () => { + let formatted = service.formatPropertyType('SingleFamily'); + expect(formatted).toBe('Single Family Home'); + formatted = service.formatPropertyType('Condo'); + expect(formatted).toBe('Condo'); + formatted = service.formatPropertyType('Family2_4'); + expect(formatted).toBe('Multi Family 2-4'); + formatted = service.formatPropertyType(null); + expect(formatted).not.toBe('Single Family Home'); + }); + + it('should format Manufactured Home property type', () => { + let formatted = service.formatMfgHomePropertyType('Single'); + expect(formatted).toBe('Single'); + formatted = service.formatMfgHomePropertyType('Double'); + expect(formatted).toBe('Double'); + formatted = service.formatMfgHomePropertyType('Triple'); + expect(formatted).toBe('Triple'); + formatted = service.formatMfgHomePropertyType('Quad'); + expect(formatted).toBe('Quad'); + formatted = service.formatMfgHomePropertyType(null); + expect(formatted).not.toBe('Single Family Home'); + }); + + it('should format vehicle seller', () => { + let formatted = service.formatVehicleSeller('Dealer'); + expect(formatted).toBe('Dealership'); + formatted = service.formatVehicleSeller('Private_Party'); + expect(formatted).toBe('Private Party'); + formatted = service.formatVehicleSeller(null); + expect(formatted).not.toBe('Dealership'); + }); + + it('should format state', () => { + let formatted = service.formatState('WI'); + expect(formatted).toBe('Wisconsin'); + formatted = service.formatState('FF'); + expect(formatted).toBe('FF'); + formatted = service.formatState(null); + expect(formatted).not.toBe(''); + }); + + it('should format vehicle type', () => { + const formatted = service.formatVehicleType('PWC'); + expect(formatted).toBe(VehicleOptions.pwcTypeOptions[2].desc); + const formatted2 = service.formatVehicleType('Motorcycle'); + expect(formatted2).toBe(VehicleOptions.vehicleTypeOptions[1].desc); + }); + + it('should format currency', () => { + let formatted = service.formatCurrency(5000); + expect(formatted).toBe('$5,000.00'); + formatted = service.formatCurrency('5000.89'); + expect(formatted).toBe('$5,000.89'); + formatted = service.formatCurrency('$5,000.89'); + expect(formatted).toBe('$5,000.89'); + }); + + it('should format currency', () => { + let formatted = service.formatCurrencyNoDecimal(5000); + expect(formatted).toBe('$5,000'); + formatted = service.formatCurrencyNoDecimal('5000.89'); + expect(formatted).toBe('$5,000'); + formatted = service.formatCurrencyNoDecimal('$5,000.89'); + expect(formatted).toBe('$5,000'); + }); + + it('should format loanCode', () => { + let formatted = service.formatLoanCode(LoanCode.BOAT); + expect(formatted).toBe('Boat'); + formatted = service.formatLoanCode(LoanCode.MANUFACTURED_HOME_LOAN); + expect(formatted).toBe('Manufactured Home'); + formatted = service.formatLoanCode(LoanCode.CC_Student); + expect(formatted).toBe('CC Student'); + formatted = service.formatLoanCode(LoanCode.CC_CashBack); + expect(formatted).toBe('CC CashBack'); + formatted = service.formatLoanCode(LoanCode.CC_Rewards); + expect(formatted).toBe('CC Rewards'); + formatted = service.formatLoanCode(LoanCode.CC_Secure); + expect(formatted).toBe('CC Secure'); + formatted = service.formatLoanCode(LoanCode.EQ_HELOC_Flex); + expect(formatted).toBe('HELOC Flex'); + formatted = service.formatLoanCode(LoanCode.EQ_HELOC_Fixed); + expect(formatted).toBe('Home Equity Fixed'); + formatted = service.formatLoanCode(LoanCode.MTG_REFI_Loan); + expect(formatted).toBe('Mtg Refi Express'); + formatted = service.formatLoanCode(LoanCode.VEHICLE_DEFAULT); + expect(formatted).toBe('Vehicle'); + formatted = service.formatLoanCode(LoanCode.VEHICLE_PRE_APPROVAL); + expect(formatted).toBe('Vehicle Preapproval'); + formatted = service.formatLoanCode(LoanCode.POWERSPORT); + expect(formatted).toBe('Powersport'); + formatted = service.formatLoanCode(LoanCode.TRAILER_MOTORHOME); + expect(formatted).toBe('Trailer Motorhome'); + formatted = service.formatLoanCode(LoanCode.SIGNATURE); + expect(formatted).toBe('Signature'); + formatted = service.formatLoanCode(LoanCode.SPECIAL); + expect(formatted).toBe('Special'); + }); + + it('should format loanCode to icon', () => { + let formatted = service.formatLoanCodeToIcon(LoanCode.BOAT); + expect(formatted).toBe('fa fa-ship'); + formatted = service.formatLoanCodeToIcon(LoanCode.CC_Student); + expect(formatted).toBe('fa fa-credit-card'); + formatted = service.formatLoanCodeToIcon(LoanCode.EQ_HELOC_Flex); + expect(formatted).toBe('fa fa-home'); + }); + + it('should format string in title case', () => { + let formatted = service.formatTitleCase('Matt Millan'); + expect(formatted).toBe('Matt Millan'); + formatted = service.formatTitleCase('MATT MILLAN'); + expect(formatted).toBe('Matt Millan'); + formatted = service.formatTitleCase('mATT'); + expect(formatted).toBe('Matt'); + formatted = service.formatTitleCase('MaTTeRs'); + expect(formatted).toBe('Matters'); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.ts new file mode 100644 index 0000000..3cf1202 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/format.service.ts @@ -0,0 +1,276 @@ +import { Injectable } from '@angular/core'; +import { CreditApplicationValues } from '../credit-application-values'; +import { DataTableService } from '../../shared/data-table.service'; +import { LoanCode } from '../loan-type/loan-code'; +import { VehicleOptions } from '../options/vehicle-options'; +import { LoanTypeSet } from '../loan-type'; +import { SelectOptions} from '../options/select-options'; + +@Injectable({ + providedIn: 'root' +}) +export class FormatService { + + constructor(private dataService: DataTableService) { + } + + public formatCurrency(moneyAmt: number | string): string { + if (typeof moneyAmt === 'string') { + moneyAmt = parseFloat(moneyAmt.replace(/[^0-9.]/g, '')); + } + return moneyAmt.toLocaleString('en-US', {style: 'currency', currency: 'USD'}); + } + + public formatCurrencyNoDecimal(moneyAmt: number | string): string { + if (typeof moneyAmt === 'string') { + moneyAmt = parseFloat(moneyAmt.replace(/[^0-9.]/g, '')); + } + return moneyAmt.toLocaleString('en-US', {style: 'currency', currency: 'USD'}).split('.')[0]; + } + + public formatSsn(ssn: string): string { + if (ssn && ssn.length === 9) { + return ssn.substring(5); + } else if (ssn && ssn.length > 8) { + return '*** - ** - ' + ssn.substring(ssn.length - 4); + } else { + return ''; + } + } + + public formatSsnNoStars(ssn: string): string { + if (ssn && ssn.length === 9) { + return ssn.substring(0, 3) + '-' + ssn.substring(3, 5) + '-' + ssn.substring(5); + } else { + return ''; + } + } + + public formatDob(dob: string): string { + if (dob && dob.length > 10) { + // get year + const year = dob.substring(0, 4); + const month = dob.substring(5, 7); + const day = dob.substring(8, 10); + return month + '/' + day + '/' + year; + } else { + return ''; + } + } + + + public formatPhoneNumber(number: string): string { + if (number && number.length === 10) { + return '(' + number.substring(0, 3) + ') ' + number.substring(3, 6) + '-' + number.substring(6); + } else { + return ''; + } + } + + public formatTitleCase(name: string): string { + return name.toLowerCase().split(' ').map(word => + word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + } + + public formatPhoneType(type: any): string { + if (type !== null) { + switch (parseInt(type, 10)) { + case 0: { + return 'Home'; + } + case 1: { + return 'Mobile'; + } + default: { + return 'Phone type not provided'; + } + } + } + } + + public formatAddressStatus(status: any): string { + if (status !== null) { + switch (parseInt(status, 10)) { + case 0: { + return 'Own'; + } + case 1: { + return 'Rent'; + } + case 2: { + return 'Other'; + } + default: { + return 'No address status provided'; + } + } + } + } + + public formatEmploymentType(type: any): string { + if (type !== null) { + switch (type) { + case 'FullTime': { + return 'Full time'; + } + case 'PartTime': { + return 'Part time'; + } + case 'Contract': { + return 'Contract'; + } + default: { + return 'No Employment type provided'; + } + } + } + } + + public formatOccupancyType(type: string): string { + if (type !== null) { + switch (type) { + case 'OwnerOccupied': { + return 'Owner Occupied'; + } + case 'SecondHome': { + return 'Second Home'; + } + case 'Rental': { + return 'Rental'; + } + case 'VacationHome': { + return 'Vacation Home'; + } + case 'Vacant': { + return 'Vacant'; + } + default: { + return 'No Occupancy type provided'; + } + } + } + } + + public formatBooleanString(type: string): string { + if (type !== null) { + switch (type) { + case 'false': { + return 'No'; + } + case 'true': { + return 'Yes'; + } + case CreditApplicationValues.CHOICE_YES: { + return 'Yes'; + } + case CreditApplicationValues.CHOICE_NO: { + return 'No'; + } + default: { + return 'Not a boolean string'; + } + } + } + } + + public formatPropertyType(type: string): string { + if (type !== null) { + switch (type) { + case 'SingleFamily': { + return 'Single Family Home'; + } + case 'Condo': { + return 'Condo'; + } + case 'Townhouse': { + return 'Duplex'; + } + case 'Family2_4': { + return 'Multi Family 2-4'; + } + case 'Other': { + return 'Other'; + } + default: { + return 'Not a Property type'; + } + } + } + } + + public formatMfgHomePropertyType(type: string): string { + if (type !== null) { + switch (type) { + case 'Single': { + return 'Single'; + } + case 'Double': { + return 'Double'; + } + case 'Triple': { + return 'Triple'; + } + case 'Quad': { + return 'Quad'; + } + default: { + return 'Not a Manufactured Home Property type'; + } + } + } + } + + public formatVehicleSeller(type: string): string { + if (type) { + switch (type) { + case 'Private_Party': { + return 'Private Party'; + } + case 'Dealer': { + return 'Dealership'; + } + default: { + return 'Not a Vehicle Seller'; + } + } + } + } + + public formatVehicleType(type: string): string { + let typeDesc = '!Not Found!'; + VehicleOptions.getAllVehicleTypes().forEach( + (vehicleType) => { + if (vehicleType.value === type) { + typeDesc = vehicleType.desc; + } + }, this); + return typeDesc; + } + + public formatState(type: string): string { + if (type) { + const stateName = this.dataService.getStateList()[type]; + if (stateName) { + return stateName; + } else { + return type; + } + } + } + + public formatLoanCode(loanCode: LoanCode): string { + return SelectOptions.loanOptions.find(x => x.value == loanCode).desc + } + + public formatLoanCodeToIcon(loanCode: LoanCode): string { + return LoanTypeSet.find(x => x.apiCode === loanCode).iconClass; + } + + public formatUploadedFiles(files: any[]): string { + let fileNames: string[] = []; + for(let file of files) { + fileNames.push(file.name); + } + return fileNames.join(", "); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-code.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-code.pipe.ts new file mode 100644 index 0000000..f9a61a8 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-code.pipe.ts @@ -0,0 +1,12 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; +import { LoanCode } from '../loan-type/loan-code'; + +@Pipe({name: 'loanCode'}) +export class LoanCodePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(loanCode: LoanCode): string { + return this.formatService.formatLoanCode(loanCode); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-icon.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-icon.pipe.ts new file mode 100644 index 0000000..2ac330a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/loan-icon.pipe.ts @@ -0,0 +1,12 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; +import { LoanCode } from '../loan-type/loan-code'; + +@Pipe({name: 'loanIcon'}) +export class LoanIconPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(loanCode: LoanCode): string { + return this.formatService.formatLoanCodeToIcon(loanCode); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/occupancy-type.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/occupancy-type.pipe.ts new file mode 100644 index 0000000..261a12c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/occupancy-type.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'occupancyType'}) +export class OccupancyTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(occupancyType): string { + return this.formatService.formatOccupancyType(occupancyType); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-number.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-number.pipe.ts new file mode 100644 index 0000000..a83d2cb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-number.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'phoneNumber'}) +export class PhoneNumberPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(phoneNumber: string): string { + return this.formatService.formatPhoneNumber(phoneNumber); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-type.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-type.pipe.ts new file mode 100644 index 0000000..489259a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/phone-type.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'phoneType'}) +export class PhoneTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(phoneType): string { + return this.formatService.formatPhoneType(phoneType); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/property-type.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/property-type.pipe.ts new file mode 100644 index 0000000..015a272 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/property-type.pipe.ts @@ -0,0 +1,20 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'propertyType'}) +export class PropertyTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(propertyType): string { + return this.formatService.formatPropertyType(propertyType); + } +} + +@Pipe({name: 'mfgHomePropertyType'}) +export class MfgHomePropertyTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(mfgHomePropertyType): string { + return this.formatService.formatMfgHomePropertyType(mfgHomePropertyType); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/ssn.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/ssn.pipe.ts new file mode 100644 index 0000000..dce23ec --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/ssn.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'ssn'}) +export class SsnPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(ssn: string): string { + return this.formatService.formatSsn(ssn); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/state.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/state.pipe.ts new file mode 100644 index 0000000..ba9b573 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/state.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'state'}) +export class StatePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(state: string): string { + return this.formatService.formatState(state); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/uploaded-files.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/uploaded-files.pipe.ts new file mode 100644 index 0000000..4d56d63 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/uploaded-files.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'uploadedFiles'}) +export class UploadedFilesPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(uploadedFiles): string { + return this.formatService.formatUploadedFiles(uploadedFiles); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-collateral.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-collateral.pipe.ts new file mode 100644 index 0000000..7465cc7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-collateral.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'vehicleSeller'}) +export class VehicleSellerPipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(seller: string): string { + return this.formatService.formatVehicleSeller(seller); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-type.pipe.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-type.pipe.ts new file mode 100644 index 0000000..e040cec --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/pipes/vehicle-type.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FormatService} from './format.service'; + +@Pipe({name: 'vehicleType'}) +export class VehicleTypePipe implements PipeTransform { + constructor(private formatService: FormatService) {} + + transform(vehicleType: string): string { + return this.formatService.formatVehicleType(vehicleType); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.html new file mode 100644 index 0000000..462db1f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.html @@ -0,0 +1,1196 @@ +
+
+
+
+
+

+ Cash Back + VISA - Terms and Conditions +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Interest Rates and Interest Charges +

+
+

+ Annual Percentage Rate (APR) for Purchases +

+
+ {{boxInfoCashBack.aprInitial}} Introductory APR for 12 months from account opening. +
After that your APR will be {{boxInfoCashBack.aprPurchase}}
based on your creditworthiness. This APR will vary with the market based on the Prime Rate. +
+

+ APR for Balance Transfers +

+
+ {{boxInfoCashBack.aprInitial}} Introductory APR for 12 months from account opening. +
After that your APR will be {{boxInfoCashBack.aprPurchase}}
based on your creditworthiness. This APR will vary with the market based on the Prime Rate. +
+

+ APR for Cash Advances +

+
+

+ {{boxInfoCashBack.aprCashAdvance}}
+

+
+

+ Penalty APR and When It Applies +

+
+

+ {{boxInfoCashBack.aprPenalty}}.
+
This APR may be applied to your account if you:
+ - Make a late payment.
+

+

How Long Will the Penalty APR Apply? If your APRs are increased for this reason, the Penalty APR will apply until you make + three consecutive minimum payments when due.

+
+

+ How to Avoid Paying Interest on Purchases +

+
+

Your due date is at least 25 days after close of each billing cycle. We will not charge you any interest + on purchases if you pay your entire balance by the due date each month.

+
+

+ Minimum Interest Charge +

+
If you are charged interest, the charge will be no less than {{boxInfoCashBack.minInterestCharge}}. +
+

+ For Credit Card Tips from the Consumer Financial Protection Bureau +

+
+

To learn more about factors to consider when applying for or using a credit card, visit the website + of the Consumer Financial Protection Bureau at + http://www.consumerfinance.gov/learnmore.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Fees +

+
+

+ Annual Fee +

+
+

+ None +

+
+

+ Transaction Fees +

+
+
+
    +
  • Balance Transfer Fees
  • +
+
+

+ {{boxInfoCashBack.introBalTransferFeePct}} of the amount of each balance transfer + during the first 60 days following the opening of Your Account. After that, You will be charged a fee of + {{boxInfoCashBack.transBalTransFeeAmt}} or {{boxInfoCashBack.transBalTransFeePct}} + of the amount of the each balance transfer, whichever is greater +

+
+
    +
  • Cash Advance Fee
  • +
+
+

+ {{boxInfoCashBack.transCashAdvFeeAmt}} or {{boxInfoRewards.transCashAdvFeePct}} + of the amount of the each cash advance, whichever is greater +

+
+
    +
  • Foreign Transaction Fee
  • +
+
+

+ {{boxInfoCashBack.transFgnTransFeePct}} + of each transaction in U.S. dollars.

+
+

+ Penalty Fees +

+
+
+
    +
  • Late Payment Fee
  • +
+
+

+ Up to {{boxInfoCashBack.penLatePaymentFee}} +

+
+
    +
  • Over-the-Credit Limit Fee
  • +
+
+

+ {{boxInfoCashBack.penOverCreditLimitFee}} +

+
+
    +
  • Returned Payment Fee
  • +
+
+

+ Up To {{boxInfoCashBack.penRtnPmtFee}} +

+
+

+ How We Will Calculate Your Balance: We use a method called “average daily balance (including + new purchases).”

+

+ Promotional Period for Introductory APR: The Introductory APR for purchases will apply to transactions posted to Your + Account during the first 12 months following the opening of Your Account. The Introductory APR for balance transfers will apply during the first 12 months following the + opening of Your Account. Balance transfers completed within the first 60 days of opening Your Account, will qualify for {{boxInfoCashBack.introBalTransferFeePct}} + balance transfer fee and Introductory APR. Any existing balances on Landmark Credit Union loan or credit card accounts + are not eligible for the Introductory APR for balance transfers. +

+

+ Loss of Introductory APR: We may end your Introductory APR for purchases and balance transfers and apply the + prevailing non-introductory APR if you are 60 days late in making a payment. +

+

+ Application of Penalty APR:  Your APR may be increased to the disclosed Penalty APR if You are 60 days late in + making a payment. If a minimum payment or any portion thereof is past due by more than 60 days, Your APR will + increase to the Penalty APR as disclosed in the table under “Penalty APR and When it Applies.” This Penalty APR will be + effective following any notice and waiting period required by law. The Penalty APR will remain on Your Account until Your + Account has been paid on time as disclosed in the “Penalty APR and When it Applies” section and with no other event of + default. +

+

+ Minimum Interest Charge: The minimum interest charge will be charged on any dollar amount. +

+

+ Effective Date: The information about the costs of the card described in this application is accurate as of: {{creditCardSharedInformation.cashBackDateUpdated}} + This information may have changed after that date. To find out what may have changed, contact the Credit Union. +

+

+ For California Borrowers, the Visa® Signature Cash Back is a secured credit card. Credit extended under this + credit card account is secured by various personal property and money including, but not limited to: (a) any + goods you purchase with this account, (b) any shares you specifically pledge as collateral for this account on a + separate Pledge of Shares, (c) all shares you have in any individual or joint account with the Credit Union + excluding shares in an Individual Retirement Account or in any other account that would lose special tax + treatment under state or federal law, and (d) collateral securing other loans you have with the Credit Union + excluding dwellings. +

+

+ Notice to New York Residents: New York residents may contact the New York State Department of Financial Services + to obtain a comparative listing of credit card rates, fees, and grace periods. The New York State Department of Financial + Services may be contacted at 1-800-342-3736 or www.dfs.ny.gov  +

+

+ Other Fees & Disclosures: 
+ Late Payment Fee: {{boxInfoCashBack.latePmtFee}} or the amount of the required minimum payment, whichever is less, if you are one or more + days late in making a payment. +
+
+ Balance Transfer Fee (Finance Charge): {{boxInfoCashBack.introBalTransferFeePct}} of the amount of each balance transfer during the first 60 days following + the opening of Your Account. After that, You will be charged a fee of {{boxInfoCashBack.balTransferFeeAmt}} or {{boxInfoCashBack.balTransferFeePct}} of the amount of each balance + transfer, whichever is greater. +
+
+ Cash Advance Fee (Finance Charge): {{boxInfoCashBack.cashAdvanceFeeAmt}} or {{boxInfoCashBack.cashAdvanceFeePct}} of the amount of each cash advance, whichever is greater. +
+
+ Returned Payment Fee: {{boxInfoCashBack.returnedPmtFee}} or the amount of the required minimum payment, whichever is less. +
+
+ Returned Convenience Check Fee: {{boxInfoCashBack.returnedConvenienceCheckFee}} or the amount of the returned convenience check, whichever is less. +
+
+ Card Replacement Fee: {{boxInfoCashBack.cardReplacementFee}}. +
+
+ Pay-by-Phone Fee: {{boxInfoCashBack.payByPhoneFee}}. +
+
+ Statement Copy Fee: {{boxInfoCashBack.statementCopyFee}}. +
+
+ Rush Card Fee: {{boxInfoCashBack.rushCardFee}}. +
+

+
+
+
+
+

+ Rewards + VISA - Terms and Conditions +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Interest Rates and Interest Charges +

+
+

+ Annual Percentage Rate (APR) for Purchases +

+
+ {{boxInfoRewards.aprInitial}} Introductory APR for 12 months from account opening. +
After that your APR will be {{boxInfoRewards.aprPurchase}}
based on your creditworthiness. This APR will vary with the market based on the Prime Rate. +
+

+ APR for Balance Transfers +

+
+ {{boxInfoRewards.aprInitial}} Introductory APR for 12 months from account opening. +
After that your APR will be {{boxInfoRewards.aprPurchase}}
based on your creditworthiness. This APR will vary with the market based on the Prime Rate. +
+

+ APR for Cash Advances +

+
+

+ + {{boxInfoRewards.aprCashAdvance}}
+

+
+

+ Penalty APR and When It Applies +

+ +
+

+ {{boxInfoRewards.aprPenalty}}.
+
This APR may be applied to your account if you:
+ - Make a late payment.
+

+

How Long Will the Penalty APR Apply? If your APRs are increased for this reason, the Penalty APR will apply until you make + three consecutive minimum payments when due.

+
+

+ How to Avoid Paying Interest on Purchases +

+
+

Your due date is at least 25 days after close of each billing cycle. We will not charge you any interest + on purchases if you pay your entire balance by the due date each month.

+
+

+ Minimum Interest Charge +

+
If you are charged interest, the charge will be no less than {{boxInfoRewards.minInterestCharge}}. +
+

+ For Credit Card Tips from the Consumer Financial Protection Bureau +

+
+

To learn more about factors to consider when applying for or using a credit card, visit the website + of the Consumer Financial Protection Bureau at + http://www.consumerfinance.gov/learnmore.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Fees +

+
+

+ Annual Fee +

+
+

+ None +

+
+

+ Transaction Fees +

+
+
+
    +
  • Balance Transfer Fees
  • +
+
+

+ {{boxInfoRewards.introBalTransferFeePct}} of the amount of each balance transfer + during the first 60 days following the opening of Your Account. After that, You will be charged a fee of + {{boxInfoRewards.transBalTransFeeAmt}} or {{boxInfoRewards.transBalTransFeePct}} + of the amount of the each balance transfer, whichever is greater +

+
+
    +
  • Cash Advance Fee
  • +
+
+

+ {{boxInfoRewards.transCashAdvFeeAmt}} or {{boxInfoRewards.transCashAdvFeePct}} + of the amount of the each cash advance, whichever is greater +

+
+
    +
  • Foreign Transaction Fee
  • +
+
+

+ {{boxInfoRewards.transFgnTransFeePct}} + of each transaction in U.S. dollars.

+
+

+ Penalty Fees +

+
+
+
    +
  • Late Payment Fee
  • +
+
+

+ Up to {{boxInfoRewards.penLatePaymentFee}} +

+
+
    +
  • Over-the-Credit Limit Fee
  • +
+
+

+ {{boxInfoRewards.penOverCreditLimitFee}} +

+
+
    +
  • Returned Payment Fee
  • +
+
+

+ Up To {{boxInfoRewards.penRtnPmtFee}} +

+
+ +
+

+ How We Will Calculate Your Balance: We use a method called “average daily balance (including + new purchases).”

+

+ Promotional Period for Introductory APR: The Introductory APR for purchases will apply to transactions posted to Your + Account during the first 12 months following the opening of Your Account. The Introductory APR for balance transfers will apply during the first 12 months following the + opening of Your Account. Balance transfers completed within the first 60 days of opening Your Account, will qualify for {{boxInfoRewards.introBalTransferFeePct}} + balance transfer fee and Introductory APR. Any existing balances on Landmark Credit Union loan or credit card accounts + are not eligible for the Introductory APR for balance transfers. +

+

+ Loss of Introductory APR: We may end your Introductory APR for purchases and balance transfers and apply the + prevailing non-introductory APR if you are 60 days late in making a payment. +

+

+ Application of Penalty APR: Your APR may be increased to the disclosed Penalty APR if you are 60 days late in making + a payment. If a minimum payment or any portion thereof is past due by more than 60 days, Your APR will increase to the + Penalty APR as disclosed in the table under “Penalty APR and When it Applies.” This Penalty APR will be effective + following any notice and waiting period required by law. The Penalty APR will remain on Your Account until Your Account + has been paid on time as disclosed in the “Penalty APR and When it Applies” section and with no other event of default. +

+

+ Minimum Interest Charge: The minimum interest charge will be charged on any dollar amount. +

+

+ Effective Date: The information about the costs of the card described in this application is accurate as of: {{creditCardSharedInformation.rewardsDateUpdated}} + This information may have changed after that date. To find out what may have changed, contact the Credit Union. +

+

+ For California Borrowers, the Rewards Visa® is a secured credit card. Credit extended under this credit card + account is secured by various personal property and money including, but not limited to: (a) any goods you + purchase with this account, (b) any shares you specifically pledge as collateral for this account on a separate + Pledge of Shares, (c) all shares you have in any individual or joint account with the Credit Union excluding + shares in an Individual Retirement Account or in any other account that would lose special tax treatment under + state or federal law, and (d) collateral securing other loans you have with the Credit Union excluding dwellings. +

+

+ Notice to New York Residents: New York residents may contact the New York State Department of Financial Services + to obtain a comparative listing of credit card rates, fees, and grace periods. The New York State Department of Financial + Services may be contacted at 1-800-342-3736 or www.dfs.ny.gov  +

+

+ Other Fees & Disclosures: 
+ Late Payment Fee: {{boxInfoRewards.latePmtFee}} or the amount of the required minimum payment, whichever is less, if you are one or more + days late in making a payment. +
+
+ Balance Transfer Fee (Finance Charge): {{boxInfoRewards.introBalTransferFeePct}} of the amount of each balance transfer during the first 60 days following + the opening of Your Account. After that, You will be charged a fee of {{boxInfoRewards.balTransferFeeAmt}} or {{boxInfoRewards.balTransferFeePct}} of the amount of each balance + transfer, whichever is greater. +
+
+ Cash Advance Fee (Finance Charge): {{boxInfoRewards.cashAdvanceFeeAmt}} or {{boxInfoRewards.cashAdvanceFeePct}} of the amount of each cash advance, whichever is greater. +
+
+ Returned Payment Fee: {{boxInfoRewards.returnedPmtFee}} or the amount of the required minimum payment, whichever is less. +
+
+ Returned Convenience Check Fee: {{boxInfoRewards.returnedConvenienceCheckFee}} or the amount of the returned convenience check, whichever is less. +
+
+ Card Replacement Fee: {{boxInfoRewards.cardReplacementFee}}. +
+
+ Pay-by-Phone Fee: {{boxInfoRewards.payByPhoneFee}}. +
+
+ Statement Copy Fee: {{boxInfoRewards.statementCopyFee}}. +
+
+ Rush Card Fee: {{boxInfoRewards.rushCardFee}}. +
+

+
+
+
+
+

+ Secured + VISA - Terms and Conditions +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Interest Rates and Interest Charges +

+
+

+ Annual Percentage Rate (APR) for Purchases +

+
+ {{boxInfoSecured.aprPurchase}} +
+
+

+ APR for Balance Transfers +

+
+

+ {{boxInfoSecured.aprTransfer}} +

+
+

+ APR for Cash Advances +

+
+

+ {{boxInfoSecured.aprCashAdvance}} +

+
+

+ Penalty APR and When It Applies +

+
+

+ {{boxInfoSecured.aprPenalty}}
+
This APR may be applied to your account if you:
+ - Make a late payment.
+

+

How Long Will the Penalty APR Apply? If your APRs are increased for this reason, the Penalty APR will apply until you make + three consecutive minimum payments when due.

+
+

+ How to Avoid Paying Interest on Purchases +

+
+

Your due date is at least 25 days after close of each billing cycle. We will not charge you any interest + on purchases if you pay your entire balance by the due date each month.

+
+

+ Minimum Interest Charge +

+
+

If you are charged interest, the charge will be no less than {{boxInfoSecured.minInterestCharge}}.

+
+

+ For Credit Card Tips from the Consumer Financial Protection Bureau +

+
+

To learn more about factors to consider when applying for or using a credit card, visit the website + of the Consumer Financial Protection Bureau at

+ http://www.consumerfinance.gov/learnmore. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Fees +

+
+

+ Annual Fee +

+
+

+ $25.00 +

+
+

+ Transaction Fees +

+
+
+
    +
  • Balance Transfer Fees
  • +
+
+

+ {{boxInfoSecured.transBalTransFeeAmt}} or {{boxInfoSecured.transBalTransFeePct}} + of the amount of the each balance transfer, whichever is greater +

+
+
    +
  • Cash Advance Fee
  • +
+
+

+ {{boxInfoSecured.transCashAdvFeeAmt}} or {{boxInfoSecured.transCashAdvFeePct}} + of the amount of the each cash advance, whichever is greater +

+
+
    +
  • Foreign Transaction Fee
  • +
+
+

+ {{boxInfoSecured.transFgnTransFeePct}} + of each transaction in U.S. dollars.

+
+

+ Penalty Fees +

+
+
+
    +
  • Late Payment Fee
  • +
+
+

+ Up to {{boxInfoSecured.penLatePaymentFee}} +

+
+
    +
  • Over-the-Credit Limit Fee
  • +
+
+

+ {{boxInfoSecured.penOverCreditLimitFee}} +

+
+
    +
  • Returned Payment Fee
  • +
+
+

+ Up To {{boxInfoSecured.penRtnPmtFee}} +

+
+

+ How We Will Calculate Your Balance: We use a method called “average daily balance (including + new purchases).” +

+

+ Application of Penalty APR:  Your APR may be increased to the disclosed Penalty APR if you are 60 days late in making + a payment. If a minimum payment or any portion thereof is past due by more than 60 days, Your APR will increase to the + Penalty APR as disclosed in the table under “Penalty APR and When it Applies.” This Penalty APR will be effective + following any notice and waiting period required by law. The Penalty APR will remain on Your Account until Your Account + has been paid on time as disclosed in the “Penalty APR and When it Applies” section and with no other event of default. +

+

+ Minimum Interest Charge: The minimum interest charge will be charged on any dollar amount. +

+

+ Effective Date: The information about the costs of the card described in this application is accurate as of: {{creditCardSharedInformation.securedDateUpdated}} + This information may have changed after that date. To find out what may have changed, contact the Credit Union. +

+

+ For California Borrowers, the Secured Visa® is a secured credit card. Credit extended under this credit card + account is secured by various personal property and money including, but not limited to: (a) any goods you + purchase with this account, (b) any shares you specifically pledge as collateral for this account on a separate + Pledge of Shares. + +

+

+ Notice to New York Residents: New York residents may contact the New York State Department of Financial Services + to obtain a comparative listing of credit card rates, fees, and grace periods. The New York State Department of Financial + Services may be contacted at 1-800-342-3736 or www.dfs.ny.gov  +

+

+ Other Fees & Disclosures: 
+ Late Payment Fee: {{boxInfoSecured.latePmtFee}} or the amount of the required minimum payment, whichever is less, if you are one or more + days late in making a payment. +
+
+ Annual Fee: {{boxInfoSecured.annualFee}}. +
+
+ Balance Transfer Fee (Finance Charge): {{boxInfoSecured.balTransferFeeAmt}} or {{boxInfoSecured.balTransferFeePct}} of the amount of each balance transfer, whichever is greater. +
+
+ Cash Advance Fee (Finance Charge): {{boxInfoSecured.cashAdvanceFeeAmt}} or {{boxInfoSecured.cashAdvanceFeePct}} of the amount of each cash advance, whichever is greater. +
+
+ Returned Payment Fee: {{boxInfoSecured.returnedPmtFee}} or the amount of the required minimum payment, whichever is less. +
+
+ Returned Convenience Check Fee: {{boxInfoSecured.returnedConvenienceCheckFee}} or the amount of the returned convenience check, whichever is less. +
+
+ Card Replacement Fee: {{boxInfoSecured.cardReplacementFee}}. +
+
+ Pay-by-Phone Fee: {{boxInfoSecured.payByPhoneFee}}. +
+
+ Statement Copy Fee: {{boxInfoSecured.statementCopyFee}}. +
+
+ Rush Card Fee: {{boxInfoSecured.rushCardFee}}. +
+

+
+
+
+
+

+ Student + VISA - Terms and Conditions +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Interest Rates and Interest Charges +

+
+

+ Annual Percentage Rate (APR) for Purchases +

+
+ {{boxInfoStudent.aprPurchase}}
+
This APR will vary with the market based on the Prime Rate. +
+

+ APR for Balance Transfers +

+
+ {{boxInfoStudent.aprTransfer}}
+
This APR will vary with the market based on the Prime Rate. +
+

+ APR for Cash Advances +

+
+

+ {{boxInfoStudent.aprCashAdvance}} +

+
+

+ Penalty APR and When It Applies +

+
+

+ {{boxInfoStudent.aprPenalty}}.
+
This APR may be applied to your account if you:
+ - Make a late payment.
+

+

How Long Will the Penalty APR Apply? If your APRs are increased for this reason, the Penalty APR will apply until you make + three consecutive minimum payments when due.

+

+
+

+ How to Avoid Paying Interest on Purchases +

+
+

Your due date is at least 25 days after close of each billing cycle. We will not charge you any interest + on purchases if you pay your entire balance by the due date each month.

+
+

+ Minimum Interest Charge +

+
+

If you are charged interest, the charge will be no less than {{boxInfoStudent.minInterestCharge}}.

+
+

+ For Credit Card Tips from the Consumer Financial Protection Bureau +

+
+

To learn more about factors to consider when applying for or using a credit card, visit the website + of the Consumer Financial Protection Bureau at + http://www.consumerfinance.gov/learnmore.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Fees +

+
+

+ Annual Fee +

+
+

+ None +

+
+

+ Transaction Fees +

+
+
+
    +
  • Balance Transfer Fees
  • +
+
+

+ {{boxInfoStudent.transBalTransFeeAmt}} or {{boxInfoStudent.transBalTransFeePct}} + of the amount of the each balance transfer, whichever is greater +

+
+
    +
  • Cash Advance Fee
  • +
+
+

+ {{boxInfoStudent.transCashAdvFeeAmt}} or {{boxInfoStudent.transCashAdvFeePct}} + of the amount of the each cash advance, whichever is greater +

+
+
    +
  • Foreign Transaction Fee
  • +
+
+

+ {{boxInfoStudent.transFgnTransFeePct}} + of each transaction in U.S. dollars.

+
+

+ Penalty Fees +

+
+
+
    +
  • Late Payment Fee
  • +
+
+

+ Up to {{boxInfoStudent.penLatePaymentFee}} +

+
+
    +
  • Over-the-Credit Limit Fee
  • +
+
+

+ {{boxInfoStudent.penOverCreditLimitFee}} +

+
+
    +
  • Returned Payment Fee
  • +
+
+

+ Up To {{boxInfoStudent.penRtnPmtFee}} +

+
+

+ How We Will Calculate Your Balance: + We use a method called "average daily balance (including new purchases)."

+

+ Application of Penalty APR:  Your APR may be increased to the disclosed Penalty APR if you are 60 days late in making + a payment. If a minimum payment or any portion thereof is past due by more than 60 days, Your APR will increase to the + Penalty APR as disclosed in the table under “Penalty APR and When it Applies.” This Penalty APR will be effective + following any notice and waiting period required by law. The Penalty APR will remain on Your Account until Your Account + has been paid on time as disclosed in the “Penalty APR and When it Applies” section and with no other event of default. +

+

+ Minimum Interest Charge: The minimum interest charge will be charged on any dollar amount. +

+

+ Effective Date: The information about the costs of the card described in this application is accurate as of: {{creditCardSharedInformation.studentDateUpdated}} + This information may have changed after that date. To find out what may have changed, contact the Credit Union. +

+

+ For California Borrowers, the Student Rewards Visa® is a secured credit card. Credit extended under this credit + card account is secured by various personal property and money including, but not limited to: (a) any goods you + purchase with this account, (b) any shares you specifically pledge as collateral for this account on a separate + Pledge of Shares, (c) all shares you have in any individual or joint account with the Credit Union excluding + shares in an Individual Retirement Account or in any other account that would lose special tax treatment under + state or federal law, and (d) collateral securing other loans you have with the Credit Union excluding dwellings. + +

+

+ Notice to New York Residents: New York residents may contact the New York State Department of Financial Services + to obtain a comparative listing of credit card rates, fees, and grace periods. The New York State Department of Financial + Services may be contacted at 1-800-342-3736 or www.dfs.ny.gov  +

+

+ Other Fees & Disclosures: 
+ Late Payment Fee: {{boxInfoStudent.latePmtFee}} or the amount of the required minimum payment, whichever is less, if you are one or more + days late in making a payment. +
+
+ Balance Transfer Fee (Finance Charge): {{boxInfoStudent.balTransferFeeAmt}} or {{boxInfoStudent.balTransferFeePct}} of the amount of each balance transfer, whichever is greater. +
+
+ Cash Advance Fee (Finance Charge): {{boxInfoStudent.cashAdvanceFeeAmt}} or {{boxInfoStudent.cashAdvanceFeePct}} of the amount of each cash advance, whichever is greater. +
+
+ Returned Payment Fee: {{boxInfoStudent.returnedPmtFee}} or the amount of the required minimum payment, whichever is less. +
+
+ Returned Convenience Check Fee: {{boxInfoStudent.returnedConvenienceCheckFee}} or the amount of the returned convenience check, whichever is less. +
+
+ Card Replacement Fee: {{boxInfoStudent.cardReplacementFee}}. +
+
+ Pay-by-Phone Fee: {{boxInfoStudent.payByPhoneFee}}. +
+
+ Statement Copy Fee: {{boxInfoStudent.statementCopyFee}}. +
+
+ Rush Card Fee: {{boxInfoStudent.rushCardFee}}. +
+

+
+
+
+ \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.scss b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.scss new file mode 100644 index 0000000..1d585d5 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.scss @@ -0,0 +1,52 @@ +$max-width: 770px; + +.schumer-text { + font-family: arial, helvetica, sans-serif; + font-size: 11pt; +} + +.schumer-text2 { + font-family: arial, helvetica, sans-serif; + font-size: 11pt; +} + +.schumer-table-td { + border-width: medium 1px; + border-style: none solid; + padding: 3px; + vertical-align: top; + border-collapse: collapse; +} + +.schumer-table-td2 { + border-width: 1px 1px medium; + border-style: solid solid none; + padding: 3px; + vertical-align: top; + border-collapse: collapse; +} + +.schumer-table-td3 { + padding: 3px; + border: 1px solid #000000; + vertical-align: top; + border-collapse: collapse; +} + +.schumer-table { + margin: 0pt 0pt 20px; + border: 2px solid #000000; + width: 100%; + font-family: arial, helvetica, sans-serif; + font-size: 11pt; + border-collapse: collapse; +} + +@media screen and (max-width: $max-width) { + .url-long { + display: none; + } + .url-short[style] { + display: inline !important; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.spec.ts new file mode 100644 index 0000000..042873c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.spec.ts @@ -0,0 +1,40 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { SchumerBoxComponent } from './schumer-box.component'; +import { WindowService } from '../window.service'; +import { configureTestSuite } from 'ng-bullet'; +import { LoanCode } from '../loan-type/loan-code'; + +describe('SchumerBoxComponent', () => { + let component: SchumerBoxComponent; + let fixture: ComponentFixture; + let windowService: WindowService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot() + ], + declarations: [ + SchumerBoxComponent + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SchumerBoxComponent); + component = fixture.componentInstance; + windowService = TestBed.get(WindowService); + }); + + it('should use the window service to print.', () => { + component.boxType = LoanCode.CC_CashBack; + fixture.detectChanges(); + spyOn(windowService, 'showPrintWindowForId').and.returnValue(undefined); + component.commandPrint(); + expect(windowService.showPrintWindowForId).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.ts new file mode 100644 index 0000000..fa29599 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/schumer-boxes/schumer-box.component.ts @@ -0,0 +1,147 @@ +import {Component, Input} from '@angular/core'; +import {WindowService} from '../window.service'; + +@Component({ + selector: 'lw-schumer-box', + templateUrl: './schumer-box.component.html', + styleUrls: ['./schumer-box.component.scss'] +}) +export class SchumerBoxComponent { + + // This is the beginning of factoring out some variables/component stuff. + public boxInfoRewards = { + aprInitial: '0.00%', + aprPurchase: '15.74% to 24.74%', + aprTransfer: '15.74% to 24.74%', + aprCashAdvance: '29.99%', + aprPenalty: '29.99%', + minInterestCharge: '$1.00', + transBalTransFeePct: '5.00%', + transBalTransFeeAmt: '$5.00', + transCashAdvFeePct: '3.00%', + transCashAdvFeeAmt: '$5.00', + transFgnTransFeePct: '1.00%', + penLatePaymentFee: '$25.00', + penOverCreditLimitFee: 'None', + penRtnPmtFee: '$30.00', + latePmtFee: '$25.00', + balTransferFeeAmt: '$5.00', + balTransferFeePct: '5.00%', + introBalTransferFeePct: '3.00%', + cashAdvanceFeeAmt: '$5.00', + cashAdvanceFeePct: '3.00%', + returnedPmtFee: '$30.00', + returnedConvenienceCheckFee: '$30.00', + cardReplacementFee: '$6.00', + payByPhoneFee: '$10.00', + statementCopyFee: '$6.00', + rushCardFee: '$45.00' + + }; + + public boxInfoCashBack = { + aprInitial: '0.00%', + aprPurchase: '15.74% to 24.74%', + aprTransfer: '15.74% to 24.74%', + aprCashAdvance: '29.99%', + aprPenalty: '29.99%', + minInterestCharge: '$1.00', + transBalTransFeePct: '5.00%', + transBalTransFeeAmt: '$5.00', + transCashAdvFeePct: '3.00%', + transCashAdvFeeAmt: '$5.00', + transFgnTransFeePct: '1.00%', + introBalTransferFeePct: '3.00%', + penLatePaymentFee: '$25.00', + penOverCreditLimitFee: 'None', + penRtnPmtFee: '$30.00', + latePmtFee: '$25.00', + balTransferFeeAmt: '$5.00', + balTransferFeePct: '5.00%', + cashAdvanceFeeAmt: '$5.00', + cashAdvanceFeePct: '3.00%', + returnedPmtFee: '$30.00', + returnedConvenienceCheckFee: '$30.00', + cardReplacementFee: '$6.00', + payByPhoneFee: '$10.00', + statementCopyFee: '$6.00', + rushCardFee: '$45.00' + }; + + public boxInfoStudent = { + aprInitial: '', + aprPurchase: '19.49%', + aprTransfer: '19.49%', + aprCashAdvance: '29.99%', + aprPenalty: '29.99%', + minInterestCharge: '$1.00', + transBalTransFeePct: '5.00%', + transBalTransFeeAmt: '$5.00', + transCashAdvFeePct: '3.00%', + transCashAdvFeeAmt: '$5.00', + transFgnTransFeePct: '1.00%', + penLatePaymentFee: '$25.00', + penOverCreditLimitFee: 'None', + penRtnPmtFee: '$30.00', + latePmtFee: '$25.00', + balTransferFeeAmt: '$5.00', + balTransferFeePct: '5.00%', + cashAdvanceFeeAmt: '$5.00', + cashAdvanceFeePct: '3.00%', + returnedPmtFee: '$30.00', + returnedConvenienceCheckFee: '$30.00', + cardReplacementFee: '$6.00', + payByPhoneFee: '$10.00', + statementCopyFee: '$6.00', + rushCardFee: '$45.00' + }; + + public boxInfoSecured = { + aprInitial: '', + aprPurchase: '18.99%', + aprTransfer: '18.99%', + aprCashAdvance: '29.99%', + aprPenalty: '29.99%', + minInterestCharge: '$1.00', + transBalTransFeePct: '5.00%', + transBalTransFeeAmt: '$5.00', + transCashAdvFeePct: '3.00%', + transCashAdvFeeAmt: '$5.00', + transFgnTransFeePct: '1.00%', + penLatePaymentFee: '$25.00', + penOverCreditLimitFee: 'None', + penRtnPmtFee: '$30.00', + latePmtFee: '$25.00', + annualFee: '$25.00', + balTransferFeeAmt: '$5.00', + balTransferFeePct: '5.00%', + cashAdvanceFeeAmt: '$5.00', + cashAdvanceFeePct: '3.00%', + returnedPmtFee: '$30.00', + returnedConvenienceCheckFee: '$30.00', + cardReplacementFee: '$6.00', + payByPhoneFee: '$10.00', + statementCopyFee: '$6.00', + rushCardFee: '$45.00' + }; + + public creditCardSharedInformation = { + cashBackDateUpdated: 'February 1, 2026', + rewardsDateUpdated: 'February 1, 2026', + studentDateUpdated: 'February 1, 2026', + securedDateUpdated: 'February 1, 2025' + } + + @Input() + public boxType: string; + + @Input() + public showSchumer: boolean; + + constructor(private windowService: WindowService) { + } + + public commandPrint(): void { + this.windowService.showPrintWindowForId('outer-schumer-box-container'); + } +} \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.spec.ts new file mode 100644 index 0000000..02fd1a6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.spec.ts @@ -0,0 +1,76 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { configureTestSuite } from 'ng-bullet'; +import { SecurityService } from './security.service'; +import { MsAdalAngular6Module } from 'microsoft-adal-angular6'; + +describe('SecurityService', () => { + + let service: SecurityService; + const testAuthorizedTester = { + profile: { + roles: [SecurityService.TESTER_ROLE_ID] + } + } as adal.User; + const testAuthorizedAdmin = { + profile: { + roles: [SecurityService.ADMIN_ROLE_ID] + } + } as adal.User; + const testNotAuthorizeduser = { + profile: { + roles: ['notamatchingid'] + } + } as adal.User; + const testNotValidData = { + profile: undefined + } as adal.User; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + MsAdalAngular6Module.forRoot({ + tenant: '1234567', + clientId: '7654321', + redirectUri: 'http://localhost', + endpoints: {}, + navigateToLoginRequestUrl: false, + cacheLocation: 'localStorage' + }) + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [] + }); + }); + + beforeEach(() => { + service = TestBed.get(SecurityService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should handle invalid calls to check roll', () => { + expect( service.checkRole(testNotValidData, null) ).toEqual(false); + expect( service.checkRole(testAuthorizedAdmin, null) ).toEqual(false); + }); + + it('should handle invalid users passed to check roll', () => { + expect( service.checkRole(testNotValidData, SecurityService.TESTER_ROLE_ID) ).toEqual(false); + }); + + it('should handle valid calls to check roll', () => { + expect( service.checkRole(testAuthorizedAdmin, SecurityService.TESTER_ROLE_ID) ).toEqual(false); + expect( service.checkRole(testAuthorizedTester, SecurityService.TESTER_ROLE_ID) ).toEqual(true); + expect( service.checkRole(testAuthorizedAdmin, SecurityService.ADMIN_ROLE_ID) ).toEqual(true); + expect( service.checkRole(testNotAuthorizeduser, SecurityService.ADMIN_ROLE_ID) ).toEqual(false); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.ts new file mode 100644 index 0000000..ce5a1af --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/security.service.ts @@ -0,0 +1,50 @@ +import { ErrorHandler, Injectable } from '@angular/core'; +import { MsAdalAngular6Service } from 'microsoft-adal-angular6'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class SecurityService { + public static readonly ADMIN_ROLE_ID = 'Admin'; + public static readonly TESTER_ROLE_ID = 'Tester'; + + constructor(private adalSvc: MsAdalAngular6Service, + private eHandler: ErrorHandler) { + } + + public isAuthenticated(): boolean { + return !!(this.adalSvc.isAuthenticated); + } + + public login(): void { + this.adalSvc.login(); + } + + public checkAdminRole(): boolean | Observable { + if (this.adalSvc.userInfo) { + return this.checkRole(this.adalSvc.userInfo, SecurityService.ADMIN_ROLE_ID); + } + return false; + } + + public checkTesterRole(): boolean | Observable { + if (this.adalSvc.userInfo) { + return this.checkRole(this.adalSvc.userInfo, SecurityService.TESTER_ROLE_ID); + } + return false; + } + + public checkRole(userInfo: adal.User, role: String): boolean | Observable { + if (userInfo && userInfo.profile) { + if (userInfo.profile.roles && userInfo.profile.roles.length) { + const roles = userInfo.profile.roles; + return roles.indexOf(role) > -1; + } else { + // @TODO ERROR_NOTIFY + } + } + return false; + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/shared.module.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/shared.module.ts new file mode 100644 index 0000000..a306d85 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/shared.module.ts @@ -0,0 +1,12 @@ +import {LoanCodePipe} from './pipes/loan-code.pipe'; +import {NgModule} from '@angular/core'; +import {SchumerBoxComponent} from './schumer-boxes/schumer-box.component'; +import {CommonModule} from '@angular/common'; + +@NgModule({ + imports: [CommonModule], + declarations: [LoanCodePipe, SchumerBoxComponent], + exports: [LoanCodePipe, SchumerBoxComponent] +}) +export class SharedModule { +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/available-terms-calculator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/available-terms-calculator.ts new file mode 100644 index 0000000..7b9af24 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/available-terms-calculator.ts @@ -0,0 +1,5 @@ +import { TermInputFields } from './term-input-fields'; + +export interface AvailableTermsCalculator { + determineTerm(): Array<{ value: any, desc: string }>; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.spec.ts new file mode 100644 index 0000000..64e6a67 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.spec.ts @@ -0,0 +1,59 @@ +import { TermInputFields } from './term-input-fields'; +import { DefaultTermCalculator } from './default-term-calculator'; +import { LoanCode } from '../loan-type/loan-code'; +import { TermOption } from '../configuration/ui-settings'; +import { DefaultSettings } from '../configuration/default-settings.mock'; + +const boatMockOptions = { + 5000: [48], + 10000: [63, 84], + 20000: [120, 180] +}; + +const mockAvailableTermOptions = >DefaultSettings.termOptionsNoYear; + +const mockInputBoat: TermInputFields = new TermInputFields(LoanCode.BOAT, 0, 5000, 0); +const mockInputBoatHighAmount: TermInputFields = new TermInputFields(LoanCode.BOAT, 0, 25000, 0); + +describe('DefaultTermCalculator', () => { + + it('should build a list of term options from an array of numbers', () => { + const calc = new DefaultTermCalculator(mockInputBoat, boatMockOptions, mockAvailableTermOptions); + + const listOfPotentialTermLengths = [36, 48, 63 , 72, 75, 84, 120, 180]; + expect(calc.buildListByFilteringTermOptions(listOfPotentialTermLengths, mockAvailableTermOptions)) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 72, desc: '72 months'}, + {value: 75, desc: '75 months'}, + {value: 84, desc: '84 months'}, + {value: 120, desc: '120 months'}, + {value: 180, desc: '180 months'}, + ]); + }); + + it('should build a list of boat terms based on 5000 amount', () => { + const calc = new DefaultTermCalculator(mockInputBoat, boatMockOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 48, desc: 'Up to 48 months'}, + ]); + }); + + it('should build a list of boat terms based on 25000 amount', () => { + const calc = new DefaultTermCalculator(mockInputBoatHighAmount, boatMockOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 48, desc: 'Up to 48 months'}, + {value: 63, desc: '63 months'}, + {value: 84, desc: '84 months'}, + {value: 120, desc: '120 months'}, + {value: 180, desc: '180 months'}, + ]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.ts new file mode 100644 index 0000000..110ab8d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/default-term-calculator.ts @@ -0,0 +1,50 @@ +import { AvailableTermsCalculator } from './available-terms-calculator'; +import { TermInputFields } from './term-input-fields'; +import { TermOptions } from 'src/app/shared/options/term-options'; +import { TermOption } from '../configuration/ui-settings'; + +export class DefaultTermCalculator implements AvailableTermsCalculator { + private fields: TermInputFields; + private termMonthsOptions: any; + private availableTermOptions: Array; + + constructor(fields: TermInputFields, termMonthsOptions: any, availableTermOptions: Array) { + this.fields = fields; + this.termMonthsOptions = termMonthsOptions; + this.availableTermOptions = availableTermOptions; + } + + public determineTerm(): Array<{ value: any, desc: string }> { + return this.buildTerms(this.termMonthsOptions, this.fields, this.availableTermOptions); + } + + public buildTerms( + termMonthsOptions: any, fields: TermInputFields, availableTermOptions: Array): Array<{ value: any, desc: string }> { + let uiTermsFromRequestedAmount = []; + + Object.keys(termMonthsOptions).forEach(tier => { + if (parseInt(tier, 10) <= this.fNum(fields.amount)) { + uiTermsFromRequestedAmount = uiTermsFromRequestedAmount.concat(termMonthsOptions[tier]); + } + }); + return this.buildListByFilteringTermOptions(uiTermsFromRequestedAmount, availableTermOptions); + } + + public buildListByFilteringTermOptions( + termMonthsOptions: Array, availableTermOptions: Array): Array<{ value: any, desc: string }> { + const uiSettingsKeyTermOptionsWithoutYears = TermOptions.uiSettingsKeyWithoutYear; + + let filteredTerms = availableTermOptions.filter(term => { + return termMonthsOptions.indexOf(term.value) > -1; + }); + if (filteredTerms.length) { + filteredTerms = TermOptions.insertTextDecorations(filteredTerms); + } + filteredTerms.unshift(TermOptions.defaultTermOption); + return filteredTerms; + } + + public fNum(num: number): number { + return parseInt(num.toString().replace(/[^0-9]/g, ''), 10); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/term-input-fields.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/term-input-fields.ts new file mode 100644 index 0000000..63c2103 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/term-input-fields.ts @@ -0,0 +1,18 @@ +import { LoanCode } from '../loan-type/loan-code'; +import { VehicleType } from '../vehicle-type/vehicle-type'; + +export class TermInputFields { + type: LoanCode; + year: number; + amount: number; + miles: number; + vehicleType?: VehicleType; + + constructor(type: LoanCode, year: number, amount: number, miles: number, vehicleType?: VehicleType) { + this.type = type; + this.year = year; + this.amount = amount; + this.miles = miles; + this.vehicleType = vehicleType; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.spec.ts new file mode 100644 index 0000000..86f4a85 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.spec.ts @@ -0,0 +1,157 @@ +import { VehicleTermCalculator } from './vehicle-term-calculator'; +import { TermInputFields } from './term-input-fields'; +import { LoanCode } from '../loan-type/loan-code'; +import { TermOption } from '../configuration/ui-settings'; +import { DefaultSettings } from '../configuration/default-settings.mock'; +import { VehicleType } from '../vehicle-type/vehicle-type'; + +const mockTermOptions = { + 2012: [36, 48], + 2013: [36, 48, 63], + 2014: [36, 48, 63, 75], + 2018: [36, 48, 63, 75], + 2019: [36, 48, 63, 75, 84], + 2022: [36, 48, 63, 75], + 2023: [36, 48, 63, 75, 84] +}; + +const vehicle1899: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 1899, 5000, 10000); +const vehicle1900: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 1900, 5000, 10000); +const vehicle1950: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 1950, 5000, 10000); +const vehicle2012: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2012, 5000, 10000); +const vehicle2023LowAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2023, 5000, 10000); +const vehicle2023HighAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2023, 30000, 1000); +const vehicle2022HighAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2022, 30000, 1000); +const vehicle2030HighAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2030, 30000, 1000); +const vehicle2030CycleLowAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2030, 14000, 1000, VehicleType.CYCLE); +const vehicle2030CycleHighAmount: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2030, 16000, 1000, VehicleType.CYCLE); +const vehicle2050: TermInputFields = new TermInputFields(LoanCode.VEHICLE_DEFAULT, 2050, 30000, 1000); + +describe('VehicleTermCalculator', () => { + const mockAvailableTermOptions = >DefaultSettings.termOptionsNoYear; + + it('should determine year options', () => { + const calc = new VehicleTermCalculator(vehicle2023HighAmount, mockTermOptions, mockAvailableTermOptions); + spyOn(calc, 'determineYearOptions').and.callThrough(); + let options = calc.determineYearOptions(mockTermOptions, vehicle2012); + expect(options).toEqual([36, 48]); + options = calc.determineYearOptions(mockTermOptions, vehicle2023HighAmount); + expect(options).toEqual([36, 48, 63, 75, 84]); + options = calc.determineYearOptions(mockTermOptions, vehicle2023HighAmount); + expect(options).toEqual([36, 48, 63, 75, 84]); + options = calc.determineYearOptions(mockTermOptions, vehicle1900); + expect(options).toEqual([36, 48]); + options = calc.determineYearOptions(mockTermOptions, vehicle1899); + expect(options).toEqual([]); + options = calc.determineYearOptions(mockTermOptions, vehicle2050); + expect(options).toEqual([]); + expect(calc.determineYearOptions).toHaveBeenCalled(); + }); + + it('should build a standard list of vehicle terms. 2030 year/high amount', () => { + const calc = new VehicleTermCalculator(vehicle2030HighAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + {value: 84, desc: '84 months'}, + ]); + }); + + it('should build a standard list of vehicle terms. 2023 year/high amount', () => { + const calc = new VehicleTermCalculator(vehicle2023HighAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + {value: 84, desc: '84 months'}, + ]); + }); + + it('should build a filtered list of vehicle terms for motorcycle below 15000', () => { + const calc = new VehicleTermCalculator(vehicle2030CycleLowAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + ]); + }); + + it('should build a standard list of vehicle terms for motorcycle at 15000 or above', () => { + const calc = new VehicleTermCalculator(vehicle2030CycleHighAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + {value: 84, desc: '84 months'}, + ]); + }); + + it('should build a filtered list of vehicle terms. 2023 year/low amount', () => { + const calc = new VehicleTermCalculator(vehicle2023LowAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'}, + ]); + }); + + it('should build a standard list of vehicle terms. 2022 year/high amount', () => { + const calc = new VehicleTermCalculator(vehicle2022HighAmount, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + {value: 63, desc: '63 months'}, + {value: 75, desc: '75 months'} + ]); + }); + + it('should build a standard list of vehicle terms based on 2012 year', () => { + const calc = new VehicleTermCalculator(vehicle2012, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + ]); + }); + + it('should build a default list of vehicle terms based on 1950 year', () => { + const calc = new VehicleTermCalculator(vehicle1950, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([ + {value: '', desc: 'FORMS.GENERAL.SELECTDEFAULT'}, + {value: 36, desc: 'Up to 36 months'}, + {value: 48, desc: '48 months'}, + ]); + }); + + it('should build a empty list of vehicle terms based on 1899 year', () => { + const calc = new VehicleTermCalculator(vehicle1899, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([]); + }); + + it('should build a empty list of vehicle terms based on 2050 year', () => { + const calc = new VehicleTermCalculator(vehicle2050, mockTermOptions, mockAvailableTermOptions); + expect(calc.determineTerm()) + .toEqual([]); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.ts new file mode 100644 index 0000000..38b6bcd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/terms/vehicle-term-calculator.ts @@ -0,0 +1,66 @@ +import { AvailableTermsCalculator } from './available-terms-calculator'; +import { TermInputFields } from './term-input-fields'; +import { TermOptions } from 'src/app/shared/options/term-options'; +import { CreditApplicationValues } from '../credit-application-values'; +import { TermOption } from '../configuration/ui-settings'; +import { VehicleType } from '../vehicle-type/vehicle-type'; + +export class VehicleTermCalculator implements AvailableTermsCalculator { + private fields: TermInputFields; + private termMonthsOptions: any; + private availableTermOptions: Array; + + constructor(fields: TermInputFields, termMonthsOptions: any, availableTermOptions: Array) { + this.fields = fields; + this.termMonthsOptions = termMonthsOptions; + this.availableTermOptions = availableTermOptions; + } + + public determineTerm(): Array<{ value: any, desc: string }> { + return this.buildTerms(this.termMonthsOptions, this.fields, this.availableTermOptions); + } + + public buildTerms( + termMonthsOptions: any, + fields: TermInputFields, + availableTermOptions: Array): Array<{ value: any, desc: string }> { + const yearOptions = this.determineYearOptions(termMonthsOptions, fields); + const minimumAmount = fields.vehicleType === VehicleType.CYCLE ? 15000 : 25000; + if (yearOptions && yearOptions.length > 0) { + let filteredTerms = availableTermOptions.filter(term => { + return yearOptions.indexOf(term.value) > -1; + }); + if (filteredTerms.length) { + filteredTerms = TermOptions.insertTextDecorations(filteredTerms); + } + filteredTerms.unshift(TermOptions.defaultTermOption); + if (this.fNum(fields.year) >= CreditApplicationValues.VEHICLE_CURRENT_YEAR) { + if (this.fNum(fields.amount) < minimumAmount) { + filteredTerms.pop(); + } + } + return filteredTerms; + } + return []; + } + + public determineYearOptions(options: any, fields: TermInputFields): Array { + if (this.fNum(fields.year) >= CreditApplicationValues.VEHICLE_CUT_OFF_YEAR_LOWER && + this.fNum(fields.year) <= CreditApplicationValues.VEHICLE_CUT_OFF_YEAR_UPPER) { + let yearOptions = options[this.fNum(fields.year)]; + if (!yearOptions) { + if (this.fNum(fields.year) > CreditApplicationValues.VEHICLE_CURRENT_YEAR) { + yearOptions = options[CreditApplicationValues.VEHICLE_CURRENT_YEAR]; + } else { + yearOptions = options[Object.keys(options)[0]]; + } + } + return yearOptions; + } + return []; + } + + public fNum(num: number): number { + return parseInt(num.toString().replace(/[^0-9]/g, ''), 10); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.html new file mode 100644 index 0000000..0830340 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.html @@ -0,0 +1,8 @@ +

TIMEOUT.TITLE

+

TIMEOUT.REASON

+

{{infoMessage}}

+
+
+ +
+
diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.spec.ts new file mode 100644 index 0000000..f5df5af --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TimeoutLandingPageComponent } from './timeout-landing-page.component'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('TimeoutPageComponent', () => { + let component: TimeoutLandingPageComponent; + let fixture: ComponentFixture; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot(), + ReactiveFormsModule + ], + declarations: [ + TimeoutLandingPageComponent + ], + providers: [ + { provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService } + ], + schemas: [ NO_ERRORS_SCHEMA ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeoutLandingPageComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should bind the home button to the correct command function', async(() => { + spyOn(component, 'commandBackHome'); + const button = fixture.debugElement.query(By.css('#buttonGoHome')).nativeElement; + button.click(); + fixture.whenStable().then(() => { + expect(component.commandBackHome).toHaveBeenCalled(); + }); + })); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.ts new file mode 100644 index 0000000..8ac20f2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-landing-page.component.ts @@ -0,0 +1,34 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'lw-timeout-landing', + templateUrl: './timeout-landing-page.component.html' +}) +export class TimeoutLandingPageComponent implements OnInit { + public readonly adminRoute = '/admin'; + public readonly defaultRoute = '/'; + public infoMessage = 'TIMEOUT.INFO'; + private home: string; + + constructor(private router: Router, private route: ActivatedRoute) { + } + + ngOnInit() { + this.route.queryParams.subscribe(params => this.home = params['home'] || this.defaultRoute); + this.infoMessage = this.getInfoMessage(); + } + + public commandBackHome() { + this.router.navigate([this.home], {relativeTo: null}); + } + + public getInfoMessage() { + if (this.home === this.adminRoute) { + return 'TIMEOUT.INFO_ADMIN'; + } else { + return 'TIMEOUT.INFO'; + } + } + +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.html b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.html new file mode 100644 index 0000000..20cae27 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.html @@ -0,0 +1,18 @@ + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.spec.ts new file mode 100644 index 0000000..da495fc --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.spec.ts @@ -0,0 +1,192 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Subject } from 'rxjs'; +import { configureTestSuite } from 'ng-bullet'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { TimeoutPopupComponent } from './timeout-popup.component'; +import { TimeoutPopupService } from './timeout-popup.service'; + +class ModalRefStub { + close = jasmine.createSpy('close'); + result = Promise.resolve('Extend'); +} + +class NgbModalStub { + open = jasmine.createSpy('open').and.callFake(() => new ModalRefStub()); +} + +class TimeoutPopupServiceStub { + private timerSubject = new Subject(); + timeout$ = this.timerSubject.asObservable(); + + start = jasmine.createSpy('start'); + restart = jasmine.createSpy('restart'); + clear = jasmine.createSpy('clear'); + clearAllData = jasmine.createSpy('clearAllData'); + navigateToTimeoutPage = jasmine.createSpy('navigateToTimeoutPage'); + navigateToHomePage = jasmine.createSpy('navigateToHomePage'); + + emitTimer(value: number) { + this.timerSubject.next(value); + } +} + +describe('TimeoutPopupComponent', () => { + let component: TimeoutPopupComponent; + let fixture: ComponentFixture; + let timeoutService: TimeoutPopupServiceStub; + let modalService: NgbModalStub; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [TimeoutPopupComponent], + providers: [ + { provide: TimeoutPopupService, useClass: TimeoutPopupServiceStub }, + { provide: NgbModal, useClass: NgbModalStub } + ], + schemas: [NO_ERRORS_SCHEMA] + }); + + TestBed.overrideComponent(TimeoutPopupComponent, { + set: { template: '' } + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeoutPopupComponent); + component = fixture.componentInstance; + timeoutService = TestBed.get(TimeoutPopupService) as TimeoutPopupServiceStub; + modalService = TestBed.get(NgbModal) as NgbModalStub; + fixture.detectChanges(); + }); + + it('should create and start the timer', () => { + expect(component).toBeTruthy(); + expect(timeoutService.start).toHaveBeenCalledWith(900, component.warningAt); + }); + + it('should open the warning modal when warning threshold is hit', () => { + spyOn(component, 'openWarning').and.callThrough(); + timeoutService.emitTimer(component.warningAt); + expect(component.openWarning).toHaveBeenCalled(); + expect(modalService.open).toHaveBeenCalled(); + }); + + it('should open warning when timer emits signal', () => { + const openWarningSpy = spyOn(component, 'openWarning').and.callThrough(); + const receivedValues: number[] = []; + const subscription = component.warningTimer$.subscribe(value => receivedValues.push(value)); + + timeoutService.emitTimer(component.warningAt); + + expect(receivedValues[0]).toBe(component.warningAt); + expect(openWarningSpy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); + + it('should bind the extend button to the correct command function (leave session)', fakeAsync(() => { + const modalRef = new ModalRefStub(); + modalRef.result = Promise.resolve('Extend'); + component.localState.appTimedOut = true; + modalService.open.and.returnValue(modalRef); + + component.openWarning(); + tick(); + + expect(timeoutService.navigateToHomePage).not.toHaveBeenCalled(); + expect(timeoutService.restart).toHaveBeenCalled(); + expect(component.localState.appTimedOut).toBeFalsy(); + })); + + it('should navigate to timeout page after timeout', fakeAsync(() => { + timeoutService.navigateToTimeoutPage.calls.reset(); + timeoutService.clearAllData.calls.reset(); + component.openWarning(); + component.localState.appTimedOut = true; + + timeoutService.emitTimer(-1); + tick(); + + expect(timeoutService.navigateToTimeoutPage).toHaveBeenCalled(); + expect(timeoutService.clearAllData).toHaveBeenCalled(); + })); + + it('should not navigate to timeout page while timer is still going', () => { + timeoutService.navigateToTimeoutPage.calls.reset(); + timeoutService.clearAllData.calls.reset(); + timeoutService.emitTimer(component.warningAt + 10); + + expect(timeoutService.navigateToTimeoutPage).not.toHaveBeenCalled(); + expect(timeoutService.clearAllData).not.toHaveBeenCalled(); + }); + + it('should trigger timeout flow when timer value drops below zero', fakeAsync(() => { + component.openWarning(); + component.localState.appTimedOut = true; + timeoutService.emitTimer(-1); + tick(); + expect(timeoutService.clearAllData).toHaveBeenCalled(); + expect(timeoutService.navigateToTimeoutPage).toHaveBeenCalled(); + })); + + it('should reset timer on user action', () => { + component.userAction(); + expect(timeoutService.restart).toHaveBeenCalled(); + }); + + it('should reset timer when user is active ( uses keyboard / mouse )', () => { + timeoutService.restart.calls.reset(); + + component.userAction(); + + expect(timeoutService.restart).toHaveBeenCalledTimes(1); + }); + + it('should allow sessionLogic to call warning', () => { + const warningSpy = spyOn(component, 'sessionTimeoutWarning'); + + component.sessionLogic(component.warningAt); + + expect(warningSpy).toHaveBeenCalled(); + }); + + it('should allow sessionLogic to call timeout with loantype', () => { + const modalRef = new ModalRefStub(); + component['modalReference'] = modalRef as any; + component.localState.appTimedOut = true; + const timeoutSpy = spyOn(component, 'sessionTimeout').and.callThrough(); + + component.sessionLogic(-1); + + expect(timeoutSpy).toHaveBeenCalled(); + expect(modalRef.close).toHaveBeenCalledWith('TimedOut'); + expect(timeoutService.clearAllData).toHaveBeenCalled(); + expect(timeoutService.navigateToTimeoutPage).toHaveBeenCalled(); + expect(component.localState.appTimedOut).toBeFalsy(); + }); + + it('should close the modal when session timeout occurs', fakeAsync(() => { + component.openWarning(); + const modalRef = modalService.open.calls.mostRecent().returnValue as ModalRefStub; + component.localState.appTimedOut = true; + component.sessionTimeout(); + tick(); + expect(modalRef.close).toHaveBeenCalledWith('TimedOut'); + })); + + it('should call sessionTimeoutWarning only once per cycle', () => { + spyOn(component, 'sessionTimeoutWarning').and.callThrough(); + timeoutService.emitTimer(component.warningAt); + timeoutService.emitTimer(component.warningAt); + expect(component.sessionTimeoutWarning).toHaveBeenCalledTimes(2); + }); + + it('should subscribe to timeout service', () => { + const subscribeSpy = spyOn(timeoutService.timeout$, 'subscribe').and.callThrough(); + + component.subscribeToTimer(); + + expect(subscribeSpy).toHaveBeenCalled(); + }); +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.ts new file mode 100644 index 0000000..8c4ffaf --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.component.ts @@ -0,0 +1,99 @@ +import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { TimeoutPopupService } from './timeout-popup.service'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, Subscription } from 'rxjs'; + +@Component({ + selector: 'lw-timeout-popup', + templateUrl: './timeout-popup.component.html' +}) +export class TimeoutPopupComponent implements OnInit, OnDestroy { + + // times are in term of seconds + private readonly timeoutDuration = 900; + private modalReference: NgbModalRef; + private warningTimerSubReference: Subscription; + + public readonly warningAt = 30; + public warningTimer$: Observable; + @ViewChild('content') modalContent: ElementRef; + + public localState = { + appTimedOut: false + }; + + // check for key input, mouse click, or mouse scroll wheel for user activity + @HostListener('document:keyup', ['$event']) + @HostListener('document:click', ['$event']) + @HostListener('document:wheel', ['$event']) + userAction() { + this.timeoutService.restart(); + } + + constructor(private timeoutService: TimeoutPopupService, private modalService: NgbModal) { + } + + public ngOnInit() { + this.timeoutService.start(this.timeoutDuration, this.warningAt); + this.subscribeToTimer(); + } + + public ngOnDestroy() { + this.timeoutService.clear(); + if ( this.warningTimerSubReference ) { + this.warningTimerSubReference.unsubscribe(); + } + } + + public subscribeToTimer() { + this.warningTimer$ = this.timeoutService.timeout$; + this.warningTimerSubReference = this.warningTimer$.subscribe( + x => this.sessionLogic(x), + e => console.error(e)); + } + + public sessionLogic( timerValue: number) { + if ( timerValue === this.warningAt ) { + this.sessionTimeoutWarning(); + } + if ( timerValue < 0) { + this.sessionTimeout(); + this.localState.appTimedOut = false; + } + } + + public openWarning() { + this.modalReference = this.modalService.open(this.modalContent); + this.modalReference.result.then((result) => { + if (result === 'Home') { + this.timeoutService.navigateToHomePage(); + } + if ( result !== 'TimedOut') { + this.resetTimer(); + } + }, () => { + // user rejects the modal, via escape/clicking on backdrop + this.resetTimer(); + }); + } + + public resetTimer() { + this.localState.appTimedOut = false; + this.timeoutService.restart(); + } + + public sessionTimeout() { + this.modalReference.close('TimedOut'); + if (this.localState.appTimedOut) { + this.timeoutService.clearAllData(); + this.timeoutService.navigateToTimeoutPage(); + } + } + + public sessionTimeoutWarning() { + if (!this.localState.appTimedOut) { + this.localState.appTimedOut = true; + this.openWarning(); + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.spec.ts new file mode 100644 index 0000000..3db5b84 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.spec.ts @@ -0,0 +1,90 @@ +import { Router } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TimeoutPopupService } from './timeout-popup.service'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { CreditApplicationDaoService } from '../credit-application-dao.service'; +import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service'; +import { TimeoutLandingPageComponent } from './timeout-landing-page.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { configureTestSuite } from 'ng-bullet'; + + +describe('Timeout Popup Service', () => { + + let service: TimeoutPopupService; + let applicationWizardService: ApplicationWizardService; + let router: Router; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: 'timeout', component: TimeoutLandingPageComponent} + ]), + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [TimeoutLandingPageComponent], + providers: [ + {provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(TimeoutPopupService); + applicationWizardService = TestBed.get(ApplicationWizardService); + router = TestBed.get(Router); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should start a timer', () => { + spyOn(service, 'start'); + service.start(); + expect(service.start).toHaveBeenCalled(); + }); + + it('should reset a timer', () => { + spyOn(service, 'restart'); + service.restart(); + expect(service.restart).toHaveBeenCalled(); + }); + + it('should clear a timer', () => { + spyOn(service, 'clear').and.callThrough(); + spyOn(service, 'start').and.callThrough(); + service.start(); + service.clear(); + expect(service.clear).toHaveBeenCalled(); + }); + + it('should clear all form data', () => { + spyOn(service, 'clearAllData').and.callThrough(); + spyOn( applicationWizardService, 'handlePostApplicationEvents'); + service.clearAllData(); + expect(service.clearAllData).toHaveBeenCalled(); + expect(applicationWizardService.handlePostApplicationEvents).toHaveBeenCalled(); + }); + + it('should navigate to timeout page', () => { + spyOn(service, 'navigateToTimeoutPage').and.callThrough(); + spyOn(router, 'navigate').and.callThrough(); + service.navigateToTimeoutPage(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should navigate to home page', () => { + spyOn(service, 'navigateToHomePage').and.callThrough(); + spyOn(router, 'navigate'); + service.navigateToHomePage(); + expect(router.navigate).toHaveBeenCalled(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.ts new file mode 100644 index 0000000..4a1103e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/timeout/timeout-popup.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service'; +import { Router } from '@angular/router'; +import { Observable, Subject, Subscription, timer } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { CreditApplication } from '../credit-application'; + +@Injectable({ + providedIn: 'root' +}) +export class TimeoutPopupService { + + private running = false; + private currentCount = 0; + private warnAt = 0; + private tickerInterval: any; + private tickerIntervalSubscription: Subscription; + private initialCountValue = 0; + private adminRoute = '/admin'; + + private timeoutSubject: Subject = new Subject(); + public timeout$: Observable; + + constructor(private applicationWizard: ApplicationWizardService, private router: Router) { + } + + public start(countdownFrom: number = 100, warnAt: number = 30) { + this.running = true; + this.currentCount = countdownFrom; + this.initialCountValue = countdownFrom; + this.warnAt = warnAt; + this.tickerInterval = timer(1000, 1000); + if (environment.enableTimeout) { + this.tickerIntervalSubscription = this.tickerInterval.subscribe(() => this.tick()); + } + this.timeout$ = this.timeoutSubject.asObservable(); + } + + public clear() { + this.running = false; + this.currentCount = this.initialCountValue; + if (this.timeoutSubject) { + this.timeoutSubject.unsubscribe(); + } + if (this.tickerIntervalSubscription) { + this.tickerIntervalSubscription.unsubscribe(); + } + } + + public restart() { + this.currentCount = this.initialCountValue; + if (!this.running) { + this.running = true; + } + } + + public tick() { + if (this.currentCount <= this.warnAt && this.running) { + this.timeoutSubject.next(this.currentCount); + } + if (this.currentCount === 0) { + this.timeoutSubject.next(-1); + this.running = false; + } + this.currentCount--; + } + + public clearAllData() { + this.applicationWizard.handlePostApplicationEvents(); + } + + public navigateToTimeoutPage() { + const creditApplication: CreditApplication = this.applicationWizard.getFullCreditApplication(); + if (creditApplication && creditApplication.hasBeenCompleted) { + location.href = 'https://www.landmarkcu.com'; + } + this.router.navigate(['/timeout'], { + queryParams: { + home: this.selectHomePage(), + }, + relativeTo: null + }).catch( + reason => (console.error('Cannot navigate to timeout: ' + reason))); + } + + public navigateToHomePage() { + this.router.navigate([this.selectHomePage()], {relativeTo: null}); + } + + public selectHomePage() { + if (this.router.url.includes(this.adminRoute)) { + return this.adminRoute; + } else { + return '/'; + } + } + +} + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/motor-type.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/motor-type.ts new file mode 100644 index 0000000..5236968 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/motor-type.ts @@ -0,0 +1,6 @@ +export enum MotorType { + INBOARD = 1, + OUTBOARD = 2, + INBOARD_OUTBOARD = 3 + } + \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/trailer-type.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/trailer-type.ts new file mode 100644 index 0000000..47aed6a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/trailer-type.ts @@ -0,0 +1,7 @@ +export enum TrailerType { + TANDEM_AXEL = 'TandemAxel', + SINGLE_AXEL = 'SingleAxel', + TRI_AXLE = 'TriAxle' +} + +// 'Axle' vs 'Axel' spelling here is correct according to ACTion mapping \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/vehicle-type.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/vehicle-type.ts new file mode 100644 index 0000000..1f29e22 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/vehicle-type/vehicle-type.ts @@ -0,0 +1,12 @@ + + +export enum VehicleType { + AUTO = 'Automobile', + CYCLE = 'Motorcycle', + BOAT = 'Boat', + TRAILER= 'TrailerMotorHome', + ATV = 'ATV', + SNOW = 'Snowmobile', + PWC = 'PWC', + RV = 'RecreationalVehicle' +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.spec.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.spec.ts new file mode 100644 index 0000000..cb4b7e4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { WindowService } from './window.service'; +import { configureTestSuite } from 'ng-bullet'; + +describe('WindowService', () => { + + let service: WindowService; + + configureTestSuite(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgbModule.forRoot() + ], + schemas: [ + NO_ERRORS_SCHEMA + ], + declarations: [ + + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(WindowService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + +}); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.ts new file mode 100644 index 0000000..82152b7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/shared/window.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class WindowService { + + public showWindowForId(windowTargetId: string): void { + let contentHtml, popupWin; + contentHtml = document.getElementById(windowTargetId).innerHTML; + popupWin = window.open('', '_blank', 'top=0,left=0,height=auto,width=auto'); + popupWin.document.open(); + popupWin.document.write(` + + + Terms + + ${contentHtml} + ` + ); + popupWin.document.close(); + } + + public showPrintWindowForId(windowTargetId: string): void { + let printContents, popupWin; + printContents = document.getElementById(windowTargetId).innerHTML; + popupWin = window.open('', '_blank', 'top=0,left=0,height=100%,width=auto'); + popupWin.document.open(); + popupWin.document.write(` + + + Print Terms + + + ${printContents} + ` + ); + popupWin.document.close(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/activated-route.mock.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/activated-route.mock.ts new file mode 100644 index 0000000..7eb4366 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/activated-route.mock.ts @@ -0,0 +1,8 @@ +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; + +export class MockActivatedRoute extends ActivatedRoute { + snapshot = new ActivatedRouteSnapshot(); + constructor(options) { + super(); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/api-translator-pre-post.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/api-translator-pre-post.ts new file mode 100644 index 0000000..73f744a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/api-translator-pre-post.ts @@ -0,0 +1,348 @@ +import { LoanTypeSet } from '../../shared/loan-type'; +import { CreditApplicationValues } from '../../shared/credit-application-values'; +import { CreditApplication } from '../../shared/credit-application'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +import { ApiStatus } from 'src/app/shared/api-models/api-status'; +import { ApiErrorCode } from 'src/app/shared/api-models/api-error'; +import { ApiLoanApplication } from 'src/app/shared/api-models/api-loan-application'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { MfgHomeTypeEnum, OccupancyTypeEnum, PhoneTypeEnum } from 'src/app/shared/api-models/api-enums'; +import {LoanTitle} from '../../shared/loan-type/loan-title'; + +export const apiTranslatorPreTranslate: CreditApplication = { + 'loanType': LoanTypeSet[8], + 'note': 'This is a note!', + 'primaryContactMethod': '262-623-8990', + 'hasBeenCompleted': false, + 'disclosureInformation': { + 'truthful': true, + 'contact': true, + 'date': new Date() + }, + 'coApplicantLivesInMaritalPropertyState': true, + 'fullInfoBorrowers': [ + { + 'addressInformation': { + 'addressLine1': '123 Main St', + 'addressLine2': '', + 'zipcode': '53202', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP + }, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '8', + 'residenceTimeMonths': '5', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Primary', + 'middleInitial': 'J', + 'lastName': 'Borrower', + 'dateOfBirth': '01/01/2010', + 'socialSecurityNumber': '666-55-5444', + 'primaryPhoneNumberType': PhoneTypeEnum.Home, + 'primaryPhoneNumber': '(555) 555-0134', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'primary@example.com', + 'maritalStatus': CreditApplicationValues.CHOICE_YES + }, + 'genderInformation': { + 'female': null, + 'male': true, + 'notProvided': null, + }, + 'ethnicityInformation': { + 'hispanicOrLatino': true, + 'mexican': null, + 'puertoRican': null, + 'cuban': null, + 'otherHispanicLatino': null, + 'otherHispanicLatinoText': null, + 'notHispanicOrLatino': null, + 'notProvided': null + }, + 'raceInformation': { + 'americanIndianAlaskaNative': null, + 'americanIndianAlaskaNativeText': null, + 'blackOrAfricanAmerican': null, + 'asian': null, + 'asianIndian': null, + 'chinese': null, + 'filipino': null, + 'japanese': null, + 'korean': null, + 'vietnamese': null, + 'otherAsian': true, + 'otherAsianText': 'Thai', + 'hawaiian': null, + 'nativeHawaiian': null, + 'guamanianChamorro': null, + 'samoan': null, + 'otherPacificIslander': null, + 'otherPacificIslanderText': null, + 'white': null, + 'notProvided': null + }, + 'employmentInformation': [{ + 'employerName': 'Petes Purple Pies', + 'employmentType': 'FullTime', + 'jobTitle': 'Lead Pie Prepper', + 'salary': 100000, + 'schedule': 'Annually', + 'employmentLengthYears': 10, + 'employmentLengthMonths': 0, + 'weeklyHours': null, + 'workPhoneNumber': '' + }] + }, { + 'addressInformation': { + 'addressLine1': '123 Main St', + 'addressLine2': '', + 'zipcode': '53202', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP + }, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '8', + 'residenceTimeMonths': '5', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Spouse', + 'middleInitial': 'R', + 'lastName': 'Borrower', + 'dateOfBirth': '01/01/2009', + 'socialSecurityNumber': '666-33-3222', + 'primaryPhoneNumberType': PhoneTypeEnum.Home, + 'primaryPhoneNumber': '(555) 555-0199', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'coapp@example.org', + 'maritalStatus': CreditApplicationValues.CHOICE_YES + }, + 'genderInformation': { + 'female': null, + 'male': null, + 'notProvided': true + }, + 'ethnicityInformation': { + 'hispanicOrLatino': null, + 'mexican': null, + 'puertoRican': null, + 'cuban': null, + 'otherHispanicLatino': null, + 'otherHispanicLatinoText': null, + 'notHispanicOrLatino': null, + 'notProvided': null + }, + 'raceInformation': { + 'americanIndianAlaskaNative': null, + 'americanIndianAlaskaNativeText': null, + 'blackOrAfricanAmerican': null, + 'asian': null, + 'asianIndian': null, + 'chinese': null, + 'filipino': null, + 'japanese': null, + 'korean': null, + 'vietnamese': null, + 'otherAsian': null, + 'otherAsianText': null, + 'hawaiian': null, + 'nativeHawaiian': null, + 'guamanianChamorro': null, + 'samoan': null, + 'otherPacificIslander': null, + 'otherPacificIslanderText': null, + 'white': null, + 'notProvided': null + }, + 'otherIncomeInformation': [{ + 'amount': 100000, + 'incomeName': 'Bingo', + 'frequency': '' + }, + { + 'amount': 2000, + 'incomeName': 'Federal Check', + 'frequency': '' + }, + { + 'amount': 1000, + 'incomeName': 'Financial Aid', + 'frequency': '' + }] + } + ], + 'purposeOfLoan': LoanPurpose.Home_Purchase, + 'purposeOfLoanSignature': 'Other', + 'termOfLoan': '120', + 'hasCoApplicant': CreditApplicationValues.CHOICE_YES, + 'limitRequested': 25000, + 'confirmJointCredit': CreditApplicationValues.CHOICE_YES, + 'hasCoApplicantSpouse': CreditApplicationValues.CHOICE_YES, + 'collateralInformation': { + 'isAddressSame': CreditApplicationValues.CHOICE_NO, + 'estimatedValue': 25000, + 'propertyType': 'Quad', + 'hasFirstMortgage' : CreditApplicationValues.CHOICE_NO, + 'hasSecondMortgage' : CreditApplicationValues.CHOICE_NO, + 'payoffFirstMortgage' : false, + 'payoffSecondMortgage' : false, + 'isBeingImproved': CreditApplicationValues.CHOICE_NO, + 'isListedForSale': CreditApplicationValues.CHOICE_NO, + 'occupancyType': 'SecondHome', + 'onRentedLot': CreditApplicationValues.CHOICE_YES, + 'rentPaid': '250', + 'addressLine1': '987 N 1st St', + 'addressLine2': '', + 'zipcode': '53151', + 'city': 'New Berlin', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP + }, + 'status': ApiStatus.IN_PROGRESS, + 'page': { + 'hasAddress': true, + 'hasEmployment': true, + 'hasPersonal': true, + 'hasMemberInfo': true, + 'hasSpouseInfo': true, + 'hasTattleSpouseInfo': true, + 'hasResidenceInfo': true, + 'hasVehicleCollateral': true, + 'hasHMDA': true, + 'hasDisclosure': true, + 'hasCollateralInfo': true, + 'hasNote': true + }, + 'error': { + 'code': ApiErrorCode.No_Error, + 'message': 'No Error' + } +}; + +export const apiTranslatorPostTranslate: ApiLoanApplication = { + 'id': 'some-hashed-cosmos-id', + 'date': 'some-hashed-timestamp', + 'purpose': LoanPurpose.Home_Refinance, + 'purposeOfLoanSignature' : 'Other', + 'loanType': LoanCode.MANUFACTURED_HOME_LOAN, + 'loanTitle': LoanTitle.ManufacturedHome, + 'primaryContactMethod': '262-623-8990', + 'requestedAmount': 25000, + 'termLengthInMonths': 120, + 'manufacturedHome': { + 'type': MfgHomeTypeEnum.Quad, + 'use': OccupancyTypeEnum.SecondHome, + 'currentlyListedForSale': false, + 'currentlyBeingRemodeledOrImproved': false, + 'estimatedHomeValuation': 25000, + 'lot': { + 'address1': '123 Main St', + 'address2': '', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP, + 'postalCode': '53202', + 'postalCodeExt': null, + 'country': 'UnitedStates', + 'isRented': true, + 'rentalAmt': 250 + } + }, + 'primaryBorrower': { + 'firstName': 'Primary', + 'middleInitial': 'J', + 'lastName': 'Borrower', + 'dob': '2010-01-01T00:00:00', + 'ssn': '666555444', + 'phone': '5555550134', + 'phoneType': 'Home', + 'email': 'primary@example.com', + 'isMarried': true, + 'address': { + 'address1': '123 Main St', + 'address2': '', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP, + 'postalCode': '53202', + 'postalCodeExt': null, + 'country': 'UnitedStates', + 'residenceType': 'Own', + 'monthsResided': 101, + 'monthlyPayment': 500 + }, + 'previousAddresses': [], + 'hmdaValues': { + 'sexes': ['notProvided'], + 'races': ['OtherAsian'], + 'ethnicities': ['HispanicOrLatino'], + 'otherAsianRace': 'Thai' + }, + 'employers': [{ + 'name': 'Petes Purple Pies', + 'currentEmployer': true, + 'employmentType': 'FullTime', + 'title': 'Lead Pie Prepper', + 'income': { + 'sourceName': 'Petes Purple Pies', + 'frequency': 'Annually', + 'amountPerPeriod': 100000, + 'incomeTypeCD': 'Salary', + 'incomeStatus': 'IncomeStatus_Included' + }, + 'monthsEmployed': 120, + 'language': 'English', + 'workPhone': null + } + ], + 'language': 'English', + 'otherIncomes': [], + 'hasOptedOutTCPA': false + }, + 'coApplicant': { + 'firstName': 'Spouse', + 'middleInitial': 'R', + 'lastName': 'Borrower', + 'dob': '01/01/2009', + 'ssn': '666333222', + 'phone': '5555550199', + 'phoneType': 'Home', + 'email': 'coapp@example.org', + 'isMarried': true, + 'address': { + 'address1': '123 Main St', + 'address2': '', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP, + 'postalCode': '53202', + 'postalCodeExt': null, + 'country': 'UnitedStates', + 'residenceType': 'Own', + 'monthsResided': 101, + 'monthlyPayment': 0 + }, + 'previousAddresses': [], + 'hmdaValues': { + 'sexes': ['notProvided'], + 'races': ['notProvided'], + 'ethnicities': ['notProvided'] + }, + 'employers': [], + 'otherIncomes': [{ + 'incomeTypeCD': 'Other', + 'sourceName': 'Bingo', + 'frequency': 'Annually', + 'amountPerPeriod': 100000, + 'incomeStatus': 'IncomeStatus_Included' + } + ], + 'language': 'English', + 'isMarriedToPrimaryBorrower': true, + 'hasOptedOutTCPA': false + } +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/broken-applications.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/broken-applications.ts new file mode 100644 index 0000000..ee797a0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/broken-applications.ts @@ -0,0 +1,216 @@ +/** + * The first credit application here is when there is a co-applicant spouse and the spouse answered yes as having same address. + * The second credit application here siw hen the application goes back and changes to not applying with spouse. The has same address + * flag remains true, but coApplicantSpouse = "no" + */ +import { LoanType, LoanTypeSet } from '../../shared/loan-type'; + +export const primaryAndCoApplicationAddressSameApplication = { + 'loanType': LoanTypeSet[0], + 'hasBeenCompleted': false, + 'disclosureInformation': {'contact': true, 'truthful': false}, + 'fullInfoBorrowers': [{ + 'addressInformation': { + 'addressLine1': '526 E LINCOLN AVE', + 'addressLine2': null, + 'city': 'MILWAUKEE', + 'state': 'WI', + 'zipcode': '53207', + 'zipcodeExt': '1721', + 'residenceStatus': '0', + 'residenceTimeYears': '', + 'residenceTimeMonths': '0', + 'residenceMonthlyPayment': null, + 'isMailingAddressSame': true + }, + 'residenceInformation': { + 'residenceStatus': '0', + 'residenceTimeYears': '7', + 'residenceTimeMonths': '0', + 'residenceMonthlyPayment': '500', + 'isMailingAddressSame': true + }, + 'personalInformation': { + 'firstName': 'George', + 'middleInitial': 'J', + 'lastName': 'Frick', + 'suffix': null, + 'dateOfBirth': '12/02/2000', + 'socialSecurityNumber': '123-12-3123', + 'primaryPhoneNumberType': 0, + 'primaryPhoneNumber': '(414) 732-9928', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'george.frick@gmail.com', + 'maritalStatus': 'yes', + 'coApplicantSpouse': 'yes' + }, + 'spouseType': null, + 'fullInfoSpouse': null, + 'tattleSpouse': null, + 'priorResidenceInformation': [], + 'priorAddressInformation': [], + 'employmentInformation': [], + 'otherIncomeInformation': [] + }, { + 'addressInformation': {'addressLine1': '', 'addressLine2': '', 'city': '', 'state': 'WI', 'zipcode': ''}, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': null, + 'residenceTimeYears': '', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Nicole', + 'middleInitial': 'J', + 'lastName': 'Frick', + 'suffix': null, + 'dateOfBirth': '12/02/2000', + 'socialSecurityNumber': '123-12-3123', + 'primaryPhoneNumberType': 0, + 'primaryPhoneNumber': '(919) 672-3828', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'george.frick@gmail.com', + 'maritalStatus': 'yes' + }, + 'hasSameAddressAsSpouse': 'yes', + 'employmentInformation': [], + 'otherIncomeInformation': [] + }], + 'status': 'In_Progress', + 'error': null, + 'page': { + 'hasAddress': true, + 'hasEmployment': true, + 'hasPersonal': true, + 'hasMemberInfo': true, + 'hasSpouseInfo': false, + 'hasTattleSpouseInfo': false, + 'hasResidenceInfo': true, + 'hasVehicleCollateral': false, + 'hasHMDA': false, + 'hasDisclosure': false, + 'hasCollateralInfo': false, + 'hasNote': false + }, + 'hasCoApplicant': 'yes', + 'limitRequested': 5000, + 'confirmJointCredit': 'yes', + 'coApplicantLivesInMaritalPropertyState': true, + 'hasCoApplicantSpouse': 'yes', + 'loanId': '964bc4-ff-4dea-c-69f9b0dbb214', + 'primaryContactMethod': 'email' +}; + +export const sameAddressFlagBroken2 = { + 'loanType': { + 'id': 'cb', + 'category': 'Credit Card', + 'title': 'Cash Back', + 'apiCode': 'CC_CashBack', + 'icon': 'credit-card', + 'imagePath': 'assets/cc-images/LCU_cashback.png', + 'message': 'COPY.TYPE_MESSAGE.CREDITCARD_CASHBACK', + 'headerMessageKey': 'HEADER.LOAN_MESSAGE.CREDITCARD_CASHBACK', + 'shouldDecision': true, + 'pages': [], + 'defaultPurpose': 'Other', + 'requiresCollateral': false + }, + 'hasBeenCompleted': false, + 'disclosureInformation': {'contact': true, 'truthful': false}, + 'fullInfoBorrowers': [{ + 'addressInformation': { + 'addressLine1': '526 E LINCOLN AVE', + 'addressLine2': null, + 'city': 'MILWAUKEE', + 'state': 'WI', + 'zipcode': '53207', + 'zipcodeExt': '1721', + 'residenceStatus': '0', + 'residenceTimeYears': '7', + 'residenceTimeMonths': '0', + 'residenceMonthlyPayment': '500', + 'isMailingAddressSame': true + }, + 'residenceInformation': { + 'residenceStatus': '0', + 'residenceTimeYears': '7', + 'residenceTimeMonths': '0', + 'residenceMonthlyPayment': '500', + 'isMailingAddressSame': true + }, + 'personalInformation': { + 'firstName': 'George', + 'middleInitial': 'J', + 'lastName': 'Frick', + 'suffix': '', + 'dateOfBirth': '12/02/2000', + 'socialSecurityNumber': '123-12-3123', + 'primaryPhoneNumberType': 0, + 'primaryPhoneNumber': '(414) 732-9928', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'george.frick@gmail.com', + 'maritalStatus': 'yes', + 'coApplicantSpouse': 'no' + }, + 'spouseType': null, + 'fullInfoSpouse': null, + 'tattleSpouse': null, + 'priorResidenceInformation': [], + 'priorAddressInformation': [], + 'employmentInformation': [], + 'otherIncomeInformation': [] + }, { + 'addressInformation': {'addressLine1': '', 'addressLine2': '', 'city': '', 'state': 'WI', 'zipcode': ''}, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': null, + 'residenceTimeYears': '', + 'residenceTimeMonths': '0', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Nicole', + 'middleInitial': 'J', + 'lastName': 'Frick', + 'suffix': '', + 'dateOfBirth': '12/02/2000', + 'socialSecurityNumber': '123-12-3123', + 'primaryPhoneNumberType': 0, + 'primaryPhoneNumber': '(919) 672-3828', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'george.frick@gmail.com', + 'maritalStatus': 'yes' + }, + 'hasSameAddressAsSpouse': 'yes', + 'employmentInformation': [], + 'otherIncomeInformation': [] + }], + 'status': 'In_Progress', + 'error': null, + 'page': { + 'hasAddress': true, + 'hasEmployment': true, + 'hasPersonal': true, + 'hasMemberInfo': true, + 'hasSpouseInfo': false, + 'hasTattleSpouseInfo': false, + 'hasResidenceInfo': true, + 'hasVehicleCollateral': false, + 'hasHMDA': false, + 'hasDisclosure': false, + 'hasCollateralInfo': false, + 'hasNote': false + }, + 'hasCoApplicant': 'yes', + 'limitRequested': '5000', + 'confirmJointCredit': 'yes', + 'coApplicantLivesInMaritalPropertyState': true, + 'hasCoApplicantSpouse': 'no', + 'loanId': '964bca44-ffd8-4dea-8e5c-69f9b0dbb214' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/configuration.mock.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/configuration.mock.service.ts new file mode 100644 index 0000000..c9349f0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/configuration.mock.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { UiSettings } from '../../shared/configuration/ui-settings'; + +@Injectable({ + providedIn: 'root' +}) +export class MockConfigurationService { + private uiSettings: UiSettings; + + // This is a mock so we don't use HttpClient. + constructor() { } + + public buildPingURL(): string { + return environment.urlBase + '/UiSettings/'; + } + + public reloadConfiguration(): Observable { + return of({}); + } + + public saveConfiguration(config: UiSettings): void { + this.uiSettings = config; + } + + public getConfigurationByField(field: string): any { + if (!this.uiSettings) { + throw new Error('UI Settings not loaded'); + } + return this.uiSettings[field]; + } + + public getAllConfiguration(): UiSettings { + if (this.uiSettings) { + return this.uiSettings; + } + throw new Error('UI Settings not loaded'); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/credit-application-dao.mock.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/credit-application-dao.mock.service.ts new file mode 100644 index 0000000..73f7c27 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/credit-application-dao.mock.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { CreditApplication } from '../../shared/credit-application'; +import { Observable, of } from 'rxjs'; +import { DecisionStatus } from '../../shared/decision/decision-status'; +import { ICreditApplicationDaoService } from '../../shared/credit-application-dao.service'; + +@Injectable({ + providedIn: 'root' +}) +export class MockCreditApplicationDaoService implements ICreditApplicationDaoService { + + // This is a mock so we don't use HttpClient. + constructor() { } + + public saveApplication(creditApplication: CreditApplication): Observable { + return of(null); + } + + createApplication(creditApplication: CreditApplication): Observable { + return of(null); + } + + decideApplication(creditApplication: CreditApplication): Observable { + return of(null); + } + + getApplicationDecision(creditApplication: CreditApplication): Observable { + return of(null); + } + + submitApplication(creditApplication: CreditApplication): Observable { + return of(null); + } + + updateApplication(creditApplication: CreditApplication): Observable { + return of(null); + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/loan-table.mock.service.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/loan-table.mock.service.ts new file mode 100644 index 0000000..7b14604 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/loan-table.mock.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpHeaders } from '@angular/common/http'; +import { Observable, of, BehaviorSubject } from 'rxjs'; +import { ApiLoanApplication } from 'src/app/shared/api-models/api-loan-application'; +import { TestApiCreditApplication } from './test-api-credit-application'; + + + +export class MockLoanTableService { + public loanApp: ApiLoanApplication = TestApiCreditApplication; + public loanApps: ApiLoanApplication[] = [this.loanApp]; + + private applicationSet: BehaviorSubject> = new BehaviorSubject>(this.loanApps); + public applicationSet$ = this.applicationSet.asObservable(); + + constructor() {} + + public buildApiURL(): string { + return environment.urlBase + '/Storage'; + } + + public buildHeader(): HttpHeaders { + return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')); + } + + public hasApplications(): boolean { + return !!(this.applicationSet.value.length); + } + + public loadApplications(): Observable { + return of({}); + } + + public reloadApplications() { + this.loadApplications().subscribe( + (success: ApiLoanApplication[]) => { + this.setApplications(success); + } + ); + } + + public deleteApplication(loanId: string): Observable { + return of({}); + } + + public deleteAndLoad(loanId: string) { + this.deleteApplication(loanId).subscribe( + () => { + this.reloadApplications(); + } + ); + } + + public submitAndLoad(loanId: string) { + this.submitApplication(loanId).subscribe( + () => { + this.reloadApplications(); + } + ); + } + + public setApplications(applications: ApiLoanApplication[]): void { + this.applicationSet.next(applications); + } + + public submitApplication(loanId: string): Observable { + return of({}); + } + + public getApplicationById(loanId: string): ApiLoanApplication { + if (!this.applicationSet.value.length) { + throw new Error('Applications are not loaded'); + } + const single = this.applicationSet.value.find(x => x.id === loanId); + if (!single) { + throw new Error('Application not found'); + } + return single; + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-credit-application.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-credit-application.ts new file mode 100644 index 0000000..06e84c0 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-credit-application.ts @@ -0,0 +1,168 @@ +import { ApiLoanApplication } from '../../shared/api-models/api-loan-application'; +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import {LoanTitle} from '../../shared/loan-type/loan-title'; + +export const TestApiCreditApplication: ApiLoanApplication = { + 'id': '0', + 'loanType': LoanCode.CC_CashBack, + 'loanTitle': LoanTitle.CashBack, + 'note': 'This is a note!', + 'primaryContactMethod': '262-623-8990', + 'requestedAmount': 1000, + 'date': '2018-07-23', + 'primaryBorrower': { + 'hasOptedOutTCPA': false, + 'firstName': 'Dennis', + 'middleInitial': 'J', + 'lastName': 'Reynolds', + 'dob': '2000-07-23', + 'ssn': 'string', + 'phone': 'string', + 'phoneType': 'Mobile', + 'email': 'string', + 'address': { + 'address1': 'string', + 'address2': 'string', + 'city': 'string', + 'state': 'AA', + 'postalCode': 'string', + 'country': 'string', + 'monthsResided': 0, + 'residenceType': 'Own', + 'id': 'string' + }, + 'previousAddresses': [ + { + 'address1': 'string', + 'address2': 'string', + 'city': 'string', + 'state': 'AA', + 'postalCode': 'string', + 'country': 'string', + 'monthsResided': 0, + 'residenceType': 'Own', + 'id': 'string' + } + ], + 'employers': [ + { + 'id': 'string', + 'name': 'string', + 'currentEmployer': true, + 'employmentType': 'NoneSelected', + 'title': 'string', + 'income': { + 'incomeTypeCD': 'Alimony', + 'sourceName': 'string', + 'alimonyStatus': 'CourtOrdered', + 'amountPerPeriod': 0, + 'frequency': 'Annually', + 'hoursPerWeek': '0', + 'incomeAdjustmentPercentage': 0, + 'adjustedIncome': 0, + 'incomeStatus': 'IncomeStatus_Excluded' + }, + 'monthsEmployed': 0, + 'startDate': '2018-07-23T19:29:57.744Z', + 'endDate': '2018-07-23T19:29:57.744Z', + 'workPhone': 'string', + 'language': 'string' + } + ], + 'otherIncomes': [ + { + 'incomeTypeCD': 'Alimony', + 'sourceName': 'string', + 'alimonyStatus': 'CourtOrdered', + 'amountPerPeriod': 0, + 'frequency': 'Annually', + 'hoursPerWeek': '0', + 'incomeAdjustmentPercentage': 0, + 'adjustedIncome': 0, + 'incomeStatus': 'IncomeStatus_Excluded' + } + ], + 'isMarried': true, + 'language': 'string', + 'customerNumber': 'string', + 'id': 'string' + }, + 'coApplicant': + { + 'hasOptedOutTCPA': false, + 'firstName': 'string', + 'middleInitial': 'string', + 'lastName': 'string', + 'dob': '2018-07-23T19:29:57.744Z', + 'ssn': 'string', + 'phone': 'string', + 'phoneType': 'Mobile', + 'email': 'string', + 'address': { + 'address1': 'string', + 'address2': 'string', + 'city': 'string', + 'state': 'AA', + 'postalCode': 'string', + 'country': 'string', + 'monthsResided': 0, + 'residenceType': 'Own', + 'id': 'string' + }, + 'previousAddresses': [ + { + 'address1': 'string', + 'address2': 'string', + 'city': 'string', + 'state': 'AA', + 'postalCode': 'string', + 'country': 'string', + 'monthsResided': 0, + 'residenceType': 'Own', + 'id': 'string' + } + ], + 'employers': [ + { + 'id': 'string', + 'name': 'string', + 'currentEmployer': true, + 'employmentType': 'NoneSelected', + 'title': 'string', + 'income': { + 'incomeTypeCD': 'Alimony', + 'sourceName': 'string', + 'alimonyStatus': 'CourtOrdered', + 'amountPerPeriod': 0, + 'frequency': 'Annually', + 'hoursPerWeek': '0', + 'incomeAdjustmentPercentage': 0, + 'adjustedIncome': 0, + 'incomeStatus': 'IncomeStatus_Excluded' + }, + 'monthsEmployed': 0, + 'startDate': '2018-07-23T19:29:57.744Z', + 'endDate': '2018-07-23T19:29:57.744Z', + 'workPhone': 'string', + 'language': 'string' + } + ], + 'otherIncomes': [ + { + 'incomeTypeCD': 'Alimony', + 'sourceName': 'string', + 'alimonyStatus': 'CourtOrdered', + 'amountPerPeriod': 0, + 'frequency': 'Annually', + 'hoursPerWeek': '0', + 'incomeAdjustmentPercentage': 0, + 'adjustedIncome': 0, + 'incomeStatus': 'IncomeStatus_Excluded' + } + ], + 'isMarried': true, + 'language': 'string', + 'customerNumber': 'string', + 'id': 'string' + } +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-loan-container.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-loan-container.ts new file mode 100644 index 0000000..e675cf7 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-api-loan-container.ts @@ -0,0 +1,156 @@ +import { LoanCode } from 'src/app/shared/loan-type/loan-code'; +import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container'; +import { ApiStatus } from 'src/app/shared/api-models/api-status'; +import { ApiError, ApiErrorCode } from 'src/app/shared/api-models/api-error'; +import { CreditApplication } from '../../shared/credit-application'; +import { LoanTypeSet } from '../../shared/loan-type'; + +export const TestTranslateCreditApplication: any = { + 'loanType': LoanTypeSet[13], + 'hasBeenCompleted': false, + 'fullInfoBorrowers': [{ + 'priorAddressInformation': [], + 'priorResidenceInformation': [], + 'addressInformation': { + 'addressLine1': '5445 S Westridge Dr', + 'addressLine2': '', + 'city': 'New Berlin', + 'state': 'WI', + 'zipcode': '53151' + }, + 'residenceInformation': { + 'residenceMonthlyPayment': 750, + 'residenceStatus': 0, + 'residenceTimeMonths': '0', + 'residenceTimeYears': '6', + 'isMailingAddressSame': true + }, + 'personalInformation': { + 'firstName': 'JOHN', + 'middleInitial': 'P', + 'lastName': 'DOE', + 'dateOfBirth': '01/01/1970', + 'socialSecurityNumber': '999-99-9990', + 'primaryPhoneNumberType': 1, + 'primaryPhoneNumber': '262-796-1100', + 'secondaryPhoneNumberType': 1, + 'secondaryPhoneNumber': '262-796-6370', + 'email': 'scottschmick@landmarkcu.com' + }, + 'ethnicityInformation': {'otherHispanicLatinoText': null}, + 'genderInformation': {}, + 'raceInformation': {'americanIndianAlaskaNativeText': null, 'otherAsianText': null, 'otherPacificIslanderText': null} + }], + 'hasCoApplicant': 'no', + 'hasCoApplicantSpouse': null, + 'primaryContactMethod': null, + // 'applicantType': 'no', + 'limitRequested': 5555, + // 'livesInMaritalPropertyState': 'yes', + 'coApplicantLivesInMaritalPropertyState': 'no', + 'purposeOfLoan': 'Other', + 'termOfLoan': 12, + 'page': null, + 'status': null, + 'error': null +}; + +export const TestApiLoanContainer: ApiLoanContainer = { + 'memberId': '1231', + 'individualId': '1231', + 'loanApplication': { + 'id': '0', + 'loanType': LoanCode.CC_CashBack, + 'note': 'This is a note!', + 'primaryContactMethod': '262-623-8990', + 'requestedAmount': 1000, + 'date': '2018-07-23', + 'primaryBorrower': { + 'hasOptedOutTCPA': false, + 'firstName': 'Dennis', + 'middleInitial': 'J', + 'lastName': 'Reynolds', + 'dob': '2018-07-23T19:29:57.744Z', + 'ssn': '123123123', + 'phone': '2622221215', + 'phoneType': 'Mobile', + 'email': 'f@f.com', + 'address': { + 'address1': '12123 Apple Lane', + 'address2': null, + 'city': 'Milwaukee', + 'state': 'WI', + 'postalCode': '53033', + 'country': 'UnitedStates', + 'monthlyPayment': 2222, + 'monthsResided': 84, + 'residenceType': 'Own', + 'id': '9e178efa' + }, + 'previousAddresses': [], + 'employers': null, + 'otherIncomes': null, + 'isMarried': false, + 'language': 'English', + 'customerNumber': '0', + 'id': 'a60d' + }, + 'coApplicant': + { + 'hasOptedOutTCPA': false, + 'firstName': 'John', + 'middleInitial': 'M', + 'lastName': 'Wick', + 'dob': '2018-07-23T19:29:57.744Z', + 'ssn': '355096548', + 'phone': '2316454567', + 'phoneType': 'Mobile', + 'email': 'r@r.com', + 'address': { + 'address1': '1243 Sugar Road', + 'address2': null, + 'city': 'Sussex', + 'state': 'WI', + 'postalCode': '53211', + 'country': 'UnitedStates', + 'monthlyPayment': 800, + 'monthsResided': 24, + 'residenceType': 'Own', + 'id': 'fja23s' + }, + 'previousAddresses': null, + 'employers': null, + 'otherIncomes': null, + 'isMarried': false, + 'language': 'English', + 'customerNumber': '0', + 'id': '12sd41' + } + }, + 'page': { + 'hasAddress': true, + 'hasEmployment': true, + 'hasPersonal': true, + 'hasMemberInfo': true, + 'hasSpouseInfo': true, + 'hasTattleSpouseInfo': true, + 'hasResidenceInfo': true, + 'hasVehicleCollateral': true, + 'hasHMDA': true, + 'hasDisclosure': true, + 'hasCollateralInfo': true, + 'hasNote': true + }, + 'status': ApiStatus.IN_PROGRESS, + 'error': { + code: ApiErrorCode.No_Error, + message: 'No Errors' + }, + 'date': new Date(), + 'id': '12341234', + 'loanId': '12341234', + 'loanType': LoanCode.CC_CashBack, + 'branch': 'milwaukee', + 'firstName': 'john', + 'lastName': 'sanders' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-individual.mock.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-individual.mock.ts new file mode 100644 index 0000000..dde34cd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/test-individual.mock.ts @@ -0,0 +1,357 @@ +import { CoreIndividual } from '../../member/models/core-individual'; +import { PersonalInformation } from '../../loan-wizard/field-groups/personal/personal-information'; +import { PhoneTypeEnum } from '../../shared/api-models/api-enums'; +import { AddressInformation } from '../../loan-wizard/field-groups/address/address-information'; + +export const convertedPrimaryAddress = { + addressLine1: '9515 W National Ave', + addressLine2: '', + city: 'West Allis', + state: 'WI', + zipcode: '53227' +}; + +export const convertedPersonalInformation = { + firstName: 'TIMMY', + middleInitial: '', + lastName: 'TESTER', + dateOfBirth: '01/01/1990', + socialSecurityNumber: '395-76-1111', + primaryPhoneNumberType: PhoneTypeEnum.Mobile, + primaryPhoneNumber: '262-796-4501', + secondaryPhoneNumberType: PhoneTypeEnum.Mobile, + secondaryPhoneNumber: '262-796-4500', + email: 'lcu@landmarkcu.com' +}; + +export const testCoreIndividual = { + 'individualId': 731085, + 'individualType': 'Individual', + 'title': '', + 'firstName': 'TIMMY', + 'middleName': '', + 'lastName': 'TESTER', + 'nameSuffix': '', + 'taxId': '395-76-1111', + 'gender': '', + 'birthDate': '1990-01-01T00:00:00', + 'branch': 2, + 'driversLicense': '', + 'driversLicenseState': '', + 'ident': '', + 'preferredContact': 'No Preference', + 'maritalStatus': 'Unknown', + 'isUSCitizen': false, + 'primaryAddressId': 3644887, + 'isEmployee': false, + 'employeeNumber': 0, + 'companyName': '', + 'employerName': '', + 'applicationCountLast90Days': 0, + 'applicationsDeclinedLast90Days': 0, + 'applicationsApprovedLast90Days': 0, + 'address': [ + { + 'addressId': 1326362, + 'line1': '8035 W LORRAINE PL', + 'line2': '', + 'line3': '', + 'city': 'MILWAUKEE', + 'state': 'WI', + 'zipCode': '53222-4933', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 8035, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': true + }, { + 'addressId': 1531702, + 'line1': '5445 S WESTRIDGE DR', + 'line2': 'Att: Jeff Knaub', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151-7948', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 1596633, + 'line1': '106 E MAIN ST', + 'line2': '', + 'line3': '', + 'city': 'BEAVER DAM', + 'state': 'WI', + 'zipCode': '53916-1837', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 106, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': true + }, { + 'addressId': 2661829, + 'line1': '123 Test St', + 'line2': '', + 'line3': '', + 'city': 'New Berlin', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 123, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '2016-01-07T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2661878, + 'line1': '5445 S Westridge Dr', + 'line2': '', + 'line3': '', + 'city': 'New Berlin', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'None', + 'startDate': '2016-01-07T00:00:00', + 'endDate': '2016-02-08T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2675139, + 'line1': '110 E CLAYTON ST', + 'line2': '', + 'line3': '', + 'city': 'ATHENS', + 'state': 'GA', + 'zipCode': '30601', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 110, + 'occupancy': 'None', + 'startDate': '2016-02-08T00:00:00', + 'endDate': '2016-02-08T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2675278, + 'line1': '110 TEST ST', + 'line2': '', + 'line3': '', + 'city': 'ATHENS', + 'state': 'GA', + 'zipCode': '30605', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 110, + 'occupancy': 'None', + 'startDate': '2016-02-08T00:00:00', + 'endDate': '2016-02-08T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2675312, + 'line1': '5445 S WESTRIDGE DR', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'None', + 'startDate': '2016-02-08T00:00:00', + 'endDate': '2016-02-09T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2675535, + 'line1': 'P.O. BOX 510868', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 0, + 'occupancy': 'None', + 'startDate': '2016-02-09T00:00:00', + 'endDate': '2016-02-11T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2676405, + 'line1': '5445 S WESTRIDGE DR', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'None', + 'startDate': '2016-02-11T00:00:00', + 'endDate': '2016-02-11T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2676427, + 'line1': 'P.O. BOX 510868', + 'line2': '', + 'line3': '', + 'city': 'NEW BERLIN', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 0, + 'occupancy': 'None', + 'startDate': '2016-02-11T00:00:00', + 'endDate': '2016-02-11T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 2676618, + 'line1': '1234 MAPLE ST', + 'line2': '', + 'line3': '', + 'city': 'MILWAUKEE', + 'state': 'WI', + 'zipCode': '53222', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 1234, + 'occupancy': 'None', + 'startDate': '2016-02-11T00:00:00', + 'endDate': '2016-12-14T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 3280018, + 'line1': '1234 TEST ST', + 'line2': '', + 'line3': '', + 'city': 'MILWAUKEE', + 'state': 'WI', + 'zipCode': '53222', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 1234, + 'occupancy': 'None', + 'startDate': '2016-12-14T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 3644885, + 'line1': '5445 S Westrdige Dr', + 'line2': '', + 'line3': '', + 'city': 'New Berlin', + 'state': 'WI', + 'zipCode': '53151', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': false, + 'isInternational': false, + 'addressVerification': 5445, + 'occupancy': 'Other', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': false + }, { + 'addressId': 3644887, + 'line1': '9515 W National Ave', + 'line2': '', + 'line3': '', + 'city': 'West Allis', + 'state': 'WI', + 'zipCode': '53227', + 'country': 'US', + 'countyName': '', + 'addressType': 'Home', + 'isPrimary': true, + 'isInternational': false, + 'addressVerification': 9515, + 'occupancy': 'None', + 'startDate': '0001-01-01T00:00:00', + 'endDate': '0001-01-01T00:00:00', + 'uspsVerified': false + }], + 'email1': 'lcu@landmarkcu.com', + 'email2': '', + 'phone': [ + { + 'phoneKey': 'ApexApiWrapper.Apex.MemberServices.PhoneKey', + 'isPrimaryForType': false, + 'isInternational': false, + 'countryCode': '', + 'areaCode': '262', + 'phoneNumber': '796-4502', + 'extension': '0' + }, { + 'phoneKey': 'ApexApiWrapper.Apex.MemberServices.PhoneKey', + 'isPrimaryForType': false, + 'isInternational': false, + 'countryCode': '', + 'areaCode': '262', + 'phoneNumber': '796-4500', + 'extension': '0' + }, { + 'phoneKey': 'ApexApiWrapper.Apex.MemberServices.PhoneKey', + 'isPrimaryForType': true, + 'isInternational': false, + 'countryCode': '', + 'areaCode': '262', + 'phoneNumber': '796-4501', + 'extension': '0' + }], + 'status': 'Active', + 'isHomeOwner': false, + 'isHeadOfHouseHold': false, + 'language': '' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/vehicle-credit-application.mock.ts b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/vehicle-credit-application.mock.ts new file mode 100644 index 0000000..c5ba6c4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/app/testing/shared-mocks/vehicle-credit-application.mock.ts @@ -0,0 +1,218 @@ +import { LoanTypeSet, LoanType } from '../../shared/loan-type'; +import { CreditApplicationValues } from '../../shared/credit-application-values'; +import { CreditApplication } from '../../shared/credit-application'; +import { LoanPurpose } from 'src/app/shared/loan-type/loan-purpose'; +import { ApiStatus } from 'src/app/shared/api-models/api-status'; +import { ApiErrorCode } from 'src/app/shared/api-models/api-error'; +import { PhoneTypeEnum } from 'src/app/shared/api-models/api-enums'; +import { VehicleType } from 'src/app/shared/vehicle-type/vehicle-type'; + + +export const vehicleCreditApplication: CreditApplication = { + 'loanType': LoanTypeSet[7], + 'note': 'This is a note!', + 'primaryContactMethod': '262-623-8990', + 'hasBeenCompleted': false, + 'disclosureInformation': { + 'truthful': true, + 'contact': true, + 'date': new Date() + }, + 'coApplicantLivesInMaritalPropertyState': true, + 'fullInfoBorrowers': [ + { + 'addressInformation': { + 'addressLine1': '123 Main St', + 'addressLine2': '', + 'zipcode': '53202', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP + }, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '8', + 'residenceTimeMonths': '5', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Primary', + 'middleInitial': 'J', + 'lastName': 'Borrower', + 'dateOfBirth': '01/01/2010', + 'socialSecurityNumber': '666-55-5444', + 'primaryPhoneNumberType': PhoneTypeEnum.Home, + 'primaryPhoneNumber': '(555) 555-0134', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'primary@example.com', + 'maritalStatus': CreditApplicationValues.CHOICE_YES + }, + 'genderInformation': { + 'female': null, + 'male': true, + 'notProvided': null, + }, + 'ethnicityInformation': { + 'hispanicOrLatino': true, + 'mexican': null, + 'puertoRican': null, + 'cuban': null, + 'otherHispanicLatino': null, + 'otherHispanicLatinoText': null, + 'notHispanicOrLatino': null, + 'notProvided': null + }, + 'raceInformation': { + 'americanIndianAlaskaNative': null, + 'americanIndianAlaskaNativeText': null, + 'blackOrAfricanAmerican': null, + 'asian': null, + 'asianIndian': null, + 'chinese': null, + 'filipino': null, + 'japanese': null, + 'korean': null, + 'vietnamese': null, + 'otherAsian': true, + 'otherAsianText': 'Thai', + 'hawaiian': null, + 'nativeHawaiian': null, + 'guamanianChamorro': null, + 'samoan': null, + 'otherPacificIslander': null, + 'otherPacificIslanderText': null, + 'white': null, + 'notProvided': null + }, + 'employmentInformation': [{ + 'employerName': 'Petes Purple Pies', + 'employmentType': 'FullTime', + 'jobTitle': 'Lead Pie Prepper', + 'salary': 100000, + 'schedule': 'Annually', + 'employmentLengthYears': 10, + 'employmentLengthMonths': 0, + 'weeklyHours': null, + 'workPhoneNumber': '' + }] + }, { + 'addressInformation': { + 'addressLine1': '123 Main St', + 'addressLine2': '', + 'zipcode': '53202', + 'city': 'Milwaukee', + 'state': CreditApplicationValues.STATE_REQUIRED_COAPP + }, + 'residenceInformation': { + 'isMailingAddressSame': true, + 'residenceMonthlyPayment': 500, + 'residenceTimeYears': '8', + 'residenceTimeMonths': '5', + 'residenceStatus': '0' + }, + 'personalInformation': { + 'firstName': 'Spouse', + 'middleInitial': 'R', + 'lastName': 'Borrower', + 'dateOfBirth': '01/01/2009', + 'socialSecurityNumber': '666-33-3222', + 'primaryPhoneNumberType': PhoneTypeEnum.Home, + 'primaryPhoneNumber': '(555) 555-0199', + 'secondaryPhoneNumberType': null, + 'secondaryPhoneNumber': null, + 'email': 'coapp@example.org', + 'maritalStatus': CreditApplicationValues.CHOICE_YES + }, + 'genderInformation': { + 'female': null, + 'male': null, + 'notProvided': true + }, + 'ethnicityInformation': { + 'hispanicOrLatino': null, + 'mexican': null, + 'puertoRican': null, + 'cuban': null, + 'otherHispanicLatino': null, + 'otherHispanicLatinoText': null, + 'notHispanicOrLatino': null, + 'notProvided': null + }, + 'raceInformation': { + 'americanIndianAlaskaNative': null, + 'americanIndianAlaskaNativeText': null, + 'blackOrAfricanAmerican': null, + 'asian': null, + 'asianIndian': null, + 'chinese': null, + 'filipino': null, + 'japanese': null, + 'korean': null, + 'vietnamese': null, + 'otherAsian': null, + 'otherAsianText': null, + 'hawaiian': null, + 'nativeHawaiian': null, + 'guamanianChamorro': null, + 'samoan': null, + 'otherPacificIslander': null, + 'otherPacificIslanderText': null, + 'white': null, + 'notProvided': null + }, + 'otherIncomeInformation': [{ + 'amount': 100000, + 'incomeName': 'Bingo', + 'frequency': '' + }, + { + 'amount': 2000, + 'incomeName': 'Federal Check', + 'frequency': '' + }, + { + 'amount': 1000, + 'incomeName': 'Financial Aid', + 'frequency': '' + }] + } + ], + 'vehicleInformation': { + 'value': 1234, + 'condition': 'New', + 'type': VehicleType.AUTO, + 'year': 1999, + 'make': 'chevrolet', + 'model': 'camaro', + 'mileage': 23222, + 'term': '24 Months', + 'loanPurpose': 'Purchase', + 'seller' : 'Private Pary' + }, + 'purposeOfLoan': LoanPurpose.Purchase, + 'termOfLoan': '60', + 'hasCoApplicant': CreditApplicationValues.CHOICE_YES, + 'limitRequested': 25000, + 'confirmJointCredit': CreditApplicationValues.CHOICE_YES, + 'hasCoApplicantSpouse': CreditApplicationValues.CHOICE_YES, + 'status': ApiStatus.IN_PROGRESS, + 'page': { + 'hasAddress': true, + 'hasEmployment': true, + 'hasPersonal': true, + 'hasMemberInfo': true, + 'hasSpouseInfo': true, + 'hasTattleSpouseInfo': true, + 'hasResidenceInfo': true, + 'hasVehicleCollateral': true, + 'hasHMDA': true, + 'hasDisclosure': true, + 'hasCollateralInfo': true, + 'hasNote': true + }, + 'error': { + 'code': ApiErrorCode.No_Error, + 'message': 'No Error' + } +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/.gitkeep b/LCU.WebApps.External.OnlineLoanApp/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/LM_Full-Logo_Main.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/LM_Full-Logo_Main.png new file mode 100644 index 0000000..2aa6b0f Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/LM_Full-Logo_Main.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_cashback.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_cashback.png new file mode 100644 index 0000000..ab2bebc Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_cashback.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_rewards.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_rewards.png new file mode 100644 index 0000000..9fc36da Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_rewards.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_secured.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_secured.png new file mode 100644 index 0000000..52164f3 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_secured.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_student.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_student.png new file mode 100644 index 0000000..e6848e7 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/cc-images/LCU_student.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/checkbox/check.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/checkbox/check.png new file mode 100644 index 0000000..f050396 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/checkbox/check.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/eqo.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/eqo.png new file mode 100644 index 0000000..a71b0d7 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/eqo.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf b/LCU.WebApps.External.OnlineLoanApp/src/assets/files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf new file mode 100644 index 0000000..84f0465 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf b/LCU.WebApps.External.OnlineLoanApp/src/assets/files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf new file mode 100644 index 0000000..0f990e7 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/i18n/en.json b/LCU.WebApps.External.OnlineLoanApp/src/assets/i18n/en.json new file mode 100644 index 0000000..cb57fb2 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/assets/i18n/en.json @@ -0,0 +1,1044 @@ +{ + "BUTTONS": { + "NEXT": "Next", + "BACK": "Previous", + "YES": "Yes", + "NO": "No", + "LANDMARK": "Go To Landmark Home", + "SUMMARY_NEXT": "Process Application", + "CONFIRM_DISCLOSURE": "Confirm Disclosures" + }, + "PAGETITLES": { + "COMMON": { + "INTRO": "", + "PERSONAL_INFO": "Personal Information", + "PRIMARYSPOUSE_PERSONAL_INFO": "Your Spouse's Personal Information", + "COAPP_PERSONAL_INFO": "Co-Applicant Personal Information", + "COAPPSPOUSE_PERSONAL_INFO": "Co-Applicant Spouse's Personal Information", + "ADDRESS_INFO": "Residence Information", + "PRIMARYSPOUSE_ADDRESS_INFO": "Your Spouse's Residence Information", + "COAPP_ADDRESS_INFO": "Co-Applicant Residence Information", + "COAPPSPOUSE_ADDRESS_INFO": "Co-Applicant Spouse's Residence Information", + "PRIMARY_SPOUSETYPE": "Spouse Applicant Type", + "COAPP_SPOUSETYPE": "Co-Applicant Spouse Applicant Type", + "PRIMARY_TATTLESPOUSE": "Spouse Information", + "COAPP_TATTLESPOUSE": "Co-Applicant Spouse Information", + "EMPLOYMENT_INFO": "Income Information", + "PRIMARYSPOUSE_EMPLOYMENT_INFO": "Your Spouse's Income Information", + "COAPP_EMPLOYMENT_INFO": "Co-Applicant Income Information", + "COAPPSPOUSE_EMPLOYMENT_INFO": "Co-Applicant Spouse's Income Information", + "DISCLOSURES": "Disclosures", + "SUMMARY": "Final Review", + "DECISION": "", + "PROCESSING": "" + }, + "EQUITY": { + "HMDA": "Home Mortgage Disclosure Act", + "COAPP_HMDA": "Co-Applicant Home Mortgage Disclosure Act", + "COLLATERAL": "Collateral Information" + }, + "VEHICLE": { + "COLLATERAL": "Vehicle Information" + } + }, + "FORMS": { + "GENERAL": { + "ANNUAL": "(Annual)", + "SELECTDEFAULT": "Choose One", + "OPTIONAL": "Optional" + }, + "FINANCIAL": { + "ALIMONY_FLAG": "I am paying child support", + "CHILD_SUPPORT": "Child support amount", + "OTHER_INCOME_FLAG": "I have other income", + "MORE_INCOME_FLAG": "I have more income", + "OTHERNAME": "Other income source", + "OTHERAMOUNT": "Other income", + "EMPLOYMENT_FLAG": "I have employment income", + "MORE_EMPLOYMENT_FLAG": "I have more employment income", + "PLACEHOLDERS": { + "OTHERNAME": "Annual income source", + "OTHERAMOUNT": "ex: 1,000", + "CHILD_SUPPORT": "ex: 1,000" + } + }, + "EMPLOYMENT": { + "NAME": "Employer name", + "TITLE": "Job/Position title", + "TYPE": "Employment Status", + "HOURS": "Hours per week", + "SCHEDULE": "Schedule", + "SALARY": "Amount", + "SALARY_HINT": "Hourly Rate / Gross Annual Income", + "LENGTH_OF_EMPLOYMENT": "Time at Employer", + "YEARS": "Years", + "MONTHS": "Months", + "PHONE": "Work Phone ", + "PLACEHOLDERS": { + "NAME": "Employer name", + "TITLE": "Job/Position title", + "HOURS": "ex: 25", + "SALARY": "ex: 1,000", + "PHONE": "(___) ___-____" + } + }, + "RESIDENCE": { + "ADDR1": "Address Line 1", + "ADDR2": "Address Line 2", + "CITY": "City", + "STATE": "State", + "ZIPCODE": "Zip Code", + "MAILINGSAME": "Mailing Address is the same as Current Address", + "TIME_AT_ADDRESS": "Time at Address", + "MONTHS": "Months", + "YEARS": "Years", + "STATUS": "Residence Status", + "PAYMENT": "Monthly Payment", + "PLACEHOLDERS": { + "CITY": "City", + "STATE": "State", + "ZIPCODE": "Zip Code", + "ADDR1": "Address", + "ADDR2": "Suite, Apartment, etc (Optional)", + "PAYMENT": "Rent/payment per month" + } + }, + "PERSONAL": { + "MARRIAGE": "Are you married?", + "IS_CO_APPLICANT_SPOUSE": "Is your spouse your co-applicant?", + "FNAME": "First Name", + "MIDDLE": "Middle Initial", + "LNAME": "Last Name", + "SUFFIX": "Suffix", + "DOB": "Date Of Birth", + "SSN": "Social Security Number", + "SSN_HINT": "Required to Run a Credit Report", + "PRIMARYPHONE": "Primary Phone #", + "PRIMARYPHONETYPE": "Type", + "SECONDARYPHONE": "Secondary Phone #", + "SECONDARYPHONETYPE": "Type", + "EMAIL": "Email Address", + "EMAIL_ESIGNATURE": "Email Address(es) for Electronic Signature Documents", + "PRIMARY_APPLICANT_EMAIL": "Primary Applicant Email Address", + "CO_APPLICANT_EMAIL": "Co-Applicant Email Address (if applicable)", + "PLACEHOLDERS": { + "FNAME": "First Name", + "MIDDLE": "Middle Initial", + "LNAME": "Last Name", + "SUFFIX": "Suffix", + "DOB": "MM/DD/YYYY", + "SSN": "___-__-____", + "PHONE": "(___) ___-____", + "EMAIL": "_@_._" + } + }, + "MEMBER": { + "APPLICANT_TYPE": "Are you applying with another person?", + "CONFIRM_JOINT_CREDIT": "Do you intend to apply for joint credit?", + "LIVES_IN_STATE": "Do you live in Wisconsin?", + "CO_APP_LIVES_IN_STATE": "Does your Co-Applicant live in Wisconsin?", + "CO_APPLICANT_HAS_SPOUSE": "Is your co-applicant married?", + "ASSOCIATION": "Association", + "EQUITY": { + "TERM": "Term", + "PURPOSE": "Purpose of Loan", + "CLIENTMESSAGE" : "Please describe your COVID-19 hardship" + }, + "PLACEHOLDERS": { + "LIMIT_REQUEST" : "ex: 1,000", + "CLIENTMESSAGE" : "Please describe your COVID-19 hardship" + }, + "DISCLOSURE": "Please review our ", + "ACCOUNT_REQUIRED": "If you are not currently a member of Landmark Credit Union and your application is approved, you will be required to become a member by opening a savings account. Please read Landmark’s eligibility for membership requirements here before you proceed.", + "AGREEMENT_REQUIREMENT": "
I have read and agree to Landmark's membership eligibility requirements.
", + "COOKIES": "This loan application uses cookies. By continuing, you agree that we may use cookies to complete your loan application session and monitor key performance metrics. You may change cookie settings for your browser, but please note, this may prevent the application from functioning correctly." + }, + "COLLATERAL": { + "OCCUPANCY": "Occupancy", + "ESTIMATEDVALUE": "Estimated Value", + "PROPERTYTYPE": "Property Type", + "LIENPOSITION": { + "FIRSTMORTGAGE" : "Does this property have a first mortgage?", + "SECONDMORTGAGE" : "Does this property have a second mortgage or HELOC?", + "PAYOFF" : "Payoff with this loan" + + }, + "IMPROVED": "Is this property currently being remodeled or improved?", + "LISTED": "Is this property currently listed for sale?", + "ON_RENTED_LOT" : "Is the home on a Rented lot?", + "RENT_PAID": "Rent paid per month", + "ANNUAL_PROPERTY_TAX": "Annual Property Taxes", + "PLACEHOLDERS": { + "ESTIMATEDVALUE": "ex: 1,000" + } + }, + "SAME_ADDRESS": { + "CO_APP": "Home Address - Same as yours?", + "HELOC": "Is this address the same as your address where you reside?", + "SPOUSE": "Spouse Address - Same as yours?" + }, + "VEHICLE": { + "TYPE": "Vehicle type", + "TYPES": { + "AUTOMOBILE": "Automobile", + "MOTORCYCLE": "Motorcycle", + "ATV": "ATV", + "SNOWMOBILE": "Snowmobile", + "PWC": "Personal Water Craft", + "RV": "Recreational Vehicle", + "BOAT": "Boat", + "TRAILER": "TrailerMotorHome" + }, + "CONDITION": "Vehicle condition", + "CONDITIONS": { + "NEW": "New", + "USED": "Used" + }, + "BOAT_TRAILER_TYPE": { + "SINGLE": "Single", + "TANDEM": "Tandem", + "TRI_AXLE": "Tri-Axle" + }, + "BOAT_MOTOR_TYPE": { + "INBOARD": "Inboard", + "OUTBOARD": "Outboard", + "INBOARD_OUTBOARD": "Inboard/Outboard" + }, + "VALUE": "Vehicle Estimated Value or Sale Price", + "YEAR": "Vehicle model year", + "MAKE": "Vehicle make", + "MODEL": "Vehicle model", + "MILEAGE": "Vehicle mileage", + "TERM": "Term", + "LOAN_PURPOSE": "Vehicle Loan Purpose", + "REFINANCE": "Refinance", + "PURCHASE": "Purchase", + "CASHOUT_REFINANCE": "Cash Out Refinance", + "LEASE": "Lease", + "PAYOFF": "Payoff", + "SELLER": "Vehicle seller", + "PRIVATE_PARTY": "Private party", + "DEALERSHIP": "Dealership", + "VIN": "Vehicle VIN Number", + "VIN_POWERSPORT": "Vehicle Serial / Hull / VIN number", + "TRANSFERRING": "Are you transferring plates from another vehicle?", + "STATE_TITLED": "State to be titled", + "COUNTY_TITLED": "County to be titled", + "TITLE_BRANDED": "Is the title branded?", + "LIEN": "Is there a lien?", + "LICENSE_PLATE_NUMBER": "License Plate Number", + "DEALER_NAME": "Dealership Name", + "DEALER_PHONE": "Dealership Phone #", + "MUNICIPALITY_WHERE_KEPT": "Municipality where the vehicle will be kept", + "BOAT_VALUE": "Boat Estimated Value or Sale Price", + "BOAT_YEAR": "Boat model year", + "BOAT_MAKE": "Boat make", + "BOAT_MODEL": "Boat model", + "BOAT_HULL": "Boat hull number", + "BOAT_LENGTH": "Length of boat", + "BOAT_LOAN_PURPOSE": "Boat Loan Purpose", + "BOAT_SAME_ADDRESS": "Is the boat kept at the same address where you reside?", + "BOAT_SELLER": "Boat Seller", + "TRAILER_YEAR": "Trailer model year", + "TRAILER_MAKE": "Trailer make", + "TRAILER_TYPE": "Trailer type", + "MOTOR_YEAR": "Motor model year", + "MOTOR_MAKE": "Motor make", + "MOTOR_MODEL": "Motor model", + "MOTOR_TYPE": "Motor type", + "MUNICIPALITY_TYPE": "Municipality Type where the vehicle will be kept", + "PLACEHOLDERS": { + "VALUE": "Value or Sale Price", + "YEAR": "Model Year", + "MAKE": "Make", + "MODEL": "Model", + "MILEAGE": "Mileage", + "VIN": "VIN Number", + "VIN_POWERSPORT": "Serial / Hull / VIN Number", + "COUNTY_TITLED": "County", + "BOAT_LENGTH": "Length in Feet", + "BOAT_HULL": "Hull Number" + } + }, + "SPOUSETYPE": { + "PRIMARY": "Do you want to provide income/credit history for your spouse to qualify for this loan?", + "COAPP": "Do you want to provide income/credit history for you co-applicant's spouse to qualify for this loan?" + }, + "SUMMARY": { + "NOTE": "Please share any additional information you wish regarding your loan application.", + "PLACEHOLDERS": { + "NOTE": "" + } + } + }, + "HEADINGS": { + "ADDITIONAL_EMPLOYER_A": "", + "ADDITIONAL_EMPLOYER_B": "Previous employer", + "ADDITIONAL_RESIDENCE_A": "", + "ADDITIONAL_RESIDENCE_B": "Previous address", + "COLLATERAL": "{{borrowerName}}, tell us about your collateral", + "VEHICLE": "{{borrowerName}}, tell us about the vehicle you want to finance", + "DECISION": "Application status", + "PROCESSING": "We are processing your application", + "GET_STARTED": "Getting started is easy", + "FINANCIAL": { + "SIGNING_APPLICANT": "{{borrowerName}}, tell us about your income", + "APPLICANT_SPOUSE": "Tell us about {{borrowerName}}'s income" + }, + "PERSONAL": { + "PRIMARY_BORROWER" : "Tell us about yourself", + "CO_BORROWER" : "Co-Applicant, tell us about yourself", + "PRIMARY_BORROWER_SPOUSE": "Tell us about your spouse", + "CO_BORROWER_SPOUSE" : "Tell us about your spouse" + }, + "RESIDENCE": { + "SIGNING_APPLICANT": "{{borrowerName}}, tell us about your residence", + "APPLICANT_SPOUSE": "Tell us about {{borrowerName}}'s residence" + }, + "DISCLOSURES": "Disclosures", + "HMDA": { + "HMDA": "HMDA", + "ETHNICITY": { + "PRIMARY_BORROWER": "Applicant Ethnicity", + "CO_BORROWER": "Co-Applicant Ethnicity" + }, + "RACE": { + "PRIMARY_BORROWER": "Applicant Race", + "CO_BORROWER": "Co-Applicant Race" + }, + "SEX": { + "PRIMARY_BORROWER": "Applicant Sex", + "CO_BORROWER": "Co-Applicant Sex" + } + }, + "SPOUSE": { + "PRIMARY_BORROWER": "Tell us about your spouse", + "PRIMARY_BORROWER_SPOUSE": "Tell us about your spouse", + "CO_BORROWER": "Tell us about your spouse", + "CO_BORROWER_SPOUSE" : "Tell us about your spouse", + "PERSONAL": "Tell us about your spouse", + "RESIDENCE": "Tell us about your co-applicant's spouse's residence" + }, + "COBORROWER": { + "PERSONAL": "Tell us about your spouse/co-applicant", + "RESIDENCE": "Tell us about your spouse/co-applicant's residence" + } + }, + "ERRORS": { + "CUSTOM": { + "CC_LIMIT": "You must enter a dollar value between 100 and 999,999.", + "ZIPCODE": "Zip code is a 5 digit numeric zipcode", + "SSN": "SSN must be in the form 123-45-6789", + "PHONE": "Phone must be in the form (123) 456-7890", + "NOTE": "Can only contain characters: .,&@-;!', and be less than 3500 characters", + "HMDA": "Cannot select both male and female, please only select one", + "ALPHAONLY": "{{title}} may have no numbers, spaces or special characters.", + "SAME_VALUE": "{{title}} cannot be the same as {{value.name}}.", + "TITLE_BRANDED": "*Vehicles with a branded title are not eligible for loans.", + "BOAT_TITLE_BRANDED": "*Boats with a branded title are not eligible for loans.", + "LIEN": "*Seller will need to be at closing to sign documents." + }, + "REQUIRED": "{{title}} is required.", + "MINLENGTH": "{{title}} must be at least {{value}} characters.", + "MINLOANAMT": "The minimum loan amount is {{value}}.", + "MAXLOANAMT": "The maximum loan amount is {{value}}.", + "MININCAMT": "The minimum income amount is {{value}}.", + "MAXINCAMT": "The maximum income amount is {{value}}.", + "MAX_AMOUNT_CURRENCY": "The maximum {{title}} amount is {{value}}.", + "MAXLENGTH": "{{title}} must not have more than {{value}} characters.", + "MAXVAL": "{{title}} must not be more than {{value}}.", + "MINVAL": "{{title}} must be at least {{value}}.", + "EMAIL": "{{title}} must be a valid email address.", + "PATTERN": "{{title}} is in an invalid format.", + "REQUIREDSTATE": "State must be {{value}}.", + "REQUIRE_WI_COLLATERAL": "Please note Real Estate used for Home Equity loan collateral must be located in Wisconsin", + "TITLE": { + "PRIMARY_CONTACT": "Primary Contact Method", + "NOTE": "Additional Information", + "SAME_ADDRESS": "Is Address the Same?", + "DISCLOSURE": "Contact disclosure", + "EMPLOYMENT_LENGTH_YEARS": "Length of employment (years)", + "EMPLOYMENT_LENGTH_MONTHS": "Length of employment (months)", + "OTHER_INCOME_SOURCE": "Other annual income source", + "OTHER_ANNUAL_INCOME": "Other annual income", + "FIRST_MORTGAGE": "First Mortgage", + "SECOND_MORTGAGE": "Second Mortgage", + "IMPROVED": "Is the property being improved", + "ON_RENTED_LOT": "Is the home on rented lot or land you own", + "APPLICANT_TYPE": "Applicant Type", + "JOINT_CREDIT": "Confirm joint credit", + "MARTIAL_PROPERTY": "Lives in Marital Property State", + "CO_APP_MARTIAL_PROPERTY": "Co-applicant lives in Marital Property State", + "SPOUSE_TYPE": "Spouse Type", + "VEHICLE_VALUE": "Vehicle value", + "BOAT_VALUE": "Boat value", + "CLIENTMESSAGE": "Client Comment" + } + }, + "TIMEOUT":{ + "TITLE": "Application timed out", + "REASON": "Due to inactivity, your session has timed out and is no longer active.", + "INFO": "To create a new loan application, use the button below to navigate back to the home page.", + "INFO_ADMIN": "Use the button below to navigate back to the admin home page.", + "BUTTON": "Go back home", + "POPUP": { + "TITLE": "Session expire warning", + "EXPIRE": "Your session will expire in {{time}} seconds.", + "QUESTION": "Do you wish to extend the session?", + "BUTTON_HOME": "Go back to home page", + "BUTTON_EXTEND": "Extend session" + } + }, + "COPY": { + "TYPE_MESSAGE": { + "CREDITCARD_CASHBACK": "Cash Back VISA® Card", + "CREDITCARD_REWARDS": "Rewards VISA® Card", + "CREDITCARD_STUDENT": "Student Rewards VISA® Card", + "CREDITCARD_SECURE": "Secured VISA® Card", + "EQUITY_HELOC_FLEX": "Home Equity Line of Credit", + "EQUITY_HELOC_FIXED": "Fixed Term Home Equity Loan", + "EQUITY_HELOC_RATE_MOD": "Home Equity Line of Credit Rate Modification", + "EQUITY_MORTGAGE_REF_EXPRESS": "Mortgage Refi Express", + "VEHICLE_AUTO": "Auto/Motorcycle Loan", + "EQUITY_MANUF_HOME": "Manufactured Home Loan", + "VEHICLE_PRE_APPROVAL": "Auto/Motorcycle Pre-Approval", + "VEHICLE_BOAT": "Boat Loan", + "VEHICLE_POWERSPORT": "Powersports Loan", + "VEHICLE_MOTORHOME": "Trailer/Motorhome Loan", + "SIGNATURE_LOAN": "Personal Signature Loan", + "SPECIAL_LOAN": "COVID-19 Loan" + }, + "MEMBER": { + "CREDIT_CARD": "Thank you for applying for a {{typeMessage}}. This application takes approximately 10-15 minutes to complete. Please make sure you have enough time to fill out the form before beginning. You will not be able to go back once you leave.", + "EQUITY_LOAN": "Thank you for applying for a {{typeMessage}}. This application takes approximately 10-15 minutes to complete. Please make sure you have enough time to fill out the form before beginning. You will not be able to go back once you leave.", + "CYCLE_LOAN": "Thank you for applying for an {{typeMessage}}. This application takes approximately 10-15 minutes to complete. Please make sure you have enough time to fill out the form before beginning. You will not be able to go back once you leave.", + "SPECIAL_LOAN": "Thank you for applying for a {{typeMessage}}. The Application takes approximately 10-15 minutes to complete. Please make sure you have enough time to fill out the form before you begin. You will not be able to get back in the application once you leave.

This loan program is available to qualified borrowers with a COVID-19-related financial hardship. Only Applicants who became members of Landmark no later than March 17, 2020 may be eligible. Borrower must be a current member of Landmark, cannot currently be in a bankruptcy proceeding or have any written off loans with Landmark Credit Union.  Loans and rates are subject to credit approval. Consumer loan rates based upon qualified credit.  Other terms and rates are available; some restrictions may apply. Rates and terms are subject to change.

This loan is available only to members who are experiencing a financial hardship related to COVID-19; we will ask you to describe the hardship as a loan eligibility criterion.", + "HELOC_OR_FIXED_EQUITY_LOAN":"Thank you for applying for a {{typeMessage}}. This application takes approximately 10-15 minutes to complete. Please make sure you have enough time to fill out the form before beginning. You will not be able to go back once you leave." + }, + "DISCLOSURE":{ + "CC_REWARDS_CASHBACK": "
Introductory offer and bonus not available to current Landmark Cash Back or Rewards consumer cardholders or previous holders of Landmark Cash Bank or Rewards accounts that were closed within six (6) months of the new application date. Offer available to existing Starter Card, Secured Card, or Student Card holders who qualify for Cash Back or Rewards Card. Existing primary or joint holders of Landmark credit card accounts may only have one (1) such account at any time.

" + }, + "PERSONAL": { + "PRIMARY_BORROWER" : "", + "CO_BORROWER" : "", + "PRIMARY_BORROWER_SPOUSE": "", + "CO_BORROWER_SPOUSE" : "" + }, + "RESIDENCE": { + "PRIMARY_BORROWER" : "", + "CO_BORROWER" : "", + "PRIMARY_BORROWER_SPOUSE": "", + "CO_BORROWER_SPOUSE" : "" + }, + "HMDA" : "Home Mortgage Disclosure Act", + "COLLATERAL": { + "PRIMARY_BORROWER": "Tell us about your collateral." + }, + "EMPLOYMENT": { + "PRIMARY_BORROWER" : "", + "CO_BORROWER" : "", + "PRIMARY_BORROWER_SPOUSE": "", + "CO_BORROWER_SPOUSE" : "", + "OTHER_INCOME_BSA": "Alimony, child support or separate maintenance Income need not be revealed if you do not wish to have it considered for repaying this loan.", + "OTHER_INCOME_BSA_CC": "Alimony, child support or separate maintenance Income need not be revealed if you do not wish to have it considered for repaying this obligation." + }, + "SPOUSE": { + "PRIMARY_BORROWER": "You can provide minumum or maximum information for you spouse.", + "CO_BORROWER" : "You can provide minimum or maximium information for your co-applicant's spouse.", + "PRIMARY_BORROWER_SPOUSE": "I UNDERSTAND THAT LANDMARK IS REQUIRED BY WISCONSIN LAW TO NOTIFY MY SPOUSE BY MAIL IF MY CREDIT CARD OR LOAN IS GRANTED. I certify that the credit being applied for, if granted, will be incurred or obtained during marriage and will be in the interest of the marriage or family. This statement is made in accordance with Wis.Stat.sec.766.55 (1).", + "CO_BORROWER_SPOUSE" : "Tell us how we can contact your co-applicant's spouse about your joint credit application." + }, + "DISCLOSURES": "For purposes of the disclosures below, the words \"I,\" \"me,\" \"we,\" \"our\" and \"my\" refer to each of the applicants individually and together; \"Landmark\" refers to Landmark Credit Union.

This application does not constitute a contract for extension of credit.

", + "SUMMARY": "", + "PROCESSING": "{{firstName}}, your application is being processed. The process shouldn't take more than a couple minutes. If you experience any issues, please contact our help number at {{phoneNumber}}. Thank you for your patience and we appreciate you choosing Landmark Credit Union.", + "DECISION_TITLE": "{{firstName}}, you've been approved", + "DECISION": "Congratulations! All applications are subject to final review and verification by our underwriters. In the meantime though, please check out our recommended next steps below.", + "TCPA": "By providing your telephone number, including your wireless telephone number, in connection with this account or loan, you have consented and authorized Landmark Credit Union or anyone acting on its behalf to contact you about your account or loan at that number using an automatic telephone dialing system or an artificial or prerecorded message." + }, + "SUMMARY": { + "GENERAL": { + "CREDIT_CARD_LIMIT": "Limit requested", + "LOAN_LIMIT": "Loan amount", + "LOAN_TERM": "Loan term", + "LOAN_TITLE": "Your new loan", + "CREDIT_CARD_TITLE": "Your new credit card", + "HELOC_RATE_MOD": "Please note that all borrowers under the original loan documents must execute the modification agreement", + "PURPOSE": "Purpose of Loan" + }, + "COLLATERAL": { + "ADDRESS_SAME": "Address: Same as primary?", + "ESTIMATED_VALUE": "Estimated Value", + "OCCUPANCY_TYPE": "Occupancy Type", + "PROPERTY_TYPE": "Property Type", + "FIRST_MORTGAGE" : "Does this property have a first mortgage?", + "SECOND_MORTGAGE" : "Does this property have a second mortgage or HELOC?", + "PAYOFF_FIRST_MORTGAGE" : "Payoff first mortgage with this loan?", + "PAYOFF_SECOND_MORTGAGE" : "Payoff second mortgage with this loan?", + "IS_BEING_IMPROVED": "Is this property currently being remodeled or improved?", + "IS_LISTED": "Is this property currently listed for sale?", + "ON_RENTED_LOT": "Is the home on a rented lot?", + "ANNUAL_PROPERTY_TAX": "Annual Property Taxes", + "RENT_PAID": "Rent paid per month" + }, + "VEHICLE": { + "TYPE": "Vehicle type", + "CONDITION": "Vehicle condition", + "VALUE": "Vehicle Estimated Value or Sale Price", + "MODEL": "Vehicle model", + "MILEAGE": "Vehicle mileage", + "TERM": "Term", + "SELLER": "Vehicle seller", + "BOAT_ADDRESS": "Boat address" + }, + "PRIMARY_HEADING": "Let's review your information", + "NOTE_HEADING": "Did we forget something important?", + "PRIMARY_SPOUSE_HEADING": "Please review your spouse's information", + "CO_APP_HEADING": "Please review your co-applicant's information", + "CO_APP_SPOUSE_HEADING": "Please review you co-applicant's spouse information", + "ADDITIONAL_INFO" : "Additional Information", + "EMPLOYER": { + "CURRENT": "Current Employer", + "OTHER": "Other Current Employer", + "PREVIOUS": "Previous Employer", + "EMPLOYERNAME": "Employer", + "EMPLOYERTYPE": "Employment Type", + "SALARY": "Salary", + "SCHEDULE": "Schedule", + "LENGTH": "Length", + "WORKPHONE": "Work Phone", + "OTHERINCOME": "Other Income", + "SOURCE": "Source", + "CHILDSUPPORT": "Child Support" + }, + "CHANGE_LINK": "change »", + "TITLES": { + "TATTLE": "Spouse's personal details", + "VEHICLE": "Your Vehicle", + "RESIDENCE": "Your current residence information", + "RESIDENCE_PRIOR": "Your prior residence information", + "INCOME": "Your income information", + "PERSONAL": "Your personal details", + "SPOUSE_DETAILS": "Your spouse's personal details", + "SPOUSE_RESIDENCE": "Your spouse's current residence information", + "SPOUSE_INCOME": "Your spouse's income information", + "COAPP_DETAILS": "Co-Applicant's personal details", + "COAPP_RESIDENCE": "Co-applicant's current residence information", + "COAPP_RESIDENCE_PRIOR": "Co-applicant's prior residence information", + "COAPP_INCOME": "Your co-applicant's income information", + "TATTLE_RESIDENCE": "Your co-applicant's spouse's current residence information", + "COAPP_SPOUSE_DETAILS": "Co-Applicant's spouse personal details", + "COAPP_SPOUSE_RESIDENCE": "Co-Applicant's spouse current residence information", + "COAPP_SPOUSE_INCOME": "Co-Applicant's spouse income information", + "COLLATERAL": "Your collateral information" + }, + "PERSONAL": { + "FULL_NAME": "Full Name", + "DOB": "Date of Birth", + "SSN": "Social Security Number", + "PRIMARY_PHONE": "Primary Phone", + "SECONDARY_PHONE": "Secondary Phone", + "EMAIL": "Email", + "PRIMARY_APPLICANT_EMAIL": "Primary Applicant Email Address", + "CO_APPLICANT_EMAIL": "Co-Applicant Email Address" + }, + "ADDRESS": { + "STREET_ADDRESS": "Address", + "TENANT_STATUS": "Tenancy", + "MONTHLY_PAYMENT": "Monthly Payment", + "LENGTH": "Duration of residence", + "MAILING_ADDRESS": "Mailing Address", + "ADDRESS_SAME": "Same address as spouse?" + }, + "ADDITIONAL": { + "PRIMARY_CONTACT": "Please select the best way to reach you", + "PROMO_CODE": "Promo Code", + "MOD_FEE": "Where to take the modification fee from?" + } + }, + "HMDA": { + "HMDA_INTRO": "The purpose of collecting this information is to help ensure that all applicants are treated fairly and that the housing needs of communities and neighborhoods are being fulfilled. For residential mortgage lending, Federal law requires that we ask applicants for their demographic information (ethnicity, race, and sex) in order to monitor our compliance with equal credit opportunity, fair housing, and home mortgage disclosure laws. You are not required to provide this information, but are encouraged to do so. You may select one or more designations for “Ethnicity” and one or more designations for “Race”. The law provides that we may not discriminate on the basis of this information, or on whether you choose to provide it. However, if you choose not to provide the information and you have made this application in person, Federal regulations require us to note your ethnicity, race, and sex on the basis of visual observation or surname. The law also provides that we may not discriminate on the basis of age or marital status information you provide in this application. If you do not wish to provide some or all of this information, please check below.", + "ETHNICITY": { + "HISPANIC_OR_LATINO": "Hispanic or Latino", + "MEXICAN": "Mexican", + "PUERTO_RICAN": "Puerto Rican", + "CUBAN": "Cuban", + "OTHER_HISPANIC_LATINO": "Other Hispanic or Latino", + "OTHER_HISPANIC_LATINO_TEXT": "Write origin, for example, Argentinean, Colombian, Dominican, Nicaraguan, Salvadoran, Spaniard, and so on:", + "NOT_HISPANIC_OR_LATINO": "Not Hispanic or Latino" + }, + "RACE": { + "AMERICAN_INDIAN_ALASKA_NATIVE": "American Indian or Alaska Native", + "AMERICAN_INDIAN_ALASKA_NATIVE_TEXT": "Write name of enrolled or principal tribe:", + "BLACK_OR_AFRICAN_AMERICAN": "Black or African American", + "ASIAN": "Asian", + "ASIAN_INDIAN": "Asian Indian", + "CHINESE": "Chinese", + "FILIPINO": "Filipino", + "JAPANESE": "Japanese", + "KOREAN": "Korean", + "VIETNAMESE": "Vietnamese", + "OTHER_ASIAN": "Other Asian", + "OTHER_ASIAN_TEXT": "Write race, for example, Hmong, Laotian, Thai,Pakistani, Cambodian and so on:", + "HAWAIIAN": "Hawaiian", + "NATIVE_HAWAIIAN": "Native Hawaiian", + "GUAMANIAN_CHAMORRO": "Guamanian or Chamorro", + "SAMOAN": "Samoan", + "OTHER_PACIFIC_ISLANDER": "Other Pacific Islander", + "OTHER_PACIFIC_ISLANDER_TEXT": "Write race, for example, Fijian, Tongan, and so on:", + "WHITE": "White" + }, + "SEX": { + "FEMALE": "Female", + "MALE": "Male" + }, + "DO_NOT_PROVIDE_INFO": "I do not wish to provide this information" + }, + "ERROR": { + "JOINT_CREDIT": { + "MESSAGE": "Your Application type will be changed to \"Individual\", if your answer to this question is \"No\".", + "TITLE": "Applying for Joint Credit", + "HEADLINE": "Error Message" + } + }, + "DISCLOSURE" : { + "CREDIT_CARD" : { + "TITLES": { + "SPOUSAL": "Notice to Married Applicant", + "ACCEPTANCE": "Acceptance Agreement" + }, + "SPOUSAL": "No provision of a marital property agreement, a unilateral statement under Wis. Stats. sec. 766.59 or a court decree under Wis. Stats. sec. 766.70 adversely affects the interests of Landmark unless prior to the time the credit is extended, Landmark is furnished with a copy of the agreement, statement or decree, or has actual knowledge of the adverse provision when the obligation to Landmark is incurred.

IF I HAVE INDICATED THAT I AM APPLYING FOR CREDIT SEPARATE FROM MY SPOUSE, THIS SECTION APPLIES TO ME. I UNDERSTAND THAT LANDMARK IS REQUIRED BY WISCONSIN LAW TO NOTIFY MY SPOUSE BY MAIL IF MY CREDIT CARD OR LOAN IS GRANTED. I certify that the credit being applied for, if granted, will be incurred or obtained during marriage and will be in the interest of the marriage or family. This statement is made in accordance with Wis.Stat.sec.766.55 (1). ", + "ACCEPTANCE": "I (we) certify that I (we) UNDERSTAND AND AGREE THAT, AS A CONDITION TO MY (OUR) ACCEPTANCE AND USE OF THE APPROVED CREDIT: 1.I (we) hereby grant to Landmark a security interest in all my (our) funds in my (our) present and future Landmark deposit accounts, except IRAs and any other accounts exempted by state and federal law, to the full amount of my credit indebtedness with Landmark; 2. If I (we) default under the terms of the credit agreement, Landmark may, at its sole option and subject to any notice and right to cure required by Wis.Stat.Sec.425.105: (i) exercise its right of set-off and apply the funds or any portion thereof toward payment of the outstanding indebtedness; and/or (ii) exercise its right to refuse to allow withdrawal from such accounts to the full extent of the delinquent amount, subject to the restrictions contained in Sec.1026.13 (d)(1) of federal Reg. Z.", + "TRUTHFUL": { + "PART1": "I (we) certify that the information provided in this application is truthful, accurate and voluntary, and authorize Landmark to investigate my (our) creditworthiness, credit history and financial responsibility through any credit bureau or by any other reasonable means at the time of underwriting this application and on an ongoing basis during the life of this loan or credit card with Landmark, including any collection proceedings.", + "PART2": "I (we) understand that it may be a Federal crime punishable by fine or imprisonment, or both, to knowingly make any false statements concerning any of the above facts, as applicable under the provisions of Title 18, United States Code, Section 1014.I agree to abide by the issuer's terms governing VISA accounts that will be provided to me in the event my application is approved. I/We have read the Terms and Conditions listed." + }, + "CONTACT": { + "PART1": "By providing a phone number, cellular phone number, or other wireless device phone number (\"Contact Number\") in connection with this application, you are expressly authorizing Landmark Credit Union to transmit, and consenting to receive, communications at that number from Landmark Credit Union and its agents regarding this application and any and all resulting accounts and/or loans held at Landmark Credit Union. These communications may include calls made using an automatic telephone dialing system or artificial or prerecorded voice messages for informational, account servicing, or debt collection purposes. If you provide a mobile telephone number, you agree that we may contact you at this number even if your mobile phone carrier charges you for the call." + } + }, + "HELOC" : { + "HEADER" : { + "REVIEW" : "Disclosures for Review" + }, + "TITLES" : { + "KNOW" : "WHAT YOU SHOULD KNOW ABOUT Home Equity Lines of Credit (HELOC)", + "CREDIT_DISCLOSURE" : "Home Equity Line of Credit Disclosure" + }, + "CONFIRM" : "To continue, confirm that you’ve reviewed the disclosures and that you acknowledge receipt" + }, + "ELECTRONIC": { + "PART1": "E-Signature and Electronic Disclosures Agreement by clicking here", + "PART2": ". By clicking the \"Next\" button below, you agree to this disclosure." + }, + "MODAL": { + "COPY": "The words \"we,\" \"us,\" and \"our\" refer to Landmark Credit Union, and the words \"you\" and \"your\" mean you, the the individual(s). As used in this Disclosure, \"Communication\" means any authorization, agreement, disclosure, notice, or other information related to your credit card/loan application, including but not limited to information that we are required by law to provide to you in writing.", + "HEADER": "This Disclosure applies to your loan/credit card application.", + "PRINT": "Print/Download", + "PART_1": { + "TITLE": "1. Scope of Communications to Be Provided in Electronic Form.", + "CONTENT": "When you proceed with the loan application, you agree that we may provide you with any Communications in electronic format, and that we may not send paper Communications to you, unless and until you withdraw your consent as described below. Your consent to receive electronic Communications includes, but is not limited to all legal and regulatory disclosures and communications associated with your application, loan and credit card, including Risk Based Pricing and Adverse Action Notices and all privacy policies and notices." + }, + "PART_2": { + "TITLE": "2. Method of Providing Communications to You in Electronic Form.", + "CONTENT": "All Communications that we provide to you in electronic form will be provided either (1) via e-mail, (2) by access to a website that we will designate in an e-mail notice we send to you at the time the information is available, or (3) to the extent permissible by law, by access to a website that we will generally designate in advance for such purpose." + }, + "PART_3": { + "TITLE": "3. How to Withdraw Consent.", + "CONTENT": "You may withdraw your consent to receive Communications in electronic form by contacting us at (262) 796-4500. At our option, we may treat your provision of an invalid e-mail address, or the subsequent malfunction of a previously valid e-mail address, as a withdrawal of your consent to receive electronic Communications. We will not impose a fee to process the withdrawal of your consent to receive electronic Communications. Any withdrawal of your consent to receive electronic Communications will be effective only after we have a reasonable period of time to process your withdrawal." + }, + "PART_4": { + "TITLE": "4. How to Update Your Records.", + "CONTENT": "It is your responsibility to provide us with true, accurate and complete e-mail address, contact, and other information related to this Disclosure and your application, loan or credit card, and to maintain and update promptly any changes in this information. You can update information (such as your e-mail address) by contacting us at (262) 796-4500." + }, + "PART_5": { + "TITLE": "5. Hardware and Software Requirements.", + "CONTENT": "In order to access, view, and retain electronic Communications that we make available to you, you must have:", + "BULLET_POINTS": { + "POINT_1": "an Internet browser that supports 128 bit encryption;", + "POINT_2": "sufficient electronic storage capacity on your computer's hard drive or other data storage unit;", + "POINT_3": "an e-mail account with an Internet service provider and e-mail software in order to participate in our electronic Communications programs;", + "POINT_4": "a personal computer (for PC's: Pentium 120 MHz or higher; for Macintosh, Power Mac 9500, Power PC 604 processor 120-MHz Base or higher), operating system and telecommunications connections to the Internet capable of receiving, accessing, displaying, and either printing or storing Communications received from us in electronic form via a plain text-formatted e-mail or by access to our web site using an internet browser." + } + }, + "PART_6": { + "TITLE": "6. Requesting Paper Copies.", + "CONTENT": "We will not be obligated to send you a paper copy of any Communication unless you request it or we otherwise deem it appropriate to do so. You can obtain a paper copy of an electronic Communication by printing it yourself or by requesting that we mail you a paper copy, provided that such request is made within a reasonable time after we first provided the electronic Communication to you. To request a paper copy, contact us by telephone at (262) 796-4500. We may charge you a reasonable service charge for the delivery of paper copies of any Communication provided to you electronically pursuant to this authorization. We reserve the right, but assume no obligation, to provide a paper (instead of electronic) copy of any Communication that you have authorized us to provide electronically." + }, + "PART_7": { + "TITLE": "7. Communications in Writing.", + "CONTENT": "All Communications in either electronic or paper format from us to you will be considered \"in writing.\" You should print or download for your records, a copy of this Disclosure and any other Communication that is important to you." + }, + "PART_8": { + "TITLE": "8. Federal Law.", + "CONTENT": "You acknowledge and agree that your consent to electronic Communications is being provided in connection with a transaction affecting interstate commerce that is subject to the federal Electronic Signatures in Global and National Commerce Act, and that you and we both intend that the Act apply to the fullest extent possible to validate our ability to conduct business with you by electronic means." + }, + "PART_9": { + "TITLE": "9. Termination/Changes.", + "CONTENT": "We reserve the right, at our sole discretion, to discontinue the provision of your electronic Communications, or to terminate or change the terms and conditions on which we provide electronic Communications. We will provide you with notice of any such termination or change as required by law." + } + } + }, + "ADDRESS_COMPARE": { + "HEADER": "Address Verification", + "UNABLE_TO_VERIFY": "We are unable to verify your address as entered, but found a close match.", + "CONFIRM": "Please confirm the correct address or edit your address above", + "UNABLE_TO_FIND_MATCH_LINE1": "We are unable to verify your address as entered.", + "UNABLE_TO_FIND_MATCH_LINE2": "Please confirm or edit your address above.", + "USE_MINE": "Use the address I provided", + "SELECT_MATCHING": "Use recommended address from USPS" + }, + "PATRIOT_ACT":{ + "HEADER": "US Patriot Act Notice", + "BODY": { + "PART1": "IMPORTANT INFORMATION ABOUT PROCEDURES FOR OPENING A NEW ACCOUNT OR CREDIT CARD", + "PART2": "To help the government fight the funding of terrorism and money laundering activities, federal law requires all financial institutions to obtain, verify, and record information that identifies each person who opens an account. ", + "PART3": "What this means for you: when you open an account or apply for credit, we will ask for your name, address, date of birth, and other information that will allow us to identify you. We may also ask to see your driver’s license or other identifying documents or require you to visit one of our branches and meet with an associate as a condition of credit approval. " + } + }, + "FOOTER":{ + "NCUA": "Federally Insured by NCUA", + "HOUSING": "Equal Housing Opportunity", + "SECURE": "Secure Site", + "COPY_RIGHT": "©{{currentYear}} Landmark Credit Union. All rights reserved.", + "ACCESSIBILITY": "Accessibility", + "SECURITY": "Security", + "TERMS": "Terms", + "PRIVACY": "Privacy" + }, + "HEADER": { + "ASSISTANCE": "Need assistance?", + "PHONE_NUM": "Call 262.796.4500", + "PROPER_PHONE_NUMBER": "+1-262-796-4500", + "LOAN_MESSAGE": { + "CREDITCARD_CASHBACK": "Cash Back VISA Card", + "CREDITCARD_REWARDS": "Rewards VISA Card", + "CREDITCARD_STUDENT": "Student Rewards VISA Card", + "CREDITCARD_SECURE": "Secured VISA Card", + "EQUITY_HELOC_FLEX": "Home Equity Line of Credit", + "EQUITY_HELOC_FIXED": "Fixed Term Home Equity Loan", + "EQUITY_HELOC_RATE_MOD": "Home Equity Line of Credit Rate Modification", + "EQUITY_MORTGAGE_REF_EXPRESS": "Mortgage Refi Express", + "VEHICLE_AUTO": "Auto/Motorcycle Loan", + "EQUITY_MANUF_HOME": "Manufactured Home Loan", + "VEHICLE_PRE_APPROVAL": "Auto/Motorcycle Pre-Approval Loan", + "VEHICLE_BOAT": "Boat Loan", + "VEHICLE_POWERSPORT": "Powersports Loan", + "VEHICLE_MOTORHOME": "Trailer/Motorhome Loan", + "SIGNATURE_LOAN": "Personal Signature Loan", + "SPECIAL_LOAN": "COVID-19 Loan" + } + }, + "DECISION": { + "APPROVED": { + "TITLE": "Your application is conditionally approved.", + "TEXT": "Your application has been conditionally approved. An associate will review your file and reach out to you within the next business day to discuss next steps. Final approval and terms are subject to change.", + "TEXT_CC": "Your application has been conditionally approved. An associate will review your file and reach out to you within the next business day to discuss next steps. Final approval and terms are subject to change." + }, + "REVIEW": { + "TITLE": "Thank you for applying with Landmark Credit Union.", + "TEXT": "Your application is being processed. A representative will respond to you within one business day.", + "TEXT_CC": "Your application is being processed. Thank you for submitting your credit card application." + }, + "DENIED": { + "TITLE": "Thank you for applying with Landmark Credit Union.", + "TEXT": "We have processed your application and are unable to honor your request at this time. A notice will be mailed to you indicating the reason for this decision. You may then want to contact Landmark Credit Union if you have further questions about your application.", + "TEXT_CC": "We have processed your application and are unable to honor your request at this time. A notice will be mailed to you indicating the reason for this decision. You may then want to contact Landmark Credit Union if you have further questions about your application." + }, + "ERROR_SUBMIT": { + "TITLE": "Thank you for applying with Landmark Credit Union.", + "TEXT": "Your application is being processed. A representative will respond to you within one business day.", + "TEXT_CC": "Your application is being processed. Thank you for submitting your credit card application." + }, + "ERROR_DECISION": { + "TITLE": "Thank you for applying with Landmark Credit Union.", + "TEXT": "Your application is being processed. A representative will respond to you within one business day.", + "TEXT_CC": "Your application is being processed. Thank you for submitting your credit card application." + } + }, + "LANDING": { + "TITLE": "Start a new loan application", + "CREDIT_CARD": "Select a credit card type", + "HOME_EQUITY": "Select a home equity type", + "VEHICLE": "Select a vehicle type", + "PERSONAL": "Select a personal type", + "START": "Start Application", + "BACK": "Back To All Loans", + "LOADING": "Loading Existing Loans", + "LOADING_LOAN": "Loading Loan. Please Wait.", + "COMPLETE_LOANS": "Complete Your Loans:", + "NO_LOANS": "You do not have any pending loan applications.", + "LOAN_ERROR": "Unable to provide pending loans at this time.", + "VEHICLE_BACK": "Back To All Vehicle Loans", + "VIEW_TYPES": "View All Types", + "TYPE":{ + "CREDIT": "Credit Card", + "HOME": "Home Equity", + "VEHICLE": "Vehicle", + "PERSONAL": "Personal", + "SPECIAL": "COVID-19 Loan" + }, + "SUB_TYPE":{ + "CASHBACK": "Cash Back VISA", + "REWARDS": "Rewards VISA", + "STUDENT": "Student Rewards VISA", + "SECURED": "Secured VISA", + "HELOC": "HELOC", + "HOME_FIXED": "Home Equity Fixed", + "MORT_REFI": "Mortgage Refi Express", + "MANUF": "Manufactured Home", + "PERSONAL": "Personal Signature", + "AUTO": "Auto/Motorcycle", + "VEHICLE_PRE": "Apply for Pre-Approval", + "VEHICLE_LOAN": "Apply for Loan", + "BOAT": "Boat", + "POWER": "Powersports", + "TRAILER": "Trailer/Motorhome", + "HELOC_RATE_MOD": "HELOC Rate Modification" + } + }, + "MEMBER":{ + "LOGIN": { + "USERNAME": "User Id", + "PASSWORD": "Password", + "LOGIN": "Login", + "MEMBER_HEADER": "Landmark Member", + "SUB_TEXT": "Log in to continue", + "ERROR_LOCKED": "Your account has been locked due to multiple invalid login attempts. Please contact us to resolve.", + "ERROR_USER": "The User ID and / or Password does not match our records. Try again.", + "ERROR_COOKIES": "Unable to authenticate. Turn on cookies.", + "ERROR_SERVER": "Server not responding. Try again.", + "NON_MEMBER_HEADER": "Let's get started.", + "MEMBER_SUBTEXT": "Sign in for a faster application.", + "NON_MEMBER_SUBTEXT": "Welcome! Apply for a loan in minutes.", + "STARTED": "Apply" + }, + "VERIFICATION": { + "SSN": "Last 4 Digits of SSN", + "BIRTH": "Date of Birth", + "ATTEMPT": "Attempt", + "OF": "Of", + "HEADER": "Member Verification", + "ERROR": "The SSN and / or DOB does not match our records. Try again.", + "FAILURE": "Something went wrong and we can't verify your identity at this time.", + "SUBTEXT": "Please share the following details.", + "CONTINUE": "Continue", + "CONTINUE_NO_VERIFY": "Continue Without Verification", + "MAX_ATTEMPTS": "You exceeded the max attempts." + } + }, + "UNAVAILABLE": { + "HEADER": "Whoops...", + "BODY_TOP": "It looks like this page is unavailable.", + "BODY_BOTTOM": "Please try again later." + }, + "DOWN": { + "HEADER": "System Maintenance", + "BODY_TOP": "We are conducting system maintenance and during this time our loan applications will be unavailable. We apologize for any inconvenience this may cause.", + "SAVE": "Return to home" + }, + "BRANCHMAP": { + "TITLE": "Branch Mapping Administration.", + "SUBTEXT": "You can use this utility to preview, download, and upload a branch mapping configuration.", + "DATETEXT": "The data below was uploaded on: ", + "DOWNLOAD": "Click below to download the current branch mapping as a CSV file.", + "MAPPING_ERROR": "The server rejected the mapping.", + "LOAD_ERROR": "Failed to load branch config" + }, + "BRANCHNAMEMAP": { + "TITLE": "Branch Name Mapping Administration.", + "SUBTEXT": "You can use this utility to view and update the branch name mapping configuration using the mappings from CRIF.", + "DATETEXT": "The data below was uploaded on: ", + "UPDATETEXT": "Click below to update the current branch name mapping with data from CRIF.", + "LOAD_ERROR": "Failed to load branch name config" + }, + "CONFIG": { + "TITLE": "Start/Stop Loan Applications", + "SUBTEXT": "Manage access to online loan applications for website maintenance, security reasons or any other reason.", + "APP_DISABLED": "Application Disabled", + "APP_ENABLED": "Application Enabled", + "SAVE": "Save and Publish", + "STATUS": "Current Status", + "LOAD_ERROR": "Failed to load stored settings", + "UPDATE_ERROR": "Failed to update stored settings" + }, + "ADMIN": { + "LANDING_TITLE": "Select an admin feature", + "LOAN_TABLE": "Loan Management", + "BRANCH_CONFIG": "Branch Mapping", + "BRANCHNAME_CONFIG": "Branch Name Mapping", + "SCHUMER": "Schumer Preview", + "NAVIGATE": "Navigate", + "HOME": "Admin Home", + "DATE_ERROR": "MM/DD/YYYY", + "SYSTEM": "System Config", + "LOAD_ERROR": "Failed to load loans", + "DELETE_ERROR": "Failed to delete loan", + "SUBMIT_ERROR": "Failed to load loans" + + }, + "BEFOREYOUBEGIN": { + "CREDITCARD_STUDENT": { + "INTRO": "Before you begin the process, you should have the following documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Documents to Upload", + "VALUES": [ + "Proof of income (i.e. most recent payroll check stub, 1040s for self-employed borrowers, bank statement)." + ] + } + ] + }, + "EQUITY_HELOC_FLEX": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": ["Real estate used for home equity loan collateral must be located in Wisconsin."] + }, + { + "HEADING": "Documents to Upload", + "VALUES": [ + "Proof of homeowner insurance.", + "If loan amount is greater than $100,000, proof of income (i.e. most recent payroll check stub, 1040s for self-employed borrows, bank statement).", + "A copy of the first mortgage balance statement." + ] + } + ] + }, + "EQUITY_HELOC_FIXED": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": ["Real estate used for home equity loan collateral must be located in Wisconsin."] + }, + { + "HEADING": "Documents to Upload", + "VALUES": [ + "Proof of homeowner insurance.", + "Proof of income (i.e. last two years of W2s and a recent payroll check stub, or 1040s for self-employed borrowers, or a bank statement).", + "A copy of the first mortgage balance statement." + ] + } + ] + }, + "EQUITY_MANUFACTURED": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": ["Real estate used for home equity loan collateral must be located in Wisconsin."] + }, + { + "HEADING": "Documents to Upload", + "VALUES": ["Manufactured home title or purchase contract."] + } + ] + }, + "EQUITY_MORTGAGE_REF_EXPRESS": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "Real estate used for home equity loan collateral must be located in Wisconsin.", + "This loan is only eligible to refinance existing mortgages on your primary residence.
  1. If you would like to cash out against the equity of your property, or the property is not your primary residence, please use the Home Equity Fixed loan application.
  2. If you prefer a home equity line of credit, please use the HELOC loan application.
" + ] + }, + { + "HEADING": "Documents to Upload", + "VALUES": [ + "Proof of homeowner insurance.", + "Proof of income (i.e. last two years of W2s and a recent payroll check stub, or 1040s for self-employed borrowers, or a bank statement)." + ] + } + ] + }, + "EQUITY_HELOC_RATE_MOD": { + "INTRO": "Before you begin the process, you should be prepared with the following information.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "Landmark Credit Union will need to pull the credit report for the primary applicant only.", + "Please note that all borrowers under the original loan documents must execute the modification agreement.", + "If you are requesting an increase to an existing line of credit, please use the HELOC loan application." + ] + } + ] + }, + "VEHICLE_AUTO": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "If working with a dealership, you will need to provide the dealership name and phone number.", + "If the vehicle is currently titled in any state other than Wisconsin, a copy of the title may be required.", + "If there is a lien, a 10 day payoff with per diem will be requested." + ] + }, + { + "HEADING": "Documents to Upload", + "VALUES": ["If working with a dealership, a copy of the purchase contract."] + } + ] + }, + "VEHICLE_BOAT": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "If working with a dealership, you will need to provide the dealership name and phone number.", + "If the boat is currently titled in any state other than Wisconsin, a copy of the title may be required.", + "If there is a lien, a 10 day payoff with per diem will be requested.", + "The boat information, and if applicable, motor and trailer information." + ] + }, + { + "HEADING": "Documents to Upload", + "VALUES": [ + "If working with a dealership, a copy of the purchase contract.", + "A copy of the registration card from DNR.", + "Title(s) needed at closing." + ] + } + ] + }, + "VEHICLE_POWERSPORT": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "If working with a dealership, you will need to provide the dealership name and phone number.", + "If the vehicle is currently titled in any state other than Wisconsin, a copy of the title may be required.", + "If there is a lien, a 10 day payoff with per diem will be requested." + ] + }, + { + "HEADING": "Documents to Upload", + "VALUES": ["If working with a dealership, a copy of the purchase contract."] + } + ] + }, + "VEHICLE_MOTORHOME": { + "INTRO": "Before you begin the process, you should be prepared with the following information and documents to be uploaded. Please note that uploading requested documents will expedite processing your loan request.", + "HEADINGS": [ + { + "HEADING": "Information", + "VALUES": [ + "If working with a dealership, you will need to provide the dealership name and phone number.", + "If the vehicle is currently titled in any other state than Wisconsin, a copy of the title may be required.", + "If there is a lien, a 10 day payoff with per diem will be requested." + ] + }, + { + "HEADING": "Documents to Upload", + "VALUES": ["If working with a dealership, a copy of the purchase contract."] + } + ] + } + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/loading.gif b/LCU.WebApps.External.OnlineLoanApp/src/assets/loading.gif new file mode 100644 index 0000000..e9bb525 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/loading.gif differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/ncua.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/ncua.png new file mode 100644 index 0000000..dee9731 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/ncua.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/settings/default_settings.json b/LCU.WebApps.External.OnlineLoanApp/src/assets/settings/default_settings.json new file mode 100644 index 0000000..2bbf20f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/assets/settings/default_settings.json @@ -0,0 +1,2324 @@ +{ + "configuration": { + "lastUpdated": "2019-04-25T13:46:05.119Z", + "lastUpdatedBy": "gfrick", + "applicationDisabled": false, + "id": "758f64a9-0e3e-41a1-8b09-e3c37b27437b" + }, + "mfdHomeAndRvByAmount": { + "0": [ + 48 + ], + "10000": [ + 48, + 60, + 72, + 84 + ], + "20000": [ + 48, + 60, + 72, + 84, + 120, + 180 + ] + }, + "homeEquity": [ + 60, + 84, + 120, + 180, + 240 + ], + "special": [ + 36 + ], + "mortgageRefiExpress": [ + 96, + 120, + 144 + ], + "vehiclePreApproval": [ + 36, + 48, + 63, + 75 + ], + "termsByYear": { + "2023": [ + 36, + 48, + 63, + 75, + 84 + ], + "2022": [ + 36, + 48, + 63, + 75 + ], + "2021": [ + 36, + 48, + 63 + ], + "2020": [ + 36, + 48 + ] + }, + "boatLoansByAmount": { + "0": [ + 48 + ], + "10000": [ + 48, + 63, + 72, + 84 + ], + "20000": [ + 48, + 63, + 72, + 84, + 120, + 180 + ] + }, + "powersportByAmount": { + "0": [ + 36, + 48, + 63 + ], + "10000": [ + 36, + 48, + 63, + 72 + ] + }, + "signatureByAmount": { + "0": [ + 12, + 24, + 36 + ], + "5000": [ + 48 + ] + }, + "typePurposes": { + "CC_CashBack": [ + "Other", + "Consolidation", + "Reestablish_Credit" + ], + "CC_Rewards": [ + "Other", + "Consolidation", + "Reestablish_Credit" + ], + "CC_Student": [ + "Other", + "Consolidation", + "Reestablish_Credit" + ], + "CC_Secure": [ + "Other", + "Consolidation", + "Reestablish_Credit" + ], + "EQ_HELOC_Flex": [ + "Home_Improvement", + "Home_Purchase", + "Home_Refinance", + "Other", + "Cash_Out_Refinance" + ], + "EQ_Mtg_Refi_Express": [ + "Home_Improvement", + "Home_Purchase", + "Home_Refinance", + "Other", + "Cash_Out_Refinance" + ], + "EQ_Home_Equity_Fixed": [ + "Home_Improvement", + "Home_Purchase", + "Home_Refinance", + "Other", + "Cash_Out_Refinance" + ], + "EQ_Mfg_Home_With_or_Without_Land": [ + "Home_Improvement", + "Purchase_Manufactured_Home", + "Home_Refinance", + "Other", + "Cash_Out_Refinance" + ], + "V_Vehicle": [ + "Purchase", + "Cash_Out_Refinance", + "Refinance" + ], + "V_VehiclePreapproval": [ + "Purchase", + "Cash_Out_Refinance", + "Refinance" + ], + "M_Boat": [ + "Purchase", + "Refinance" + ], + "P_Powersport": [ + "Purchase", + "Refinance" + ], + "T_TrailerMotorhome": [ + "Purchase", + "Refinance" + ], + "S_Signature": [ + "Other", + "Consolidation" + ], + "S_Special": [ + "Other", + "Consolidation" + ] + }, + "limits": { + "EQ_HELOC_Flex": { + "min": 10000.0, + "max": 500000.0 + }, + "EQ_Mtg_Refi_Express": { + "min": 10000.0, + "max": 250000.0 + }, + "EQ_Home_Equity_Fixed": { + "min": 10000.0, + "max": 500000.0 + }, + "EQ_Mfg_Home_With_or_Without_Land": { + "min": 250.0, + "max": 250000.0 + }, + "V_Vehicle": { + "min": 250.0, + "max": 250000.0 + }, + "V_VehiclePreapproval": { + "min": 250.0, + "max": 250000.0 + }, + "M_Boat": { + "min": 250.0, + "max": 250000.0 + }, + "P_Powersport": { + "min": 250.0, + "max": 250000.0 + }, + "T_TrailerMotorhome": { + "min": 250.0, + "max": 250000.0 + }, + "S_Signature": { + "min": 250.0, + "max": 25000.0 + }, + "S_Special": { + "min": 250.0, + "max": 10000.0 + } + }, + "statesRequiringSpouseInformation": [ + "WI" + ], + "stateOptions": [ + "AA", + "AE", + "AK", + "AL", + "AP", + "AR", + "AS", + "AZ", + "CA", + "CO", + "CT", + "DC", + "DE", + "FL", + "GA", + "GU", + "HI", + "IA", + "ID", + "IL", + "IN", + "KS", + "KY", + "LA", + "MA", + "MD", + "ME", + "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" + ], + "incomeLimits": { + "Hourly": { + "min": 0.0, + "max": 999.0 + }, + "Annually": { + "min": 0.0, + "max": 1000000000.0 + } + }, + "branchMapping": { + "branchCodesByZip": [ + { + "zipcode": "53001", + "channelId": 1561599 + }, + { + "zipcode": "53002", + "channelId": 1561599 + }, + { + "zipcode": "53003", + "channelId": 1561245 + }, + { + "zipcode": "53004", + "channelId": 1561591 + }, + { + "zipcode": "53005", + "channelId": 1561255 + }, + { + "zipcode": "53006", + "channelId": 1561657 + }, + { + "zipcode": "53007", + "channelId": 1561255 + }, + { + "zipcode": "53008", + "channelId": 1561255 + }, + { + "zipcode": "53010", + "channelId": 1561599 + }, + { + "zipcode": "53011", + "channelId": 1561599 + }, + { + "zipcode": "53012", + "channelId": 1561591 + }, + { + "zipcode": "53013", + "channelId": 1561591 + }, + { + "zipcode": "53014", + "channelId": 1561657 + }, + { + "zipcode": "53015", + "channelId": 1561241 + }, + { + "zipcode": "53016", + "channelId": 1561665 + }, + { + "zipcode": "53017", + "channelId": 1561664 + }, + { + "zipcode": "53018", + "channelId": 1561267 + }, + { + "zipcode": "53019", + "channelId": 1561599 + }, + { + "zipcode": "53020", + "channelId": 1561599 + }, + { + "zipcode": "53021", + "channelId": 1561604 + }, + { + "zipcode": "53022", + "channelId": 1561288 + }, + { + "zipcode": "53023", + "channelId": 1561288 + }, + { + "zipcode": "53024", + "channelId": 1561591 + }, + { + "zipcode": "53026", + "channelId": 1561591 + }, + { + "zipcode": "53027", + "channelId": 1561662 + }, + { + "zipcode": "53029", + "channelId": 1561267 + }, + { + "zipcode": "53031", + "channelId": 1561599 + }, + { + "zipcode": "53032", + "channelId": 1561657 + }, + { + "zipcode": "53033", + "channelId": 1561664 + }, + { + "zipcode": "53034", + "channelId": 1561665 + }, + { + "zipcode": "53035", + "channelId": 1561657 + }, + { + "zipcode": "53036", + "channelId": 1561284 + }, + { + "zipcode": "53037", + "channelId": 1561604 + }, + { + "zipcode": "53038", + "channelId": 1561284 + }, + { + "zipcode": "53039", + "channelId": 1561665 + }, + { + "zipcode": "53040", + "channelId": 1561599 + }, + { + "zipcode": "53042", + "channelId": 1561599 + }, + { + "zipcode": "53044", + "channelId": 1561599 + }, + { + "zipcode": "53045", + "channelId": 1561588 + }, + { + "zipcode": "53046", + "channelId": 1561265 + }, + { + "zipcode": "53047", + "channelId": 1561284 + }, + { + "zipcode": "53048", + "channelId": 1561662 + }, + { + "zipcode": "53049", + "channelId": 1561657 + }, + { + "zipcode": "53050", + "channelId": 1561657 + }, + { + "zipcode": "53051", + "channelId": 1561288 + }, + { + "zipcode": "53052", + "channelId": 1561288 + }, + { + "zipcode": "53056", + "channelId": 1561267 + }, + { + "zipcode": "53057", + "channelId": 1561599 + }, + { + "zipcode": "53058", + "channelId": 1561267 + }, + { + "zipcode": "53059", + "channelId": 1561662 + }, + { + "zipcode": "53060", + "channelId": 1561599 + }, + { + "zipcode": "53061", + "channelId": 1561599 + }, + { + "zipcode": "53062", + "channelId": 1561599 + }, + { + "zipcode": "53063", + "channelId": 1561599 + }, + { + "zipcode": "53064", + "channelId": 1561267 + }, + { + "zipcode": "53065", + "channelId": 1561657 + }, + { + "zipcode": "53066", + "channelId": 1561245 + }, + { + "zipcode": "53069", + "channelId": 1561267 + }, + { + "zipcode": "53070", + "channelId": 1561591 + }, + { + "zipcode": "53072", + "channelId": 1561262 + }, + { + "zipcode": "53073", + "channelId": 1561599 + }, + { + "zipcode": "53074", + "channelId": 1561591 + }, + { + "zipcode": "53075", + "channelId": 1561599 + }, + { + "zipcode": "53076", + "channelId": 1561664 + }, + { + "zipcode": "53078", + "channelId": 1561662 + }, + { + "zipcode": "53079", + "channelId": 1561599 + }, + { + "zipcode": "53080", + "channelId": 1561591 + }, + { + "zipcode": "53081", + "channelId": 1561591 + }, + { + "zipcode": "53082", + "channelId": 1561604 + }, + { + "zipcode": "53083", + "channelId": 1561599 + }, + { + "zipcode": "53085", + "channelId": 1561599 + }, + { + "zipcode": "53086", + "channelId": 1561662 + }, + { + "zipcode": "53088", + "channelId": 1561657 + }, + { + "zipcode": "53089", + "channelId": 1561265 + }, + { + "zipcode": "53090", + "channelId": 1561599 + }, + { + "zipcode": "53091", + "channelId": 1561657 + }, + { + "zipcode": "53092", + "channelId": 1561591 + }, + { + "zipcode": "53093", + "channelId": 1561599 + }, + { + "zipcode": "53094", + "channelId": 1561284 + }, + { + "zipcode": "53095", + "channelId": 1561604 + }, + { + "zipcode": "53096", + "channelId": 1561599 + }, + { + "zipcode": "53097", + "channelId": 1561591 + }, + { + "zipcode": "53098", + "channelId": 1561284 + }, + { + "zipcode": "53099", + "channelId": 1561662 + }, + { + "zipcode": "53101", + "channelId": 1561631 + }, + { + "zipcode": "53102", + "channelId": 1561631 + }, + { + "zipcode": "53103", + "channelId": 2081492 + }, + { + "zipcode": "53104", + "channelId": 1561624 + }, + { + "zipcode": "53105", + "channelId": 1561631 + }, + { + "zipcode": "53108", + "channelId": 1561598 + }, + { + "zipcode": "53109", + "channelId": 1561631 + }, + { + "zipcode": "53110", + "channelId": 1561616 + }, + { + "zipcode": "53114", + "channelId": 1561631 + }, + { + "zipcode": "53115", + "channelId": 1561631 + }, + { + "zipcode": "53118", + "channelId": 1561245 + }, + { + "zipcode": "53119", + "channelId": 1561245 + }, + { + "zipcode": "53120", + "channelId": 1561631 + }, + { + "zipcode": "53121", + "channelId": 1561631 + }, + { + "zipcode": "53122", + "channelId": 1561588 + }, + { + "zipcode": "53125", + "channelId": 1561631 + }, + { + "zipcode": "53126", + "channelId": 1561624 + }, + { + "zipcode": "53127", + "channelId": 1561262 + }, + { + "zipcode": "53128", + "channelId": 1561631 + }, + { + "zipcode": "53129", + "channelId": 1561289 + }, + { + "zipcode": "53130", + "channelId": 1561289 + }, + { + "zipcode": "53132", + "channelId": 1561289 + }, + { + "zipcode": "53137", + "channelId": 1561284 + }, + { + "zipcode": "53138", + "channelId": 1561661 + }, + { + "zipcode": "53139", + "channelId": 1561631 + }, + { + "zipcode": "53140", + "channelId": 1561639 + }, + { + "zipcode": "53141", + "channelId": 1561639 + }, + { + "zipcode": "53142", + "channelId": 1561639 + }, + { + "zipcode": "53143", + "channelId": 1561639 + }, + { + "zipcode": "53144", + "channelId": 1561639 + }, + { + "zipcode": "53146", + "channelId": 1561243 + }, + { + "zipcode": "53147", + "channelId": 1561631 + }, + { + "zipcode": "53148", + "channelId": 1561631 + }, + { + "zipcode": "53149", + "channelId": 2081492 + }, + { + "zipcode": "53150", + "channelId": 2081492 + }, + { + "zipcode": "53151", + "channelId": 1561243 + }, + { + "zipcode": "53152", + "channelId": 1561631 + }, + { + "zipcode": "53153", + "channelId": 1561262 + }, + { + "zipcode": "53154", + "channelId": 1561598 + }, + { + "zipcode": "53156", + "channelId": 1561245 + }, + { + "zipcode": "53157", + "channelId": 1561631 + }, + { + "zipcode": "53158", + "channelId": 1561639 + }, + { + "zipcode": "53159", + "channelId": 1561631 + }, + { + "zipcode": "53167", + "channelId": 1561631 + }, + { + "zipcode": "53168", + "channelId": 1561631 + }, + { + "zipcode": "53170", + "channelId": 1561631 + }, + { + "zipcode": "53171", + "channelId": 1561639 + }, + { + "zipcode": "53172", + "channelId": 1561598 + }, + { + "zipcode": "53176", + "channelId": 1561631 + }, + { + "zipcode": "53177", + "channelId": 1561624 + }, + { + "zipcode": "53178", + "channelId": 1561245 + }, + { + "zipcode": "53179", + "channelId": 1561631 + }, + { + "zipcode": "53181", + "channelId": 1561631 + }, + { + "zipcode": "53182", + "channelId": 1561624 + }, + { + "zipcode": "53183", + "channelId": 1561267 + }, + { + "zipcode": "53184", + "channelId": 1561631 + }, + { + "zipcode": "53185", + "channelId": 1561631 + }, + { + "zipcode": "53186", + "channelId": 1561262 + }, + { + "zipcode": "53187", + "channelId": 1561262 + }, + { + "zipcode": "53188", + "channelId": 1561262 + }, + { + "zipcode": "53189", + "channelId": 1561262 + }, + { + "zipcode": "53190", + "channelId": 1561245 + }, + { + "zipcode": "53191", + "channelId": 1561631 + }, + { + "zipcode": "53192", + "channelId": 1561631 + }, + { + "zipcode": "53194", + "channelId": 1561624 + }, + { + "zipcode": "53195", + "channelId": 1561631 + }, + { + "zipcode": "53199", + "channelId": 1561631 + }, + { + "zipcode": "53201", + "channelId": 1561241 + }, + { + "zipcode": "53202", + "channelId": 1561615 + }, + { + "zipcode": "53203", + "channelId": 1561241 + }, + { + "zipcode": "53204", + "channelId": 1561241 + }, + { + "zipcode": "53205", + "channelId": 1561241 + }, + { + "zipcode": "53206", + "channelId": 1561241 + }, + { + "zipcode": "53207", + "channelId": 1561615 + }, + { + "zipcode": "53208", + "channelId": 1561241 + }, + { + "zipcode": "53209", + "channelId": 1561255 + }, + { + "zipcode": "53210", + "channelId": 1561241 + }, + { + "zipcode": "53211", + "channelId": 1561615 + }, + { + "zipcode": "53212", + "channelId": 1561241 + }, + { + "zipcode": "53213", + "channelId": 1561241 + }, + { + "zipcode": "53214", + "channelId": 1561587 + }, + { + "zipcode": "53215", + "channelId": 1561283 + }, + { + "zipcode": "53216", + "channelId": 1561241 + }, + { + "zipcode": "53217", + "channelId": 1561255 + }, + { + "zipcode": "53218", + "channelId": 1561255 + }, + { + "zipcode": "53219", + "channelId": 1561241 + }, + { + "zipcode": "53220", + "channelId": 1561283 + }, + { + "zipcode": "53221", + "channelId": 1561283 + }, + { + "zipcode": "53222", + "channelId": 1561255 + }, + { + "zipcode": "53223", + "channelId": 1561255 + }, + { + "zipcode": "53224", + "channelId": 1561255 + }, + { + "zipcode": "53225", + "channelId": 1561255 + }, + { + "zipcode": "53226", + "channelId": 1561255 + }, + { + "zipcode": "53227", + "channelId": 1561587 + }, + { + "zipcode": "53228", + "channelId": 1561587 + }, + { + "zipcode": "53233", + "channelId": 1561241 + }, + { + "zipcode": "53234", + "channelId": 1561241 + }, + { + "zipcode": "53235", + "channelId": 1561616 + }, + { + "zipcode": "53237", + "channelId": 1561283 + }, + { + "zipcode": "53259", + "channelId": 1561615 + }, + { + "zipcode": "53263", + "channelId": 1561255 + }, + { + "zipcode": "53267", + "channelId": 1561241 + }, + { + "zipcode": "53268", + "channelId": 1561615 + }, + { + "zipcode": "53274", + "channelId": 1561283 + }, + { + "zipcode": "53278", + "channelId": 1561241 + }, + { + "zipcode": "53288", + "channelId": 1561615 + }, + { + "zipcode": "53290", + "channelId": 1561241 + }, + { + "zipcode": "53293", + "channelId": 1561241 + }, + { + "zipcode": "53295", + "channelId": 1561241 + }, + { + "zipcode": "53401", + "channelId": 1561639 + }, + { + "zipcode": "53402", + "channelId": 1561285 + }, + { + "zipcode": "53403", + "channelId": 1561639 + }, + { + "zipcode": "53404", + "channelId": 1561639 + }, + { + "zipcode": "53405", + "channelId": 1561285 + }, + { + "zipcode": "53406", + "channelId": 1561624 + }, + { + "zipcode": "53407", + "channelId": 1561639 + }, + { + "zipcode": "53408", + "channelId": 1561624 + }, + { + "zipcode": "53490", + "channelId": 1561285 + }, + { + "zipcode": "53501", + "channelId": 1561661 + }, + { + "zipcode": "53502", + "channelId": 1561661 + }, + { + "zipcode": "53503", + "channelId": 1561661 + }, + { + "zipcode": "53505", + "channelId": 1561631 + }, + { + "zipcode": "53506", + "channelId": 1561661 + }, + { + "zipcode": "53507", + "channelId": 1561661 + }, + { + "zipcode": "53508", + "channelId": 1561661 + }, + { + "zipcode": "53511", + "channelId": 1561661 + }, + { + "zipcode": "53512", + "channelId": 1561661 + }, + { + "zipcode": "53515", + "channelId": 1561661 + }, + { + "zipcode": "53517", + "channelId": 1561661 + }, + { + "zipcode": "53520", + "channelId": 1561661 + }, + { + "zipcode": "53521", + "channelId": 1561661 + }, + { + "zipcode": "53522", + "channelId": 1561661 + }, + { + "zipcode": "53523", + "channelId": 1561284 + }, + { + "zipcode": "53525", + "channelId": 1561661 + }, + { + "zipcode": "53526", + "channelId": 1561661 + }, + { + "zipcode": "53527", + "channelId": 1561661 + }, + { + "zipcode": "53528", + "channelId": 1561661 + }, + { + "zipcode": "53529", + "channelId": 1561661 + }, + { + "zipcode": "53531", + "channelId": 1561661 + }, + { + "zipcode": "53532", + "channelId": 1561661 + }, + { + "zipcode": "53533", + "channelId": 1561661 + }, + { + "zipcode": "53534", + "channelId": 1561661 + }, + { + "zipcode": "53535", + "channelId": 1561661 + }, + { + "zipcode": "53536", + "channelId": 1561661 + }, + { + "zipcode": "53537", + "channelId": 1561661 + }, + { + "zipcode": "53538", + "channelId": 1561284 + }, + { + "zipcode": "53542", + "channelId": 1561661 + }, + { + "zipcode": "53543", + "channelId": 1561661 + }, + { + "zipcode": "53544", + "channelId": 1561661 + }, + { + "zipcode": "53545", + "channelId": 1561661 + }, + { + "zipcode": "53546", + "channelId": 1561631 + }, + { + "zipcode": "53547", + "channelId": 1561661 + }, + { + "zipcode": "53548", + "channelId": 1561661 + }, + { + "zipcode": "53549", + "channelId": 1561284 + }, + { + "zipcode": "53550", + "channelId": 1561661 + }, + { + "zipcode": "53551", + "channelId": 1561284 + }, + { + "zipcode": "53553", + "channelId": 1561661 + }, + { + "zipcode": "53555", + "channelId": 1561661 + }, + { + "zipcode": "53557", + "channelId": 1561665 + }, + { + "zipcode": "53558", + "channelId": 1561661 + }, + { + "zipcode": "53559", + "channelId": 1561660 + }, + { + "zipcode": "53560", + "channelId": 1561661 + }, + { + "zipcode": "53562", + "channelId": 1561661 + }, + { + "zipcode": "53563", + "channelId": 1561284 + }, + { + "zipcode": "53565", + "channelId": 1561661 + }, + { + "zipcode": "53566", + "channelId": 1561661 + }, + { + "zipcode": "53570", + "channelId": 1561661 + }, + { + "zipcode": "53571", + "channelId": 1561661 + }, + { + "zipcode": "53572", + "channelId": 1561661 + }, + { + "zipcode": "53574", + "channelId": 1561661 + }, + { + "zipcode": "53575", + "channelId": 1561661 + }, + { + "zipcode": "53576", + "channelId": 1561661 + }, + { + "zipcode": "53579", + "channelId": 1561660 + }, + { + "zipcode": "53580", + "channelId": 1561661 + }, + { + "zipcode": "53582", + "channelId": 1561661 + }, + { + "zipcode": "53585", + "channelId": 1561631 + }, + { + "zipcode": "53589", + "channelId": 1561661 + }, + { + "zipcode": "53590", + "channelId": 1561661 + }, + { + "zipcode": "53593", + "channelId": 1561661 + }, + { + "zipcode": "53594", + "channelId": 1561284 + }, + { + "zipcode": "53595", + "channelId": 1561661 + }, + { + "zipcode": "53596", + "channelId": 1561661 + }, + { + "zipcode": "53597", + "channelId": 1561661 + }, + { + "zipcode": "53598", + "channelId": 1561661 + }, + { + "zipcode": "53701", + "channelId": 1561661 + }, + { + "zipcode": "53702", + "channelId": 1561661 + }, + { + "zipcode": "53703", + "channelId": 1561661 + }, + { + "zipcode": "53704", + "channelId": 1561661 + }, + { + "zipcode": "53705", + "channelId": 1561661 + }, + { + "zipcode": "53706", + "channelId": 1561661 + }, + { + "zipcode": "53707", + "channelId": 1561661 + }, + { + "zipcode": "53708", + "channelId": 1561661 + }, + { + "zipcode": "53711", + "channelId": 1561661 + }, + { + "zipcode": "53713", + "channelId": 1561661 + }, + { + "zipcode": "53714", + "channelId": 1561661 + }, + { + "zipcode": "53715", + "channelId": 1561661 + }, + { + "zipcode": "53716", + "channelId": 1561661 + }, + { + "zipcode": "53717", + "channelId": 1561661 + }, + { + "zipcode": "53718", + "channelId": 1561661 + }, + { + "zipcode": "53719", + "channelId": 1561661 + }, + { + "zipcode": "53725", + "channelId": 1561661 + }, + { + "zipcode": "53726", + "channelId": 1561661 + }, + { + "zipcode": "53744", + "channelId": 1561661 + }, + { + "zipcode": "53774", + "channelId": 1561661 + }, + { + "zipcode": "53777", + "channelId": 1561661 + }, + { + "zipcode": "53778", + "channelId": 1561661 + }, + { + "zipcode": "53779", + "channelId": 1561661 + }, + { + "zipcode": "53782", + "channelId": 1561661 + }, + { + "zipcode": "53783", + "channelId": 1561661 + }, + { + "zipcode": "53784", + "channelId": 1561661 + }, + { + "zipcode": "53785", + "channelId": 1561661 + }, + { + "zipcode": "53786", + "channelId": 1561661 + }, + { + "zipcode": "53788", + "channelId": 1561661 + }, + { + "zipcode": "53789", + "channelId": 1561661 + }, + { + "zipcode": "53790", + "channelId": 1561661 + }, + { + "zipcode": "53791", + "channelId": 1561661 + }, + { + "zipcode": "53792", + "channelId": 1561661 + }, + { + "zipcode": "53793", + "channelId": 1561661 + }, + { + "zipcode": "53794", + "channelId": 1561661 + }, + { + "zipcode": "53901", + "channelId": 1561660 + }, + { + "zipcode": "53911", + "channelId": 1561661 + }, + { + "zipcode": "53916", + "channelId": 1561649 + }, + { + "zipcode": "53919", + "channelId": 1561649 + }, + { + "zipcode": "53920", + "channelId": 1561660 + }, + { + "zipcode": "53922", + "channelId": 1561657 + }, + { + "zipcode": "53923", + "channelId": 1561660 + }, + { + "zipcode": "53925", + "channelId": 1561660 + }, + { + "zipcode": "53926", + "channelId": 1561660 + }, + { + "zipcode": "53928", + "channelId": 1561660 + }, + { + "zipcode": "53930", + "channelId": 1561660 + }, + { + "zipcode": "53931", + "channelId": 1561649 + }, + { + "zipcode": "53932", + "channelId": 1561660 + }, + { + "zipcode": "53933", + "channelId": 1561649 + }, + { + "zipcode": "53935", + "channelId": 1561660 + }, + { + "zipcode": "53939", + "channelId": 1561649 + }, + { + "zipcode": "53946", + "channelId": 1561649 + }, + { + "zipcode": "53947", + "channelId": 1561649 + }, + { + "zipcode": "53949", + "channelId": 1561660 + }, + { + "zipcode": "53952", + "channelId": 1561660 + }, + { + "zipcode": "53953", + "channelId": 1561660 + }, + { + "zipcode": "53954", + "channelId": 1561660 + }, + { + "zipcode": "53955", + "channelId": 1561660 + }, + { + "zipcode": "53956", + "channelId": 1561660 + }, + { + "zipcode": "53957", + "channelId": 1561649 + }, + { + "zipcode": "53960", + "channelId": 1561660 + }, + { + "zipcode": "53963", + "channelId": 1561657 + }, + { + "zipcode": "53964", + "channelId": 1561660 + }, + { + "zipcode": "53965", + "channelId": 1561660 + }, + { + "zipcode": "53969", + "channelId": 1561661 + }, + { + "zipcode": "54106", + "channelId": 1561657 + }, + { + "zipcode": "54110", + "channelId": 1561599 + }, + { + "zipcode": "54113", + "channelId": 1561657 + }, + { + "zipcode": "54115", + "channelId": 1561657 + }, + { + "zipcode": "54123", + "channelId": 1561657 + }, + { + "zipcode": "54126", + "channelId": 1561599 + }, + { + "zipcode": "54129", + "channelId": 1561657 + }, + { + "zipcode": "54130", + "channelId": 1561657 + }, + { + "zipcode": "54131", + "channelId": 1561657 + }, + { + "zipcode": "54136", + "channelId": 1561657 + }, + { + "zipcode": "54140", + "channelId": 1561657 + }, + { + "zipcode": "54152", + "channelId": 1561657 + }, + { + "zipcode": "54155", + "channelId": 1561657 + }, + { + "zipcode": "54160", + "channelId": 1561599 + }, + { + "zipcode": "54162", + "channelId": 1561657 + }, + { + "zipcode": "54165", + "channelId": 1561657 + }, + { + "zipcode": "54169", + "channelId": 1561657 + }, + { + "zipcode": "54170", + "channelId": 1561657 + }, + { + "zipcode": "54173", + "channelId": 1561657 + }, + { + "zipcode": "54180", + "channelId": 1561657 + }, + { + "zipcode": "54207", + "channelId": 1561599 + }, + { + "zipcode": "54208", + "channelId": 1561599 + }, + { + "zipcode": "54214", + "channelId": 1561599 + }, + { + "zipcode": "54215", + "channelId": 1561599 + }, + { + "zipcode": "54220", + "channelId": 1561599 + }, + { + "zipcode": "54221", + "channelId": 1561599 + }, + { + "zipcode": "54227", + "channelId": 1561599 + }, + { + "zipcode": "54228", + "channelId": 1561599 + }, + { + "zipcode": "54229", + "channelId": 1561599 + }, + { + "zipcode": "54230", + "channelId": 1561599 + }, + { + "zipcode": "54232", + "channelId": 1561599 + }, + { + "zipcode": "54240", + "channelId": 1561599 + }, + { + "zipcode": "54241", + "channelId": 1561599 + }, + { + "zipcode": "54245", + "channelId": 1561599 + }, + { + "zipcode": "54247", + "channelId": 1561599 + }, + { + "zipcode": "54301", + "channelId": 1561657 + }, + { + "zipcode": "54302", + "channelId": 1561599 + }, + { + "zipcode": "54303", + "channelId": 1561657 + }, + { + "zipcode": "54304", + "channelId": 1561657 + }, + { + "zipcode": "54305", + "channelId": 1561657 + }, + { + "zipcode": "54306", + "channelId": 1561657 + }, + { + "zipcode": "54307", + "channelId": 1561657 + }, + { + "zipcode": "54308", + "channelId": 1561657 + }, + { + "zipcode": "54311", + "channelId": 1561599 + }, + { + "zipcode": "54313", + "channelId": 1561657 + }, + { + "zipcode": "54324", + "channelId": 1561657 + }, + { + "zipcode": "54344", + "channelId": 1561657 + }, + { + "zipcode": "54901", + "channelId": 1561657 + }, + { + "zipcode": "54902", + "channelId": 1561657 + }, + { + "zipcode": "54903", + "channelId": 1561657 + }, + { + "zipcode": "54904", + "channelId": 1561657 + }, + { + "zipcode": "54906", + "channelId": 1561657 + }, + { + "zipcode": "54911", + "channelId": 1561657 + }, + { + "zipcode": "54912", + "channelId": 1561657 + }, + { + "zipcode": "54913", + "channelId": 1561657 + }, + { + "zipcode": "54914", + "channelId": 1561657 + }, + { + "zipcode": "54915", + "channelId": 1561657 + }, + { + "zipcode": "54919", + "channelId": 1561657 + }, + { + "zipcode": "54922", + "channelId": 1561657 + }, + { + "zipcode": "54923", + "channelId": 1561615 + }, + { + "zipcode": "54927", + "channelId": 1561657 + }, + { + "zipcode": "54931", + "channelId": 1561657 + }, + { + "zipcode": "54932", + "channelId": 1561657 + }, + { + "zipcode": "54934", + "channelId": 1561649 + }, + { + "zipcode": "54935", + "channelId": 1561657 + }, + { + "zipcode": "54936", + "channelId": 1561657 + }, + { + "zipcode": "54937", + "channelId": 1561657 + }, + { + "zipcode": "54941", + "channelId": 1561649 + }, + { + "zipcode": "54942", + "channelId": 1561657 + }, + { + "zipcode": "54944", + "channelId": 1561657 + }, + { + "zipcode": "54947", + "channelId": 1561657 + }, + { + "zipcode": "54952", + "channelId": 1561657 + }, + { + "zipcode": "54956", + "channelId": 1561657 + }, + { + "zipcode": "54957", + "channelId": 1561657 + }, + { + "zipcode": "54960", + "channelId": 1561649 + }, + { + "zipcode": "54963", + "channelId": 1561657 + }, + { + "zipcode": "54964", + "channelId": 1561657 + }, + { + "zipcode": "54968", + "channelId": 1561649 + }, + { + "zipcode": "54971", + "channelId": 1561649 + }, + { + "zipcode": "54974", + "channelId": 1561657 + }, + { + "zipcode": "54979", + "channelId": 1561657 + }, + { + "zipcode": "54980", + "channelId": 1561649 + }, + { + "zipcode": "54985", + "channelId": 1561657 + }, + { + "zipcode": "54986", + "channelId": 1561657 + }, + { + "zipcode": "60001", + "channelId": 1561631 + }, + { + "zipcode": "60002", + "channelId": 1561624 + }, + { + "zipcode": "60010", + "channelId": 1561631 + }, + { + "zipcode": "60011", + "channelId": 1561631 + }, + { + "zipcode": "60012", + "channelId": 1561631 + }, + { + "zipcode": "60013", + "channelId": 1561631 + }, + { + "zipcode": "60014", + "channelId": 1561631 + }, + { + "zipcode": "60015", + "channelId": 1561639 + }, + { + "zipcode": "60020", + "channelId": 1561631 + }, + { + "zipcode": "60021", + "channelId": 1561631 + }, + { + "zipcode": "60030", + "channelId": 1561631 + }, + { + "zipcode": "60031", + "channelId": 1561639 + }, + { + "zipcode": "60033", + "channelId": 1561631 + }, + { + "zipcode": "60034", + "channelId": 1561631 + }, + { + "zipcode": "60035", + "channelId": 1561639 + }, + { + "zipcode": "60037", + "channelId": 1561639 + }, + { + "zipcode": "60039", + "channelId": 1561631 + }, + { + "zipcode": "60040", + "channelId": 1561639 + }, + { + "zipcode": "60041", + "channelId": 1561631 + }, + { + "zipcode": "60042", + "channelId": 1561631 + }, + { + "zipcode": "60044", + "channelId": 1561639 + }, + { + "zipcode": "60045", + "channelId": 1561639 + }, + { + "zipcode": "60046", + "channelId": 1561631 + }, + { + "zipcode": "60047", + "channelId": 1561631 + }, + { + "zipcode": "60048", + "channelId": 1561639 + }, + { + "zipcode": "60049", + "channelId": 1561631 + }, + { + "zipcode": "60050", + "channelId": 1561631 + }, + { + "zipcode": "60051", + "channelId": 1561631 + }, + { + "zipcode": "60060", + "channelId": 1561639 + }, + { + "zipcode": "60061", + "channelId": 1561639 + }, + { + "zipcode": "60064", + "channelId": 1561639 + }, + { + "zipcode": "60069", + "channelId": 1561639 + }, + { + "zipcode": "60071", + "channelId": 1561631 + }, + { + "zipcode": "60072", + "channelId": 1561631 + }, + { + "zipcode": "60073", + "channelId": 1561631 + }, + { + "zipcode": "60075", + "channelId": 1561639 + }, + { + "zipcode": "60079", + "channelId": 1561639 + }, + { + "zipcode": "60081", + "channelId": 1561631 + }, + { + "zipcode": "60083", + "channelId": 1561639 + }, + { + "zipcode": "60084", + "channelId": 1561631 + }, + { + "zipcode": "60085", + "channelId": 1561639 + }, + { + "zipcode": "60086", + "channelId": 1561631 + }, + { + "zipcode": "60087", + "channelId": 1561639 + }, + { + "zipcode": "60088", + "channelId": 1561639 + }, + { + "zipcode": "60089", + "channelId": 1561639 + }, + { + "zipcode": "60092", + "channelId": 1561631 + }, + { + "zipcode": "60096", + "channelId": 1561639 + }, + { + "zipcode": "60097", + "channelId": 1561631 + }, + { + "zipcode": "60098", + "channelId": 1561631 + }, + { + "zipcode": "60099", + "channelId": 1561639 + }, + { + "zipcode": "60102", + "channelId": 1561631 + }, + { + "zipcode": "60142", + "channelId": 1561631 + }, + { + "zipcode": "60152", + "channelId": 1561631 + }, + { + "zipcode": "60156", + "channelId": 1561631 + }, + { + "zipcode": "60180", + "channelId": 1561631 + } + ] + }, + "termOptions": [ + { + "value": 12, + "desc": "12 months / 1 year" + }, + { + "value": 24, + "desc": "24 months / 2 years" + }, + { + "value": 36, + "desc": "36 months / 3 years" + }, + { + "value": 48, + "desc": "48 months / 4 years" + }, + { + "value": 60, + "desc": "60 months / 5 years" + }, + { + "value": 63, + "desc": "63 months" + }, + { + "value": 72, + "desc": "72 months / 6 years" + }, + { + "value": 75, + "desc": "75 months" + }, + { + "value": 84, + "desc": "84 months / 7 years" + }, + { + "value": 120, + "desc": "120 months / 10 years" + }, + { + "value": 144, + "desc": "144 months / 12 years" + }, + { + "value": 180, + "desc": "180 months / 15 years" + }, + { + "value": 240, + "desc": "240 months / 20 years" + } + ], + "termOptionsNoYear": [ + { + "value": 12, + "desc": "12 months" + }, + { + "value": 24, + "desc": "24 months" + }, + { + "value": 36, + "desc": "36 months" + }, + { + "value": 48, + "desc": "48 months" + }, + { + "value": 60, + "desc": "60 months" + }, + { + "value": 63, + "desc": "63 months" + }, + { + "value": 72, + "desc": "72 months" + }, + { + "value": 75, + "desc": "75 months" + }, + { + "value": 84, + "desc": "84 months" + }, + { + "value": 120, + "desc": "120 months" + }, + { + "value": 144, + "desc": "144 months" + }, + { + "value": 180, + "desc": "180 months" + }, + { + "value": 240, + "desc": "240 months" + } + ], + "abaRouting": "275079714", + "contact": { + "HelpPhone": "262-796-4500" + }, + "links": { + "PrivacyPolicy": "https://www.example.com/", + "MemberSite": "https://www.example.net", + "HomePage": "https://www.landmarkcu.com/" + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-blue-01.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-blue-01.png new file mode 100644 index 0000000..59d8063 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-blue-01.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-gray-01.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-gray-01.png new file mode 100644 index 0000000..d6f3c58 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/applicant-icon-gray-01.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-blue.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-blue.png new file mode 100644 index 0000000..84e9a17 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-blue.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-gray.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-gray.png new file mode 100644 index 0000000..18e94a6 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/collateral-icon-gray.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/completed-check-icon.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/completed-check-icon.png new file mode 100644 index 0000000..8c7d915 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/completed-check-icon.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/finish-icon-gray.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/finish-icon-gray.png new file mode 100644 index 0000000..4f20e39 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/finish-icon-gray.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-blue.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-blue.png new file mode 100644 index 0000000..d0afbfb Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-blue.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-gray.png b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-gray.png new file mode 100644 index 0000000..f84cd62 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/assets/waypoint/review-icon-gray.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/browserslist b/LCU.WebApps.External.OnlineLoanApp/src/browserslist new file mode 100644 index 0000000..8e09ab4 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/browserslist @@ -0,0 +1,9 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +> 0.5% +last 2 versions +Firefox ESR +not dead +# IE 9-11 \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/enivronment-common.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/enivronment-common.ts new file mode 100644 index 0000000..6917a60 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/enivronment-common.ts @@ -0,0 +1,2 @@ +export const ENVIRONMENT_COMMON = { +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.dev.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.dev.ts new file mode 100644 index 0000000..2d79abb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.dev.ts @@ -0,0 +1,33 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +// There is an runner in package.json that will update this version file. +import { VERSION, BUILD_STAMP } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'dev', + version: VERSION, + buildTs: BUILD_STAMP, + production: false, + testGuard: true, + memberGuard: false, + coreResolver: true, + useLocalSettings: false, + enableTimeout: true, + displayLoanId: true, + urlBase: 'https://qa-online-loan-app-api.apps.azure.lcu-internal.com/api', + authUrlBase: 'https://qa-vendor-api.apps.azure.lcu-internal.com/api', + obsUri: 'https://www.landmarkcuonline.com/tob/beta/usp-core/app/login/consumer', + msAuthRedirectUri: 'https://qa-online-loan-app.apps.azure.lcu-internal.com/loanapp/admin', + baseHref: '/loanapp' +}; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.e2e.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.e2e.ts new file mode 100644 index 0000000..5e88b8a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.e2e.ts @@ -0,0 +1,16 @@ +import { VERSION } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'e2e', + version: VERSION, + production: false, + testGuard: false, + memberGuard: false, + coreResolver: false, + useLocalSettings: true, + enableTimeout: false, + displayLoanId: true, + urlBase: 'https://qa-online-loan-app-api.apps.azure.lcu-internal.com/api', + baseHref: '/loanapp' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.local.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.local.ts new file mode 100644 index 0000000..0c9a644 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.local.ts @@ -0,0 +1,19 @@ +import { VERSION } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'local', + version: VERSION, + production: false, + testGuard: false, + memberGuard: false, + coreResolver: true, + useLocalSettings: true, + enableTimeout: false, + displayLoanId: true, + urlBase: 'https://localhost:44384/api', + authUrlBase: 'https://qa-vendor-api.apps.azure.lcu-internal.com/api', + obsUri: 'https://www.landmarkcuonline.com/tob/beta/usp-core/app/login/consumer', + msAuthRedirectUri: 'https://localhost:8080/loanapp/admin', + baseHref: '/loanapp' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.prod.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.prod.ts new file mode 100644 index 0000000..98df96a --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.prod.ts @@ -0,0 +1,19 @@ +import { VERSION } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'prod', + version: VERSION, + production: true, + testGuard: false, + memberGuard: false, + coreResolver: true, + useLocalSettings: false, + enableTimeout: true, + displayLoanId: false, + urlBase: 'https://prd-online-loan-app-api.apps.azure.lcu-internal.com/api', + authUrlBase: 'https://prd-vendor-api.apps.azure.lcu-internal.com/api', + obsUri: 'https://www.landmarkcuonline.com/tob/live/usp-core/app/login/consumer', + msAuthRedirectUri: 'https://prd-online-loan-app.apps.azure.lcu-internal.com/loanapp/admin', + baseHref: '/loanapp' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.qa.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.qa.ts new file mode 100644 index 0000000..da7b9cb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.qa.ts @@ -0,0 +1,19 @@ +import { VERSION } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'qa', + version: VERSION, + production: true, + testGuard: true, + memberGuard: false, + coreResolver: false, + useLocalSettings: false, + enableTimeout: true, + displayLoanId: true, + urlBase: 'https://qa-online-loan-app-api.apps.azure.lcu-internal.com/api', + authUrlBase: 'https://qa-vendor-api.apps.azure.lcu-internal.com/api', + obsUri: 'https://www.landmarkcuonline.com/tob/beta/usp-core/app/login/consumer', + msAuthRedirectUri: 'https://qa-online-loan-app.apps.azure.lcu-internal.com/loanapp/admin', + baseHref: '/loanapp' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.ts new file mode 100644 index 0000000..4cc3ebd --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.ts @@ -0,0 +1,90 @@ +import { environment as devEnvironment } from './environment.dev'; +import { environment as localEnvironment } from './environment.local'; +import { environment as prodEnvironment } from './environment.prod'; +import { environment as qaEnvironment } from './environment.qa'; +import { environment as uatEnvironment } from './environment.uat'; + +export type EnvironmentConfiguration = + | typeof devEnvironment + | typeof localEnvironment + | typeof prodEnvironment + | typeof qaEnvironment + | typeof uatEnvironment; + +function normalizeHost(hostname?: string): string { + return (hostname || '').toLowerCase(); +} + +function normalizePath(pathname?: string): string { + return (pathname || '').toLowerCase(); +} + +function matchHostname( + hostname: string, + patterns: ReadonlyArray +): boolean { + return patterns.some((pattern) => hostname.indexOf(pattern) !== -1); +} + +function shouldUseAppsLoanApi(hostname: string): boolean { + return hostname.endsWith('apps.landmarkcu.com'); +} + +export function getEnvironmentFromWindowLocation(): EnvironmentConfiguration { + if (typeof window === 'undefined') { + return prodEnvironment; + } + + const location = window.location; + const hostInput = location ? location.hostname : undefined; + const hostname = normalizeHost(hostInput); + + if (!hostname) { + return prodEnvironment; + } + + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return localEnvironment; + } + + if (hostname.startsWith('qa-')) { + return qaEnvironment; + } + + if (hostname.startsWith('uat-')) { + return uatEnvironment; + } + + if (hostname.startsWith('prd-')) { + return prodEnvironment; + } + + return prodEnvironment; +} + +export const environment: EnvironmentConfiguration = prodEnvironment; + +function applyRuntimeEnvironment(): void { + const runtimeEnvironment = getEnvironmentFromWindowLocation(); + + if (runtimeEnvironment !== environment) { + for (const key in runtimeEnvironment) { + if (!Object.prototype.hasOwnProperty.call(runtimeEnvironment, key)) { + continue; + } + + (environment as any)[key] = (runtimeEnvironment as any)[key]; + } + } + + if (typeof window !== 'undefined') { + const location = window.location; + const hostname = normalizeHost(location ? location.hostname : undefined); + + if (shouldUseAppsLoanApi(hostname)) { + environment.urlBase = `https://${hostname}/loanapi/api`; + } + } +} + +applyRuntimeEnvironment(); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.uat.ts b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.uat.ts new file mode 100644 index 0000000..9faaa1e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/environments/environment.uat.ts @@ -0,0 +1,19 @@ +import { VERSION } from './version'; +import { ENVIRONMENT_COMMON } from './enivronment-common'; + +export const environment = { + name: 'uat', + version: VERSION, + production: true, + testGuard: true, + memberGuard: false, + coreResolver: false, + useLocalSettings: false, + enableTimeout: true, + displayLoanId: true, + urlBase: 'https://uat-online-loan-app-api.apps.azure.lcu-internal.com/api', + authUrlBase: 'https://uat-vendor-api.apps.azure.lcu-internal.com/api', + obsUri: 'https://www.landmarkcuonline.com/tob/beta/usp-core/app/login/consumer', + msAuthRedirectUri: 'https://uat-online-loan-app.apps.azure.lcu-internal.com/loanapp/admin', + baseHref: '/loanapp' +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/favicon.ico b/LCU.WebApps.External.OnlineLoanApp/src/favicon.ico new file mode 100644 index 0000000..2f13c10 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/src/favicon.ico differ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/index.html b/LCU.WebApps.External.OnlineLoanApp/src/index.html new file mode 100644 index 0000000..7f13f12 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/index.html @@ -0,0 +1,26 @@ + + + + + + + + + Landmark Credit Union Application For Credit + + + + + + + + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/src/karma.conf.js b/LCU.WebApps.External.OnlineLoanApp/src/karma.conf.js new file mode 100644 index 0000000..9edd22d --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/karma.conf.js @@ -0,0 +1,75 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '../', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-ie-launcher'), + require('karma-firefox-launcher'), + require('karma-edge-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('karma-junit-reporter'), + require('karma-spec-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + captureConsole: true, + mocha: { bail: false } + }, + browserConsoleLogOptions: { + level: 'debug', + format: '%b %T: %m', + terminal: true + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'spec', 'junit', 'kjhtml'], + files: [ + { pattern: 'src/assets/**/*', watched: false, included: false, served: true, nocache: false } + ], + proxies: { + '/assets/': '/base/src/assets/' + }, + specReporter: { + maxLogLines: 5, + suppressErrorSummary: false, + suppressFailed: false, + suppressPassed: true, + showSpecTiming: true + }, + junitReporter: { + outputDir: '../test-results', // results will be saved as $outputDir/$browserName.xml + outputFile: 'test-results.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: '', // suite will become the package name attribute in xml testsuite element + useBrowserName: true, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {}, // key value pair of properties to add to the section of the report + xmlVersion: 1 // use '1' if reporting to be per SonarQube 6.2 XML format + }, + port: 9876, + colors: true, + logLevel: config.LOG_DEBUG, + autoWatch: true, + customLaunchers: { + ChromeDebug: { + base: 'ChromeHeadless', + flags: [ + '--remote-debugging-port=9222', + '--disable-web-security', + ] + } + }, + browsers: ['ChromeHeadless'], + singleRun: false + }); +}; diff --git a/LCU.WebApps.External.OnlineLoanApp/src/main.ts b/LCU.WebApps.External.OnlineLoanApp/src/main.ts new file mode 100644 index 0000000..91ec6da --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/polyfills.ts b/LCU.WebApps.External.OnlineLoanApp/src/polyfills.ts new file mode 100644 index 0000000..00250f1 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/polyfills.ts @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +import 'core-js/es6/symbol'; +import 'core-js/es6/object'; +import 'core-js/es6/function'; +import 'core-js/es6/parse-int'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; +import 'core-js/es6/regexp'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; +import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/LCU.WebApps.External.OnlineLoanApp/src/styles.scss b/LCU.WebApps.External.OnlineLoanApp/src/styles.scss new file mode 100644 index 0000000..cdf8df6 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/styles.scss @@ -0,0 +1,455 @@ +/* You can add global styles to this file, and also import other style files */ +@import 'variables'; +@import "~node_modules/bootstrap/scss/bootstrap"; +@import '~@angular/material/prebuilt-themes/indigo-pink.css'; + + +/* Form specific styles to match Landmark requirements */ +$base: #353432; +$primary: #005674; +$primary-dark: #053251; +$secondary: #0D3151; +$success: #2c5e29; +$success-dark: #004B00; /* Success 20% Darker */ +$warning: #f7941d; +$error: #f63440; +$light-gray: #d5d3d3; +$disabled: #949699; +$white: #ffffff; +$background: #f8f9fa; +$width-to-mobile: 767px; + +.landmark-hero { + background-color: $primary; + padding: 0.5em 3em; +} + +.landmark-hero-emoji { + font-size: 4.5em; + color: $white; + text-align: center; +} + +.landmark-hero-status { + font-size: 2.25em; + color: $white; + text-align: center; + margin: 0; +} + +.landmark-hero-copy { + font-size: 1.15em; + color: $white; + text-align: center; +} + +.muted-background { + background-color: $background !important; +} +/*** Errors display ***/ +.form-control.ng-invalid.ng-touched .form-control.ng-invalid.ng-dirty { + border-color: $error; +} +.invalid-feedback { + display: block; +} + +/*** Custom buttons***/ +.btn-custom , .btn-custom:visited ,.btn-custom:hover{ + padding-left: 30px !important; + padding-right: 30px !important; +} + +.btn-custom:active, .btn-custom.active { + color: $white; + background-color: $primary !important; +} + +.btn-success { + background-color: $success; + font-size: 16px; + font-family: Arial, sans-serif; +} +.btn-success:hover{ + background-color: $success-dark; +} + +.btn-continue { + margin: 0 0 1em 1em; +} + +.btn-previous { + margin: 0 0 1em 0; + text-decoration: underline; +} + +.btn-disabled { + background-color: #999999; + color: #ffffff; + margin: 0 0 1em 1em; +} + +@media(min-width: 1200px) { + .btn-vertical-divider { + border-right: 1px solid #EAEAEA; + margin-right: 48px; + margin-top: 24px; + } +} + +@media(min-width: 992px) { + .btn-vertical-divider { + border-right: 1px solid #EAEAEA; + margin-right: 48px; + margin-top: 24px; + } +} + +/*** Watermarks ***/ +.watermark-primary {} +.watermark-secondary +{ + max-width: 200px; + position:absolute; + right:10%; + opacity:1; + z-index: -1; +} +//Resize +@media screen and (max-width: $width-to-mobile) { + .watermark-primary { + display:none; + } + .watermark-secondary { + max-width: 75px; + position: absolute; + top:4%; + left: 0; + right: 0; + margin: auto; + } +} + +/*** Color of icons before text***/ +.input-group-prepend .input-group-text { + background-color: $primary; + border-color: $primary; + color: #FFFFFF; +} + +.limited-width-col{ + max-width: 400px; +} + +// checkbox restyling came from a stack overflow question, seems to work really well +// note that it has been customized to look like graydient's design +// https://stackoverflow.com/questions/43520218/how-to-create-a-tick-mark-in-a-custom-checkbox +[type="checkbox"]:not(:checked), +[type="checkbox"]:checked { + position: absolute; + left: -9999px; +} + +[type="checkbox"]:not(:checked)+label, +[type="checkbox"]:checked+label { + position: relative; + padding-left: 25px; + cursor: pointer; +} + +[type="checkbox"]:not(:checked)+label:before, +[type="checkbox"]:checked+label:before { + content: ''; + position: absolute; + left: 0; + top: 4px; + width: 15px; + height: 15px; + background: white; + border: solid gray 1px; + outline: none; +} + +[type="checkbox"]:checked:hover +label:before, +[type="checkbox"]:not(:checked):hover +label:before, +[type="checkbox"]:focus + label::before { + background: $light-gray; + border: solid $primary 1px; +} + +[type="checkbox"]:not(:checked)+label:after, +[type="checkbox"]:checked+label:after { + content: url('/assets/checkbox/check.png'); + position: absolute; + top: -10px; + left: -5px; + font-size: 34px; + line-height: 0.8; + color: green; + transition: all .2s; +} + +[type="checkbox"]:not(:checked)+label:after { + opacity: 0; + transform: scale(0); +} + +[type="checkbox"]:checked+label:after { + opacity: 1; + transform: scale(1); +} + +.checkbox-label { + left: -25px; + top: 40px; +} + +.padding { + padding-left: 1rem; + padding-right: 1rem; +} + +.icon-input-row { + display: flex; + flex-direction: row; + align-items: baseline; +} + +.input-auto { + width: auto; +} + +.input-lmcu-fixed-width { + width: 20em; +} + +.summary-card > .col-4 { + padding-left: 2em; +} +.submit-row { + margin: 2em 0 2em 0; +} + +.disclosure-text { + font-family: Arial, Helvetica, sans-serif; + font-size: 0.97em; + line-height: 1.15em; + } + +/*** Placeholder ***/ +::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: $light-gray !important; + font-family: Arial, sans-serif; + font-style: italic !important; + font-size: 16px !important; + opacity: 1; /* Firefox */ +} + +:-ms-input-placeholder { /* Internet Explorer 10-11 */ + color: $light-gray !important; + font-style: italic !important; + font-size: 16px !important; +} + +::-ms-input-placeholder { /* Microsoft Edge */ + color: $light-gray !important; + font-style: italic !important; + font-size: 1rem !important; +} + +/*** Headers ***/ +h1, h2, h3, h4, h5, h6 { + font-family: 'Open Sans', sans-serif !important; +} + +h1, h2 { + font-size: 44px; + font-weight: 400; + color: $primary; +} + +h3 { + font-size: 24px; + font-weight: 700; + color: $base; +} + +h4 { + font-size: 20px; + font-weight: 400; + color: $base; +} +/*** Hint / helper text ***/ +.hint-field .control-label{ + margin-bottom: 0; +} + +.hint { + display: inline-block; + margin-bottom: 0.5rem; + font-size: 14px; + color: $light-gray; + font-style: italic; +} +/*** Accordion ***/ +.accordion .card{ + border-radius: .25rem; + border: 2px solid $secondary; +} + +.accordion .card:first-of-type{ + border-bottom: 2px solid $secondary; + border-radius: 0.25rem; +} + +.accordion .card .card-header { + position: relative; + background-color: $secondary; + color: $white; + font-size: 1.2rem; + letter-spacing: 1.5px; + border-radius: 0; +} +.accordion .card .card-header a { + color: $white; +} + +.accordion .card .card-header a:hover { + color: $light-gray; +} + +.accordion .card .card-header p { + margin-bottom: 0; +} + +.accordion .card .card-header .card-icon { + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); + font-size: 1.8rem; +} + +html * { + font-family: Arial, sans-serif; +} + +.legal-text { + color: #000000; + border-color: $warning; + border-width: 2px; + font-size: 0.85em; +} + +/*** Modal ***/ +.modal-title { + color: $white !important; +} +.close { + color: $white !important; +} + +.modal-header { + background-color: $primary-dark !important; +} + +.title-hr { + margin: 0 0 0.67em 0; + color: #DFDFDF; + height: 4px; + border: none; + background-color: #DFDFDF; +} + +.optional-text { + color: $light-gray; + font-size: 14px; + font-style: italic; +} + +.form-group { + margin-bottom: 1rem; +} + +/*** Select **/ +.custom-drop-down select { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; + outline: none; + z-index: 0 !important; + } + +.custom-drop-down .input-group :before { + z-index: 1; + content: ''; + right: 0; + top: 0; + width: 35px; + height: 38px; + background: $primary; + position: absolute; + pointer-events: none; + font-size: 26px; + font-family: monospace !important; + font-style: normal; + font-weight: normal; + border: 4px solid $primary; + border-top-right-radius: .25rem; + border-bottom-right-radius: .25rem; + padding: 2px 2px 2px 2px; +} +.custom-drop-down { position: relative; } +.custom-drop-down:after { + z-index: 1; + content: "▾"; + color: white; + font-size: 26px; + font-family: monospace !important; + font-style: normal; + font-weight: normal; + right: 25px; + top: -3px; + bottom: 0; + background: transparent; + position: absolute; + pointer-events: none; +} + +@-moz-document url-prefix() { + // alignment for firefox + .custom-drop-down:after { + right: 22px; + top: 1px; + } +} +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + // alignment for IE + .custom-drop-down:after { + right: 22px; + top: -2px; + } + // fix input box height + .form-control { + padding: 0.55rem 0.75rem !important; + } +} + +// fix height/width for input icons +span.input-group-text { + height: 38px !important; + width: 40px !important; +} +div.input-group-text { + height: 38px !important; + width: 40px !important; +} +// remove internet explorer / edge +input::-ms-clear { display: none; } + +.custom-checkbox-mt { + position: relative; + top: 2px; +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/test.ts b/LCU.WebApps.External.OnlineLoanApp/src/test.ts new file mode 100644 index 0000000..daf1d5f --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/test.ts @@ -0,0 +1,35 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const contextComponents = require.context('./', true, /\.component\.spec\.ts$/); +const contextServices = require.context('./', true, /\.service\.spec\.ts$/); +const contextPages = require.context('./', true, /-page\.spec\.ts$/); +const translatorClasses = require.context('./', true, /-translator\.spec\.ts$/); +const calculatorClasses = require.context('./', true, /-calculator\.spec\.ts$/); +const utilClasses = require.context('./', true, /-util\.spec\.ts$/); +const optionClasses = require.context('./', true, /-options\.spec\.ts$/); +const routerGuards = require.context('./', true, /.guard\.spec\.ts$/); + +// And load the modules. +contextComponents.keys().map(contextComponents); +contextServices.keys().map(contextServices); +contextPages.keys().map(contextPages); +translatorClasses.keys().map(translatorClasses); +calculatorClasses.keys().map(calculatorClasses); +utilClasses.keys().map(utilClasses); +optionClasses.keys().map(optionClasses); +routerGuards.keys().map(routerGuards); diff --git a/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.app.json b/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.app.json new file mode 100644 index 0000000..722c370 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.spec.json b/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.spec.json new file mode 100644 index 0000000..8f7cede --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "module": "commonjs", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/tslint.json b/LCU.WebApps.External.OnlineLoanApp/src/tslint.json new file mode 100644 index 0000000..ddaef76 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lw", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lw", + "kebab-case" + ] + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/src/web.config b/LCU.WebApps.External.OnlineLoanApp/src/web.config new file mode 100644 index 0000000..e61d741 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/src/web.config @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.WebApps.External.OnlineLoanApp/tsconfig.json b/LCU.WebApps.External.OnlineLoanApp/tsconfig.json new file mode 100644 index 0000000..ef44e28 --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/tslint.json b/LCU.WebApps.External.OnlineLoanApp/tslint.json new file mode 100644 index 0000000..3ea984c --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/tslint.json @@ -0,0 +1,130 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/LCU.WebApps.External.OnlineLoanApp/version.js b/LCU.WebApps.External.OnlineLoanApp/version.js new file mode 100644 index 0000000..97864ea --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/version.js @@ -0,0 +1,22 @@ +const { gitDescribeSync } = require('git-describe'); +const { version } = require('./package.json'); +const { resolve, relative } = require('path'); +const { writeFileSync } = require('fs-extra'); + +const gitInfo = gitDescribeSync({ + dirtyMark: false, + dirtySemver: false +}); + +gitInfo.version = version; + +const file = resolve(__dirname, '.', 'src', 'environments', 'version.ts'); +writeFileSync(file, + `// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! +/* tslint:disable */ +export const VERSION = ${JSON.stringify(gitInfo, null, 4)}; +export const BUILD_STAMP = ${JSON.stringify(new Date())}; +/* tslint:enable */ +`, { encoding: 'utf-8' }); + +console.log(`Wrote version info ${gitInfo.raw} to ${relative(resolve(__dirname, '..'), file)}`); diff --git a/LCU.WebApps.External.OnlineLoanApp/wiki/.order b/LCU.WebApps.External.OnlineLoanApp/wiki/.order new file mode 100644 index 0000000..ee154eb --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/wiki/.order @@ -0,0 +1,2 @@ +File-Upload-Component +API-to-Storage-to-Blob-to-ACTion \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/wiki/API-to-Storage-to-Blob-to-ACTion.md b/LCU.WebApps.External.OnlineLoanApp/wiki/API-to-Storage-to-Blob-to-ACTion.md new file mode 100644 index 0000000..e69de29 diff --git a/LCU.WebApps.External.OnlineLoanApp/wiki/File-Upload-Component.md b/LCU.WebApps.External.OnlineLoanApp/wiki/File-Upload-Component.md new file mode 100644 index 0000000..714c44e --- /dev/null +++ b/LCU.WebApps.External.OnlineLoanApp/wiki/File-Upload-Component.md @@ -0,0 +1,203 @@ +# File Upload Component +The file upload component in ```/src/app/shared/file-upload``` allows for files to be uploaded to Azure Blob Storage for a given loan. Once the loan is created in ACTion it will upload the files to ACTion and show up in Attachments. An example of the component shown here: + +![Image alt text](./file-upload-student.png) + +## Enabling Employment Income/Other Income file uploads for a loan +Set ```showProofOfIncomeUploadForm``` value for the loan to true in ```src\app\shared\loan-type.ts``` + +## Adding the upload component to a form (see end of document for an example implementation) +1. Update interface by adding two variables: ```files``` and ```isUploading```, example in ```src\app\loan-wizard\field-groups\personal\personal-information.ts``` +1. Add new values to form group in ```src\app\loan-wizard\field-groups\common-groups.ts```, see ```PERSONAL_INFO``` in this file for an example +1. Add the HTML to the form, example below and in ```src\app\loan-wizard\field-groups\personal\personal-info-form.component.html``` +1. Update TS by adding variables needed, example below and in ```src\app\loan-wizard\field-groups\personal\personal-info-form.component.ts``` +1. Update ```src\assets\i18n\en.json``` to have verbiage for uploading document +1. Update ```src\app\shared\loan-type.ts``` to set ```beforeYouBeginKey``` value on a given loan to the corresponding key in ```en.json``` to enable the checklist +1. Update the summary component to show files if they're present, example in ```src\app\loan-wizard\wizards\common\summary\personal-summary.component.html``` + +## Inputs +1. ```loanId``` _required_ + - The CosmosDB loan ID that uploaded files will be associated with +1. ```section``` _optional_ + - The section that the form is in, e.g. Collateral. + - If left blank, the description in ACTion will not include this. +1. ```sectionId``` _required_ + - The sectionId is used to be more specific, when there are multiple upload components in a single form. + - An example would be the Income section that shows up to 3 upload components for any borrower on the loan, for which the ```sectionId``` is set to ```Primary Borrower-#```, where ```#``` would be which income section it is associated with, 1, 2, or 3. +1. ```description``` _required_ + - Description that is used to set the attachment type in ACTion + - This text will be displayed in the component after "Upload" + - This text can be overridden by passing a value in for the ```label``` input +1. ```label``` _optional_ + - This text will be displayed in the component after "Upload" + - This was added in after the verbiage requirements needed something more custom in certain areas to show something like ```Upload a copy of first mortgage balance statement``` instead of ```Upload Copy of first mortgage balance statement```. + - If ```label``` is not provided, ```description``` will be shown. +1. ```removeAllFilesSubject``` _optional_ + - This input is used by passing in a ```Subject``` and then triggered by calling the ```.next()``` function on the subject, which will remove all files and emit when completed. + - It is meant to be used along with the ```removedAllFiles``` output to notify the parent component that the files have been removed and the form can be updated. + +## Outputs +1. ```sendFiles``` _required_ + - Used to capture when files change and need to be updated on the parent component. +1. ```uploadingFiles``` _required_ + - Used for form validation so that the Next button cannot be pressed while a file is being uploaded or deleted. +1. ```removedAllFiles``` _optional_ + - Used when a form is closed/not needed and the files that were uploaded need to be deleted. + - Once ```removedAllFilesSubject``` is called and completed, this output is emitted so that the form can be updated as needed. + +## Notes +- When the component loads, it creates two requests for the API: one for the files associated with the ```loanId``` and the other for the settings, which controls the file size and types. +- The `````` component should be surround with a ```div``` that controls the width and visibility, e.g.: +```html +
+``` + +## ACTion +Files that are uploaded into ACTion will be given a type: Attachment, Proof of Income, or Title. This is done by using the ```description``` input and mapping it to a type on the backend API defined in ```appsettings.json```. If a description is mistyped or not found in ```appsettings.json```, it will default to Attachment. + +The description within action is created with the inputs: ```section```, ```sectionId```, and ```description```. + +If ```section``` is null or blank, it is not used as shown in the code and screenshot below. + +```csharp +// Code segment from LoanWizardAPI/CRIF.ACTion/Files/AddAppFilesSection.cs +$"{file.Section} - {file.SectionId}: {file.Description}" +$"{file.SectionId}: {file.Description}" +``` + +![Image alt text](./file-upload-action.png) + +# Example Implementation +## HTML +```html +
+ + +
+``` + +## TypeScript +```ts + @Input() + public group: FormGroup; + + public loanId: string; + + public showUploadForm: boolean; + + public removeAllFilesSubject: Subject; + + constructor(private collateralInfoService: CollateralInfoFormService) { + this.loanId = collateralInfoService.getLoanId(); // not real, just an example + this.showUploadForm = true; + this.removeAllFilesSubject = new Subject(); + } + + // Somewhat of a contrived example, but essentially when the form is supposed to be deleted, the .next() should be called. + // Once files are removed, the removedAllFiles output of the component will emit and call the function to complete removing the form handleRemovedAllFiles (below) + public closeForm() { + this.removeAllFilesSubject.next(); + } + + // Sets the files value of the group, called when component is initialized and when all files have been removed. + public handleFiles(files: any[]): void { + this.group.patchValue({ 'files': files }); + } + + // Sets a boolean to true/false for form validation so that Next is disabled + public handleUploadingFiles(isUploading: boolean): void { + this.group.patchValue({ 'isUploading': isUploading }); + } + + public handleRemovedAllFiles(removedAllFiles: boolean): void { + if(removedAllFiles) { + // Code to clear/delete form here... + } + } +``` + + +## Interface +This is the interface that the files variable should be added to. If it is in the Personal Info section, it would be added to ```src\app\loan-wizard\field-groups\personal\personal-information.ts``` + +```ts +{ + ... + // not all variables shown + files?: any[]; + isUploading?: boolean; + ... +} +``` + +## src\app\loan-wizard\field-groups\common-groups.ts +This file is where the form group for the page is generated. It will also need to have both variables added to the correct section so they can be used with the validators. +```ts +{ + ... + // not all variables shown + { + field: 'files', + validation: [WizardValidators.FileValidator] + }, + { + field: 'isUploading', + validation: [WizardValidators.MustBeFalse] + } + ... +} +``` + +## src\assets\i18n\en.json +```ts +{ + ... + // not all variables shown + "BEFOREYOUBEGIN": { + "EXAMPLE": { + "INTRO": "Before you begin the process, you should have the following documents:", + "HEADINGS": [ + { + "HEADING": "Documents to Upload", + "VALUES": [ + "Proof of income (i.e. most recent payroll check stub, 1040s for self-employed borrowers, bank statement).", + "Proof that you are a student (i.e. Student ID from current school year or current enrollment documentation such as a class schedule)." + ] + } + ] + }, + ... + } +} +``` + +## src\app\shared\loan-type.ts +Add needed keys for checklist component, coming from en.json (see above) +```js + { + ... + // not all variables shown + beforeYouBeginKey: 'BEFOREYOUBEGIN.EXAMPLE', // Should be added/updated whenever the upload form is added + showProofOfIncomeUploadForm: true // Set to true if the proof of income file uploads should show, be sure to update en.json! + ... + } +``` + +## Summary component +Add ```divs``` to display files. The description can be pulled from the first file, as shown (it is done with income files as there are multiple sections and borrowers), or the description can be hard coded. The files array is passed into the ```uploadedFiles``` pipe which parses them into a comma-separated list. The pipe is located at ```src\app\shared\pipes\uploaded-files.pipe.ts``` +```html +
+ +
{{personalInformation.files[0].description}} document(s)
+
{{personalInformation.files | uploadedFiles}}
+
+``` \ No newline at end of file diff --git a/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-action.png b/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-action.png new file mode 100644 index 0000000..f45c0b4 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-action.png differ diff --git a/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-student.png b/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-student.png new file mode 100644 index 0000000..b54fe50 Binary files /dev/null and b/LCU.WebApps.External.OnlineLoanApp/wiki/file-upload-student.png differ